Skip to content

Commit 9e0d306

Browse files
committed
Merge branch 'issue-11-console-tests-r060-stack' into release/0.6.0
2 parents 43af45a + 7a25559 commit 9e0d306

2 files changed

Lines changed: 137 additions & 2 deletions

File tree

crates/solverforge-console/src/format.rs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,3 +295,136 @@ fn calculate_problem_scale(entity_count: usize, value_count: usize) -> String {
295295

296296
format!("{:.3} x 10^{}", mantissa, exponent)
297297
}
298+
299+
#[cfg(test)]
300+
mod tests {
301+
use super::*;
302+
use std::sync::{Arc, Mutex};
303+
use tracing::{Event, Level, Subscriber};
304+
use tracing_subscriber::layer::{Context, SubscriberExt};
305+
use tracing_subscriber::{Layer, Registry};
306+
307+
#[derive(Clone)]
308+
struct CaptureLayer {
309+
outputs: Arc<Mutex<Vec<String>>>,
310+
}
311+
312+
impl<S: Subscriber> Layer<S> for CaptureLayer {
313+
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
314+
let mut visitor = EventVisitor::default();
315+
event.record(&mut visitor);
316+
317+
let output = format_event(&visitor, *event.metadata().level());
318+
if !output.is_empty() {
319+
self.outputs.lock().unwrap().push(output);
320+
}
321+
}
322+
}
323+
324+
fn capture_events(f: impl FnOnce()) -> Vec<String> {
325+
let outputs = Arc::new(Mutex::new(Vec::new()));
326+
let subscriber = Registry::default().with(CaptureLayer {
327+
outputs: outputs.clone(),
328+
});
329+
330+
tracing::subscriber::with_default(subscriber, f);
331+
let captured = outputs.lock().unwrap().clone();
332+
captured
333+
}
334+
335+
#[test]
336+
fn format_duration_covers_milliseconds_seconds_and_minutes() {
337+
assert_eq!(format_duration_ms(750), "750ms");
338+
assert_eq!(format_duration_ms(2_500), "2.50s");
339+
assert_eq!(format_duration_ms(125_000), "2m 5s");
340+
}
341+
342+
#[test]
343+
fn calculate_problem_scale_handles_zero_and_nonzero_inputs() {
344+
assert_eq!(calculate_problem_scale(0, 10), "0");
345+
assert_eq!(calculate_problem_scale(10, 100), "1.000 x 10^20");
346+
}
347+
348+
#[test]
349+
fn format_score_handles_hard_soft_and_simple_scores() {
350+
let hard_soft = format_score("-2hard/5soft");
351+
assert!(hard_soft.contains("-2hard"));
352+
assert!(hard_soft.contains("5soft"));
353+
354+
let simple = format_score("-7");
355+
assert!(simple.contains("-7"));
356+
357+
let fallback = format_score("N/A");
358+
assert!(fallback.contains("N/A"));
359+
}
360+
361+
#[test]
362+
fn format_event_renders_progress_and_trace_steps() {
363+
let progress = EventVisitor {
364+
event: Some("progress".to_string()),
365+
steps: Some(12_345),
366+
speed: Some(678),
367+
score: Some("0hard/9soft".to_string()),
368+
..EventVisitor::default()
369+
};
370+
let progress_output = format_event(&progress, Level::INFO);
371+
assert!(progress_output.contains("steps"));
372+
assert!(progress_output.contains("678"));
373+
assert!(progress_output.contains("0hard"));
374+
375+
let outputs = capture_events(|| {
376+
tracing::trace!(
377+
target: "solverforge_solver::test",
378+
event = "step",
379+
step = 42u64,
380+
move_index = 3u64,
381+
score = "-1hard/0soft",
382+
accepted = true,
383+
);
384+
});
385+
386+
let step_output = outputs
387+
.iter()
388+
.find(|output| output.contains("Step"))
389+
.cloned()
390+
.expect("expected trace step output");
391+
assert!(step_output.contains("Step"));
392+
assert!(step_output.contains("Entity"));
393+
assert!(step_output.contains("3"));
394+
assert!(step_output.contains("-1hard"));
395+
}
396+
397+
#[test]
398+
fn format_event_renders_solve_start_and_end_summaries() {
399+
let outputs = capture_events(|| {
400+
tracing::info!(
401+
target: "solverforge_solver::test",
402+
event = "solve_start",
403+
entity_count = 120u64,
404+
element_count = 25u64,
405+
constraint_count = 7u64,
406+
time_limit_secs = 30u64,
407+
);
408+
});
409+
410+
let start_output = outputs
411+
.iter()
412+
.find(|output| output.contains("Solving"))
413+
.cloned()
414+
.expect("expected solve_start output");
415+
assert!(start_output.contains("Solving"));
416+
assert!(start_output.contains("120"));
417+
assert!(start_output.contains("25"));
418+
assert!(start_output.contains("constraints"));
419+
420+
let end = EventVisitor {
421+
event: Some("solve_end".to_string()),
422+
score: Some("0hard/-15soft".to_string()),
423+
..EventVisitor::default()
424+
};
425+
let end_output = format_event(&end, Level::INFO);
426+
assert!(end_output.contains("Solving complete"));
427+
assert!(end_output.contains("FEASIBLE"));
428+
assert!(end_output.contains("Final Score:"));
429+
}
430+
}

crates/solverforge-console/src/visitor.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,12 @@ impl Visit for EventVisitor {
4141
"steps" => self.steps = Some(value),
4242
"speed" => self.speed = Some(value),
4343
"step" => self.step = Some(value),
44-
"entity" => self.entity = Some(value),
44+
// TRACE step events emit `move_index`; keep `entity` as a legacy alias.
45+
"entity" | "move_index" => self.entity = Some(value),
4546
"duration_ms" => self.duration_ms = Some(value),
4647
"entity_count" => self.entity_count = Some(value),
47-
"value_count" => self.value_count = Some(value),
48+
// List solves emit `element_count`; keep `value_count` for legacy/basic solves.
49+
"value_count" | "element_count" => self.value_count = Some(value),
4850
"constraint_count" => self.constraint_count = Some(value),
4951
"time_limit_secs" => self.time_limit_secs = Some(value),
5052
"moves_speed" => self.moves_speed = Some(value),

0 commit comments

Comments
 (0)