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