1mod fsck;
17pub mod logstats;
18
19#[cfg(feature = "mcap")]
20pub mod mcap_export;
21
22#[cfg(feature = "mcap")]
23pub mod serde_to_jsonschema;
24
25use bincode::Decode;
26use bincode::config::standard;
27use bincode::decode_from_std_read;
28use bincode::error::DecodeError;
29use clap::{Parser, Subcommand, ValueEnum};
30use cu29::UnifiedLogType;
31use cu29::prelude::*;
32use cu29_intern_strs::read_interned_strings;
33use fsck::check;
34#[cfg(feature = "mcap")]
35use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
36use logstats::{compute_logstats, write_logstats};
37use serde::Serialize;
38use std::fmt::{Display, Formatter};
39#[cfg(feature = "mcap")]
40use std::io::IsTerminal;
41use std::io::Read;
42use std::path::{Path, PathBuf};
43
44#[cfg(feature = "mcap")]
45pub use mcap_export::{McapExportStats, export_to_mcap, export_to_mcap_with_schemas, mcap_info};
46
47#[cfg(feature = "mcap")]
48#[allow(deprecated)]
49pub use cu29::prelude::PayloadSchemas;
50
51#[cfg(feature = "mcap")]
52pub use serde_to_jsonschema::trace_type_to_jsonschema;
53
54#[cfg(feature = "python")]
59pub use python::register_copperlist_python_type;
60
61#[cfg(feature = "python")]
68pub fn copperlist_iterator_unified_typed_py<P>(
69 unified_src_path: &str,
70 py: pyo3::Python<'_>,
71) -> pyo3::PyResult<pyo3::Py<pyo3::PyAny>>
72where
73 P: CopperListTuple + 'static,
74{
75 let _ = cu29::logcodec::seed_effective_config_from_log::<P>(Path::new(unified_src_path))
76 .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
77 register_copperlist_python_type::<P>()
78 .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
79 let iter = python::copperlist_iterator_unified(unified_src_path)?;
80 pyo3::Py::new(py, iter).map(|obj| obj.into())
81}
82
83#[cfg(feature = "python")]
88pub fn runtime_lifecycle_iterator_unified_py(
89 unified_src_path: &str,
90 py: pyo3::Python<'_>,
91) -> pyo3::PyResult<pyo3::Py<pyo3::PyAny>> {
92 let iter = python::runtime_lifecycle_iterator_unified(unified_src_path)?;
93 pyo3::Py::new(py, iter).map(|obj| obj.into())
94}
95#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
96pub enum ExportFormat {
97 Json,
98 Csv,
99}
100
101impl Display for ExportFormat {
102 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
103 match self {
104 ExportFormat::Json => write!(f, "json"),
105 ExportFormat::Csv => write!(f, "csv"),
106 }
107 }
108}
109
110#[derive(Parser)]
112#[command(author, version, about)]
113pub struct LogReaderCli {
114 pub unifiedlog_base: PathBuf,
117
118 #[command(subcommand)]
119 pub command: Command,
120}
121
122#[derive(Subcommand)]
123pub enum Command {
124 ExtractTextLog { log_index: PathBuf },
126 ExtractCopperlists {
128 #[arg(short, long, default_value_t = ExportFormat::Json)]
129 export_format: ExportFormat,
130 },
131 Fsck {
133 #[arg(short, long, action = clap::ArgAction::Count)]
134 verbose: u8,
135 #[arg(long)]
137 dump_runtime_lifecycle: bool,
138 },
139 LogStats {
141 #[arg(short, long, default_value = "cu29_logstats.json")]
143 output: PathBuf,
144 #[arg(long, default_value = "copperconfig.ron")]
146 config: PathBuf,
147 #[arg(long)]
149 mission: Option<String>,
150 #[arg(long, value_delimiter = ',')]
152 features: Vec<String>,
153 },
154 #[cfg(feature = "mcap")]
156 ExportMcap {
157 #[arg(short, long)]
159 output: PathBuf,
160 #[arg(long)]
162 progress: bool,
163 #[arg(long)]
165 quiet: bool,
166 },
167 #[cfg(feature = "mcap")]
169 McapInfo {
170 mcap_file: PathBuf,
172 #[arg(short, long)]
174 schemas: bool,
175 #[arg(short = 'n', long, default_value_t = 0)]
177 sample_messages: usize,
178 },
179}
180
181fn write_json_pretty<T: Serialize + ?Sized>(value: &T) -> CuResult<()> {
182 serde_json::to_writer_pretty(std::io::stdout(), value)
183 .map_err(|e| CuError::new_with_cause("Failed to write JSON output", e))
184}
185
186fn write_json<T: Serialize + ?Sized>(value: &T) -> CuResult<()> {
187 serde_json::to_writer(std::io::stdout(), value)
188 .map_err(|e| CuError::new_with_cause("Failed to write JSON output", e))
189}
190
191fn build_read_logger(unifiedlog_base: &Path) -> CuResult<UnifiedLoggerRead> {
192 let logger = UnifiedLoggerBuilder::new()
193 .file_base_name(unifiedlog_base)
194 .build()
195 .map_err(|e| CuError::new_with_cause("Failed to create logger", e))?;
196 match logger {
197 UnifiedLogger::Read(dl) => Ok(dl),
198 UnifiedLogger::Write(_) => Err(CuError::from(
199 "Expected read-only unified logger in export CLI",
200 )),
201 }
202}
203
204#[cfg(feature = "mcap")]
208pub fn run_cli<P>() -> CuResult<()>
209where
210 P: CopperListTuple + CuPayloadRawBytes + 'static,
211{
212 #[cfg(feature = "python")]
213 let _ = python::register_copperlist_python_type::<P>();
214
215 run_cli_inner::<P>()
216}
217
218#[cfg(not(feature = "mcap"))]
221pub fn run_cli<P>() -> CuResult<()>
222where
223 P: CopperListTuple + CuPayloadRawBytes + 'static,
224{
225 #[cfg(feature = "python")]
226 let _ = python::register_copperlist_python_type::<P>();
227
228 run_cli_inner::<P>()
229}
230
231#[cfg(feature = "mcap")]
232fn run_cli_inner<P>() -> CuResult<()>
233where
234 P: CopperListTuple + CuPayloadRawBytes + 'static,
235{
236 let args = LogReaderCli::parse();
237 let unifiedlog_base = args.unifiedlog_base;
238 let _ = cu29::logcodec::seed_effective_config_from_log::<P>(&unifiedlog_base)?;
239
240 let mut dl = build_read_logger(&unifiedlog_base)?;
241
242 match args.command {
243 Command::ExtractTextLog { log_index } => {
244 let reader = UnifiedLoggerIOReader::new(dl, UnifiedLogType::StructuredLogLine);
245 textlog_dump(reader, &log_index)?;
246 }
247 Command::ExtractCopperlists { export_format } => {
248 println!("Extracting copperlists with format: {export_format}");
249 let mut reader = UnifiedLoggerIOReader::new(dl, UnifiedLogType::CopperList);
250 let iter = copperlists_reader::<P>(&mut reader);
251
252 match export_format {
253 ExportFormat::Json => {
254 for entry in iter {
255 write_json_pretty(&entry)?;
256 }
257 }
258 ExportFormat::Csv => {
259 let mut first = true;
260 for origin in P::get_all_task_ids() {
261 if !first {
262 print!(", ");
263 } else {
264 print!("id, ");
265 }
266 print!("{origin}_time, {origin}_tov, {origin},");
267 first = false;
268 }
269 println!();
270 for entry in iter {
271 let mut first = true;
272 for msg in entry.cumsgs() {
273 if let Some(payload) = msg.payload() {
274 if !first {
275 print!(", ");
276 } else {
277 print!("{}, ", entry.id);
278 }
279 let metadata = msg.metadata();
280 print!("{}, {}, ", metadata.process_time(), msg.tov());
281 write_json(payload)?; first = false;
283 }
284 }
285 println!();
286 }
287 }
288 }
289 }
290 Command::Fsck {
291 verbose,
292 dump_runtime_lifecycle,
293 } => {
294 if let Some(value) = check::<P>(&mut dl, verbose, dump_runtime_lifecycle) {
295 return value;
296 }
297 }
298 Command::LogStats {
299 output,
300 config,
301 mission,
302 features,
303 } => {
304 run_logstats::<P>(&unifiedlog_base, dl, output, config, mission, &features)?;
305 }
306 #[cfg(feature = "mcap")]
307 Command::ExportMcap {
308 output,
309 progress,
310 quiet,
311 } => {
312 println!("Exporting copperlists to MCAP format: {}", output.display());
313
314 let show_progress = should_show_progress(progress, quiet);
315 let total_bytes = if show_progress {
316 Some(copperlist_total_bytes(&unifiedlog_base)?)
317 } else {
318 None
319 };
320
321 let reader = UnifiedLoggerIOReader::new(dl, UnifiedLogType::CopperList);
322
323 let stats = if let Some(total_bytes) = total_bytes {
325 let progress_bar = make_progress_bar(total_bytes);
326 let reader = ProgressReader::new(reader, progress_bar.clone());
327 let result = export_to_mcap_impl::<P>(reader, &output);
328 progress_bar.finish_and_clear();
329 result?
330 } else {
331 export_to_mcap_impl::<P>(reader, &output)?
332 };
333 println!("{stats}");
334 }
335 #[cfg(feature = "mcap")]
336 Command::McapInfo {
337 mcap_file,
338 schemas,
339 sample_messages,
340 } => {
341 mcap_info(&mcap_file, schemas, sample_messages)?;
342 }
343 }
344
345 Ok(())
346}
347
348#[cfg(not(feature = "mcap"))]
349fn run_cli_inner<P>() -> CuResult<()>
350where
351 P: CopperListTuple + CuPayloadRawBytes + 'static,
352{
353 let args = LogReaderCli::parse();
354 let unifiedlog_base = args.unifiedlog_base;
355 let _ = cu29::logcodec::seed_effective_config_from_log::<P>(&unifiedlog_base)?;
356
357 let mut dl = build_read_logger(&unifiedlog_base)?;
358
359 match args.command {
360 Command::ExtractTextLog { log_index } => {
361 let reader = UnifiedLoggerIOReader::new(dl, UnifiedLogType::StructuredLogLine);
362 textlog_dump(reader, &log_index)?;
363 }
364 Command::ExtractCopperlists { export_format } => {
365 println!("Extracting copperlists with format: {export_format}");
366 let mut reader = UnifiedLoggerIOReader::new(dl, UnifiedLogType::CopperList);
367 let iter = copperlists_reader::<P>(&mut reader);
368
369 match export_format {
370 ExportFormat::Json => {
371 for entry in iter {
372 write_json_pretty(&entry)?;
373 }
374 }
375 ExportFormat::Csv => {
376 let mut first = true;
377 for origin in P::get_all_task_ids() {
378 if !first {
379 print!(", ");
380 } else {
381 print!("id, ");
382 }
383 print!("{origin}_time, {origin}_tov, {origin},");
384 first = false;
385 }
386 println!();
387 for entry in iter {
388 let mut first = true;
389 for msg in entry.cumsgs() {
390 if let Some(payload) = msg.payload() {
391 if !first {
392 print!(", ");
393 } else {
394 print!("{}, ", entry.id);
395 }
396 let metadata = msg.metadata();
397 print!("{}, {}, ", metadata.process_time(), msg.tov());
398 write_json(payload)?;
399 first = false;
400 }
401 }
402 println!();
403 }
404 }
405 }
406 }
407 Command::Fsck {
408 verbose,
409 dump_runtime_lifecycle,
410 } => {
411 if let Some(value) = check::<P>(&mut dl, verbose, dump_runtime_lifecycle) {
412 return value;
413 }
414 }
415 Command::LogStats {
416 output,
417 config,
418 mission,
419 features,
420 } => {
421 run_logstats::<P>(&unifiedlog_base, dl, output, config, mission, &features)?;
422 }
423 }
424
425 Ok(())
426}
427
428fn run_logstats<P>(
429 unifiedlog_base: &Path,
430 dl: UnifiedLoggerRead,
431 output: PathBuf,
432 config: PathBuf,
433 mission: Option<String>,
434 features: &[String],
435) -> CuResult<()>
436where
437 P: CopperListTuple + CuPayloadRawBytes,
438{
439 let config_path = config
440 .to_str()
441 .ok_or_else(|| CuError::from("Config path is not valid UTF-8"))?;
442 let feature_refs = features.iter().map(String::as_str).collect::<Vec<_>>();
443 let cfg = cu29::config::read_configuration_with_features(config_path, &feature_refs)
444 .map_err(|e| CuError::new_with_cause("Failed to read configuration", e))?;
445 let logged_mission =
446 if mission.is_none() && matches!(&cfg.graphs, cu29::config::ConfigGraphs::Missions(_)) {
447 unified_log_mission(unifiedlog_base)?
448 } else {
449 None
450 };
451 let mission = resolve_logstats_mission(&cfg, mission, logged_mission);
452 let reader = UnifiedLoggerIOReader::new(dl, UnifiedLogType::CopperList);
453 let stats = compute_logstats::<P>(reader, &cfg, mission.as_deref())?;
454 write_logstats(&stats, &output)
455}
456
457fn resolve_logstats_mission(
458 config: &CuConfig,
459 requested: Option<String>,
460 logged: Option<String>,
461) -> Option<String> {
462 if requested.is_some() {
463 return requested;
464 }
465 let cu29::config::ConfigGraphs::Missions(graphs) = &config.graphs else {
466 return None;
467 };
468 if let Some(logged) = logged
469 && graphs.contains_key(&logged)
470 {
471 return Some(logged);
472 }
473 if graphs.contains_key("default") {
474 return Some("default".to_string());
475 }
476 graphs.keys().min().cloned()
477}
478
479#[cfg(feature = "mcap")]
483fn export_to_mcap_impl<P>(src: impl Read, output: &Path) -> CuResult<McapExportStats>
484where
485 P: CopperListTuple,
486{
487 mcap_export::export_to_mcap::<P, _>(src, output)
488}
489
490#[cfg(feature = "mcap")]
491struct ProgressReader<R> {
492 inner: R,
493 progress: ProgressBar,
494}
495
496#[cfg(feature = "mcap")]
497impl<R> ProgressReader<R> {
498 fn new(inner: R, progress: ProgressBar) -> Self {
499 Self { inner, progress }
500 }
501}
502
503#[cfg(feature = "mcap")]
504impl<R: Read> Read for ProgressReader<R> {
505 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
506 let read = self.inner.read(buf)?;
507 if read > 0 {
508 self.progress.inc(read as u64);
509 }
510 Ok(read)
511 }
512}
513
514#[cfg(feature = "mcap")]
515fn make_progress_bar(total_bytes: u64) -> ProgressBar {
516 let progress_bar = ProgressBar::new(total_bytes);
517 progress_bar.set_draw_target(ProgressDrawTarget::stderr_with_hz(5));
518
519 let style = ProgressStyle::with_template(
520 "[{elapsed_precise}] {bar:40} {bytes}/{total_bytes} ({bytes_per_sec}, ETA {eta})",
521 )
522 .unwrap_or_else(|_| ProgressStyle::default_bar());
523
524 progress_bar.set_style(style.progress_chars("=>-"));
525 progress_bar
526}
527
528#[cfg(feature = "mcap")]
529fn should_show_progress(force_progress: bool, quiet: bool) -> bool {
530 !quiet && (force_progress || std::io::stderr().is_terminal())
531}
532
533#[cfg(feature = "mcap")]
534fn copperlist_total_bytes(log_base: &Path) -> CuResult<u64> {
535 let mut reader = UnifiedLoggerRead::new(log_base)
536 .map_err(|e| CuError::new_with_cause("Failed to open log for progress estimation", e))?;
537 reader
538 .scan_section_bytes(UnifiedLogType::CopperList)
539 .map_err(|e| CuError::new_with_cause("Failed to scan log for progress estimation", e))
540}
541
542fn read_next_entry<T: Decode<()>>(src: &mut impl Read) -> Option<T> {
543 let entry = decode_from_std_read::<T, _, _>(src, standard());
544 match entry {
545 Ok(entry) => Some(entry),
546 Err(DecodeError::UnexpectedEnd { .. }) => None,
547 Err(DecodeError::Io { inner, additional }) => {
548 if inner.kind() == std::io::ErrorKind::UnexpectedEof {
549 None
550 } else {
551 println!("Error {inner:?} additional:{additional}");
552 None
553 }
554 }
555 Err(e) => {
556 println!("Error {e:?}");
557 None
558 }
559 }
560}
561
562pub fn copperlists_reader<P: CopperListTuple>(
565 mut src: impl Read,
566) -> impl Iterator<Item = CopperList<P>> {
567 std::iter::from_fn(move || read_next_entry::<CopperList<P>>(&mut src))
568}
569
570pub fn keyframes_reader(mut src: impl Read) -> impl Iterator<Item = KeyFrame> {
572 std::iter::from_fn(move || read_next_entry::<KeyFrame>(&mut src))
573}
574
575pub fn runtime_lifecycle_reader(
577 mut src: impl Read,
578) -> impl Iterator<Item = RuntimeLifecycleRecord> {
579 std::iter::from_fn(move || read_next_entry::<RuntimeLifecycleRecord>(&mut src))
580}
581
582pub fn unified_log_mission(unifiedlog_base: &Path) -> CuResult<Option<String>> {
584 let dl = build_read_logger(unifiedlog_base)?;
585 let reader = UnifiedLoggerIOReader::new(dl, UnifiedLogType::RuntimeLifecycle);
586 Ok(
587 runtime_lifecycle_reader(reader).find_map(|entry| match entry.event {
588 RuntimeLifecycleEvent::MissionStarted { mission } => Some(mission),
589 _ => None,
590 }),
591 )
592}
593
594pub fn assert_unified_log_mission(unifiedlog_base: &Path, expected_mission: &str) -> CuResult<()> {
596 match unified_log_mission(unifiedlog_base)? {
597 Some(actual_mission) if actual_mission == expected_mission => Ok(()),
598 Some(actual_mission) => Err(CuError::from(format!(
599 "Mission mismatch: expected '{expected_mission}', found '{actual_mission}'"
600 ))),
601 None => Err(CuError::from(format!(
602 "No MissionStarted runtime lifecycle event found while validating expected mission '{expected_mission}'"
603 ))),
604 }
605}
606
607pub fn structlog_reader(mut src: impl Read) -> impl Iterator<Item = CuResult<CuLogEntry>> {
608 std::iter::from_fn(move || {
609 let entry = decode_from_std_read::<CuLogEntry, _, _>(&mut src, standard());
610
611 match entry {
612 Err(DecodeError::UnexpectedEnd { .. }) => None,
613 Err(DecodeError::Io {
614 inner,
615 additional: _,
616 }) => {
617 if inner.kind() == std::io::ErrorKind::UnexpectedEof {
618 None
619 } else {
620 Some(Err(CuError::new_with_cause("Error reading log", inner)))
621 }
622 }
623 Err(e) => Some(Err(CuError::new_with_cause("Error reading log", e))),
624 Ok(entry) => {
625 if entry.msg_index == 0 {
626 None
627 } else {
628 Some(Ok(entry))
629 }
630 }
631 }
632 })
633}
634
635pub fn textlog_dump(src: impl Read, index: &Path) -> CuResult<()> {
640 let all_strings = read_interned_strings(index).map_err(|e| {
641 CuError::new_with_cause(
642 "Failed to read interned strings from index",
643 std::io::Error::other(e),
644 )
645 })?;
646
647 for result in structlog_reader(src) {
648 let entry = result?;
649 match rebuild_logline(&all_strings, &entry) {
650 Ok(line) => println!("{line}"),
651 Err(e) => println!("Failed to rebuild log line: {e:?}"),
652 }
653 }
654
655 Ok(())
656}
657
658#[cfg(feature = "python")]
660mod python {
661 use bincode::config::standard;
662 use bincode::decode_from_std_read;
663 use bincode::error::DecodeError;
664 use cu29::bevy_reflect::{PartialReflect, ReflectRef, VariantType};
665 use cu29::prelude::*;
666 use cu29_intern_strs::read_interned_strings;
667 use pyo3::exceptions::{PyIOError, PyRuntimeError};
668 use pyo3::prelude::*;
669 use pyo3::types::{PyDelta, PyDict, PyList};
670 use std::io::Read;
671 use std::path::Path;
672 use std::sync::OnceLock;
673
674 type CopperListDecodeFn =
675 for<'py> fn(&mut Box<dyn Read + Send + Sync>, Python<'py>) -> Option<PyResult<Py<PyAny>>>;
676 static COPPERLIST_DECODER: OnceLock<CopperListDecodeFn> = OnceLock::new();
677
678 #[pyclass]
680 pub struct PyLogIterator {
681 reader: Box<dyn Read + Send + Sync>,
682 }
683
684 #[pyclass]
686 pub struct PyCopperListIterator {
687 reader: Box<dyn Read + Send + Sync>,
688 decode_next: CopperListDecodeFn,
689 }
690
691 #[pyclass]
693 pub struct PyRuntimeLifecycleIterator {
694 reader: Box<dyn Read + Send + Sync>,
695 }
696
697 #[pyclass(get_all)]
699 pub struct PyUnitValue {
700 pub value: f64,
701 pub unit: String,
702 }
703
704 pub fn register_copperlist_python_type<P>() -> CuResult<()>
709 where
710 P: CopperListTuple,
711 {
712 if COPPERLIST_DECODER.get().is_none() {
713 COPPERLIST_DECODER
714 .set(decode_next_copperlist::<P>)
715 .map_err(|_| CuError::from("Failed to register CopperList Python decoder"))?;
716 }
717 Ok(())
718 }
719 #[pymethods]
720 impl PyLogIterator {
721 fn __iter__(slf: PyRefMut<Self>) -> PyRefMut<Self> {
722 slf
723 }
724
725 fn __next__(mut slf: PyRefMut<Self>) -> Option<PyResult<PyCuLogEntry>> {
726 match decode_from_std_read::<CuLogEntry, _, _>(&mut slf.reader, standard()) {
727 Ok(entry) => {
728 if entry.msg_index == 0 {
729 None
730 } else {
731 Some(Ok(PyCuLogEntry { inner: entry }))
732 }
733 }
734 Err(DecodeError::UnexpectedEnd { .. }) => None,
735 Err(DecodeError::Io { inner, .. })
736 if inner.kind() == std::io::ErrorKind::UnexpectedEof =>
737 {
738 None
739 }
740 Err(e) => Some(Err(PyIOError::new_err(e.to_string()))),
741 }
742 }
743 }
744
745 #[pymethods]
746 impl PyCopperListIterator {
747 fn __iter__(slf: PyRefMut<Self>) -> PyRefMut<Self> {
748 slf
749 }
750
751 fn __next__(mut slf: PyRefMut<Self>, py: Python<'_>) -> Option<PyResult<Py<PyAny>>> {
752 (slf.decode_next)(&mut slf.reader, py)
753 }
754 }
755
756 #[pymethods]
757 impl PyRuntimeLifecycleIterator {
758 fn __iter__(slf: PyRefMut<Self>) -> PyRefMut<Self> {
759 slf
760 }
761
762 fn __next__(mut slf: PyRefMut<Self>, py: Python<'_>) -> Option<PyResult<Py<PyAny>>> {
763 let entry = super::read_next_entry::<RuntimeLifecycleRecord>(&mut slf.reader)?;
764 Some(runtime_lifecycle_record_to_py(&entry, py))
765 }
766 }
767 #[pyfunction]
773 pub fn struct_log_iterator_bare(
774 bare_struct_src_path: &str,
775 index_path: &str,
776 ) -> PyResult<(PyLogIterator, Vec<String>)> {
777 let file = std::fs::File::open(bare_struct_src_path)
778 .map_err(|e| PyIOError::new_err(e.to_string()))?;
779 let all_strings = read_interned_strings(Path::new(index_path))
780 .map_err(|e| PyIOError::new_err(e.to_string()))?;
781 Ok((
782 PyLogIterator {
783 reader: Box::new(file),
784 },
785 all_strings,
786 ))
787 }
788 #[pyfunction]
793 pub fn struct_log_iterator_unified(
794 unified_src_path: &str,
795 index_path: &str,
796 ) -> PyResult<(PyLogIterator, Vec<String>)> {
797 let all_strings = read_interned_strings(Path::new(index_path))
798 .map_err(|e| PyIOError::new_err(e.to_string()))?;
799
800 let logger = UnifiedLoggerBuilder::new()
801 .file_base_name(Path::new(unified_src_path))
802 .build()
803 .map_err(|e| PyIOError::new_err(e.to_string()))?;
804 let dl = match logger {
805 UnifiedLogger::Read(dl) => dl,
806 UnifiedLogger::Write(_) => {
807 return Err(PyIOError::new_err(
808 "Expected read-only unified logger for Python export",
809 ));
810 }
811 };
812
813 let reader = UnifiedLoggerIOReader::new(dl, UnifiedLogType::StructuredLogLine);
814 Ok((
815 PyLogIterator {
816 reader: Box::new(reader),
817 },
818 all_strings,
819 ))
820 }
821
822 #[pyfunction]
827 pub fn copperlist_iterator_unified(unified_src_path: &str) -> PyResult<PyCopperListIterator> {
828 let decode_next = *COPPERLIST_DECODER.get().ok_or_else(|| {
829 PyRuntimeError::new_err(
830 "CopperList decoder is not registered. \
831Call register_copperlist_python_type::<P>() from Rust before using this function.",
832 )
833 })?;
834
835 let logger = UnifiedLoggerBuilder::new()
836 .file_base_name(Path::new(unified_src_path))
837 .build()
838 .map_err(|e| PyIOError::new_err(e.to_string()))?;
839 let dl = match logger {
840 UnifiedLogger::Read(dl) => dl,
841 UnifiedLogger::Write(_) => {
842 return Err(PyIOError::new_err(
843 "Expected read-only unified logger for Python export",
844 ));
845 }
846 };
847
848 let reader = UnifiedLoggerIOReader::new(dl, UnifiedLogType::CopperList);
849 Ok(PyCopperListIterator {
850 reader: Box::new(reader),
851 decode_next,
852 })
853 }
854
855 #[pyfunction]
857 pub fn runtime_lifecycle_iterator_unified(
858 unified_src_path: &str,
859 ) -> PyResult<PyRuntimeLifecycleIterator> {
860 let logger = UnifiedLoggerBuilder::new()
861 .file_base_name(Path::new(unified_src_path))
862 .build()
863 .map_err(|e| PyIOError::new_err(e.to_string()))?;
864 let dl = match logger {
865 UnifiedLogger::Read(dl) => dl,
866 UnifiedLogger::Write(_) => {
867 return Err(PyIOError::new_err(
868 "Expected read-only unified logger for Python export",
869 ));
870 }
871 };
872
873 let reader = UnifiedLoggerIOReader::new(dl, UnifiedLogType::RuntimeLifecycle);
874 Ok(PyRuntimeLifecycleIterator {
875 reader: Box::new(reader),
876 })
877 }
878 #[pyclass]
880 pub struct PyCuLogEntry {
881 pub inner: CuLogEntry,
882 }
883
884 #[pymethods]
885 impl PyCuLogEntry {
886 pub fn ts<'a>(&self, py: Python<'a>) -> PyResult<Bound<'a, PyDelta>> {
888 let nanoseconds: u64 = self.inner.time.into();
889
890 let days = (nanoseconds / 86_400_000_000_000) as i32;
892 let seconds = (nanoseconds / 1_000_000_000) as i32;
893 let microseconds = ((nanoseconds % 1_000_000_000) / 1_000) as i32;
894
895 PyDelta::new(py, days, seconds, microseconds, false)
896 }
897
898 pub fn msg_index(&self) -> u32 {
900 self.inner.msg_index
901 }
902
903 pub fn culistid(&self) -> Option<u64> {
905 self.inner.origin.culistid
906 }
907
908 pub fn component_id(&self) -> Option<u32> {
910 self.inner.origin.component_id
911 }
912
913 pub fn task_index(&self) -> Option<u32> {
915 self.inner.origin.task_index
916 }
917
918 pub fn paramname_indexes(&self) -> Vec<u32> {
920 self.inner.paramname_indexes.iter().copied().collect()
921 }
922
923 pub fn params(&self, py: Python<'_>) -> PyResult<Vec<Py<PyAny>>> {
925 self.inner
926 .params
927 .iter()
928 .map(|value| value_to_py(value, py))
929 .collect()
930 }
931 }
932
933 #[pymodule(name = "libcu29_export")]
935 fn cu29_export(m: &Bound<'_, PyModule>) -> PyResult<()> {
936 m.add_class::<PyCuLogEntry>()?;
937 m.add_class::<PyLogIterator>()?;
938 m.add_class::<PyCopperListIterator>()?;
939 m.add_class::<PyRuntimeLifecycleIterator>()?;
940 m.add_class::<PyUnitValue>()?;
941 m.add_function(wrap_pyfunction!(struct_log_iterator_bare, m)?)?;
942 m.add_function(wrap_pyfunction!(struct_log_iterator_unified, m)?)?;
943 m.add_function(wrap_pyfunction!(copperlist_iterator_unified, m)?)?;
944 m.add_function(wrap_pyfunction!(runtime_lifecycle_iterator_unified, m)?)?;
945 Ok(())
946 }
947
948 fn decode_next_copperlist<P>(
949 reader: &mut Box<dyn Read + Send + Sync>,
950 py: Python<'_>,
951 ) -> Option<PyResult<Py<PyAny>>>
952 where
953 P: CopperListTuple,
954 {
955 let entry = super::read_next_entry::<CopperList<P>>(reader)?;
956 Some(copperlist_to_py::<P>(&entry, py))
957 }
958
959 fn copperlist_to_py<P>(entry: &CopperList<P>, py: Python<'_>) -> PyResult<Py<PyAny>>
960 where
961 P: CopperListTuple,
962 {
963 let task_ids = P::get_all_task_ids();
964 let root = PyDict::new(py);
965 root.set_item("id", entry.id)?;
966 root.set_item("state", entry.get_state().to_string())?;
967
968 let mut messages: Vec<Py<PyAny>> = Vec::new();
969 for (idx, msg) in entry.cumsgs().into_iter().enumerate() {
970 let message = PyDict::new(py);
971 message.set_item("task_id", task_ids.get(idx).copied().unwrap_or("unknown"))?;
972 message.set_item("tov", tov_to_py(msg.tov(), py)?)?;
973 message.set_item("metadata", metadata_to_py(msg.metadata(), py)?)?;
974 match msg.payload_reflect() {
975 Some(payload) => message.set_item(
976 "payload",
977 partial_reflect_to_py(payload.as_partial_reflect(), py)?,
978 )?,
979 None => message.set_item("payload", py.None())?,
980 }
981 messages.push(dict_to_namespace(message, py)?);
982 }
983
984 root.set_item("messages", PyList::new(py, messages)?)?;
985 dict_to_namespace(root, py)
986 }
987
988 fn runtime_lifecycle_record_to_py(
989 entry: &RuntimeLifecycleRecord,
990 py: Python<'_>,
991 ) -> PyResult<Py<PyAny>> {
992 let root = PyDict::new(py);
993 root.set_item("timestamp_ns", entry.timestamp.as_nanos())?;
994 root.set_item("event", runtime_lifecycle_event_to_py(&entry.event, py)?)?;
995 dict_to_namespace(root, py)
996 }
997
998 fn runtime_lifecycle_event_to_py(
999 event: &RuntimeLifecycleEvent,
1000 py: Python<'_>,
1001 ) -> PyResult<Py<PyAny>> {
1002 let root = PyDict::new(py);
1003 match event {
1004 RuntimeLifecycleEvent::Instantiated {
1005 config_source,
1006 effective_config_ron,
1007 stack,
1008 } => {
1009 root.set_item("kind", "instantiated")?;
1010 root.set_item("config_source", runtime_config_source_to_py(config_source))?;
1011 root.set_item("effective_config_ron", effective_config_ron)?;
1012
1013 let stack_py = PyDict::new(py);
1014 stack_py.set_item("app_name", &stack.app_name)?;
1015 stack_py.set_item("app_version", &stack.app_version)?;
1016 stack_py.set_item("git_commit", &stack.git_commit)?;
1017 stack_py.set_item("git_dirty", stack.git_dirty)?;
1018 stack_py.set_item("subsystem_id", &stack.subsystem_id)?;
1019 stack_py.set_item("subsystem_code", stack.subsystem_code)?;
1020 stack_py.set_item("instance_id", stack.instance_id)?;
1021 root.set_item("stack", dict_to_namespace(stack_py, py)?)?;
1022 }
1023 RuntimeLifecycleEvent::MissionStarted { mission } => {
1024 root.set_item("kind", "mission_started")?;
1025 root.set_item("mission", mission)?;
1026 }
1027 RuntimeLifecycleEvent::MissionStopped { mission, reason } => {
1028 root.set_item("kind", "mission_stopped")?;
1029 root.set_item("mission", mission)?;
1030 root.set_item("reason", reason)?;
1031 }
1032 RuntimeLifecycleEvent::Panic {
1033 message,
1034 file,
1035 line,
1036 column,
1037 } => {
1038 root.set_item("kind", "panic")?;
1039 root.set_item("message", message)?;
1040 root.set_item("file", file)?;
1041 root.set_item("line", line)?;
1042 root.set_item("column", column)?;
1043 }
1044 RuntimeLifecycleEvent::ShutdownCompleted => {
1045 root.set_item("kind", "shutdown_completed")?;
1046 }
1047 }
1048
1049 dict_to_namespace(root, py)
1050 }
1051
1052 fn runtime_config_source_to_py(source: &RuntimeLifecycleConfigSource) -> &'static str {
1053 match source {
1054 RuntimeLifecycleConfigSource::ProgrammaticOverride => "programmatic_override",
1055 RuntimeLifecycleConfigSource::ExternalFile => "external_file",
1056 RuntimeLifecycleConfigSource::BundledDefault => "bundled_default",
1057 }
1058 }
1059
1060 fn metadata_to_py(metadata: &dyn CuMsgMetadataTrait, py: Python<'_>) -> PyResult<Py<PyAny>> {
1061 let process = metadata.process_time();
1062 let start: Option<CuTime> = process.start.into();
1063 let end: Option<CuTime> = process.end.into();
1064
1065 let process_time = PyDict::new(py);
1066 process_time.set_item("start_ns", start.map(|t| t.as_nanos()))?;
1067 process_time.set_item("end_ns", end.map(|t| t.as_nanos()))?;
1068
1069 let metadata_py = PyDict::new(py);
1070 metadata_py.set_item("process_time", dict_to_namespace(process_time, py)?)?;
1071 metadata_py.set_item("status_txt", metadata.status_txt().0.to_string())?;
1072 if let Some(origin) = metadata.origin() {
1073 let origin_py = PyDict::new(py);
1074 origin_py.set_item("subsystem_code", origin.subsystem_code)?;
1075 origin_py.set_item("instance_id", origin.instance_id)?;
1076 origin_py.set_item("cl_id", origin.cl_id)?;
1077 metadata_py.set_item("origin", dict_to_namespace(origin_py, py)?)?;
1078 } else {
1079 metadata_py.set_item("origin", py.None())?;
1080 }
1081 dict_to_namespace(metadata_py, py)
1082 }
1083
1084 fn tov_to_py(tov: Tov, py: Python<'_>) -> PyResult<Py<PyAny>> {
1085 let tov_py = PyDict::new(py);
1086 match tov {
1087 Tov::None => {
1088 tov_py.set_item("kind", "none")?;
1089 }
1090 Tov::Time(t) => {
1091 tov_py.set_item("kind", "time")?;
1092 tov_py.set_item("time_ns", t.as_nanos())?;
1093 }
1094 Tov::Range(r) => {
1095 tov_py.set_item("kind", "range")?;
1096 tov_py.set_item("start_ns", r.start.as_nanos())?;
1097 tov_py.set_item("end_ns", r.end.as_nanos())?;
1098 }
1099 }
1100 dict_to_namespace(tov_py, py)
1101 }
1102
1103 fn partial_reflect_to_py(value: &dyn PartialReflect, py: Python<'_>) -> PyResult<Py<PyAny>> {
1104 #[allow(unreachable_patterns)]
1105 match value.reflect_ref() {
1106 ReflectRef::Struct(s) => struct_to_py(s, py),
1107 ReflectRef::TupleStruct(ts) => tuple_struct_to_py(ts, py),
1108 ReflectRef::Tuple(t) => tuple_to_py(t, py),
1109 ReflectRef::List(list) => list_to_py(list, py),
1110 ReflectRef::Array(array) => array_to_py(array, py),
1111 ReflectRef::Map(map) => map_to_py(map, py),
1112 ReflectRef::Set(set) => set_to_py(set, py),
1113 ReflectRef::Enum(e) => enum_to_py(e, py),
1114 ReflectRef::Opaque(opaque) => opaque_to_py(opaque, py),
1115 _ => Ok(py.None()),
1116 }
1117 }
1118
1119 fn struct_to_py(value: &dyn cu29::bevy_reflect::Struct, py: Python<'_>) -> PyResult<Py<PyAny>> {
1120 let dict = PyDict::new(py);
1121 for idx in 0..value.field_len() {
1122 if let Some(field) = value.field_at(idx) {
1123 let name = value
1124 .name_at(idx)
1125 .map(str::to_owned)
1126 .unwrap_or_else(|| format!("field_{idx}"));
1127 dict.set_item(name, partial_reflect_to_py(field, py)?)?;
1128 }
1129 }
1130
1131 if let Some(unit) = unit_abbrev_for_type_path(value.reflect_type_path())
1132 && let Some(raw_value) = dict.get_item("value")?
1133 {
1134 if let Ok(v) = raw_value.extract::<f64>() {
1135 let unit_value = PyUnitValue {
1136 value: v,
1137 unit: unit.to_string(),
1138 };
1139 return Ok(Py::new(py, unit_value)?.into());
1140 }
1141 if let Ok(v) = raw_value.extract::<f32>() {
1142 let unit_value = PyUnitValue {
1143 value: v as f64,
1144 unit: unit.to_string(),
1145 };
1146 return Ok(Py::new(py, unit_value)?.into());
1147 }
1148 }
1149
1150 dict_to_namespace(dict, py)
1151 }
1152
1153 fn tuple_struct_to_py(
1154 value: &dyn cu29::bevy_reflect::TupleStruct,
1155 py: Python<'_>,
1156 ) -> PyResult<Py<PyAny>> {
1157 let mut fields = Vec::with_capacity(value.field_len());
1158 for idx in 0..value.field_len() {
1159 if let Some(field) = value.field(idx) {
1160 fields.push(partial_reflect_to_py(field, py)?);
1161 } else {
1162 fields.push(py.None());
1163 }
1164 }
1165 Ok(PyList::new(py, fields)?.into_pyobject(py)?.into())
1166 }
1167
1168 fn tuple_to_py(value: &dyn cu29::bevy_reflect::Tuple, py: Python<'_>) -> PyResult<Py<PyAny>> {
1169 let mut fields = Vec::with_capacity(value.field_len());
1170 for idx in 0..value.field_len() {
1171 if let Some(field) = value.field(idx) {
1172 fields.push(partial_reflect_to_py(field, py)?);
1173 } else {
1174 fields.push(py.None());
1175 }
1176 }
1177 Ok(PyList::new(py, fields)?.into_pyobject(py)?.into())
1178 }
1179
1180 fn list_to_py(value: &dyn cu29::bevy_reflect::List, py: Python<'_>) -> PyResult<Py<PyAny>> {
1181 let mut items = Vec::with_capacity(value.len());
1182 for item in value.iter() {
1183 items.push(partial_reflect_to_py(item, py)?);
1184 }
1185 Ok(PyList::new(py, items)?.into_pyobject(py)?.into())
1186 }
1187
1188 fn array_to_py(value: &dyn cu29::bevy_reflect::Array, py: Python<'_>) -> PyResult<Py<PyAny>> {
1189 let mut items = Vec::with_capacity(value.len());
1190 for item in value.iter() {
1191 items.push(partial_reflect_to_py(item, py)?);
1192 }
1193 Ok(PyList::new(py, items)?.into_pyobject(py)?.into())
1194 }
1195
1196 fn map_to_py(value: &dyn cu29::bevy_reflect::Map, py: Python<'_>) -> PyResult<Py<PyAny>> {
1197 let dict = PyDict::new(py);
1198 for (key, val) in value.iter() {
1199 let key_str = reflect_key_to_string(key);
1200 dict.set_item(key_str, partial_reflect_to_py(val, py)?)?;
1201 }
1202 Ok(dict.into_pyobject(py)?.into())
1203 }
1204
1205 fn set_to_py(value: &dyn cu29::bevy_reflect::Set, py: Python<'_>) -> PyResult<Py<PyAny>> {
1206 let mut items = Vec::with_capacity(value.len());
1207 for item in value.iter() {
1208 items.push(partial_reflect_to_py(item, py)?);
1209 }
1210 Ok(PyList::new(py, items)?.into_pyobject(py)?.into())
1211 }
1212
1213 fn enum_to_py(value: &dyn cu29::bevy_reflect::Enum, py: Python<'_>) -> PyResult<Py<PyAny>> {
1214 let dict = PyDict::new(py);
1215 dict.set_item("variant", value.variant_name())?;
1216
1217 match value.variant_type() {
1218 VariantType::Unit => {}
1219 VariantType::Tuple => {
1220 let mut fields = Vec::with_capacity(value.field_len());
1221 for idx in 0..value.field_len() {
1222 if let Some(field) = value.field_at(idx) {
1223 fields.push(partial_reflect_to_py(field, py)?);
1224 } else {
1225 fields.push(py.None());
1226 }
1227 }
1228 dict.set_item("fields", PyList::new(py, fields)?)?;
1229 }
1230 VariantType::Struct => {
1231 let fields = PyDict::new(py);
1232 for idx in 0..value.field_len() {
1233 if let Some(field) = value.field_at(idx) {
1234 let name = value
1235 .name_at(idx)
1236 .map(str::to_owned)
1237 .unwrap_or_else(|| format!("field_{idx}"));
1238 fields.set_item(name, partial_reflect_to_py(field, py)?)?;
1239 }
1240 }
1241 dict.set_item("fields", fields)?;
1242 }
1243 }
1244
1245 dict_to_namespace(dict, py)
1246 }
1247
1248 fn dict_to_namespace(dict: Bound<'_, PyDict>, py: Python<'_>) -> PyResult<Py<PyAny>> {
1249 let types = py.import("types")?;
1250 let namespace_ctor = types.getattr("SimpleNamespace")?;
1251 let namespace = namespace_ctor.call((), Some(&dict))?;
1252 Ok(namespace.into())
1253 }
1254
1255 fn reflect_key_to_string(value: &dyn PartialReflect) -> String {
1256 if let Some(v) = value.try_downcast_ref::<String>() {
1257 return v.clone();
1258 }
1259 if let Some(v) = value.try_downcast_ref::<&'static str>() {
1260 return (*v).to_string();
1261 }
1262 if let Some(v) = value.try_downcast_ref::<char>() {
1263 return v.to_string();
1264 }
1265 if let Some(v) = value.try_downcast_ref::<bool>() {
1266 return v.to_string();
1267 }
1268 if let Some(v) = value.try_downcast_ref::<u64>() {
1269 return v.to_string();
1270 }
1271 if let Some(v) = value.try_downcast_ref::<i64>() {
1272 return v.to_string();
1273 }
1274 if let Some(v) = value.try_downcast_ref::<usize>() {
1275 return v.to_string();
1276 }
1277 if let Some(v) = value.try_downcast_ref::<isize>() {
1278 return v.to_string();
1279 }
1280 format!("{value:?}")
1281 }
1282
1283 fn unit_abbrev_for_type_path(type_path: &str) -> Option<&'static str> {
1284 match type_path.rsplit("::").next()? {
1285 "Acceleration" => Some("m/s^2"),
1286 "Angle" => Some("rad"),
1287 "AngularVelocity" => Some("rad/s"),
1288 "ElectricPotential" => Some("V"),
1289 "Length" => Some("m"),
1290 "MagneticFluxDensity" => Some("T"),
1291 "Pressure" => Some("Pa"),
1292 "Ratio" => Some("1"),
1293 "ThermodynamicTemperature" => Some("K"),
1294 "Time" => Some("s"),
1295 "Velocity" => Some("m/s"),
1296 _ => None,
1297 }
1298 }
1299
1300 fn opaque_to_py(value: &dyn PartialReflect, py: Python<'_>) -> PyResult<Py<PyAny>> {
1301 macro_rules! downcast_copy {
1302 ($ty:ty) => {
1303 if let Some(v) = value.try_downcast_ref::<$ty>() {
1304 return Ok(v.into_pyobject(py)?.to_owned().into());
1305 }
1306 };
1307 }
1308
1309 downcast_copy!(bool);
1310 downcast_copy!(u8);
1311 downcast_copy!(u16);
1312 downcast_copy!(u32);
1313 downcast_copy!(u64);
1314 downcast_copy!(u128);
1315 downcast_copy!(usize);
1316 downcast_copy!(i8);
1317 downcast_copy!(i16);
1318 downcast_copy!(i32);
1319 downcast_copy!(i64);
1320 downcast_copy!(i128);
1321 downcast_copy!(isize);
1322 downcast_copy!(f32);
1323 downcast_copy!(f64);
1324 downcast_copy!(char);
1325
1326 if let Some(v) = value.try_downcast_ref::<String>() {
1327 return Ok(v.into_pyobject(py)?.into());
1328 }
1329 if let Some(v) = value.try_downcast_ref::<&'static str>() {
1330 return Ok(v.into_pyobject(py)?.into());
1331 }
1332 if let Some(v) = value.try_downcast_ref::<Vec<u8>>() {
1333 return Ok(v.into_pyobject(py)?.into());
1334 }
1335
1336 let fallback = format!("{value:?}");
1337 Ok(fallback.into_pyobject(py)?.into())
1338 }
1339 fn value_to_py(value: &cu29::prelude::Value, py: Python<'_>) -> PyResult<Py<PyAny>> {
1340 match value {
1341 Value::String(s) => Ok(s.into_pyobject(py)?.into()),
1342 Value::U64(u) => Ok(u.into_pyobject(py)?.into()),
1343 Value::U128(u) => Ok(u.into_pyobject(py)?.into()),
1344 Value::I64(i) => Ok(i.into_pyobject(py)?.into()),
1345 Value::I128(i) => Ok(i.into_pyobject(py)?.into()),
1346 Value::F64(f) => Ok(f.into_pyobject(py)?.into()),
1347 Value::Bool(b) => Ok(b.into_pyobject(py)?.to_owned().into()),
1348 Value::CuTime(t) => Ok(t.0.into_pyobject(py)?.into()),
1349 Value::Bytes(b) => Ok(b.into_pyobject(py)?.into()),
1350 Value::Char(c) => Ok(c.into_pyobject(py)?.into()),
1351 Value::I8(i) => Ok(i.into_pyobject(py)?.into()),
1352 Value::U8(u) => Ok(u.into_pyobject(py)?.into()),
1353 Value::I16(i) => Ok(i.into_pyobject(py)?.into()),
1354 Value::U16(u) => Ok(u.into_pyobject(py)?.into()),
1355 Value::I32(i) => Ok(i.into_pyobject(py)?.into()),
1356 Value::U32(u) => Ok(u.into_pyobject(py)?.into()),
1357 Value::Map(m) => {
1358 let dict = PyDict::new(py);
1359 for (k, v) in m.iter() {
1360 dict.set_item(value_to_py(k, py)?, value_to_py(v, py)?)?;
1361 }
1362 Ok(dict.into_pyobject(py)?.into())
1363 }
1364 Value::F32(f) => Ok(f.into_pyobject(py)?.into()),
1365 Value::Option(o) => match o.as_ref() {
1366 Some(value) => value_to_py(value, py),
1367 None => Ok(py.None()),
1368 },
1369 Value::Unit => Ok(py.None()),
1370 Value::Newtype(v) => value_to_py(v, py),
1371 Value::Seq(s) => {
1372 let items: Vec<Py<PyAny>> = s
1373 .iter()
1374 .map(|value| value_to_py(value, py))
1375 .collect::<PyResult<_>>()?;
1376 let list = PyList::new(py, items)?;
1377 Ok(list.into_pyobject(py)?.into())
1378 }
1379 }
1380 }
1381
1382 #[cfg(test)]
1383 mod tests {
1384 use super::*;
1385
1386 #[test]
1387 fn value_to_py_preserves_128_bit_integers() {
1388 Python::initialize();
1389 Python::attach(|py| {
1390 let u128_value = u128::from(u64::MAX) + 99;
1391 let u128_py = value_to_py(&Value::U128(u128_value), py).unwrap();
1392 assert_eq!(u128_py.bind(py).extract::<u128>().unwrap(), u128_value);
1393
1394 let i128_value = i128::from(i64::MIN) - 99;
1395 let i128_py = value_to_py(&Value::I128(i128_value), py).unwrap();
1396 assert_eq!(i128_py.bind(py).extract::<i128>().unwrap(), i128_value);
1397 });
1398 }
1399 }
1400}
1401
1402#[cfg(test)]
1403mod tests {
1404 use super::*;
1405 use bincode::{Decode, Encode, encode_into_slice};
1406 use serde::Deserialize;
1407 use std::env;
1408 use std::fs;
1409 use std::io::Cursor;
1410 use std::path::PathBuf;
1411 use std::sync::{Arc, Mutex};
1412 use tempfile::{TempDir, tempdir};
1413
1414 fn copy_stringindex_to_temp(tmpdir: &TempDir) -> PathBuf {
1415 let fake_out_dir = tmpdir.path().join("build").join("out").join("dir");
1417 fs::create_dir_all(&fake_out_dir).unwrap();
1418 unsafe {
1420 env::set_var("LOG_INDEX_DIR", &fake_out_dir);
1421 }
1422
1423 let _ = cu29_intern_strs::intern_string("unused to start counter");
1425 let _ = cu29_intern_strs::intern_string("Just a String {}");
1426 let _ = cu29_intern_strs::intern_string("Just a String (low level) {}");
1427 let _ = cu29_intern_strs::intern_string("Just a String (end to end) {}");
1428
1429 let index_dir = cu29_intern_strs::default_log_index_dir();
1430 cu29_intern_strs::read_interned_strings(&index_dir).unwrap();
1431 index_dir
1432 }
1433
1434 #[test]
1435 fn test_extract_low_level_cu29_log() {
1436 let temp_dir = TempDir::new().unwrap();
1437 let temp_path = copy_stringindex_to_temp(&temp_dir);
1438 let entry = CuLogEntry::new(3, CuLogLevel::Info);
1439 let bytes = bincode::encode_to_vec(&entry, standard()).unwrap();
1440 let reader = Cursor::new(bytes.as_slice());
1441 textlog_dump(reader, temp_path.as_path()).unwrap();
1442 }
1443
1444 #[test]
1445 fn end_to_end_datalogger_and_structlog_test() {
1446 let dir = tempdir().expect("Failed to create temp dir");
1447 let path = dir
1448 .path()
1449 .join("end_to_end_datalogger_and_structlog_test.copper");
1450 {
1451 let UnifiedLogger::Write(logger) = UnifiedLoggerBuilder::new()
1453 .write(true)
1454 .create(true)
1455 .file_base_name(&path)
1456 .preallocated_size(100000)
1457 .build()
1458 .expect("Failed to create logger")
1459 else {
1460 panic!("Failed to create logger")
1461 };
1462 let data_logger = Arc::new(Mutex::new(logger));
1463 let stream = stream_write(data_logger.clone(), UnifiedLogType::StructuredLogLine, 1024)
1464 .expect("Failed to create stream");
1465 let rt = LoggerRuntime::init(RobotClock::default(), stream, None::<NullLog>);
1466
1467 let mut entry = CuLogEntry::new(4, CuLogLevel::Info); entry.add_param(0, Value::String("Parameter for the log line".into()));
1469 log(&mut entry).expect("Failed to log");
1470 let mut entry = CuLogEntry::new(2, CuLogLevel::Info); entry.add_param(0, Value::String("Parameter for the log line".into()));
1472 log(&mut entry).expect("Failed to log");
1473
1474 drop(rt);
1476 }
1477 let UnifiedLogger::Read(logger) = UnifiedLoggerBuilder::new()
1479 .file_base_name(
1480 &dir.path()
1481 .join("end_to_end_datalogger_and_structlog_test.copper"),
1482 )
1483 .build()
1484 .expect("Failed to create logger")
1485 else {
1486 panic!("Failed to create logger")
1487 };
1488 let reader = UnifiedLoggerIOReader::new(logger, UnifiedLogType::StructuredLogLine);
1489 let temp_dir = TempDir::new().unwrap();
1490 textlog_dump(
1491 reader,
1492 Path::new(copy_stringindex_to_temp(&temp_dir).as_path()),
1493 )
1494 .expect("Failed to dump log");
1495 }
1496
1497 #[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize, Encode, Decode, Default)]
1499 struct MyMsgs((u8, i32, f32));
1500
1501 impl ErasedCuStampedDataSet for MyMsgs {
1502 fn cumsgs(&self) -> Vec<&dyn ErasedCuStampedData> {
1503 Vec::new()
1504 }
1505 }
1506
1507 impl MatchingTasks for MyMsgs {
1508 fn get_all_task_ids() -> &'static [&'static str] {
1509 &[]
1510 }
1511 }
1512
1513 #[test]
1515 fn test_copperlists_dump() {
1516 let mut data = vec![0u8; 10000];
1517 let mypls: [MyMsgs; 4] = [
1518 MyMsgs((1, 2, 3.0)),
1519 MyMsgs((2, 3, 4.0)),
1520 MyMsgs((3, 4, 5.0)),
1521 MyMsgs((4, 5, 6.0)),
1522 ];
1523
1524 let mut offset: usize = 0;
1525 for pl in mypls.iter() {
1526 let cl = CopperList::<MyMsgs>::new(1, *pl);
1527 offset +=
1528 encode_into_slice(&cl, &mut data.as_mut_slice()[offset..], standard()).unwrap();
1529 }
1530
1531 let reader = Cursor::new(data);
1532
1533 let mut iter = copperlists_reader::<MyMsgs>(reader);
1534 assert_eq!(iter.next().unwrap().msgs, MyMsgs((1, 2, 3.0)));
1535 assert_eq!(iter.next().unwrap().msgs, MyMsgs((2, 3, 4.0)));
1536 assert_eq!(iter.next().unwrap().msgs, MyMsgs((3, 4, 5.0)));
1537 assert_eq!(iter.next().unwrap().msgs, MyMsgs((4, 5, 6.0)));
1538 }
1539
1540 #[test]
1541 fn runtime_lifecycle_reader_extracts_started_mission() {
1542 let records = vec![
1543 RuntimeLifecycleRecord {
1544 timestamp: CuTime::default(),
1545 event: RuntimeLifecycleEvent::Instantiated {
1546 config_source: RuntimeLifecycleConfigSource::BundledDefault,
1547 effective_config_ron: "(missions: [])".to_string(),
1548 stack: RuntimeLifecycleStackInfo {
1549 app_name: "demo".to_string(),
1550 app_version: "0.1.0".to_string(),
1551 git_commit: None,
1552 git_dirty: None,
1553 subsystem_id: Some("ping".to_string()),
1554 subsystem_code: 7,
1555 instance_id: 42,
1556 },
1557 },
1558 },
1559 RuntimeLifecycleRecord {
1560 timestamp: CuTime::from_nanos(42),
1561 event: RuntimeLifecycleEvent::MissionStarted {
1562 mission: "gnss".to_string(),
1563 },
1564 },
1565 ];
1566 let mut bytes = Vec::new();
1567 for record in &records {
1568 bytes.extend(bincode::encode_to_vec(record, standard()).unwrap());
1569 }
1570
1571 let mission =
1572 runtime_lifecycle_reader(Cursor::new(bytes)).find_map(|entry| match entry.event {
1573 RuntimeLifecycleEvent::MissionStarted { mission } => Some(mission),
1574 _ => None,
1575 });
1576 assert_eq!(mission.as_deref(), Some("gnss"));
1577 }
1578
1579 #[test]
1580 fn logstats_mission_defaults_to_log_then_named_default_then_first() {
1581 let config = CuConfig::deserialize_ron(
1582 r#"(
1583 missions: [(id: "zeta"), (id: "default"), (id: "alpha")],
1584 tasks: [],
1585 cnx: [],
1586 )"#,
1587 )
1588 .unwrap();
1589 assert_eq!(
1590 resolve_logstats_mission(&config, None, Some("zeta".to_string())).as_deref(),
1591 Some("zeta")
1592 );
1593 assert_eq!(
1594 resolve_logstats_mission(&config, None, Some("missing".to_string())).as_deref(),
1595 Some("default")
1596 );
1597 assert_eq!(
1598 resolve_logstats_mission(&config, Some("alpha".to_string()), None).as_deref(),
1599 Some("alpha")
1600 );
1601
1602 let config = CuConfig::deserialize_ron(
1603 r#"(
1604 missions: [(id: "zeta"), (id: "alpha")],
1605 tasks: [],
1606 cnx: [],
1607 )"#,
1608 )
1609 .unwrap();
1610 assert_eq!(
1611 resolve_logstats_mission(&config, None, None).as_deref(),
1612 Some("alpha")
1613 );
1614 }
1615}