Решение на Logging от Антонио Миндов

Обратно към всички решения

Към профила на Антонио Миндов

Резултати

  • 17 точки от тестове
  • 0 бонус точки
  • 17 точки общо
  • 13 успешни тест(а)
  • 2 неуспешни тест(а)

Код

use std::cell::RefCell;
use std::io;
use std::io::Write;
use std::rc::Rc;
use std::time::Instant;
pub trait Logger {
fn push(&mut self, time: Instant, text: &str);
fn log(&mut self, text: &str) {
self.push(Instant::now(), text)
}
fn try_flush(&mut self) -> io::Result<()> {
Ok(())
}
fn flush(&mut self) {
if let Err(err) = self.try_flush() {
eprintln!("{}", err)
}
}

Добре си се сетил за това, че log просто може да използва push в trait-а. Аз обаче не бих сложил default-на имплементация на try_flush. Всеки един от типовете я override-ва, така че тя не се използва за нищо. А е лесно да я забрави човек, и по default да не прави нищо. В зависимост от това каква е целта на това, което пишеш, това може да доведе до лесно вкарване на бъгове. Но си признавам, че това е по-скоро интуиция, отколкото практически проблем в случая.

}
pub struct BufferedLogger<W: Write> {
logs: Rc<RefCell<Vec<(Instant, String)>>>,
out: Rc<RefCell<W>>,
buffer_size: usize,
}
impl<W: Write> BufferedLogger<W> {
pub fn new(out: W, buffer_size: usize) -> Self {
BufferedLogger {
out: Rc::new(RefCell::new(out)),
logs: Rc::new(RefCell::new(Vec::with_capacity(buffer_size))),
buffer_size
}
}
pub fn buffered_entries(&self) -> Vec<String> {
self.logs.borrow().iter().map(|entry| entry.1.clone()).collect()
}
}
impl<W: Write> Clone for BufferedLogger<W> {
fn clone(&self) -> Self {
BufferedLogger {
out: Rc::clone(&self.out),
logs: Rc::clone(&self.logs),
buffer_size: self.buffer_size,
}
}
}
impl<W: Write> Logger for BufferedLogger<W> {
fn push(&mut self, time: Instant, text: &str) {
self.logs.borrow_mut().push((time, String::from(text)));
let curr_idx = self.logs.borrow().len()-1;
while curr_idx > 0 && self.logs.borrow()[curr_idx].0 < self.logs.borrow()[curr_idx-1].0 {
self.logs.borrow_mut().swap(curr_idx - 1, curr_idx);
}
if self.logs.borrow().len() >= self.buffer_size {
self.flush()
}
}
fn try_flush(&mut self) -> io::Result<()> {
let mut result = Ok(());
for log in self.logs.borrow().iter() {
let line = format!("{}\n", log.1);
if let Err(err) = self.out.borrow_mut().write(line.as_bytes()) {
result = Err(err);
break;
}

Вместо да пазиш тази временна променлива, по-лесно би било да сложиш един return Err(err) тук. Няма да имаш нужда и от break. Накрая на функцията можеш да оставиш Ok(()) като "ако си стигнал дотук, всичко е наред".

По този начин, можеш и да махнеш и if-клаузата с оператора ?:

let line = format!("{}\n", log.1);
self.out.borrow_mut().write(line.as_bytes())?;

Или нещо подобно (не съм пускал този конкретен код).

Зависи. Ако има грешка при писане, може би има смисъл да не ги чистиш, за да може при следващия опит за flush-ване, да се пробват пак. Разбира се, в тази ситуация, биха се повторили предните, които не са fail-нали. Бих пробвал метода .drain() на вектор, за да чистя елементи в движение, макар че признавам, че не знам performance характеристиките му. Не бих се учудил Drain итератора да има Drop имплементация, която чисти цял range от вектора.

(Update: да, има: https://doc.rust-lang.org/src/alloc/vec.rs.html#2478)

Но и това е вариант -- не сме указали дали да се чисти или не при грешка, така че това е валидно решение на проблема, признавам.

}
self.logs.borrow_mut().clear();
return result;
}
}
pub struct MultiLogger {
loggers: Vec<Box<dyn Logger>>
}
impl MultiLogger {
pub fn new() -> Self {
MultiLogger {loggers: Vec::new()}
}
pub fn log_to<L: Logger + 'static>(&mut self, logger: L) {
self.loggers.push(Box::new(logger));
}
}
impl Logger for MultiLogger {
fn push(&mut self, time: Instant, text: &str) {
for logger in self.loggers.iter_mut() {
logger.push(time, text);
}
}
fn try_flush(&mut self) -> io::Result<()> {
for logger in self.loggers.iter_mut() {
logger.try_flush()?;
}
return Ok(())
}
}
pub struct ScopedLogger<L: Logger> {
base_logger: L,
tag: String,
}
impl<L: Logger> ScopedLogger<L> {
pub fn new(tag: &str, base_logger: L) -> Self {
let tag = format!("[{}] ", tag);
ScopedLogger {tag, base_logger}
}
}
impl<L: Logger> Logger for ScopedLogger<L> {
fn push(&mut self, time: Instant, text: &str) {
let mut new_text = self.tag.clone();
new_text.push_str(text);
self.base_logger.push(time, &new_text);
}
fn try_flush(&mut self) -> io::Result<()> {
self.base_logger.try_flush()
}
}

Лог от изпълнението

Compiling solution v0.1.0 (/tmp/d20190123-22631-arbbfw/solution)
    Finished dev [unoptimized + debuginfo] target(s) in 4.80s
     Running target/debug/deps/solution-2e785d603b538f71

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

     Running target/debug/deps/solution_test-29808948fb50ed3a

running 15 tests
test solution_test::test_automatic_flushing_when_buffer_limit_is_reached ... ok
test solution_test::test_automatic_flushing_when_zero_buffer_limit ... ok
test solution_test::test_basic_log ... ok
test solution_test::test_basic_push ... ok
test solution_test::test_cloning_a_logger_shares_a_buffer ... ok
test solution_test::test_cloning_a_logger_shares_their_io ... ok
test solution_test::test_erroring_io ... ok
test solution_test::test_flushing_the_buffer ... ok
test solution_test::test_logger_combinations ... ok
test solution_test::test_multilogger_logs_and_flushes_when_needed ... ok
test solution_test::test_multilogger_logs_to_several_ios ... ok
test solution_test::test_reordering_logs_in_buffer ... FAILED
test solution_test::test_reordering_logs_in_io ... FAILED
test solution_test::test_scoped_logger ... ok
test solution_test::test_scoped_logger_with_a_string_tag ... ok

failures:

---- solution_test::test_reordering_logs_in_buffer stdout ----
thread 'solution_test::test_reordering_logs_in_buffer' panicked at 'assertion failed: `(left == right)`
  left: `["Second", "Third", "First", "Fourth"]`,
 right: `["First", "Second", "Third", "Fourth"]`', tests/solution_test.rs:111:5
note: Run with `RUST_BACKTRACE=1` for a backtrace.

---- solution_test::test_reordering_logs_in_io stdout ----
thread 'solution_test::test_reordering_logs_in_io' panicked at 'assertion failed: `(left == right)`
  left: `"Second\nThird\nFirst\nFourth\n"`,
 right: `"First\nSecond\nThird\nFourth\n"`', tests/solution_test.rs:134:5


failures:
    solution_test::test_reordering_logs_in_buffer
    solution_test::test_reordering_logs_in_io

test result: FAILED. 13 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out

error: test failed, to rerun pass '--test solution_test'

История (1 версия и 6 коментара)

Антонио обнови решението на 17.12.2018 01:57 (преди почти 2 години)

+use std::cell::RefCell;
+use std::io;
+use std::io::Write;
+use std::rc::Rc;
+use std::time::Instant;
+
+pub trait Logger {
+ fn push(&mut self, time: Instant, text: &str);
+
+ fn log(&mut self, text: &str) {
+ self.push(Instant::now(), text)
+ }
+
+ fn try_flush(&mut self) -> io::Result<()> {
+ Ok(())
+ }
+
+ fn flush(&mut self) {
+ if let Err(err) = self.try_flush() {
+ eprintln!("{}", err)
+ }
+ }

Добре си се сетил за това, че log просто може да използва push в trait-а. Аз обаче не бих сложил default-на имплементация на try_flush. Всеки един от типовете я override-ва, така че тя не се използва за нищо. А е лесно да я забрави човек, и по default да не прави нищо. В зависимост от това каква е целта на това, което пишеш, това може да доведе до лесно вкарване на бъгове. Но си признавам, че това е по-скоро интуиция, отколкото практически проблем в случая.

+}
+
+
+pub struct BufferedLogger<W: Write> {
+ logs: Rc<RefCell<Vec<(Instant, String)>>>,
+ out: Rc<RefCell<W>>,
+ buffer_size: usize,
+}
+
+impl<W: Write> BufferedLogger<W> {
+ pub fn new(out: W, buffer_size: usize) -> Self {
+ BufferedLogger {
+ out: Rc::new(RefCell::new(out)),
+ logs: Rc::new(RefCell::new(Vec::with_capacity(buffer_size))),
+ buffer_size
+ }
+ }
+
+ pub fn buffered_entries(&self) -> Vec<String> {
+ self.logs.borrow().iter().map(|entry| entry.1.clone()).collect()
+ }
+}
+
+impl<W: Write> Clone for BufferedLogger<W> {
+ fn clone(&self) -> Self {
+ BufferedLogger {
+ out: Rc::clone(&self.out),
+ logs: Rc::clone(&self.logs),
+ buffer_size: self.buffer_size,
+ }
+ }
+}
+
+impl<W: Write> Logger for BufferedLogger<W> {
+ fn push(&mut self, time: Instant, text: &str) {
+ self.logs.borrow_mut().push((time, String::from(text)));
+ let curr_idx = self.logs.borrow().len()-1;
+ while curr_idx > 0 && self.logs.borrow()[curr_idx].0 < self.logs.borrow()[curr_idx-1].0 {
+ self.logs.borrow_mut().swap(curr_idx - 1, curr_idx);
+ }
+
+ if self.logs.borrow().len() >= self.buffer_size {
+ self.flush()
+ }
+ }
+
+ fn try_flush(&mut self) -> io::Result<()> {
+ let mut result = Ok(());
+ for log in self.logs.borrow().iter() {
+ let line = format!("{}\n", log.1);
+ if let Err(err) = self.out.borrow_mut().write(line.as_bytes()) {
+ result = Err(err);
+ break;
+ }

Вместо да пазиш тази временна променлива, по-лесно би било да сложиш един return Err(err) тук. Няма да имаш нужда и от break. Накрая на функцията можеш да оставиш Ok(()) като "ако си стигнал дотук, всичко е наред".

По този начин, можеш и да махнеш и if-клаузата с оператора ?:

let line = format!("{}\n", log.1);
self.out.borrow_mut().write(line.as_bytes())?;

Или нещо подобно (не съм пускал този конкретен код).

Зависи. Ако има грешка при писане, може би има смисъл да не ги чистиш, за да може при следващия опит за flush-ване, да се пробват пак. Разбира се, в тази ситуация, биха се повторили предните, които не са fail-нали. Бих пробвал метода .drain() на вектор, за да чистя елементи в движение, макар че признавам, че не знам performance характеристиките му. Не бих се учудил Drain итератора да има Drop имплементация, която чисти цял range от вектора.

(Update: да, има: https://doc.rust-lang.org/src/alloc/vec.rs.html#2478)

Но и това е вариант -- не сме указали дали да се чисти или не при грешка, така че това е валидно решение на проблема, признавам.

+ }
+
+ self.logs.borrow_mut().clear();
+ return result;
+ }
+}
+
+
+pub struct MultiLogger {
+ loggers: Vec<Box<dyn Logger>>
+}
+
+impl MultiLogger {
+ pub fn new() -> Self {
+ MultiLogger {loggers: Vec::new()}
+ }
+
+ pub fn log_to<L: Logger + 'static>(&mut self, logger: L) {
+ self.loggers.push(Box::new(logger));
+ }
+}
+
+impl Logger for MultiLogger {
+ fn push(&mut self, time: Instant, text: &str) {
+ for logger in self.loggers.iter_mut() {
+ logger.push(time, text);
+ }
+ }
+
+ fn try_flush(&mut self) -> io::Result<()> {
+ for logger in self.loggers.iter_mut() {
+ logger.try_flush()?;
+ }
+
+ return Ok(())
+ }
+}
+
+
+pub struct ScopedLogger<L: Logger> {
+ base_logger: L,
+ tag: String,
+}
+
+impl<L: Logger> ScopedLogger<L> {
+ pub fn new(tag: &str, base_logger: L) -> Self {
+ let tag = format!("[{}] ", tag);
+ ScopedLogger {tag, base_logger}
+ }
+}
+
+impl<L: Logger> Logger for ScopedLogger<L> {
+ fn push(&mut self, time: Instant, text: &str) {
+ let mut new_text = self.tag.clone();
+ new_text.push_str(text);
+ self.base_logger.push(time, &new_text);
+ }
+
+ fn try_flush(&mut self) -> io::Result<()> {
+ self.base_logger.try_flush()
+ }
+}

Разумно решение, освен бъга със сортирането. Един тест с повече от два елемента щеше да го хване. Иначе, можеше да използваш sort метода, или, ако се притесняваш за performance, да смениш вектора с BinaryHeap. Виж моето решение за детайли.