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