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 STABLEy 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 entrada 11+1 debería hacer algo así:
    1. Encuentra el 1 y lo inserta en una cadena de caracteres vacía.
    2. Encuentra el siguiente 1 y lo inserta en la cadena de caracteres que ahora contendrá "11".
    3. Encuentra el carácter +, entiende que el número se ha terminado y guarda la cadena de caracteres en el vec 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 en OKAY_CHARACTERS, pero debería convertirlo a 5+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.