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 paramname_indexes(&self) -> Vec<u32> {
870 self.inner.paramname_indexes.iter().copied().collect()
871 }
872
873 pub fn params(&self, py: Python<'_>) -> PyResult<Vec<Py<PyAny>>> {
875 self.inner
876 .params
877 .iter()
878 .map(|value| value_to_py(value, py))
879 .collect()
880 }
881 }
882
883 #[pymodule(name = "libcu29_export")]
885 fn cu29_export(m: &Bound<'_, PyModule>) -> PyResult<()> {
886 m.add_class::<PyCuLogEntry>()?;
887 m.add_class::<PyLogIterator>()?;
888 m.add_class::<PyCopperListIterator>()?;
889 m.add_class::<PyRuntimeLifecycleIterator>()?;
890 m.add_class::<PyUnitValue>()?;
891 m.add_function(wrap_pyfunction!(struct_log_iterator_bare, m)?)?;
892 m.add_function(wrap_pyfunction!(struct_log_iterator_unified, m)?)?;
893 m.add_function(wrap_pyfunction!(copperlist_iterator_unified, m)?)?;
894 m.add_function(wrap_pyfunction!(runtime_lifecycle_iterator_unified, m)?)?;
895 Ok(())
896 }
897
898 fn decode_next_copperlist<P>(
899 reader: &mut Box<dyn Read + Send + Sync>,
900 py: Python<'_>,
901 ) -> Option<PyResult<Py<PyAny>>>
902 where
903 P: CopperListTuple,
904 {
905 let entry = super::read_next_entry::<CopperList<P>>(reader)?;
906 Some(copperlist_to_py::<P>(&entry, py))
907 }
908
909 fn copperlist_to_py<P>(entry: &CopperList<P>, py: Python<'_>) -> PyResult<Py<PyAny>>
910 where
911 P: CopperListTuple,
912 {
913 let task_ids = P::get_all_task_ids();
914 let root = PyDict::new(py);
915 root.set_item("id", entry.id)?;
916 root.set_item("state", entry.get_state().to_string())?;
917
918 let mut messages: Vec<Py<PyAny>> = Vec::new();
919 for (idx, msg) in entry.cumsgs().into_iter().enumerate() {
920 let message = PyDict::new(py);
921 message.set_item("task_id", task_ids.get(idx).copied().unwrap_or("unknown"))?;
922 message.set_item("tov", tov_to_py(msg.tov(), py)?)?;
923 message.set_item("metadata", metadata_to_py(msg.metadata(), py)?)?;
924 match msg.payload_reflect() {
925 Some(payload) => message.set_item(
926 "payload",
927 partial_reflect_to_py(payload.as_partial_reflect(), py)?,
928 )?,
929 None => message.set_item("payload", py.None())?,
930 }
931 messages.push(dict_to_namespace(message, py)?);
932 }
933
934 root.set_item("messages", PyList::new(py, messages)?)?;
935 dict_to_namespace(root, py)
936 }
937
938 fn runtime_lifecycle_record_to_py(
939 entry: &RuntimeLifecycleRecord,
940 py: Python<'_>,
941 ) -> PyResult<Py<PyAny>> {
942 let root = PyDict::new(py);
943 root.set_item("timestamp_ns", entry.timestamp.as_nanos())?;
944 root.set_item("event", runtime_lifecycle_event_to_py(&entry.event, py)?)?;
945 dict_to_namespace(root, py)
946 }
947
948 fn runtime_lifecycle_event_to_py(
949 event: &RuntimeLifecycleEvent,
950 py: Python<'_>,
951 ) -> PyResult<Py<PyAny>> {
952 let root = PyDict::new(py);
953 match event {
954 RuntimeLifecycleEvent::Instantiated {
955 config_source,
956 effective_config_ron,
957 stack,
958 } => {
959 root.set_item("kind", "instantiated")?;
960 root.set_item("config_source", runtime_config_source_to_py(config_source))?;
961 root.set_item("effective_config_ron", effective_config_ron)?;
962
963 let stack_py = PyDict::new(py);
964 stack_py.set_item("app_name", &stack.app_name)?;
965 stack_py.set_item("app_version", &stack.app_version)?;
966 stack_py.set_item("git_commit", &stack.git_commit)?;
967 stack_py.set_item("git_dirty", stack.git_dirty)?;
968 stack_py.set_item("subsystem_id", &stack.subsystem_id)?;
969 stack_py.set_item("subsystem_code", stack.subsystem_code)?;
970 stack_py.set_item("instance_id", stack.instance_id)?;
971 root.set_item("stack", dict_to_namespace(stack_py, py)?)?;
972 }
973 RuntimeLifecycleEvent::MissionStarted { mission } => {
974 root.set_item("kind", "mission_started")?;
975 root.set_item("mission", mission)?;
976 }
977 RuntimeLifecycleEvent::MissionStopped { mission, reason } => {
978 root.set_item("kind", "mission_stopped")?;
979 root.set_item("mission", mission)?;
980 root.set_item("reason", reason)?;
981 }
982 RuntimeLifecycleEvent::Panic {
983 message,
984 file,
985 line,
986 column,
987 } => {
988 root.set_item("kind", "panic")?;
989 root.set_item("message", message)?;
990 root.set_item("file", file)?;
991 root.set_item("line", line)?;
992 root.set_item("column", column)?;
993 }
994 RuntimeLifecycleEvent::ShutdownCompleted => {
995 root.set_item("kind", "shutdown_completed")?;
996 }
997 }
998
999 dict_to_namespace(root, py)
1000 }
1001
1002 fn runtime_config_source_to_py(source: &RuntimeLifecycleConfigSource) -> &'static str {
1003 match source {
1004 RuntimeLifecycleConfigSource::ProgrammaticOverride => "programmatic_override",
1005 RuntimeLifecycleConfigSource::ExternalFile => "external_file",
1006 RuntimeLifecycleConfigSource::BundledDefault => "bundled_default",
1007 }
1008 }
1009
1010 fn metadata_to_py(metadata: &dyn CuMsgMetadataTrait, py: Python<'_>) -> PyResult<Py<PyAny>> {
1011 let process = metadata.process_time();
1012 let start: Option<CuTime> = process.start.into();
1013 let end: Option<CuTime> = process.end.into();
1014
1015 let process_time = PyDict::new(py);
1016 process_time.set_item("start_ns", start.map(|t| t.as_nanos()))?;
1017 process_time.set_item("end_ns", end.map(|t| t.as_nanos()))?;
1018
1019 let metadata_py = PyDict::new(py);
1020 metadata_py.set_item("process_time", dict_to_namespace(process_time, py)?)?;
1021 metadata_py.set_item("status_txt", metadata.status_txt().0.to_string())?;
1022 if let Some(origin) = metadata.origin() {
1023 let origin_py = PyDict::new(py);
1024 origin_py.set_item("subsystem_code", origin.subsystem_code)?;
1025 origin_py.set_item("instance_id", origin.instance_id)?;
1026 origin_py.set_item("cl_id", origin.cl_id)?;
1027 metadata_py.set_item("origin", dict_to_namespace(origin_py, py)?)?;
1028 } else {
1029 metadata_py.set_item("origin", py.None())?;
1030 }
1031 dict_to_namespace(metadata_py, py)
1032 }
1033
1034 fn tov_to_py(tov: Tov, py: Python<'_>) -> PyResult<Py<PyAny>> {
1035 let tov_py = PyDict::new(py);
1036 match tov {
1037 Tov::None => {
1038 tov_py.set_item("kind", "none")?;
1039 }
1040 Tov::Time(t) => {
1041 tov_py.set_item("kind", "time")?;
1042 tov_py.set_item("time_ns", t.as_nanos())?;
1043 }
1044 Tov::Range(r) => {
1045 tov_py.set_item("kind", "range")?;
1046 tov_py.set_item("start_ns", r.start.as_nanos())?;
1047 tov_py.set_item("end_ns", r.end.as_nanos())?;
1048 }
1049 }
1050 dict_to_namespace(tov_py, py)
1051 }
1052
1053 fn partial_reflect_to_py(value: &dyn PartialReflect, py: Python<'_>) -> PyResult<Py<PyAny>> {
1054 #[allow(unreachable_patterns)]
1055 match value.reflect_ref() {
1056 ReflectRef::Struct(s) => struct_to_py(s, py),
1057 ReflectRef::TupleStruct(ts) => tuple_struct_to_py(ts, py),
1058 ReflectRef::Tuple(t) => tuple_to_py(t, py),
1059 ReflectRef::List(list) => list_to_py(list, py),
1060 ReflectRef::Array(array) => array_to_py(array, py),
1061 ReflectRef::Map(map) => map_to_py(map, py),
1062 ReflectRef::Set(set) => set_to_py(set, py),
1063 ReflectRef::Enum(e) => enum_to_py(e, py),
1064 ReflectRef::Opaque(opaque) => opaque_to_py(opaque, py),
1065 _ => Ok(py.None()),
1066 }
1067 }
1068
1069 fn struct_to_py(value: &dyn cu29::bevy_reflect::Struct, py: Python<'_>) -> PyResult<Py<PyAny>> {
1070 let dict = PyDict::new(py);
1071 for idx in 0..value.field_len() {
1072 if let Some(field) = value.field_at(idx) {
1073 let name = value
1074 .name_at(idx)
1075 .map(str::to_owned)
1076 .unwrap_or_else(|| format!("field_{idx}"));
1077 dict.set_item(name, partial_reflect_to_py(field, py)?)?;
1078 }
1079 }
1080
1081 if let Some(unit) = unit_abbrev_for_type_path(value.reflect_type_path())
1082 && let Some(raw_value) = dict.get_item("value")?
1083 {
1084 if let Ok(v) = raw_value.extract::<f64>() {
1085 let unit_value = PyUnitValue {
1086 value: v,
1087 unit: unit.to_string(),
1088 };
1089 return Ok(Py::new(py, unit_value)?.into());
1090 }
1091 if let Ok(v) = raw_value.extract::<f32>() {
1092 let unit_value = PyUnitValue {
1093 value: v as f64,
1094 unit: unit.to_string(),
1095 };
1096 return Ok(Py::new(py, unit_value)?.into());
1097 }
1098 }
1099
1100 dict_to_namespace(dict, py)
1101 }
1102
1103 fn tuple_struct_to_py(
1104 value: &dyn cu29::bevy_reflect::TupleStruct,
1105 py: Python<'_>,
1106 ) -> PyResult<Py<PyAny>> {
1107 let mut fields = Vec::with_capacity(value.field_len());
1108 for idx in 0..value.field_len() {
1109 if let Some(field) = value.field(idx) {
1110 fields.push(partial_reflect_to_py(field, py)?);
1111 } else {
1112 fields.push(py.None());
1113 }
1114 }
1115 Ok(PyList::new(py, fields)?.into_pyobject(py)?.into())
1116 }
1117
1118 fn tuple_to_py(value: &dyn cu29::bevy_reflect::Tuple, py: Python<'_>) -> PyResult<Py<PyAny>> {
1119 let mut fields = Vec::with_capacity(value.field_len());
1120 for idx in 0..value.field_len() {
1121 if let Some(field) = value.field(idx) {
1122 fields.push(partial_reflect_to_py(field, py)?);
1123 } else {
1124 fields.push(py.None());
1125 }
1126 }
1127 Ok(PyList::new(py, fields)?.into_pyobject(py)?.into())
1128 }
1129
1130 fn list_to_py(value: &dyn cu29::bevy_reflect::List, py: Python<'_>) -> PyResult<Py<PyAny>> {
1131 let mut items = Vec::with_capacity(value.len());
1132 for item in value.iter() {
1133 items.push(partial_reflect_to_py(item, py)?);
1134 }
1135 Ok(PyList::new(py, items)?.into_pyobject(py)?.into())
1136 }
1137
1138 fn array_to_py(value: &dyn cu29::bevy_reflect::Array, py: Python<'_>) -> PyResult<Py<PyAny>> {
1139 let mut items = Vec::with_capacity(value.len());
1140 for item in value.iter() {
1141 items.push(partial_reflect_to_py(item, py)?);
1142 }
1143 Ok(PyList::new(py, items)?.into_pyobject(py)?.into())
1144 }
1145
1146 fn map_to_py(value: &dyn cu29::bevy_reflect::Map, py: Python<'_>) -> PyResult<Py<PyAny>> {
1147 let dict = PyDict::new(py);
1148 for (key, val) in value.iter() {
1149 let key_str = reflect_key_to_string(key);
1150 dict.set_item(key_str, partial_reflect_to_py(val, py)?)?;
1151 }
1152 Ok(dict.into_pyobject(py)?.into())
1153 }
1154
1155 fn set_to_py(value: &dyn cu29::bevy_reflect::Set, py: Python<'_>) -> PyResult<Py<PyAny>> {
1156 let mut items = Vec::with_capacity(value.len());
1157 for item in value.iter() {
1158 items.push(partial_reflect_to_py(item, py)?);
1159 }
1160 Ok(PyList::new(py, items)?.into_pyobject(py)?.into())
1161 }
1162
1163 fn enum_to_py(value: &dyn cu29::bevy_reflect::Enum, py: Python<'_>) -> PyResult<Py<PyAny>> {
1164 let dict = PyDict::new(py);
1165 dict.set_item("variant", value.variant_name())?;
1166
1167 match value.variant_type() {
1168 VariantType::Unit => {}
1169 VariantType::Tuple => {
1170 let mut fields = Vec::with_capacity(value.field_len());
1171 for idx in 0..value.field_len() {
1172 if let Some(field) = value.field_at(idx) {
1173 fields.push(partial_reflect_to_py(field, py)?);
1174 } else {
1175 fields.push(py.None());
1176 }
1177 }
1178 dict.set_item("fields", PyList::new(py, fields)?)?;
1179 }
1180 VariantType::Struct => {
1181 let fields = PyDict::new(py);
1182 for idx in 0..value.field_len() {
1183 if let Some(field) = value.field_at(idx) {
1184 let name = value
1185 .name_at(idx)
1186 .map(str::to_owned)
1187 .unwrap_or_else(|| format!("field_{idx}"));
1188 fields.set_item(name, partial_reflect_to_py(field, py)?)?;
1189 }
1190 }
1191 dict.set_item("fields", fields)?;
1192 }
1193 }
1194
1195 dict_to_namespace(dict, py)
1196 }
1197
1198 fn dict_to_namespace(dict: Bound<'_, PyDict>, py: Python<'_>) -> PyResult<Py<PyAny>> {
1199 let types = py.import("types")?;
1200 let namespace_ctor = types.getattr("SimpleNamespace")?;
1201 let namespace = namespace_ctor.call((), Some(&dict))?;
1202 Ok(namespace.into())
1203 }
1204
1205 fn reflect_key_to_string(value: &dyn PartialReflect) -> String {
1206 if let Some(v) = value.try_downcast_ref::<String>() {
1207 return v.clone();
1208 }
1209 if let Some(v) = value.try_downcast_ref::<&'static str>() {
1210 return (*v).to_string();
1211 }
1212 if let Some(v) = value.try_downcast_ref::<char>() {
1213 return v.to_string();
1214 }
1215 if let Some(v) = value.try_downcast_ref::<bool>() {
1216 return v.to_string();
1217 }
1218 if let Some(v) = value.try_downcast_ref::<u64>() {
1219 return v.to_string();
1220 }
1221 if let Some(v) = value.try_downcast_ref::<i64>() {
1222 return v.to_string();
1223 }
1224 if let Some(v) = value.try_downcast_ref::<usize>() {
1225 return v.to_string();
1226 }
1227 if let Some(v) = value.try_downcast_ref::<isize>() {
1228 return v.to_string();
1229 }
1230 format!("{value:?}")
1231 }
1232
1233 fn unit_abbrev_for_type_path(type_path: &str) -> Option<&'static str> {
1234 match type_path.rsplit("::").next()? {
1235 "Acceleration" => Some("m/s^2"),
1236 "Angle" => Some("rad"),
1237 "AngularVelocity" => Some("rad/s"),
1238 "ElectricPotential" => Some("V"),
1239 "Length" => Some("m"),
1240 "MagneticFluxDensity" => Some("T"),
1241 "Pressure" => Some("Pa"),
1242 "Ratio" => Some("1"),
1243 "ThermodynamicTemperature" => Some("K"),
1244 "Time" => Some("s"),
1245 "Velocity" => Some("m/s"),
1246 _ => None,
1247 }
1248 }
1249
1250 fn opaque_to_py(value: &dyn PartialReflect, py: Python<'_>) -> PyResult<Py<PyAny>> {
1251 macro_rules! downcast_copy {
1252 ($ty:ty) => {
1253 if let Some(v) = value.try_downcast_ref::<$ty>() {
1254 return Ok(v.into_pyobject(py)?.to_owned().into());
1255 }
1256 };
1257 }
1258
1259 downcast_copy!(bool);
1260 downcast_copy!(u8);
1261 downcast_copy!(u16);
1262 downcast_copy!(u32);
1263 downcast_copy!(u64);
1264 downcast_copy!(u128);
1265 downcast_copy!(usize);
1266 downcast_copy!(i8);
1267 downcast_copy!(i16);
1268 downcast_copy!(i32);
1269 downcast_copy!(i64);
1270 downcast_copy!(i128);
1271 downcast_copy!(isize);
1272 downcast_copy!(f32);
1273 downcast_copy!(f64);
1274 downcast_copy!(char);
1275
1276 if let Some(v) = value.try_downcast_ref::<String>() {
1277 return Ok(v.into_pyobject(py)?.into());
1278 }
1279 if let Some(v) = value.try_downcast_ref::<&'static str>() {
1280 return Ok(v.into_pyobject(py)?.into());
1281 }
1282 if let Some(v) = value.try_downcast_ref::<Vec<u8>>() {
1283 return Ok(v.into_pyobject(py)?.into());
1284 }
1285
1286 let fallback = format!("{value:?}");
1287 Ok(fallback.into_pyobject(py)?.into())
1288 }
1289 fn value_to_py(value: &cu29::prelude::Value, py: Python<'_>) -> PyResult<Py<PyAny>> {
1290 match value {
1291 Value::String(s) => Ok(s.into_pyobject(py)?.into()),
1292 Value::U64(u) => Ok(u.into_pyobject(py)?.into()),
1293 Value::U128(u) => Ok(u.into_pyobject(py)?.into()),
1294 Value::I64(i) => Ok(i.into_pyobject(py)?.into()),
1295 Value::I128(i) => Ok(i.into_pyobject(py)?.into()),
1296 Value::F64(f) => Ok(f.into_pyobject(py)?.into()),
1297 Value::Bool(b) => Ok(b.into_pyobject(py)?.to_owned().into()),
1298 Value::CuTime(t) => Ok(t.0.into_pyobject(py)?.into()),
1299 Value::Bytes(b) => Ok(b.into_pyobject(py)?.into()),
1300 Value::Char(c) => Ok(c.into_pyobject(py)?.into()),
1301 Value::I8(i) => Ok(i.into_pyobject(py)?.into()),
1302 Value::U8(u) => Ok(u.into_pyobject(py)?.into()),
1303 Value::I16(i) => Ok(i.into_pyobject(py)?.into()),
1304 Value::U16(u) => Ok(u.into_pyobject(py)?.into()),
1305 Value::I32(i) => Ok(i.into_pyobject(py)?.into()),
1306 Value::U32(u) => Ok(u.into_pyobject(py)?.into()),
1307 Value::Map(m) => {
1308 let dict = PyDict::new(py);
1309 for (k, v) in m.iter() {
1310 dict.set_item(value_to_py(k, py)?, value_to_py(v, py)?)?;
1311 }
1312 Ok(dict.into_pyobject(py)?.into())
1313 }
1314 Value::F32(f) => Ok(f.into_pyobject(py)?.into()),
1315 Value::Option(o) => match o.as_ref() {
1316 Some(value) => value_to_py(value, py),
1317 None => Ok(py.None()),
1318 },
1319 Value::Unit => Ok(py.None()),
1320 Value::Newtype(v) => value_to_py(v, py),
1321 Value::Seq(s) => {
1322 let items: Vec<Py<PyAny>> = s
1323 .iter()
1324 .map(|value| value_to_py(value, py))
1325 .collect::<PyResult<_>>()?;
1326 let list = PyList::new(py, items)?;
1327 Ok(list.into_pyobject(py)?.into())
1328 }
1329 }
1330 }
1331
1332 #[cfg(test)]
1333 mod tests {
1334 use super::*;
1335
1336 #[test]
1337 fn value_to_py_preserves_128_bit_integers() {
1338 Python::initialize();
1339 Python::attach(|py| {
1340 let u128_value = u128::from(u64::MAX) + 99;
1341 let u128_py = value_to_py(&Value::U128(u128_value), py).unwrap();
1342 assert_eq!(u128_py.bind(py).extract::<u128>().unwrap(), u128_value);
1343
1344 let i128_value = i128::from(i64::MIN) - 99;
1345 let i128_py = value_to_py(&Value::I128(i128_value), py).unwrap();
1346 assert_eq!(i128_py.bind(py).extract::<i128>().unwrap(), i128_value);
1347 });
1348 }
1349 }
1350}
1351
1352#[cfg(test)]
1353mod tests {
1354 use super::*;
1355 use bincode::{Decode, Encode, encode_into_slice};
1356 use serde::Deserialize;
1357 use std::env;
1358 use std::fs;
1359 use std::io::Cursor;
1360 use std::path::PathBuf;
1361 use std::sync::{Arc, Mutex};
1362 use tempfile::{TempDir, tempdir};
1363
1364 fn copy_stringindex_to_temp(tmpdir: &TempDir) -> PathBuf {
1365 let fake_out_dir = tmpdir.path().join("build").join("out").join("dir");
1367 fs::create_dir_all(&fake_out_dir).unwrap();
1368 unsafe {
1370 env::set_var("LOG_INDEX_DIR", &fake_out_dir);
1371 }
1372
1373 let _ = cu29_intern_strs::intern_string("unused to start counter");
1375 let _ = cu29_intern_strs::intern_string("Just a String {}");
1376 let _ = cu29_intern_strs::intern_string("Just a String (low level) {}");
1377 let _ = cu29_intern_strs::intern_string("Just a String (end to end) {}");
1378
1379 let index_dir = cu29_intern_strs::default_log_index_dir();
1380 cu29_intern_strs::read_interned_strings(&index_dir).unwrap();
1381 index_dir
1382 }
1383
1384 #[test]
1385 fn test_extract_low_level_cu29_log() {
1386 let temp_dir = TempDir::new().unwrap();
1387 let temp_path = copy_stringindex_to_temp(&temp_dir);
1388 let entry = CuLogEntry::new(3, CuLogLevel::Info);
1389 let bytes = bincode::encode_to_vec(&entry, standard()).unwrap();
1390 let reader = Cursor::new(bytes.as_slice());
1391 textlog_dump(reader, temp_path.as_path()).unwrap();
1392 }
1393
1394 #[test]
1395 fn end_to_end_datalogger_and_structlog_test() {
1396 let dir = tempdir().expect("Failed to create temp dir");
1397 let path = dir
1398 .path()
1399 .join("end_to_end_datalogger_and_structlog_test.copper");
1400 {
1401 let UnifiedLogger::Write(logger) = UnifiedLoggerBuilder::new()
1403 .write(true)
1404 .create(true)
1405 .file_base_name(&path)
1406 .preallocated_size(100000)
1407 .build()
1408 .expect("Failed to create logger")
1409 else {
1410 panic!("Failed to create logger")
1411 };
1412 let data_logger = Arc::new(Mutex::new(logger));
1413 let stream = stream_write(data_logger.clone(), UnifiedLogType::StructuredLogLine, 1024)
1414 .expect("Failed to create stream");
1415 let rt = LoggerRuntime::init(RobotClock::default(), stream, None::<NullLog>);
1416
1417 let mut entry = CuLogEntry::new(4, CuLogLevel::Info); entry.add_param(0, Value::String("Parameter for the log line".into()));
1419 log(&mut entry).expect("Failed to log");
1420 let mut entry = CuLogEntry::new(2, CuLogLevel::Info); entry.add_param(0, Value::String("Parameter for the log line".into()));
1422 log(&mut entry).expect("Failed to log");
1423
1424 drop(rt);
1426 }
1427 let UnifiedLogger::Read(logger) = UnifiedLoggerBuilder::new()
1429 .file_base_name(
1430 &dir.path()
1431 .join("end_to_end_datalogger_and_structlog_test.copper"),
1432 )
1433 .build()
1434 .expect("Failed to create logger")
1435 else {
1436 panic!("Failed to create logger")
1437 };
1438 let reader = UnifiedLoggerIOReader::new(logger, UnifiedLogType::StructuredLogLine);
1439 let temp_dir = TempDir::new().unwrap();
1440 textlog_dump(
1441 reader,
1442 Path::new(copy_stringindex_to_temp(&temp_dir).as_path()),
1443 )
1444 .expect("Failed to dump log");
1445 }
1446
1447 #[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize, Encode, Decode, Default)]
1449 struct MyMsgs((u8, i32, f32));
1450
1451 impl ErasedCuStampedDataSet for MyMsgs {
1452 fn cumsgs(&self) -> Vec<&dyn ErasedCuStampedData> {
1453 Vec::new()
1454 }
1455 }
1456
1457 impl MatchingTasks for MyMsgs {
1458 fn get_all_task_ids() -> &'static [&'static str] {
1459 &[]
1460 }
1461 }
1462
1463 #[test]
1465 fn test_copperlists_dump() {
1466 let mut data = vec![0u8; 10000];
1467 let mypls: [MyMsgs; 4] = [
1468 MyMsgs((1, 2, 3.0)),
1469 MyMsgs((2, 3, 4.0)),
1470 MyMsgs((3, 4, 5.0)),
1471 MyMsgs((4, 5, 6.0)),
1472 ];
1473
1474 let mut offset: usize = 0;
1475 for pl in mypls.iter() {
1476 let cl = CopperList::<MyMsgs>::new(1, *pl);
1477 offset +=
1478 encode_into_slice(&cl, &mut data.as_mut_slice()[offset..], standard()).unwrap();
1479 }
1480
1481 let reader = Cursor::new(data);
1482
1483 let mut iter = copperlists_reader::<MyMsgs>(reader);
1484 assert_eq!(iter.next().unwrap().msgs, MyMsgs((1, 2, 3.0)));
1485 assert_eq!(iter.next().unwrap().msgs, MyMsgs((2, 3, 4.0)));
1486 assert_eq!(iter.next().unwrap().msgs, MyMsgs((3, 4, 5.0)));
1487 assert_eq!(iter.next().unwrap().msgs, MyMsgs((4, 5, 6.0)));
1488 }
1489
1490 #[test]
1491 fn runtime_lifecycle_reader_extracts_started_mission() {
1492 let records = vec![
1493 RuntimeLifecycleRecord {
1494 timestamp: CuTime::default(),
1495 event: RuntimeLifecycleEvent::Instantiated {
1496 config_source: RuntimeLifecycleConfigSource::BundledDefault,
1497 effective_config_ron: "(missions: [])".to_string(),
1498 stack: RuntimeLifecycleStackInfo {
1499 app_name: "demo".to_string(),
1500 app_version: "0.1.0".to_string(),
1501 git_commit: None,
1502 git_dirty: None,
1503 subsystem_id: Some("ping".to_string()),
1504 subsystem_code: 7,
1505 instance_id: 42,
1506 },
1507 },
1508 },
1509 RuntimeLifecycleRecord {
1510 timestamp: CuTime::from_nanos(42),
1511 event: RuntimeLifecycleEvent::MissionStarted {
1512 mission: "gnss".to_string(),
1513 },
1514 },
1515 ];
1516 let mut bytes = Vec::new();
1517 for record in &records {
1518 bytes.extend(bincode::encode_to_vec(record, standard()).unwrap());
1519 }
1520
1521 let mission =
1522 runtime_lifecycle_reader(Cursor::new(bytes)).find_map(|entry| match entry.event {
1523 RuntimeLifecycleEvent::MissionStarted { mission } => Some(mission),
1524 _ => None,
1525 });
1526 assert_eq!(mission.as_deref(), Some("gnss"));
1527 }
1528}