Pruebas (testing)
Ahora que ya se entiende el funcionamiento de los módulos es un buen momento para aprender sobre las pruebas. En Rust es muy fácil probar el código porque se pueden escribir los tests junto al propio código.
La forma más sencilla de empezar a probar es añadir #[test]
a una función. Por ejemplo:
#![allow(unused)] fn main() { #[test] fn dos_es_dos() { assert_eq!(2, 2); } }
Pero si se intenta ejecutar en Playground, da un error: error[E0601]: `main` function not found in crate `playground
. Esto se debe a que no se utiliza Run para ejecutar las pruebas. En playground se puede pulsar junto a RUN en los ···
y cambiar a TEST. Ahora, si se pulsa, ejecutará las pruebas. Si se tiene ya instalado Rust en el ordenador, se debe ejecutar cargo test
en lugar de cargo run
.
Este es el resultado de ejecutar el test anterior:
running 1 test
test dos_es_dos ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Si se modifica assert_eq!(2, 2)
a assert_eq!(2, 3)
el test falla y devuelve una información mucho más detallada:
running 1 test
test dos_es_dos ... FAILED
failures:
---- dos_es_dos stdout ----
thread 'dos_es_dos' panicked at 'assertion failed: `(left == right)`
left: `2`,
right: `3`', src/lib.rs:3:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
dos_es_dos
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
En Rust, assert_eq!(left, right)
es el principal modo de probar una función. Si no funciona, mostará los dos valores que son diferentes: left has 2, but right has 3
.
¿Qué significa RUST_BACKTRACE=1
? Es una variable del sistema que se puede activar para dar más información sobre el error. En Playground se puede activar pulsando ···
junto a STABLE
y establecer la traza (backtrace) a ENABLED
. Si se hace así, se mostrará mucha información:
running 1 test
test dos_es_dos ... FAILED
failures:
---- dos_es_dos stdout ----
thread 'dos_es_dos' panicked at 'assertion failed: 2 == 3', src/lib.rs:3:5
stack backtrace:
0: backtrace::backtrace::libunwind::trace
at /cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.46/src/backtrace/libunwind.rs:86
1: backtrace::backtrace::trace_unsynchronized
at /cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.46/src/backtrace/mod.rs:66
2: std::sys_common::backtrace::_print_fmt
at src/libstd/sys_common/backtrace.rs:78
3: <std::sys_common::backtrace::_print::DisplayBacktrace as core::fmt::Display>::fmt
at src/libstd/sys_common/backtrace.rs:59
4: core::fmt::write
at src/libcore/fmt/mod.rs:1076
5: std::io::Write::write_fmt
at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libstd/io/mod.rs:1537
6: std::io::impls::<impl std::io::Write for alloc::boxed::Box<W>>::write_fmt
at src/libstd/io/impls.rs:176
7: std::sys_common::backtrace::_print
at src/libstd/sys_common/backtrace.rs:62
8: std::sys_common::backtrace::print
at src/libstd/sys_common/backtrace.rs:49
9: std::panicking::default_hook::{{closure}}
at src/libstd/panicking.rs:198
10: std::panicking::default_hook
at src/libstd/panicking.rs:215
11: std::panicking::rust_panic_with_hook
at src/libstd/panicking.rs:486
12: std::panicking::begin_panic
at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libstd/panicking.rs:410
13: playground::dos_es_dos
at src/lib.rs:3
14: playground::dos_es_dos::{{closure}}
at src/lib.rs:2
15: core::ops::function::FnOnce::call_once
at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libcore/ops/function.rs:232
16: <alloc::boxed::Box<F> as core::ops::function::FnOnce<A>>::call_once
at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/liballoc/boxed.rs:1076
17: <std::panic::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libstd/panic.rs:318
18: std::panicking::try::do_call
at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libstd/panicking.rs:297
19: std::panicking::try
at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libstd/panicking.rs:274
20: std::panic::catch_unwind
at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libstd/panic.rs:394
21: test::run_test_in_process
at src/libtest/lib.rs:541
22: test::run_test::run_test_inner::{{closure}}
at src/libtest/lib.rs:450
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
failures:
dos_es_dos
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
No es necesario disponer de toda la traza, salvo que no se pueda encontrar cuál es el problema. Normalmente, tampoco es necesario comprender toda la traza. En ella, se observa que en la línea 13, se habla de dos_es_dos
. A partir de ahí es donde empieza a hablar del código de la aplicación. Todo lo demás es sobre lo que Rust está haciendo en otras librerías para ejecutar el programa. Estas dos líneas (13 y 14) muestran que son errores en las líneas 2 y 3 de playground, es ahí donde está el error.
13: playground::dos_es_dos
at src/lib.rs:3
14: playground::dos_es_dos::{{closure}}
at src/lib.rs:2
Nota: Rust mejoró los mensajes de traza a comienzos de 2021 para mostrar solo la información más útil. Ahora es más fácil de leer:
failures:
---- dos_es_dos stdout ----
thread 'dos_es_dos' panicked at 'assertion failed: `(left == right)`
left: `2`,
right: `3`', src/lib.rs:3:5
stack backtrace:
0: rust_begin_unwind
at /rustc/cb75ad5db02783e8b0222fee363c5f63f7e2cf5b/library/std/src/panicking.rs:493:5
1: core::panicking::panic_fmt
at /rustc/cb75ad5db02783e8b0222fee363c5f63f7e2cf5b/library/core/src/panicking.rs:92:14
2: playground::dos_es_dos
at ./src/lib.rs:3:5
3: playground::dos_es_dos::{{closure}}
at ./src/lib.rs:2:1
4: core::ops::function::FnOnce::call_once
at /rustc/cb75ad5db02783e8b0222fee363c5f63f7e2cf5b/library/core/src/ops/function.rs:227:5
5: core::ops::function::FnOnce::call_once
at /rustc/cb75ad5db02783e8b0222fee363c5f63f7e2cf5b/library/core/src/ops/function.rs:227:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
failures:
dos_es_dos
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
En el siguiente ejemplo se desactiva la traza y se añaden algunas funciones que se probarán a través de unas funciones de prueba:
#![allow(unused)] fn main() { fn return_two() -> i8 { 2 } #[test] fn it_returns_two() { assert_eq!(return_two(), 2); } fn return_six() -> i8 { 4 + return_two() } #[test] fn it_returns_six() { assert_eq!(return_six(), 6) } }
Ahora se ejecutan los dos tests:
running 2 tests
test it_returns_two ... ok
test it_returns_six ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Normalmente, los tests se llevarán a un módulo específico. Para ello, se usa mod
como ya se conoce y se antecede con #[cfg(test)]
. Delante de las funciones de prueba se sigue poniendo #[test]
. Desde la línea de comando de Rust, esto permitirá realizar diferentes tipos de prueba, ejecutar solo una función de prueba, todas o unas algunas de ellas. En el módulo de prueba será necesario escribir use super::*
para que pueda tener acceso sencillo a todas las funciones del módulo principal que se está probando. Queda así:
#![allow(unused)] fn main() { fn return_two() -> i8 { 2 } fn return_six() -> i8 { 4 + return_two() } #[cfg(test)] mod tests { use super::*; #[test] fn it_returns_six() { assert_eq!(return_six(), 6) } #[test] fn it_returns_two() { assert_eq!(return_two(), 2); } } }
Desarrollo dirigido por pruebas
En inglés se denomina TDD, test driven development. Es una forma de escribir programas en la que primero se escribe el código de prueba de una función y luego se escribe su código. Así, siempre se dispone de pruebas para todo el código que se escribe. Normalmente, se escriben las pruebas, se ejecutan y fallan, puesto que no se ha escrito el código. Después, se escribe el código de la función hasta que pasan todas las pruebas existentes. Esto es muy sencillo en Rust ya que el compilador da mucha información sobre lo que hay que arreglar. A continuación, se escribe un pequeño ejemplo de desarrollo orientado por pruebas para ver cómo se hace.
Se va a suponer que se desarrolla una calculadora que suma y resta. Si el usuario escribe "5 + 6" debería dar como resultado 11. Si el usuario escribe "5 + 6 - 7", debería devolver 4, y así para cada entrada. En primer lugar, se escriben las funciones de prueba. Los nombres de las funciones de prueba suelen ser largos, para dejar claro cual es cada prueba y conocer bien qué significa si falla.
El código real de esta calculadora se encontrará en una función math()
que realiza los cálculos. Devolverá un i32 (no realiza cálculos con decimales). En primer lugar, esta función solo devolverá un número fijo, el 6. Evidentemente, esto hace que el código de prueba falle:
#![allow(unused)] fn main() { fn math(input: &str) -> i32 { 6 } #[cfg(test)] mod tests { use super::*; #[test] fn one_plus_one_is_two() { assert_eq!(math("1 + 1"), 2); } #[test] fn one_minus_two_is_minus_one() { assert_eq!(math("1 - 2"), -1); } #[test] fn one_minus_minus_one_is_two() { assert_eq!(math("1 - -1"), 2); } } }
La ejecución de las pruebas, en este momento, da el siguiente resultado:
running 3 tests
test tests::one_minus_minus_one_is_two ... FAILED
test tests::one_minus_two_is_minus_one ... FAILED
test tests::one_plus_one_is_two ... FAILED
El texto anterior, está resumido. Además, se dispone de toda la información del motivo de cada fallo, pero no se reproduce aquí.
Ahora se va a diseñar la calculadora. Esta aceptará los dígitos, los símbolos +
y -
y el espacio en blanco. Y nada más. En primer lugar, se crea un const
con todos los caracteres posibles. La función math()
iterará a través de todos los caracteres que tenga el parámetro y comprobará si están incluidos entre los caracteres válidos.
Este es el momento para añadir una prueba que tenga que fallar debido a que se le pase algún carácter no válido. Para que el resultado de una prueba que falla sea considerado que es el resultado que debe valoes se debe añadir el atributo #[should_panic]
a la prueba. Es decir, que la función de prueba entre en pánico es lo correcto en este caso.
El código queda así:
#![allow(unused)] fn main() { const OKAY_CHARACTERS: &str = "1234567890+- "; // Con el espacio entre los caracteres válidos fn math(input: &str) -> i32 { if !input.chars().all(|character| OKAY_CHARACTERS.contains(character)) { panic!("Please only input numbers, +-, or spaces"); } 6 // por ahora se sigue devolviendo un 6 } #[cfg(test)] mod tests { use super::*; #[test] fn one_plus_one_is_two() { assert_eq!(math("1 + 1"), 2); } #[test] fn one_minus_two_is_minus_one() { assert_eq!(math("1 - 2"), -1); } #[test] fn one_minus_minus_one_is_two() { assert_eq!(math("1 - -1"), 2); } #[test] #[should_panic] // Esta es la nueva prueba - que debe lanzar panic como resultado correcto fn panics_when_characters_not_right() { math("7 + seven"); } } }
Ahora, el resultado de ejecutar los tests es:
running 4 tests
test tests::one_minus_two_is_minus_one ... FAILED
test tests::one_minus_minus_one_is_two ... FAILED
test tests::panics_when_characters_not_right ... ok
test tests::one_plus_one_is_two ... FAILED
Una prueba ha funcionado, la función solo acepta caracteres válidos.
El siguiente paso consiste en escribir el código que calcula los resultados. La lógica de la calculadora será así:
- Todos los espacios se eliminarán. Esto se hace con
.filter()
. - La entrada se convertirá en un
Vec
con todas las entradas.+
no necesita ser una entrada, pero cuando el programa vea un+
debería saber que el número está completo. Por ejemplo, para la entrada11+1
debería hacer algo así:- Encuentra el 1 y lo inserta en una cadena de caracteres vacía.
- Encuentra el siguiente 1 y lo inserta en la cadena de caracteres que ahora contendrá "11".
- Encuentra el carácter
+
, entiende que el número se ha terminado y guarda la cadena de caracteres en elvec
y limpia la cadena de caracteres.
- El programa debe contar el núemro de
-
. Un número impar (1,3,5,...) significará restar, un número par significará sumar. Así, 1--9 debería dar 10, no -8. Es decir, 1 menos el -9. - El programa debería eliminar todos lo que aparezca después del último número.
5+5+++++-----
está compuesto de todos los caracteres enOKAY_CHARACTERS
, pero debería convertirlo a5+5
. Es fácil hacer esto con.trim_end_matches()
. Esta función elimina todo lo que coincida con ella al final de una&str
.
(Por cierto, .trim_end_matches()
y .trim_start_matches()
se denominaban antes .trim_right_matches()
y .trim_left_matches()
. Pero se observó que algunos lenguajes se leen de derecha a izquierda (persa, hebreo, etc), por lo que la denominación de izquierda y derecha era errónea. En algún código antiguo, aún pueden estar usándose las funciones antiguas).
En primer lugar, se debe conseguir que la función pase todas las pruebas. Después, se puede refactorizar el código para hacerlo mejor:
#![allow(unused)] fn main() { const OKAY_CHARACTERS: &str = "1234567890+- "; fn math(input: &str) -> i32 { if !input.chars().all(|character| OKAY_CHARACTERS.contains(character)) || !input.chars().take(2).any(|character| character.is_numeric()) { panic!("Please only input numbers, +-, or spaces."); } let input = input.trim_end_matches(|x| "+- ".contains(x)) // Elimina +, - y espacios al final .chars().filter(|x| *x != ' ') // Elimina todos los espacios restantes .collect::<String>(); let mut result_vec = vec![]; // Los resultados van aquí let mut push_string = String::new(); // Esta cadena es para hacer push de lo que se va encontrando. Se reutiliza entre diferentes números for character in input.chars() { match character { '+' => { if !push_string.is_empty() { // Si la cadena es vacía, no se quiere añadir "" al result_vec result_vec.push(push_string.clone()); // Pero si no es vacía, es un número que se añade al vector push_string.clear(); // Y se limpia la cadena temporal } }, '-' => { // Si llega un símbolo -, if push_string.contains('-') || push_string.is_empty() { // Hay que ver si está vacía o ya tiene - push_string.push(character) // si es así, se añade. } else { // en otro caso, contendrá un número result_vec.push(push_string.clone()); // añade el valor a result_vec push_string.clear(); // limpia la cadena temporal push_string.push(character); // y añade el - } }, number => { // cualquier otra cosa que venga, que serán dígitos, pasa por aquí if push_string.contains('-') { // Su hay - en la cadena temporal, hay que añadirlos. result_vec.push(push_string.clone()); push_string.clear(); push_string.push(number); } else { // si no es así, se añade a la cadena temporal push_string.push(number); } }, } } result_vec.push(push_string); // Cuando se acaba el bucle, se añade lo que quedara en a cadena. No es necesario .clone() porque no se usa la variable más. let mut total = 0; // Ahora es el momento de hacer los cálculos let mut adds = true; // true = sumar, false = restar let mut math_iter = result_vec.into_iter(); while let Some(entry) = math_iter.next() { // Itera a través de los elementos if entry.contains('-') { // Si contiene -, se comprueba si son par o impar if entry.chars().count() % 2 == 1 { adds = match adds { true => false, false => true }; continue; // va al siguiente elemento } else { continue; } } if adds == true { total += entry.parse::<i32>().unwrap(); // Si llega aquí, tiene que ser un número, por lo que es seguro hacer unwrap } else { total -= entry.parse::<i32>().unwrap(); adds = true; // Después de restar, se resetea a suma. } } total // Finalmente, devuelve el total } /// Se añaden más tests para añadir seguridad #[cfg(test)] mod tests { use super::*; #[test] fn one_plus_one_is_two() { assert_eq!(math("1 + 1"), 2); } #[test] fn one_minus_two_is_minus_one() { assert_eq!(math("1 - 2"), -1); } #[test] fn one_minus_minus_one_is_two() { assert_eq!(math("1 - -1"), 2); } #[test] fn nine_plus_nine_minus_nine_minus_nine_is_zero() { assert_eq!(math("9+9-9-9"), 0); // Este es nuevo } #[test] fn eight_minus_nine_plus_nine_is_eight_even_with_characters_on_the_end() { assert_eq!(math("8 - 9 +9-----+++++"), 8); // Este es nuevo } #[test] #[should_panic] fn panics_when_characters_not_right() { math("7 + seven"); } } }
Y ahora, las pruebas pasan:
running 6 tests
test tests::one_minus_minus_one_is_two ... ok
test tests::nine_plus_nine_minus_nine_minus_nine_is_zero ... ok
test tests::one_minus_two_is_minus_one ... ok
test tests::eight_minus_nine_plus_nine_is_eight_even_with_characters_on_the_end ... ok
test tests::one_plus_one_is_two ... ok
test tests::panics_when_characters_not_right ... ok
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Se observa que, durante el proceso de desarrollo orientado a pruebas, existe un continuo ir y venir que sigue el siguiente patrón:
- Primero se escriben todos los casos de prueba que se puedan imaginar.
- Luego se comienza a escribir código.
- Según se escribe el código, aparecen ideas para hacer otras pruebas.
- Se añaden estas otras pruebas. Así, las pruebas van creciendo según se programa. Cuantos más pruebas (NT: pruebas que cubran casos nuevos) se codifiquen, más veces pruebas el código.
En todo caso, las pruebas no aseguran que todo esté correcto. Pero sí son muy útiles cuando se va a modificar el código más tarde. Facilitan encontrar posibles fallos introducidos por el nuevo código.
Ahora se va a reescribir un poco el código (refactorizar). Una buena forma de comenzar es usar clippy
. Si se ha instalado Rust, se puede usar cargo clippy
. En Playgroudn se puede pulsar en TOOLS
y seleccionar Clippy
. Clippy analizará el código y dará pistas para hacerlo más simple.
En este caso, Clippy dice dos cosas:
warning: this loop could be written as a `for` loop
--> src/lib.rs:44:5
|
44 | while let Some(entry) = math_iter.next() { // Iter through the items
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `for entry in math_iter`
|
= note: `#[warn(clippy::while_let_on_iterator)]` on by default
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#while_let_on_iterator
warning: equality checks against true are unnecessary
--> src/lib.rs:53:12
|
53 | if adds == true {
| ^^^^^^^^^^^^ help: try simplifying it as shown: `adds`
|
= note: `#[warn(clippy::bool_comparison)]` on by default
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#bool_comparison
Esto es cierto: for entry in math_iter
es mucho más simple que while let Some(entry) = math_iter(next)
. Un bucle for
ya realiza la iteración, por lo que no hay que escribir .iter()
. Y tampoco se necesitaba usar math_iter
, se puede esribir for entry in result_vec
.
Además, se va a refactorizar parte del código. En lugar de variables separadas, se crea una estructura Calculator
que une las variables necesarias. Se cambiarán dos nombres por claridad. result_vec
se convierte en results
y push_string
en current_input
(la entrada actual). En este momento, esta estructura solo tiene un método: new()
.
#![allow(unused)] fn main() { // 🚧 #[derive(Clone)] struct Calculator { results: Vec<String>, current_input: String, total: i32, adds: bool, } impl Calculator { fn new() -> Self { Self { results: vec![], current_input: String::new(), total: 0, adds: true, } } } }
Ahora el código es un poco más largo, pero más fácil de leer. Por ejemplo, if adds
ahora es if calculator.adds
, que es casi como leer inglés. Queda así:
#![allow(unused)] fn main() { #[derive(Clone)] struct Calculator { results: Vec<String>, current_input: String, total: i32, adds: bool, } impl Calculator { fn new() -> Self { Self { results: vec![], current_input: String::new(), total: 0, adds: true, } } } const OKAY_CHARACTERS: &str = "1234567890+- "; fn math(input: &str) -> i32 { if !input.chars().all(|character| OKAY_CHARACTERS.contains(character)) || !input.chars().take(2).any(|character| character.is_numeric()) { panic!("Please only input numbers, +-, or spaces"); } let input = input.trim_end_matches(|x| "+- ".contains(x)).chars().filter(|x| *x != ' ').collect::<String>(); let mut calculator = Calculator::new(); for character in input.chars() { match character { '+' => { if !calculator.current_input.is_empty() { calculator.results.push(calculator.current_input.clone()); calculator.current_input.clear(); } }, '-' => { if calculator.current_input.contains('-') || calculator.current_input.is_empty() { calculator.current_input.push(character) } else { calculator.results.push(calculator.current_input.clone()); calculator.current_input.clear(); calculator.current_input.push(character); } }, number => { if calculator.current_input.contains('-') { calculator.results.push(calculator.current_input.clone()); calculator.current_input.clear(); calculator.current_input.push(number); } else { calculator.current_input.push(number); } }, } } calculator.results.push(calculator.current_input); for entry in calculator.results { if entry.contains('-') { if entry.chars().count() % 2 == 1 { calculator.adds = match calculator.adds { true => false, false => true }; continue; } else { continue; } } if calculator.adds { calculator.total += entry.parse::<i32>().unwrap(); } else { calculator.total -= entry.parse::<i32>().unwrap(); calculator.adds = true; } } calculator.total } #[cfg(test)] mod tests { use super::*; #[test] fn one_plus_one_is_two() { assert_eq!(math("1 + 1"), 2); } #[test] fn one_minus_two_is_minus_one() { assert_eq!(math("1 - 2"), -1); } #[test] fn one_minus_minus_one_is_two() { assert_eq!(math("1 - -1"), 2); } #[test] fn nine_plus_nine_minus_nine_minus_nine_is_zero() { assert_eq!(math("9+9-9-9"), 0); } #[test] fn eight_minus_nine_plus_nine_is_eight_even_with_characters_on_the_end() { assert_eq!(math("8 - 9 +9-----+++++"), 8); } #[test] #[should_panic] fn panics_when_characters_not_right() { math("7 + seven"); } } }
Finalmente, se añaden dos métodos nuevos. Uno se denomina .clear()
que vacía la entrada actual. El otro se denomina push_char()
que añade una caracter a la entrada actual. Así queda el código refactorizado.
#![allow(unused)] fn main() { #[derive(Clone)] struct Calculator { results: Vec<String>, current_input: String, total: i32, adds: bool, } impl Calculator { fn new() -> Self { Self { results: vec![], current_input: String::new(), total: 0, adds: true, } } fn clear(&mut self) { self.current_input.clear(); } fn push_char(&mut self, character: char) { self.current_input.push(character); } } const OKAY_CHARACTERS: &str = "1234567890+- "; fn math(input: &str) -> i32 { if !input.chars().all(|character| OKAY_CHARACTERS.contains(character)) || !input.chars().take(2).any(|character| character.is_numeric()) { panic!("Please only input numbers, +-, or spaces"); } let input = input.trim_end_matches(|x| "+- ".contains(x)).chars().filter(|x| *x != ' ').collect::<String>(); let mut calculator = Calculator::new(); for character in input.chars() { match character { '+' => { if !calculator.current_input.is_empty() { calculator.results.push(calculator.current_input.clone()); calculator.clear(); } }, '-' => { if calculator.current_input.contains('-') || calculator.current_input.is_empty() { calculator.push_char(character) } else { calculator.results.push(calculator.current_input.clone()); calculator.clear(); calculator.push_char(character); } }, number => { if calculator.current_input.contains('-') { calculator.results.push(calculator.current_input.clone()); calculator.clear(); calculator.push_char(number); } else { calculator.push_char(number); } }, } } calculator.results.push(calculator.current_input); for entry in calculator.results { if entry.contains('-') { if entry.chars().count() % 2 == 1 { calculator.adds = match calculator.adds { true => false, false => true }; continue; } else { continue; } } if calculator.adds { calculator.total += entry.parse::<i32>().unwrap(); } else { calculator.total -= entry.parse::<i32>().unwrap(); calculator.adds = true; } } calculator.total } #[cfg(test)] mod tests { use super::*; #[test] fn one_plus_one_is_two() { assert_eq!(math("1 + 1"), 2); } #[test] fn one_minus_two_is_minus_one() { assert_eq!(math("1 - 2"), -1); } #[test] fn one_minus_minus_one_is_two() { assert_eq!(math("1 - -1"), 2); } #[test] fn nine_plus_nine_minus_nine_minus_nine_is_zero() { assert_eq!(math("9+9-9-9"), 0); } #[test] fn eight_minus_nine_plus_nine_is_eight_even_with_characters_on_the_end() { assert_eq!(math("8 - 9 +9-----+++++"), 8); } #[test] #[should_panic] fn panics_when_characters_not_right() { math("7 + seven"); } } }
Se pueden escribir más métodos, pero líneas como calculator.results.push(calculator.current_input.clone());
ya quedan suficientemente claras. Cuando se refactoriza, es bueno que el código quede legible, no se trata de hacerlo más corto: clc.clr()
es peor que calculator.clear()
, por ejemplo.