Ir al contenido

Documentación y doc tests en Rust

Cada página de docs.rs y cada página de la documentación de la biblioteca estándar la escribió un programador, dentro del propio código fuente, usando comentarios especiales. Rust convierte esos comentarios en un sitio web con un solo comando. Tú puedes hacer exactamente lo mismo con tu código.

Hay una segunda razón para que te importe. Los ejemplos de código de tu documentación son pruebas de verdad. Cuando ejecutas cargo test, Rust los compila y los ejecuta. Si cambias tu función y te olvidas de actualizar el ejemplo, la prueba falla. Tu documentación no puede quedarse desactualizada sin que nadie se entere. Eso es raro en los lenguajes de programación, y es una de las cosas más bonitas de Rust.

Los comentarios normales los viste en la página de Comentarios. Los comentarios de documentación se parecen, pero llevan tres caracteres:

  • /// documenta el elemento que tiene debajo: una función, un struct, un enum, un campo o un módulo.
  • //! documenta el elemento dentro del que está. Al principio de src/lib.rs o src/main.rs, describe todo el crate.
//! Un pequeño programa de recetas.
/// Reparte `total` gramos a partes iguales en `portions` porciones.
fn split_evenly(total: u32, portions: u32) -> u32 {
total / portions
}

La primera línea de un comentario de documentación debería ser una frase corta que lo resuma. Las herramientas muestran esa línea en las listas y en los resultados de búsqueda, así que vale la pena cuidarla.

Los comentarios de documentación no cambian lo que hace tu programa. Aquí tienes una función documentada que se usa desde main:

/// Reparte `total` gramos a partes iguales en `portions` porciones.
///
/// Los gramos que sobran se descartan: 10 gramos entre 3 da 3.
///
/// # Panics
///
/// Hace panic si `portions` es cero.
fn split_evenly(total: u32, portions: u32) -> u32 {
if portions == 0 {
panic!("el número de porciones debe ser mayor que cero");
}
total / portions
}
fn main() {
println!("{}", split_evenly(600, 3));
println!("{}", split_evenly(10, 3));
}

Salida:

200
3
▶ Pruébalo tú mismo

En la mayoría de los editores, si pasas el ratón por encima de split_evenly dentro de main, ahora verás este texto en una ventanita. Solo por eso ya vale la pena el esfuerzo.

Markdown en los comentarios de documentación

Sección titulada «Markdown en los comentarios de documentación»

Los comentarios de documentación se escriben en Markdown, el mismo formato que se usa en GitHub y en muchas aplicaciones de chat. Puedes usar `código`, **negrita**, *cursiva*, listas, enlaces y títulos:

/// Devuelve la temperatura del horno para un plato, en **grados Celsius**.
///
/// Platos admitidos:
///
/// - `"bread"`: 220 grados
/// - `"cake"`: 180 grados
/// - cualquier otro: 200 grados
///
/// Comprueba *siempre* tu propio horno, porque cada horno es distinto.
/// Mira [Celsius en Wikipedia](https://es.wikipedia.org/wiki/Grado_Celsius).
pub fn oven_temperature(dish: &str) -> u32 {
match dish {
"bread" => 220,
"cake" => 180,
_ => 200,
}
}

Deja una línea /// vacía entre párrafos, igual que dejarías una línea en blanco en un archivo Markdown normal. Escribe los nombres de funciones, tipos y valores entre comillas invertidas (backticks) para que se vean como código.

Los programadores de Rust usan unos pocos títulos una y otra vez, para que quien lee sepa dónde mirar. Ya viste uno. Estos son los más comunes (se escriben en inglés, por convención):

  • # Examples: cómo usar el elemento. Casi todos los elementos públicos deberían tener uno.
  • # Panics: cuándo puede la función hacer panic, para que quien la llama pueda evitarlo.
  • # Errors: cuándo una función que devuelve un Result devuelve un Err, y por qué.
  • # Safety: en una unsafe fn, las reglas que debe cumplir quien la llama (mira Unsafe en Rust).

Aquí tienes # Errors y # Safety en un archivo de biblioteca, src/lib.rs:

/// Lee una cantidad como `"250g"` y devuelve el número de gramos.
///
/// # Errors
///
/// Devuelve un error si el texto no termina en `g`,
/// o si lo que va delante no es un número entero.
pub fn parse_grams(text: &str) -> Result<u32, String> {
let number = text
.strip_suffix('g')
.ok_or(format!("'{text}' debería terminar en g"))?;
number
.parse()
.map_err(|_| format!("'{number}' no es un número entero"))
}
/// Devuelve el primer ingrediente sin comprobar si la lista está vacía.
///
/// # Safety
///
/// Quien llama debe asegurarse de que `ingredients` no está vacío.
/// Llamarla con un slice vacío es comportamiento indefinido.
pub unsafe fn first_unchecked(ingredients: &[String]) -> &String {
unsafe { ingredients.get_unchecked(0) }
}

Fíjate en que los títulos usan un solo #. Dentro de un comentario de documentación, # es un título de Markdown, no un atributo de Rust.

En el resto de la página vamos a trabajar con una pequeña biblioteca. Créala con cargo new --lib kitchen. Luego, dentro del proyecto, ejecuta:

Terminal window
cargo doc --open
Documenting kitchen v0.1.0 (/path/to/kitchen)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.91s
Generated /path/to/kitchen/target/doc/kitchen/index.html

Cargo construye un sitio web en HTML dentro de target/doc/, y --open lo abre en tu navegador. Se ve exactamente igual que las páginas de docs.rs: una barra lateral, un buscador y una página por cada función y cada tipo. También documenta todos los crates de los que dependes, algo muy práctico cuando no tienes conexión.

Por defecto solo aparecen los elementos públicos (marcados con pub), porque la documentación es para las personas que usan tu biblioteca. Añade --document-private-items para incluirlo todo.

Este es el principio de src/lib.rs en la biblioteca kitchen. La función tiene una sección # Examples con un bloque de código dentro del comentario de documentación:

//! Pequeñas utilidades para la cocina.
//!
//! `kitchen` convierte temperaturas del horno, reparte comida en porciones
//! y lee cantidades como `"250g"`.
/// Convierte una temperatura de grados Celsius a Fahrenheit.
///
/// # Examples
///
/// ```
/// let f = kitchen::celsius_to_fahrenheit(180.0);
/// assert_eq!(f, 356.0);
/// ```
pub fn celsius_to_fahrenheit(celsius: f64) -> f64 {
celsius * 9.0 / 5.0 + 32.0
}

Ahora ejecuta cargo test:

Doc-tests kitchen
running 1 test
test src/lib.rs - celsius_to_fahrenheit (line 10) ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Cargo encontró el bloque de código de la línea 10, lo convirtió en un programa diminuto, lo compiló y lo ejecutó. El assert_eq! comprobó el resultado. Estos doc tests (pruebas de documentación) tienen algunas cosas que conviene notar:

  • El ejemplo llama a kitchen::celsius_to_fahrenheit, con el nombre del crate delante. Un doc test se compila como si fuera el programa de otra persona que usa tu biblioteca, así que solo ve los elementos pub, y necesitas la ruta completa o una línea use kitchen::...;.
  • No escribes fn main. Rustdoc envuelve tu ejemplo en uno por ti.
  • Usa assert! y assert_eq! (de la página de Pruebas) para mostrar el resultado y comprobarlo a la vez.

¿Y si el ejemplo está mal? Cambia 356.0 por 350.0 y vuelve a ejecutar las pruebas:

running 1 test
test src/lib.rs - celsius_to_fahrenheit (line 10) ... FAILED
failures:
---- src/lib.rs - celsius_to_fahrenheit (line 10) stdout ----
Test executable failed (exit status: 101).
stderr:
assertion `left == right` failed
left: 356.0
right: 350.0
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Cargo escribe este informe en inglés: la prueba falló (FAILED), porque el valor real (left) era 356.0 y el esperado (right) era 350.0. Esa es justo la idea. Un ejemplo equivocado se detecta al momento, en lugar de confundir a alguien meses después. Para ejecutar solo los doc tests, usa cargo test --doc.

A veces un ejemplo necesita líneas que distraerían a quien lo lee. Una línea de un doc test que empieza con # (almohadilla y espacio) se compila pero no se muestra en la documentación generada.

El uso más habitual: quieres mostrar el operador ?, pero ? solo funciona dentro de una función que devuelve un Result. Así que ocultas tu propio main:

/// Lee una cantidad como `"250g"` y devuelve el número de gramos.
///
/// # Examples
///
/// ```
/// use kitchen::parse_grams;
///
/// # fn main() -> Result<(), String> {
/// let flour = parse_grams("250g")?;
/// assert_eq!(flour, 250);
///
/// assert!(parse_grams("mucha").is_err());
/// # Ok(())
/// # }
/// ```
pub fn parse_grams(text: &str) -> Result<u32, String> {
let number = text
.strip_suffix('g')
.ok_or(format!("'{text}' debería terminar en g"))?;
number
.parse()
.map_err(|_| format!("'{number}' no es un número entero"))
}

En el navegador, quien lee solo ve las cuatro líneas interesantes: el use, la llamada con ? y los dos asserts. La prueba sigue compilándolo todo.

No todos los ejemplos pueden ejecutarse sin más. Puedes poner una palabra después de las comillas invertidas de apertura para decirle a rustdoc qué hacer:

  • ```no_run: compila el ejemplo, pero no lo ejecuta. Va bien para código que lee archivos, abre conexiones de red o se queda en un bucle infinito.
  • ```should_panic: lo ejecuta y espera un panic. La prueba falla si no hace panic.
  • ```ignore: no lo compila en absoluto. Úsalo para bocetos y pseudocódigo. Prueba antes con no_run, porque un ejemplo ignorado puede quedarse desactualizado sin que nadie se dé cuenta.
  • ```text: no es código Rust, solo texto (por ejemplo, una salida de muestra).
/// Reparte `total` gramos a partes iguales en `portions` porciones.
///
/// # Examples
///
/// ```
/// assert_eq!(kitchen::split_evenly(600, 3), 200);
/// ```
///
/// Cero porciones no tiene sentido, así que esto hace panic:
///
/// ```should_panic
/// kitchen::split_evenly(600, 0);
/// ```
pub fn split_evenly(total: u32, portions: u32) -> u32 {
if portions == 0 {
panic!("el número de porciones debe ser mayor que cero");
}
total / portions
}
/// Carga una lista de la compra desde un archivo, un artículo por línea.
///
/// ```no_run
/// let items = kitchen::load_list("shopping.txt").unwrap();
/// println!("{} cosas que comprar", items.len());
/// ```
pub fn load_list(path: &str) -> std::io::Result<Vec<String>> {
let text = std::fs::read_to_string(path)?;
Ok(text.lines().map(String::from).collect())
}

Con todo esto en kitchen, más un boceto ignore en el comentario //! del crate, cargo test informa:

running 7 tests
test src/lib.rs - (line 8) ... ignored
test src/lib.rs - load_list (line 87) - compile ... ok
test src/lib.rs - split_evenly (line 34) ... ok
test src/lib.rs - split_evenly (line 40) - should panic ... ok
test src/lib.rs - parse_grams (line 59) ... ok
test src/lib.rs - celsius_to_fahrenheit (line 16) ... ok
test src/lib.rs - Recipe (line 104) ... ok
test result: ok. 6 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.01s

La prueba no_run dice - compile: solo se compiló. El orden de las líneas puede cambiar de una ejecución a otra, porque las pruebas se ejecutan en paralelo.

Dentro de un comentario de documentación, escribe un nombre entre comillas invertidas y corchetes, como [`Vec`], y rustdoc lo convierte en un enlace a la página de ese elemento. Esto se llama intra-doc link (enlace interno de la documentación). Funciona con tus propios elementos y con cualquier cosa de la biblioteca estándar:

//! Un pequeño programa de recetas.
/// Una receta: un nombre y una lista de ingredientes con su peso en gramos.
///
/// Se crea con [`Recipe::new`] y se llena con [`Recipe::add`].
/// Los ingredientes se guardan en un [`Vec`], en el orden en que los añades.
struct Recipe {
/// El nombre que aparece arriba de la receta.
name: String,
/// Cada ingrediente y su peso en gramos.
ingredients: Vec<(String, u32)>,
}
impl Recipe {
/// Crea una receta vacía con el nombre indicado.
fn new(name: &str) -> Recipe {
Recipe { name: name.to_string(), ingredients: Vec::new() }
}
/// Añade un ingrediente al final de la lista.
fn add(&mut self, ingredient: &str, grams: u32) {
self.ingredients.push((ingredient.to_string(), grams));
}
/// Suma el peso de todos los ingredientes.
fn total_grams(&self) -> u32 {
self.ingredients.iter().map(|(_, grams)| grams).sum()
}
}
fn main() {
let mut pancakes = Recipe::new("Crepes");
pancakes.add("harina", 200);
pancakes.add("leche", 300);
pancakes.add("huevo", 50);
println!("{}", pancakes.name);
for (ingredient, grams) in &pancakes.ingredients {
println!("- {ingredient}: {grams} g");
}
println!("Total: {} g", pancakes.total_grams());
}

Salida:

Crepes
- harina: 200 g
- leche: 300 g
- huevo: 50 g
Total: 550 g
▶ Pruébalo tú mismo

Los campos y los métodos también pueden tener sus propios comentarios ///, y cada uno aparece en la página del struct. Si un enlace apunta a algo que no existe, cargo doc te avisa. Aquí, Recipe::push era una errata en lugar de Recipe::add:

warning: unresolved link to `Recipe::push`
--> src/lib.rs:100:50
|
100 | /// Se crea con [`Recipe::new`] y se llena con [`Recipe::push`].
| ^^^^^^^^^^^^ the struct `Recipe` has no field or associated item named `push`
|
= note: `#[warn(rustdoc::broken_intra_doc_links)]` on by default

El aviso está en inglés, como todos los mensajes del compilador: dice que no encuentra el enlace a Recipe::push porque el struct Recipe no tiene ningún campo ni elemento asociado con ese nombre.

En una biblioteca que van a usar otras personas, puedes pedirle al compilador que te avise de cada elemento público sin documentar. Pon esto al principio del todo de src/lib.rs:

#![warn(missing_docs)]

Ahora, si te olvidas de un comentario de documentación, recibes un aviso:

warning: missing documentation for a function
--> src/lib.rs:138:1
|
138 | pub fn grams_to_ounces(grams: f64) -> f64 {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
note: the lint level is defined here
--> src/lib.rs:1:9
|
1 | #![warn(missing_docs)]
| ^^^^^^^^^^^^

El #! (con signo de exclamación) significa que el atributo se aplica a todo el crate. También te pide un comentario //! para el propio crate. Los elementos privados nunca se señalan, así que no es una carga para las funciones auxiliares internas.

docs.rs: alojamiento gratuito para tu documentación

Sección titulada «docs.rs: alojamiento gratuito para tu documentación»

Cuando publicas un crate en crates.io (mira Cargo y crates), docs.rs genera su documentación automáticamente y la aloja gratis, para cada versión. No tienes que hacer nada. Por eso casi todos los crates de Rust tienen una documentación con el mismo aspecto y fácil de buscar.

Dos consejos más:

  • Ejecuta rustup doc --std para abrir la documentación de la biblioteca estándar sin conexión en tu navegador.
  • En cualquier página de docs.rs o de std, haz clic en el enlace Source para leer el código que hay detrás de una función. Es una manera estupenda de aprender.

Olvidar el nombre del crate en un doc test. Un doc test está fuera de tu crate, así que esto falla:

error[E0425]: cannot find function `celsius_to_fahrenheit` in this scope
--> src/lib.rs:17:9
|
17 | let f = celsius_to_fahrenheit(180.0);
| ^^^^^^^^^^^^^^^^^^^^^ not found in this scope

El compilador dice que no encuentra la función en este ámbito. La solución es kitchen::celsius_to_fahrenheit(180.0), o una línea use kitchen::celsius_to_fahrenheit; al principio del ejemplo (oculta con #, si quieres).

Un comentario de documentación que no documenta nada. /// tiene que ir justo antes de un elemento. Dentro del cuerpo de una función, usa //:

error[E0585]: found a documentation comment that doesn't document anything
--> src/main.rs:3:5
|
3 | /// Divide la comida en tres porciones.
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: doc comments must come before what they document, if a comment was intended use `//`

El mensaje dice que encontró un comentario de documentación que no documenta nada, y te sugiere usar // si solo querías un comentario normal.

Esperar que se ejecuten los doc tests de un binario. Los doc tests se ejecutan en los crates de biblioteca (src/lib.rs). Los ejemplos de los comentarios de documentación de src/main.rs no se prueban. Si tu programa crece, mueve la lógica a src/lib.rs y deja main.rs pequeño.

Escribir documentación que repite el código. “Suma dos números” encima de fn add(a, b) no ayuda. Explica lo que quien lee no puede ver: las unidades (¿gramos?, ¿segundos?), los casos límite y cuándo falla.

  • /// documenta el siguiente elemento, y //! documenta el crate o el módulo dentro del que está. Los dos usan Markdown.
  • Usa las secciones habituales: # Examples, # Panics, # Errors y # Safety.
  • cargo doc --open genera un sitio en HTML para tu crate y sus dependencias.
  • Los bloques de código de los comentarios de documentación son doc tests: cargo test los compila y los ejecuta. Oculta las líneas de preparación con # y usa no_run, should_panic o ignore cuando haga falta.
  • [`Nombre`] crea un intra-doc link, y #![warn(missing_docs)] te avisa de los elementos públicos sin documentar.
  • Los crates publicados en crates.io tienen su documentación alojada en docs.rs automáticamente.

Comprueba lo aprendido

1. ¿Qué comentario documenta todo el crate cuando lo pones al principio de src/lib.rs?

2. ¿Qué hace cargo test con un bloque de código dentro de un comentario /// de una biblioteca?

3. En un doc test, ¿qué hace una línea que empieza con # (almohadilla y espacio)?

4. Un ejemplo abre una conexión de red. Quieres que se compile pero que no se ejecute. ¿Qué anotación usas?

Puntuación: 0 / 4