Tests en Rust

Mientras los programas no puedan verificarse de forma matemática de forma sencilla, la única manera de asegurarse que un programa más o menos funciona es con tests. Rust soporta tests de forma nativa. Gracias a la directiva #[test].

Definiendo una función de test

Una función de test es cualquiera que lleve la directiva #[test]. Normalmente, estos tests se dejan dentro de un módulo con la directiva #[cfg(test)].

fn suma(a: i32,b: i32) -> i32{
    a+b
}

#[cfg(test)]
mod test{

    #[test]
    fn suma(){
        assert_eq!(super::suma(4,5),9);
    }
}

La macro assert_eq! provocará un panic! si sus argumentos no coinciden. También es posible hacer fallar los tests llamando a panic! manualmente. ¿Cómo se ejecutan estos test te preguntarás? Sencillo, con cargo test. Automáticamente Cargo selecciona las funciones de test y las ejecuta todas, generando un informe de tests existosos y tests que han fallado.

Obviamente, existen más opciones dentro de los tests. assert! comprobará que una expresión sea true. #[should_panic] se deberá indicar en aquellas funciones de test en lo que lo correcto sea que ocurra un panic!. Por otro lado, la trait Debug es interesante.

Es posible ejecutar tests de forma manual, con cargo test NOMBRE_TEST.

 

loading...

Cargo y módulos en Rust

El software va creciendo poco a poco en un proyecto y es vital que esté bien organizado. Rust incluye un potente sistema de módulos para manejar estas situaciones y Cargo nos ayuda a gestionar las dependencias del proyecto.

Módulos

Los módulos se definen con la palabra reservada mod y por defecto todo es privado. Es necesario indicar pub para hacer esos campos públicos. Con use podemos acortar la manera con la que hacemos referencia a los componentes del módulo. Así pues, use es similar a using namespace de C++.

mod network{
    pub fn version() -> String{
        String::from("Network 1.0.0")
    }
    pub mod server{
        fn private_thing(){

        }
        pub fn public_thing(){

        }
    }
}

fn main() {
    println!("{}",network::version());
    {
        use network::server::public_thing;
        public_thing();
    }
}

Los módulos resultan una opción más interesante si tenemos múltiples archivos. Por norma general, si un módulo se define en el fichero de entrada del compilador, este se busca en dos sitios:

  • Un fichero nombre_del_modulo.rs
  • Un fichero nombre_del_modulo/mod.rs (opción recomendada para módulos de varios archivos)

Por lo que el código anterior puede sustituirse por esto en el fichero principal:

mod network;

fn main() {
    println!("{}",network::version());
    {
        use network::server::public_thing;
        public_thing();
    }
}

Y esto en un fichero network.rs

pub fn version() -> String{
    String::from("Network 1.0.0")
}
pub mod server{
    fn private_thing(){

    }
    pub fn public_thing(){

    }
}

Podemos usar use-as para modificar el nombre con el que se importa una función.

mod network;

fn main() {
    println!("{}",network::version());
    {
        use network::server::public_thing as super_thing;
        super_thing();
    }
}

 

Crates

El código en Rust se organiza en crates, que son colecciones de módulos que se distribuyen. Piensa en ello como si fuesen gemas de Ruby o librerías de C++. Las crates se pueden compartir con la gente. El mayor repositorio de crates de la comunidad Rust es crates.io.

Las crates se pueden generar con rustc o con cargo, aunque es habitual usar la segunda opción. Vamos a ver primero como usaríamos una crate externa. En este caso voy a usar regex. Sé que en esta parte no vas a saber como descargar regex y usarla, pero voy a explicar el código.

extern crate regex;

use regex::Regex;

fn main() {
    let r = Regex::new(r"([A-Za-z0-9])([.]|[_]|[A-Za-z0-9])+@gmail.com").unwrap();
    if r.is_match("pucela_segunda@gmail.com") {
        println!("Correo válido de Gmail");
    }else{
        println!("Correo no válido de Gmail");
    }
}

Básicamente se usa extern crate para importar una crate externa, en este caso regex. Con use lo único que hacemos es acortar la manera de llamar a las funciones. Si no estuviese podríamos poner perfectamente regex::Regex::new en vez de Regex::new y funcionaría.

Cargo

Cargo es una herramienta que cumple dos funciones dentro del ecosistema Rust. Por un lado es un gestor de dependencias y por otro lado gestiona también las compilaciones.

En su parte de gestor de dependencias nos permite especificar que crates necesita el proyecto para compilar, su versión y si la crate lo soporta, con qué características activadas.

En su parte de gestor de compilaciones, realiza la compilación con rustc y añade las flags pertinentes al compilador. También se le pueden especificar cosas más complejas, aunque en Rust no suele ser habitual tener configuraciones de compilación excesivamente complejas.

Todo lo relativo a Cargo se define en el archivo Cargo.toml. Que tiene una estructura similar a esta:

[package]
name = "super_app"
version = "0.1.0"
authors = ["Adrián Arroyo Calle"]

[dependencies]
regex = "0.2.2"

En la sección dependencies puedes añadir línea por línea la crate que te haga falta. Consulta la documentación de cada crate para esto, pues puede haber variaciones.

Comandos básicos de Cargo

Crear un proyecto con Cargo

cargo new –bin mi_proyecto # si queremos que sea ejecutable

cargo new mi_crate # si queremos que sea una crate

Compilar y ejecutar

cargo run

cargo build # solo compilar

cargo build –release # compilar en modo Release

Cargo automáticamente se encarga de obtener las dependencias en la fase de compilación.

Instalar aplicaciones

cargo install APLICACION

Muchas herramientas de pueden distribuir a través de Cargo. Por ejemplo, Racer, un programa que sirve de autocompletado para IDEs como Eclipse o Visual Studio.

Ejecutar tests

cargo test

Generar documentación

cargo doc

Plugins de Cargo

Cargo es extensible gracias a plugins. Algunos interesantes son Clippy, cargo-audit, rustfmt,

Box, Rc y RefCell, punteros inteligentes en Rust

Hasta ahora hemos trabajado con tipos primitivos, con estructuras o con tipos de la librería estándar como String o Vec. Sin embargo, ¿si queremos implementar algo similar a String como lo haríamos? Aquí entran en juego, los punteros inteligentes, punteros con capacidades extra. Los más importantes son Box, Rc y RefCell.

Box, reservar memoria en el heap

Box es parecido a malloc de C. Reservan memoria en la parte alta de la memoria. El uso principal de Box en Rust es el de implementar estructuras de datos cíclicas o recursivas.

fn main() {
    let n = Box::new(42);
    println!("n = {}", n);
}

No es muy útil usarlo así. Solo compensa usarlo en situaciones donde es necesario que la variable ocupe un tamaño fijo, que a priori es indeterminado.

Rc, ¡viva la multipropiedad!

Rc es un puntero inteligente algo más interesante. Permite que un dato sea propiedad de varias variables a la vez. Funciona con un mecanismo de recolector de basura. La clave está en que cuando hagamos clone de un Rc no obtendremos una copia exacta, sino una referencia más al dato original. Esto permite ahorrar memoria. Veamos en un ejemplo, como Regex se comparte entre varias variables. Se trata del mismo Regex, en ningún momento ocurre una duplicidad en memoria.

extern crate regex;

use regex::Regex;
use std::rc::Rc;


fn main() {
    let r = Rc::new(Regex::new(r"([A-Za-z0-9])([.]|[_]|[A-Za-z0-9])+@gmail.com").unwrap());
    if r.is_match("pucela_segunda@gmail.com") {
        println!("Correo válido de Gmail");
    }else{
        println!("Correo no válido de Gmail");
    }

    let puntero = r.clone();
    println!("Reference Count: {}",Rc::strong_count(&puntero));
    puntero.is_match("perro@gmail.com");
    r.is_match("_pepe@gmail.com");
}

La línea Reference Count nos dice cuantas variables tienen ahora mismo acceso a ese dato. Rc funciona a la perfección en un solo hilo, pero si quieres hacer lo mismo entre hilos debes usar Arc. Rc por sí mismo, solo permite lecturas, es cuando lo juntamos con RefCell cuando obtenemos soporte de escritura.

RefCell, saltándonos las normas

En primer lugar, RefCell es muy interesante y poderoso, pero no es recomendable usarlo si se pueden usar otros métodos. Digamos que RefCell permite llevar las normas del compilador de Rust sobre préstamos y dueños al runtime. Esto puede provocar crasheos así que normalmente se usa con Rc que previene estas situaciones.

use std::rc::Rc;
use std::cell::RefCell;

fn main(){
    let n = Rc::new(RefCell::new(42));
    let x = n.clone();
    let y = n.clone();
    *x.borrow_mut() += 10;
    *y.borrow_mut() += 10;
    println!("N: {:?}",n.borrow());
}

El resultado de este código es 62. Por tanto, hemos conseguido que distintas variables pudiesen mutar el dato.

Con esto ya hemos visto los punteros inteligentes más importantes de Rust. Existe alguno más como Cell que sin embargo, no es tan usado.

 

Concurrencia en Rust

Rust destaca por su soporte nativo a la concurrencia. Aquí veremos exactamente qué es lo que hace.

Crear threads

Todas las funciones relacionadas con threads están en std::thread así que primero hemos de importarlo con use. Para crear un thread usamos thread::spawn, que toma una función como argumento. Esta operación devuelve un JoinHandler que puede ser usado para esperar a que finalicen los hilos de ejecución.

use std::thread;

fn main(){
    let handle = thread::spawn(||{
        for i in 1..100{
            println!("Attens ou va t'en - France Gall");
        }
    });
    handle.join();
    println!("Hilo finalizado");
}

Aquí ya se presenta una cuestión interesante, ¿qué pasa si queremos llevar datos del hilo principal al nuevo thread? Si lo intentamos hacer, Rust se quejará. En realidad, tenemos que activar la closure con move. Lo que hace es transferir la propiedad del dato a la closure. Con eso podemos procesarlo tranquilamente en el hilo nuevo, siempre y cuando no intentemos acceder a él desde el hilo principal.

use std::thread;

fn main(){
    let v = vec!["Attens ou va t'en - Paul Mauriat","Attens ou va t'en - France Gall"];
    let handle = thread::spawn(move ||{
        for i in v{
            println!("{}",i);
        }
    });
    handle.join();
    println!("Hilo finalizado");
}

Mensajes

Una de las opciones que permite Rust para concurrencia es el paso de mensajes.

use std::thread;
use std::sync::mpsc;
use std::time::Duration;


fn main(){
    let (tx,rx) = mpsc::channel();

    let h = thread::spawn(move ||{
        thread::sleep(Duration::new(5,0));
        let val = String::from("Attens ou va t'en - France Gall");
        tx.send(val).unwrap();
    });

    loop {
        if let Ok(msg) = rx.try_recv() {
            println!("{}",msg);
            break;
        }
    }
    // Opción síncrona
    //let msg = rx.recv().unwrap();
    //println!("{}",msg);

    // Opción Iterable
    // for msg in rx{
    //     println!("{}",msg);
    // }
}

Tx es el objeto usado para transmitir y Rx para recibir. Tx lo puede tener cualquier hilo para mandar mensajes mientras que Rx está limitado al hilo principal. Existen varias formas de recibir los mensajes. try_recv no bloquea el hilo de ejecución, por lo que puede usarse en un bucle con if-let. recv es síncrono y bloquea el hilo de ejecución si hace falta. Tx también es Iterable así que podemos leer los mensajes en un bucle for-in.

Mutex y Arc

Cuando queremos compartir memoria en Rust tenemos que recurrir a una combinación de Mutex y Arc.

Mutex provee de gestión del acceso a memoria. Para acceder al dato es necesario hacer lock. No es estrictamente necesario hacer unlock una vez hayamos modificado el dato, pues cuando Rust libere memoria, también hará unlock. Sin embargo, Mutex por sí solo no es suficiente.

Arc (Atomic Reference Counter) permite tener datos con múltiples dueños. Esto funciona con una especie de recolector de basura y es útil porque cuando hablamos de hilos podría darse la situación de que el hilo dueño muriese y un hilo con los datos prestados todavía siguiese vivo. Con clone conseguimos una copia para mover libremente que en realidad referencia al mismo dato en memoria.

use std::thread;
use std::sync::{Mutex,Arc};

fn main(){
    let counter = Arc::new(Mutex::new(42));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = counter.clone();
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();

            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}

Con esto ya tendremos lo suficiente para manejar concurrencia en Rust. Por supuesto, esto es más complejo que esto y si de verdad quieres aprovechar el potencial al máximo quizá debas revisar la documentación de las traits Send y Sync.

 

 

Gestión de errores en Rust, Option y Result

¿Conoces el error del billón de dólares? Tony Hoare es un reputado estudioso de la ciencia de la computación. Cuando se encontraba diseñando ALGOL W, se le ocurrió incorporar la referencia a NULL. Sin embargo, los errores informáticos que ha propiciado su existencia han supuesto pérdidas económicas superiores al billón de dólares. Es por ello que él mismo denominó a su creación, el error del billón de dólares.

El uso de NULL puede traer muchas consecuencias negativas, pero como el propio Hoare dijo: era tan fácil de implementar que no pude resistirme. Además, usarlo es fácil. Existen lenguajes sin NULL desde hace tiempo pero su uso no es muy frecuente y ciertamente, añaden complejidad a la programación. Rust soluciona esto con dos objetos muy sencillos de utilizar, Option y Result.

Option y Result

Ambos son objetos que sirven para llevar cualquier otro valor. Hemos de desempapelarlos para sacar el verdadero valor, si es que existe. Option en realidad es un enum con dos posibles valores: Some y None. Result también es un enum con dos posibles valores: Ok y Err.

¿Cuándo usar Option y cuándo usar Result?

Esta duda es muy frecuente ya que funcionan de manera muy similar. La regla general es que Result tiene que usarse con errores y si no se tratan, cerrar el programa, mientras que Option lleva cosas que pueden estar o no, pero su inexistencia no conlleva un error.

Uso

Option es algo más sencilla de usar.

let opt: Option<i32> = Some(42);
let opt: Option<i32> = None;

En Result hay que indicar el tipo del valor correcto y del valor de error que se comunique.

let operacion_peligrosa: Result<i32,String> = Ok(42);
let operacion_peligrosa: Result<i32,String> = Err(String::from("La operación ha fallado"));

if-let

Option y Result pueden integrarse con estructuras de control para un manejo idomático de estas situaciones. Un ejemplo es if-let. Imagina un perfil de una aplicación web. Hay campos obligatorios, como usuario y contraseña, pero otros no, como la página web.

struct Perfil{
    username: String,
    password: String,
    url: Option<String>
}

impl Perfil{
    fn new(u: String, p: String) -> Self{
        Perfil {username: u, password: p, url: None}
    }
}

fn main(){
    let mut p1 = Perfil::new(String::from("aarroyoc"),String::from("1234"));
    let mut p2 = Perfil::new(String::from("The42"),String::from("incorrect"));

    p1.url = Some(String::from("https://blog.adrianistan.eu"));

    for perfil in [p1,p2].iter() {
        let url = perfil.url.clone();
        if let Some(url) = url{
            println!("URL: {}",url);
        }
    }

}

try!

try! es una macro que permite manejar con mayor claridad las operaciones que puedan generar muchos errores. try! solo se puede usar en funciones que devuelvan Result. Básicamente, try! devuelve inmediatamente el Err correspondiente si la operación resultante no ha sido exitosa, permitiendo una salida antes de tiempo y un código mucho más limpio.

fn peligro() -> Result<i32,String>{
    Err(String::from("Operación inválida"))
}

fn funcion_error() -> Result<i32,String>{
    let n = try!(peligro()); // aquí ya se sale de la función
    Ok(n)
}

fn main(){
    let n = funcion_error().unwrap();
    
}

La macro try! es tan usada dentro de Rust que dispone de azúcar sintáctico, el símbolo de interrogación ?.

fn peligro() -> Result<i32,String>{
    Err(String::from("Operación inválida"))
}

fn funcion_error() -> Result<i32,String>{
    let n = peligro()?;
    Ok(n)
}

fn main(){
    let n = funcion_error().unwrap();
}

 

Unwrap

Vamos a observar las operaciones de desempapelado que existen para Option y Result. La más básica es unwrap. Intenta desempapelar el valor, si resulta que el Result/Option era un Err/None, el programa crashea.

unwrap_or intenta desempapelar y si el valor no existe, se sustituye por el valor indicado en el or.

unwrap_or_else es similar a unwrap_or pero toma una función como parámetro. Así es posible crear una cadena de intentos.

fn main(){
    // let n = function_error().unwrap(); crash
    let n = function_error().unwrap_or(7);
    let n = funcion_error().unwrap_or_else(|t|{
        7
    });
    println!("{}",n);
}

Existen muchas más opciones, como and, expect o iterar sobre un Option/Result.

panic!

Si nos gustan los crasheos (!) podemos forzarlos con la macro panic!. Crashea el programa con el mensaje que indiquemos

fn main(){
    panic!("Adiós muy buenas");
}

Y con esto hemos visto lo suficiente del manejo de errores en Rust.