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