aboutsummaryrefslogtreecommitdiffstats
path: root/crates/asist-app/tests
diff options
context:
space:
mode:
authorelvis <elvis@claros.ar>2026-09-26 20:20:19 -0300
committerelvis <elvis@claros.ar>2026-09-26 20:20:19 -0300
commit8518a63f55153e7f45fd49ad6caff5555f4e374f (patch)
tree636684eea3fa6f35d78282ab95eb687ef49154af /crates/asist-app/tests
parent69de76dc9cbedc6092d1e5ce84094a8030de1470 (diff)
downloadasist-p-main.tar.gz
asist-p-main.zip
Translate code, comments, logs and terminal UI to English; add English README; rename scriptsHEADmain
Diffstat (limited to 'crates/asist-app/tests')
-rw-r--r--crates/asist-app/tests/integration.rs (renamed from crates/asist-app/tests/integracion.rs)318
1 files changed, 155 insertions, 163 deletions
diff --git a/crates/asist-app/tests/integracion.rs b/crates/asist-app/tests/integration.rs
index d0fca38..0fdaf89 100644
--- a/crates/asist-app/tests/integracion.rs
+++ b/crates/asist-app/tests/integration.rs
@@ -1,12 +1,11 @@
-//! Pruebas contra los servidores de verdad.
+//! Tests against the real servers.
//!
-//! Se saltan solas cuando no hay nada escuchando, para que `cargo test` siga
-//! siendo útil sin 5 GB de modelos cargados. Con los servidores arriba
-//! comprueban lo que las pruebas unitarias no pueden: que el protocolo que se
-//! ha escrito es el que los servidores hablan, y que las latencias siguen
-//! donde se midieron.
+//! They skip themselves when nothing is listening, so `cargo test` stays
+//! useful without 5 GB of models loaded. With the servers up they check what
+//! unit tests cannot: that the protocol written here is the one the servers
+//! speak, and that latencies are still where they were measured.
//!
-//! Para ejecutarlas: scripts/servidores.sh arrancar && cargo test -- --ignored
+//! To run them: scripts/servers.sh start && cargo test -- --ignored
use std::path::PathBuf;
use std::sync::{Mutex, MutexGuard, OnceLock};
@@ -20,14 +19,14 @@ use asist_llm::{Delta, LlmClient, Message};
use asist_tools::FrameSource;
use asist_tts::TtsClient;
-/// Las pruebas se turnan la GPU.
+/// The tests take turns on the GPU.
///
-/// Los dos servidores comparten una tarjeta de 4 GB, y medido: con la prueba
-/// de síntesis corriendo a la vez, el primer token del LLM pasa de ~0,5 s a
-/// ~5 s. En paralelo esto no mide latencia, mide quién llegó primero. El
-/// pipeline de verdad no tiene el problema porque la síntesis no arranca
-/// hasta que el modelo cierra su primera frase.
-fn en_exclusiva() -> MutexGuard<'static, ()> {
+/// Both servers share a 4 GB card, and measured: with the synthesis test
+/// running at the same time, the LLM first token goes from ~0.5 s to ~5 s.
+/// In parallel this does not measure latency, it measures who got there
+/// first. The real pipeline does not have this problem because synthesis
+/// does not start until the model closes its first sentence.
+fn exclusive() -> MutexGuard<'static, ()> {
static GPU: OnceLock<Mutex<()>> = OnceLock::new();
GPU.get_or_init(|| Mutex::new(()))
.lock()
@@ -36,11 +35,11 @@ fn en_exclusiva() -> MutexGuard<'static, ()> {
fn config() -> Config {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../config/asistente.toml");
- Config::load(path).expect("no se pudo cargar la configuración")
+ Config::load(path).expect("could not load the configuration")
}
-/// Devuelve los clientes, o `None` si los servidores no están arriba.
-fn clientes() -> Option<(Config, LlmClient, TtsClient)> {
+/// Returns the clients, or `None` if the servers are not up.
+fn clients() -> Option<(Config, LlmClient, TtsClient)> {
let config = config();
let llm = LlmClient::new(config.llm_authority(), &config.llm);
let tts = TtsClient::new(config.tts_authority(), &config.tts);
@@ -52,9 +51,9 @@ fn clientes() -> Option<(Config, LlmClient, TtsClient)> {
}
#[test]
-fn el_llm_responde_en_streaming_sin_razonar_en_voz_alta() {
- let _gpu = en_exclusiva();
- let Some((config, llm, _)) = clientes() else {
+fn llm_streams_without_thinking_aloud() {
+ let _gpu = exclusive();
+ let Some((config, llm, _)) = clients() else {
return;
};
@@ -70,27 +69,27 @@ fn el_llm_responde_en_streaming_sin_razonar_en_voz_alta() {
}
true
})
- .expect("la petición al LLM falló");
+ .expect("the LLM request failed");
- assert!(!outcome.text.trim().is_empty(), "respuesta vacía");
- // La plantilla con <think> sin cerrar deja esto en 7-9 s. Si esta prueba
- // se cae, casi seguro que llama-server arrancó sin --chat-template-file.
- let ttft = ttft.expect("no llegó ningún fragmento con texto");
+ assert!(!outcome.text.trim().is_empty(), "empty answer");
+ // The template with an unclosed <think> puts this at 7-9 s. If this test
+ // fails, llama-server was almost certainly started without --chat-template-file.
+ let ttft = ttft.expect("no fragment with text arrived");
assert!(
ttft < Duration::from_secs(3),
- "el primer token tardó {ttft:?}: ¿arrancó llama-server con la plantilla sin razonamiento?"
+ "the first token took {ttft:?}: was llama-server started with the no-reasoning template?"
);
assert!(
!outcome.text.contains("<think>"),
- "el razonamiento se está colando en la respuesta: {}",
+ "reasoning is leaking into the answer: {}",
outcome.text
);
}
#[test]
-fn el_llm_puede_pedir_una_herramienta() {
- let _gpu = en_exclusiva();
- let Some((config, llm, _)) = clientes() else {
+fn llm_can_request_a_tool() {
+ let _gpu = exclusive();
+ let Some((config, llm, _)) = clients() else {
return;
};
@@ -105,60 +104,60 @@ fn el_llm_puede_pedir_una_herramienta() {
let outcome = llm
.stream(&chat, Some(&tools), &Cancel::new(), |_| true)
- .expect("la petición al LLM falló");
+ .expect("the LLM request failed");
if outcome.tool_calls.is_empty() {
- // Un modelo de 2B no siempre acierta a llamar; lo que se comprueba es
- // que el ida y vuelta funciona, no que el modelo sea listo.
- eprintln!("el modelo no pidió herramienta: {}", outcome.text);
+ // A 2B model does not always manage to call; what is checked is that the
+ // round trip works, not that the model is smart.
+ eprintln!("the model did not ask for a tool: {}", outcome.text);
return;
}
let call = &outcome.tool_calls[0];
assert_eq!(call.name, "hora_actual");
let result = tools.dispatch(call);
- assert!(result.ok, "la herramienta falló: {}", result.output);
+ assert!(result.ok, "the tool failed: {}", result.output);
assert!(!result.output.trim().is_empty());
}
#[test]
-fn la_sintesis_empieza_a_sonar_antes_de_terminar() {
- let _gpu = en_exclusiva();
- let Some((config, _, tts)) = clientes() else {
+fn synthesis_starts_playing_before_it_finishes() {
+ let _gpu = exclusive();
+ let Some((config, _, tts)) = clients() else {
return;
};
if let Some(reference) = &config.tts.reference {
tts.register_voice(reference)
- .expect("no se registró la voz");
+ .expect("the voice was not registered");
}
- // Sin precalentar, la primera síntesis carga los grafos y mide el arranque
- // del servidor en vez del régimen normal.
- tts.warmup().expect("falló el precalentado");
+ // Without warmup, the first synthesis loads the graphs and measures the
+ // server startup instead of the steady state.
+ tts.warmup().expect("warmup failed");
- let mut bloques = 0usize;
+ let mut blocks = 0usize;
let outcome = tts
.speak(
"Hola, esto es una prueba de latencia del sintetizador de voz.",
&Cancel::new(),
|samples| {
if !samples.is_empty() {
- bloques += 1;
+ blocks += 1;
}
true
},
)
- .expect("la síntesis falló");
+ .expect("synthesis failed");
- assert!(outcome.samples > 0, "no se recibió audio");
+ assert!(outcome.samples > 0, "no audio received");
assert!(
- bloques > 1,
- "el audio llegó de una sola vez ({bloques} bloque): revisa --codec-chunk-dur, \
- que de fábrica son 24 s y bloquean el streaming"
+ blocks > 1,
+ "the audio arrived all at once ({blocks} block): check --codec-chunk-dur, \
+ whose 24 s default blocks streaming"
);
- let ttfb = outcome.ttfb.expect("sin marca de primer bloque");
+ let ttfb = outcome.ttfb.expect("no first-block mark");
assert!(
ttfb < Duration::from_secs(2),
- "el primer audio tardó {ttfb:?}; con --codec-chunk-dur 1.0 se midió 585 ms"
+ "first audio took {ttfb:?}; 585 ms was measured with --codec-chunk-dur 1.0"
);
eprintln!(
"ttfb={:?} audio={:.2}s rtf={:.2}",
@@ -169,120 +168,120 @@ fn la_sintesis_empieza_a_sonar_antes_de_terminar() {
}
#[test]
-fn una_sintesis_se_puede_cortar_a_media_frase() {
- let _gpu = en_exclusiva();
- let Some((config, _, tts)) = clientes() else {
+fn synthesis_can_be_cut_mid_sentence() {
+ let _gpu = exclusive();
+ let Some((config, _, tts)) = clients() else {
return;
};
if let Some(reference) = &config.tts.reference {
let _ = tts.register_voice(reference);
}
- // Es la operación que sostiene el barge-in: si no se pudiera cortar, el
- // asistente seguiría hablando encima del usuario hasta acabar la frase.
+ // It is the operation barge-in relies on: if it could not be cut, the
+ // assistant would keep talking over the user until the sentence ended.
let cancel = Cancel::new();
- let mut recibidos = 0usize;
+ let mut received = 0usize;
let outcome = tts
.speak(
"Esta es una frase larga que no debería llegar a escucharse entera \
porque se va a cortar en cuanto empiece a sonar el primer bloque.",
&cancel,
|samples| {
- recibidos += samples.len();
- // Cortar en el primer bloque.
+ received += samples.len();
+ // Cut at the first block.
false
},
)
- .expect("la síntesis falló");
+ .expect("synthesis failed");
- assert!(outcome.cancelled, "debió quedar marcada como cancelada");
+ assert!(outcome.cancelled, "it should have been marked as cancelled");
assert!(
outcome.audio_secs() < 3.0,
- "se recibieron {:.1} s de audio: el corte no surtió efecto",
+ "{:.1} s of audio received: the cut had no effect",
outcome.audio_secs()
);
}
#[test]
-fn la_voz_clonada_queda_registrada() {
- let _gpu = en_exclusiva();
- let Some((config, _, tts)) = clientes() else {
+fn cloned_voice_gets_registered() {
+ let _gpu = exclusive();
+ let Some((config, _, tts)) = clients() else {
return;
};
let Some(reference) = &config.tts.reference else {
- eprintln!("sin voz de referencia configurada; prueba omitida");
+ eprintln!("no reference voice configured; test skipped");
return;
};
tts.register_voice(reference)
- .expect("no se registró la voz");
- let voces = tts.voices().expect("no se pudieron listar las voces");
+ .expect("the voice was not registered");
+ let voices = tts.voices().expect("could not list the voices");
assert!(
- voces.contains(&reference.name),
- "«{}» no aparece entre {voces:?}",
+ voices.contains(&reference.name),
+ "«{}» no aparece entre {voices:?}",
reference.name
);
}
#[test]
-#[ignore = "carga el modelo de ASR (~200 MB) y decodifica; lento"]
-fn el_asr_transcribe_lo_que_sintetiza_el_tts() {
- let _gpu = en_exclusiva();
- // El bucle completo sin micrófono: se sintetiza una frase conocida y se
- // comprueba que el reconocedor la recupera. Es la única prueba que ejerce
- // ASR y TTS contra el mismo audio.
- let Some((config, _, tts)) = clientes() else {
+#[ignore = "loads the ASR model (~200 MB) and decodes; slow"]
+fn asr_transcribes_what_tts_synthesizes() {
+ let _gpu = exclusive();
+ // The full loop without a microphone: a known sentence is synthesized and
+ // the recognizer is checked to get it back. It is the only test that
+ // exercises ASR and TTS on the same audio.
+ let Some((config, _, tts)) = clients() else {
return;
};
if let Some(reference) = &config.tts.reference {
let _ = tts.register_voice(reference);
}
- let frase = "hola qué tal estás hoy";
+ let sentence = "hola qué tal estás hoy";
let mut audio: Vec<f32> = Vec::new();
- tts.speak(frase, &Cancel::new(), |samples| {
+ tts.speak(sentence, &Cancel::new(), |samples| {
audio.extend_from_slice(samples);
true
})
- .expect("la síntesis falló");
- assert!(!audio.is_empty(), "no se generó audio");
+ .expect("synthesis failed");
+ assert!(!audio.is_empty(), "no audio was generated");
let recognizer =
- asist_asr::Recognizer::load(&config.asr).expect("no se pudo cargar el modelo de ASR");
- let texto = recognizer
+ asist_asr::Recognizer::load(&config.asr).expect("could not load the ASR model");
+ let text_in = recognizer
.transcribe(&audio, asist_tts::SAMPLE_RATE)
- .expect("la transcripción falló")
+ .expect("transcription failed")
.to_lowercase();
- eprintln!("dicho: «{frase}» / oído: «{texto}»");
- // No se exige una coincidencia exacta —24 kHz remuestreados a 16 kHz y un
- // modelo de 180M dan para lo que dan—, sino que reconozca algo.
- let acertadas = frase
+ eprintln!("said: «{sentence}» / heard: «{text_in}»");
+ // An exact match is not required (24 kHz resampled to 16 kHz and a 180M
+ // model only go so far), just that it recognizes something.
+ let hits = sentence
.split_whitespace()
- .filter(|palabra| texto.contains(palabra))
+ .filter(|word| text_in.contains(word))
.count();
assert!(
- acertadas >= 2,
- "sólo se reconocieron {acertadas} palabras de «{frase}» en «{texto}»"
+ hits >= 2,
+ "only {hits} words of «{sentence}» were recognized in «{text_in}»"
);
}
// ---------------------------------------------------------------------------
-// Buscar en internet y mirar por la cámara
+// Web search and camera
// ---------------------------------------------------------------------------
#[test]
-fn la_busqueda_devuelve_algo_pronunciable() {
+fn search_returns_something_speakable() {
let config = config();
if !config.search.enabled {
- eprintln!("búsqueda desactivada; prueba omitida");
+ eprintln!("search disabled; test skipped");
return;
}
let Ok(key) = std::env::var(&config.search.api_key_env) else {
- eprintln!("sin ${}; prueba omitida", config.search.api_key_env);
+ eprintln!("no ${}; test skipped", config.search.api_key_env);
return;
};
if key.trim().is_empty() {
- eprintln!("${} vacía; prueba omitida", config.search.api_key_env);
+ eprintln!("${} is empty; test skipped", config.search.api_key_env);
return;
}
@@ -294,29 +293,29 @@ fn la_busqueda_devuelve_algo_pronunciable() {
let started = Instant::now();
let out = tool
.call(&serde_json::json!({ "consulta": "capital de Australia" }))
- .expect("la búsqueda falló");
+ .expect("the search failed");
eprintln!(
- "búsqueda en {:?}: {}",
+ "search in {:?}: {}",
started.elapsed(),
&out[..out.len().min(200)]
);
assert!(!out.trim().is_empty());
assert!(
out.to_lowercase().contains("canberra"),
- "esperaba la respuesta en el resumen, salió: {out}"
+ "expected the answer in the summary, got: {out}"
);
- // Lo que devuelve se lee en voz alta; una parrafada de miles de caracteres
- // ahoga al modelo que tiene que resumirla.
+ // What it returns is read aloud; a wall of thousands of characters drowns
+ // the model that has to summarize it.
assert!(
out.len() < 6000,
- "el resultado es demasiado largo: {} bytes",
+ "the result is too long: {} bytes",
out.len()
);
}
#[test]
-fn una_consulta_inventada_no_revienta_el_turno() {
+fn made_up_query_does_not_break_the_turn() {
let config = config();
let Ok(key) = std::env::var(&config.search.api_key_env) else {
return;
@@ -324,8 +323,8 @@ fn una_consulta_inventada_no_revienta_el_turno() {
if key.trim().is_empty() {
return;
}
- // Vía el registro, que es como llega de verdad: un fallo tiene que volver
- // como texto para el modelo, nunca como un error que corte la respuesta.
+ // Through the registry, which is how it really arrives: a failure must come
+ // back as text for the model, never as an error that cuts the answer.
let mut registry = ToolRegistry::new();
registry.register(std::sync::Arc::new(asist_tools::WebSearch::new(
asist_tools::SearchBackend::Tavily {
@@ -342,14 +341,14 @@ fn una_consulta_inventada_no_revienta_el_turno() {
assert!(!outcome.ok);
assert!(
!outcome.output.contains("clave-invalida"),
- "el mensaje de error no debe llevar la clave dentro: {}",
+ "the error message must not contain the key: {}",
outcome.output
);
}
fn camera_source(config: &Config) -> Option<asist_tools::Camera> {
if !config.camera.enabled || !config.camera.device.exists() {
- eprintln!("sin cámara; prueba omitida");
+ eprintln!("no camera; test skipped");
return None;
}
Some(asist_tools::Camera::new(asist_tools::CameraConfig {
@@ -371,13 +370,13 @@ fn screen_source(config: &Config) -> Option<asist_tools::Screen> {
save_dir: None,
});
if !config.screen.enabled || screen.available().is_err() {
- eprintln!("sin pantalla capturable; prueba omitida");
+ eprintln!("no capturable screen; test skipped");
return None;
}
Some(screen)
}
-/// Ancho de un JPEG, leído de su cabecera SOF.
+/// Width of a JPEG, read from its SOF header.
fn jpeg_width(bytes: &[u8]) -> Option<u32> {
let mut i = 2;
while i + 9 < bytes.len() {
@@ -395,60 +394,53 @@ fn jpeg_width(bytes: &[u8]) -> Option<u32> {
}
#[test]
-fn la_camara_captura_un_jpeg() {
+fn camera_captures_a_jpeg() {
let config = config();
let Some(source) = camera_source(&config) else {
return;
};
let started = Instant::now();
- let frame = source.capture().expect("la captura falló");
- eprintln!(
- "fotograma de {} KB en {:?}",
- frame.len() / 1024,
- started.elapsed()
- );
+ let frame = source.capture().expect("the capture failed");
+ eprintln!("{} KB frame in {:?}", frame.len() / 1024, started.elapsed());
- assert!(
- frame.len() > 1000,
- "el fotograma es sospechosamente pequeño"
- );
- // Cabecera JPEG: si no está, ffmpeg devolvió otra cosa y el modelo la
- // rechazaría sin decir por qué.
- assert_eq!(&frame[..2], &[0xFF, 0xD8], "no parece un JPEG");
+ assert!(frame.len() > 1000, "the frame is suspiciously small");
+ // JPEG header: if it is missing, ffmpeg returned something else and the
+ // model would reject it without saying why.
+ assert_eq!(&frame[..2], &[0xFF, 0xD8], "does not look like a JPEG");
}
#[test]
-fn la_pantalla_se_captura_al_ancho_configurado() {
+fn screen_is_captured_at_the_configured_width() {
let config = config();
let Some(source) = screen_source(&config) else {
return;
};
let started = Instant::now();
- let frame = source.capture().expect("la captura falló");
+ let frame = source.capture().expect("the capture failed");
eprintln!(
- "captura de {} KB en {:?}",
+ "{} KB capture in {:?}",
frame.len() / 1024,
started.elapsed()
);
- assert_eq!(&frame[..2], &[0xFF, 0xD8], "no parece un JPEG");
+ assert_eq!(&frame[..2], &[0xFF, 0xD8], "does not look like a JPEG");
- // El ancho es lo que separa leer de inventar, así que se comprueba que el
- // guion de verdad redujo la imagen y no la mandó a tamaño completo.
- let width = jpeg_width(&frame).expect("no se pudo leer el ancho del JPEG");
+ // Width is what separates reading from making things up, so it is checked
+ // that the script really scaled the image down and did not send it full size.
+ let width = jpeg_width(&frame).expect("could not read the JPEG width");
assert_eq!(
width, config.screen.width,
- "la captura salió a {width} px y se pedían {}",
+ "the capture came out at {width} px and {} was requested",
config.screen.width
);
}
#[test]
-#[ignore = "enciende la cámara y hace una pasada de visión; lento"]
-fn el_modelo_describe_lo_que_ve_la_camara() {
- let _gpu = en_exclusiva();
- let Some((config, llm, _)) = clientes() else {
+#[ignore = "turns the camera on and runs a vision pass; slow"]
+fn model_describes_what_the_camera_sees() {
+ let _gpu = exclusive();
+ let Some((config, llm, _)) = clients() else {
return;
};
let Some(source) = camera_source(&config) else {
@@ -459,25 +451,25 @@ fn el_modelo_describe_lo_que_ve_la_camara() {
let started = Instant::now();
let out = tool
.call(&serde_json::json!({ "pregunta": "¿Qué se ve en la imagen?" }))
- .expect("la descripción falló");
+ .expect("the description failed");
- eprintln!("visión en {:?}: {out}", started.elapsed());
+ eprintln!("vision in {:?}: {out}", started.elapsed());
assert!(
out.split_whitespace().count() >= 3,
- "descripción vacía o mínima: {out}"
+ "empty or minimal description: {out}"
);
assert!(
started.elapsed() < Duration::from_secs(20),
- "tardó {:?}; revisa camera.width/height",
+ "took {:?}; check camera.width/height",
started.elapsed()
);
}
#[test]
-#[ignore = "captura la pantalla y hace una pasada de visión; lento"]
-fn el_modelo_describe_la_pantalla() {
- let _gpu = en_exclusiva();
- let Some((config, llm, _)) = clientes() else {
+#[ignore = "captures the screen and runs a vision pass; slow"]
+fn model_describes_the_screen() {
+ let _gpu = exclusive();
+ let Some((config, llm, _)) = clients() else {
return;
};
let Some(source) = screen_source(&config) else {
@@ -488,30 +480,30 @@ fn el_modelo_describe_la_pantalla() {
let started = Instant::now();
let out = tool
.call(&serde_json::json!({ "pregunta": "¿Qué hay en la pantalla?" }))
- .expect("la descripción falló");
+ .expect("the description failed");
- eprintln!("pantalla en {:?}: {out}", started.elapsed());
+ eprintln!("screen in {:?}: {out}", started.elapsed());
assert!(
out.split_whitespace().count() >= 3,
- "descripción vacía o mínima: {out}"
+ "empty or minimal description: {out}"
);
- // A 1280 px se midieron 7,6 s. Si esto se dispara, o la GPU está ocupada o
- // alguien subió screen.width sin mirar el coste.
+ // 7.6 s were measured at 1280 px. If this shoots up, either the GPU is busy
+ // or someone raised screen.width without looking at the cost.
assert!(
started.elapsed() < Duration::from_secs(30),
- "tardó {:?}; revisa screen.width",
+ "took {:?}; check screen.width",
started.elapsed()
);
}
#[test]
-#[ignore = "sale a internet con los dos buscadores; lento"]
-fn los_dos_buscadores_responden_a_lo_mismo() {
- // Compara lo que devuelve cada uno para las mismas preguntas. No afirma
- // cuál es mejor —eso depende de la pregunta— sino que ambos funcionan y
- // deja las cifras a la vista para elegir con datos.
+#[ignore = "goes online with both search backends; slow"]
+fn both_search_backends_answer_the_same() {
+ // Compares what each one returns for the same questions. It does not claim
+ // which is better (that depends on the question) but that both work, and it
+ // leaves the numbers visible to choose with data.
let config = config();
- let preguntas = [
+ let questions = [
"capital de Australia",
"qué tiempo hace hoy en Buenos Aires",
];
@@ -531,26 +523,26 @@ fn los_dos_buscadores_responden_a_lo_mismo() {
}
}
if backends.is_empty() {
- eprintln!("ningún buscador configurado; prueba omitida");
+ eprintln!("no search backend configured; test skipped");
return;
}
- for (nombre, backend) in backends {
+ for (name_in, backend) in backends {
let tool = asist_tools::WebSearch::new(backend, 3, Duration::from_secs(30));
- for pregunta in preguntas {
+ for question in questions {
let started = Instant::now();
- match tool.call(&serde_json::json!({ "consulta": pregunta })) {
+ match tool.call(&serde_json::json!({ "consulta": question })) {
Ok(out) => {
- let primera = out.lines().next().unwrap_or("").to_string();
+ let first_answer = out.lines().next().unwrap_or("").to_string();
eprintln!(
- "{nombre:7} {:>6.2}s {:>5} bytes «{pregunta}»\n {}",
+ "{name_in:7} {:>6.2}s {:>5} bytes «{question}»\n {}",
started.elapsed().as_secs_f32(),
out.len(),
- &primera[..primera.len().min(150)]
+ &first_answer[..first_answer.len().min(150)]
);
assert!(!out.trim().is_empty());
}
- Err(err) => panic!("{nombre} falló en «{pregunta}»: {err}"),
+ Err(err) => panic!("{name_in} failed on «{question}»: {err}"),
}
}
}