@@ -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+ }
0 commit comments