@@ -5,6 +5,74 @@ use std::sync::atomic::{AtomicU64, Ordering};
55
66use crate :: align:: transcript:: { CigarOp , Transcript } ;
77use crate :: junction:: encode_motif;
8+ use crate :: params:: Parameters ;
9+
10+ /// Shared timestamp format used across Log.final.out / Log.out / Log.progress.out.
11+ pub const LOG_TIME_FMT : & str = "%b %d %H:%M:%S" ;
12+
13+ /// Write a minimal STAR-compatible Log.out alongside Log.final.out.
14+ ///
15+ /// Carries a parameters dump and per-phase timestamps. Intentionally a stub
16+ /// rather than a full STAR-verbose-log reproduction.
17+ pub fn write_log_out (
18+ path : & Path ,
19+ params : & Parameters ,
20+ time_start : chrono:: DateTime < chrono:: Local > ,
21+ time_map_start : chrono:: DateTime < chrono:: Local > ,
22+ time_finish : chrono:: DateTime < chrono:: Local > ,
23+ ) -> std:: io:: Result < ( ) > {
24+ use std:: io:: Write ;
25+
26+ let mut f = std:: fs:: File :: create ( path) ?;
27+
28+ writeln ! ( f, "##### Run parameters" ) ?;
29+ writeln ! ( f, "{:#?}" , params) ?;
30+ writeln ! ( f) ?;
31+ writeln ! ( f, "##### Run started" ) ?;
32+ writeln ! ( f, "{}" , time_start. format( LOG_TIME_FMT ) ) ?;
33+ writeln ! ( f) ?;
34+ writeln ! ( f, "##### Mapping started" ) ?;
35+ writeln ! ( f, "{}" , time_map_start. format( LOG_TIME_FMT ) ) ?;
36+ writeln ! ( f) ?;
37+ writeln ! ( f, "##### Mapping finished" ) ?;
38+ writeln ! ( f, "{}" , time_finish. format( LOG_TIME_FMT ) ) ?;
39+
40+ Ok ( ( ) )
41+ }
42+
43+ /// Write a minimal STAR-compatible Log.progress.out alongside Log.final.out.
44+ ///
45+ /// A header line plus a single "done" line with the final timestamp and
46+ /// mapping speed (million reads per hour). Intentionally a stub.
47+ pub fn write_log_progress (
48+ path : & Path ,
49+ total_reads : u64 ,
50+ time_map_start : chrono:: DateTime < chrono:: Local > ,
51+ time_finish : chrono:: DateTime < chrono:: Local > ,
52+ ) -> std:: io:: Result < ( ) > {
53+ use std:: io:: Write ;
54+
55+ let elapsed_hours = {
56+ let elapsed = time_finish - time_map_start;
57+ elapsed. num_milliseconds ( ) as f64 / 3_600_000.0
58+ } ;
59+ let mapping_speed = if elapsed_hours > 0.0 {
60+ total_reads as f64 / elapsed_hours / 1_000_000.0
61+ } else {
62+ 0.0
63+ } ;
64+
65+ let mut f = std:: fs:: File :: create ( path) ?;
66+ writeln ! ( f, "# completed\t mapping_speed_M_reads_per_hour" ) ?;
67+ writeln ! (
68+ f,
69+ "{}\t {:.2}" ,
70+ time_finish. format( LOG_TIME_FMT ) ,
71+ mapping_speed
72+ ) ?;
73+
74+ Ok ( ( ) )
75+ }
876
977/// Reason a read could not be mapped
1078#[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
@@ -1018,4 +1086,69 @@ mod tests {
10181086 stats. record_half_mapped ( ) ;
10191087 assert_eq ! ( stats. half_mapped_pairs. load( Ordering :: Relaxed ) , 3 ) ;
10201088 }
1089+
1090+ #[ test]
1091+ fn test_write_log_out_minimal ( ) {
1092+ use chrono:: TimeZone ;
1093+ use clap:: Parser ;
1094+
1095+ let params = Parameters :: parse_from ( [ "rustar-aligner" , "--readFilesIn" , "reads.fq" ] ) ;
1096+ let t_start = chrono:: Local
1097+ . with_ymd_and_hms ( 2026 , 5 , 12 , 14 , 14 , 23 )
1098+ . unwrap ( ) ;
1099+ let t_map = chrono:: Local
1100+ . with_ymd_and_hms ( 2026 , 5 , 12 , 14 , 14 , 30 )
1101+ . unwrap ( ) ;
1102+ let t_finish = chrono:: Local
1103+ . with_ymd_and_hms ( 2026 , 5 , 12 , 14 , 14 , 58 )
1104+ . unwrap ( ) ;
1105+
1106+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
1107+ let path = dir. path ( ) . join ( "Log.out" ) ;
1108+ write_log_out ( & path, & params, t_start, t_map, t_finish) . unwrap ( ) ;
1109+
1110+ let content = std:: fs:: read_to_string ( & path) . unwrap ( ) ;
1111+ assert ! ( !content. is_empty( ) , "Log.out should not be empty" ) ;
1112+ assert ! ( content. contains( "##### Run parameters" ) ) ;
1113+ assert ! ( content. contains( "##### Run started" ) ) ;
1114+ assert ! ( content. contains( "##### Mapping started" ) ) ;
1115+ assert ! ( content. contains( "##### Mapping finished" ) ) ;
1116+ // Each phase header should be followed by a timestamp shaped like "May 12 14:14:23".
1117+ assert ! ( content. contains( "May 12 14:14:23" ) ) ;
1118+ assert ! ( content. contains( "May 12 14:14:58" ) ) ;
1119+ // The parameters dump should mention at least one familiar field.
1120+ assert ! ( content. contains( "read_files_in" ) ) ;
1121+ }
1122+
1123+ #[ test]
1124+ fn test_write_log_progress_minimal ( ) {
1125+ use chrono:: TimeZone ;
1126+
1127+ let t_map = chrono:: Local
1128+ . with_ymd_and_hms ( 2026 , 5 , 12 , 14 , 14 , 30 )
1129+ . unwrap ( ) ;
1130+ let t_finish = chrono:: Local
1131+ . with_ymd_and_hms ( 2026 , 5 , 12 , 14 , 14 , 58 )
1132+ . unwrap ( ) ;
1133+
1134+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
1135+ let path = dir. path ( ) . join ( "Log.progress.out" ) ;
1136+ write_log_progress ( & path, 10_000 , t_map, t_finish) . unwrap ( ) ;
1137+
1138+ let content = std:: fs:: read_to_string ( & path) . unwrap ( ) ;
1139+ let lines: Vec < & str > = content. lines ( ) . collect ( ) ;
1140+ assert ! ( lines. len( ) >= 2 , "expected header + data line" ) ;
1141+ assert ! ( lines[ 0 ] . starts_with( '#' ) , "first line should be a header" ) ;
1142+ assert ! (
1143+ lines[ 1 ] . contains( "May 12 14:14:58" ) ,
1144+ "second line should contain the finish timestamp, got: {}" ,
1145+ lines[ 1 ]
1146+ ) ;
1147+ // Mapping speed column should be a number with two decimals.
1148+ let cols: Vec < & str > = lines[ 1 ] . split ( '\t' ) . collect ( ) ;
1149+ assert_eq ! ( cols. len( ) , 2 , "expected 2 tab-separated columns" ) ;
1150+ cols[ 1 ]
1151+ . parse :: < f64 > ( )
1152+ . expect ( "second column should parse as a float" ) ;
1153+ }
10211154}
0 commit comments