1mod fsck;
2pub mod logstats;
3
4#[cfg(feature = "mcap")]
5pub mod mcap_export;
6
7#[cfg(feature = "mcap")]
8pub mod serde_to_jsonschema;
9
10use bincode::Decode;
11use bincode::config::standard;
12use bincode::decode_from_std_read;
13use bincode::error::DecodeError;
14use clap::{Parser, Subcommand, ValueEnum};
15use cu29::UnifiedLogType;
16use cu29::prelude::*;
17use cu29_intern_strs::read_interned_strings;
18use fsck::check;
19#[cfg(feature = "mcap")]
20use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
21use logstats::{compute_logstats, write_logstats};
22use serde::Serialize;
23use std::fmt::{Display, Formatter};
24#[cfg(feature = "mcap")]
25use std::io::IsTerminal;
26use std::io::Read;
27use std::path::{Path, PathBuf};
28
29#[cfg(feature = "mcap")]
30pub use mcap_export::{
31 McapExportStats, PayloadSchemas, export_to_mcap, export_to_mcap_with_schemas, mcap_info,
32};
33
34#[cfg(feature = "mcap")]
35pub use serde_to_jsonschema::trace_type_to_jsonschema;
36
37#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
38pub enum ExportFormat {
39 Json,
40 Csv,
41}
42
43impl Display for ExportFormat {
44 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
45 match self {
46 ExportFormat::Json => write!(f, "json"),
47 ExportFormat::Csv => write!(f, "csv"),
48 }
49 }
50}
51
52#[derive(Parser)]
54#[command(author, version, about)]
55pub struct LogReaderCli {
56 pub unifiedlog_base: PathBuf,
59
60 #[command(subcommand)]
61 pub command: Command,
62}
63
64#[derive(Subcommand)]
65pub enum Command {
66 ExtractTextLog { log_index: PathBuf },
68 ExtractCopperlists {
70 #[arg(short, long, default_value_t = ExportFormat::Json)]
71 export_format: ExportFormat,
72 },
73 Fsck {
75 #[arg(short, long, action = clap::ArgAction::Count)]
76 verbose: u8,
77 },
78 LogStats {
80 #[arg(short, long, default_value = "cu29_logstats.json")]
82 output: PathBuf,
83 #[arg(long, default_value = "copperconfig.ron")]
85 config: PathBuf,
86 #[arg(long)]
88 mission: Option<String>,
89 },
90 #[cfg(feature = "mcap")]
92 ExportMcap {
93 #[arg(short, long)]
95 output: PathBuf,
96 #[arg(long)]
98 progress: bool,
99 #[arg(long)]
101 quiet: bool,
102 },
103 #[cfg(feature = "mcap")]
105 McapInfo {
106 mcap_file: PathBuf,
108 #[arg(short, long)]
110 schemas: bool,
111 #[arg(short = 'n', long, default_value_t = 0)]
113 sample_messages: usize,
114 },
115}
116
117fn write_json_pretty<T: Serialize + ?Sized>(value: &T) -> CuResult<()> {
118 serde_json::to_writer_pretty(std::io::stdout(), value)
119 .map_err(|e| CuError::new_with_cause("Failed to write JSON output", e))
120}
121
122fn write_json<T: Serialize + ?Sized>(value: &T) -> CuResult<()> {
123 serde_json::to_writer(std::io::stdout(), value)
124 .map_err(|e| CuError::new_with_cause("Failed to write JSON output", e))
125}
126
127fn build_read_logger(unifiedlog_base: &Path) -> CuResult<UnifiedLoggerRead> {
128 let logger = UnifiedLoggerBuilder::new()
129 .file_base_name(unifiedlog_base)
130 .build()
131 .map_err(|e| CuError::new_with_cause("Failed to create logger", e))?;
132 match logger {
133 UnifiedLogger::Read(dl) => Ok(dl),
134 UnifiedLogger::Write(_) => Err(CuError::from(
135 "Expected read-only unified logger in export CLI",
136 )),
137 }
138}
139
140#[cfg(feature = "mcap")]
145pub fn run_cli<P>() -> CuResult<()>
146where
147 P: CopperListTuple + CuPayloadRawBytes + mcap_export::PayloadSchemas,
148{
149 run_cli_inner::<P>()
150}
151
152#[cfg(not(feature = "mcap"))]
155pub fn run_cli<P>() -> CuResult<()>
156where
157 P: CopperListTuple + CuPayloadRawBytes,
158{
159 run_cli_inner::<P>()
160}
161
162#[cfg(feature = "mcap")]
163fn run_cli_inner<P>() -> CuResult<()>
164where
165 P: CopperListTuple + CuPayloadRawBytes + mcap_export::PayloadSchemas,
166{
167 let args = LogReaderCli::parse();
168 let unifiedlog_base = args.unifiedlog_base;
169
170 let mut dl = build_read_logger(&unifiedlog_base)?;
171
172 match args.command {
173 Command::ExtractTextLog { log_index } => {
174 let reader = UnifiedLoggerIOReader::new(dl, UnifiedLogType::StructuredLogLine);
175 textlog_dump(reader, &log_index)?;
176 }
177 Command::ExtractCopperlists { export_format } => {
178 println!("Extracting copperlists with format: {export_format}");
179 let mut reader = UnifiedLoggerIOReader::new(dl, UnifiedLogType::CopperList);
180 let iter = copperlists_reader::<P>(&mut reader);
181
182 match export_format {
183 ExportFormat::Json => {
184 for entry in iter {
185 write_json_pretty(&entry)?;
186 }
187 }
188 ExportFormat::Csv => {
189 let mut first = true;
190 for origin in P::get_all_task_ids() {
191 if !first {
192 print!(", ");
193 } else {
194 print!("id, ");
195 }
196 print!("{origin}_time, {origin}_tov, {origin},");
197 first = false;
198 }
199 println!();
200 for entry in iter {
201 let mut first = true;
202 for msg in entry.cumsgs() {
203 if let Some(payload) = msg.payload() {
204 if !first {
205 print!(", ");
206 } else {
207 print!("{}, ", entry.id);
208 }
209 let metadata = msg.metadata();
210 print!("{}, {}, ", metadata.process_time(), msg.tov());
211 write_json(payload)?; first = false;
213 }
214 }
215 println!();
216 }
217 }
218 }
219 }
220 Command::Fsck { verbose } => {
221 if let Some(value) = check::<P>(&mut dl, verbose) {
222 return value;
223 }
224 }
225 Command::LogStats {
226 output,
227 config,
228 mission,
229 } => {
230 run_logstats::<P>(dl, output, config, mission)?;
231 }
232 #[cfg(feature = "mcap")]
233 Command::ExportMcap {
234 output,
235 progress,
236 quiet,
237 } => {
238 println!("Exporting copperlists to MCAP format: {}", output.display());
239
240 let show_progress = should_show_progress(progress, quiet);
241 let total_bytes = if show_progress {
242 Some(copperlist_total_bytes(&unifiedlog_base)?)
243 } else {
244 None
245 };
246
247 let reader = UnifiedLoggerIOReader::new(dl, UnifiedLogType::CopperList);
248
249 let stats = if let Some(total_bytes) = total_bytes {
252 let progress_bar = make_progress_bar(total_bytes);
253 let reader = ProgressReader::new(reader, progress_bar.clone());
254 let result = export_to_mcap_impl::<P>(reader, &output);
255 progress_bar.finish_and_clear();
256 result?
257 } else {
258 export_to_mcap_impl::<P>(reader, &output)?
259 };
260 println!("{stats}");
261 }
262 #[cfg(feature = "mcap")]
263 Command::McapInfo {
264 mcap_file,
265 schemas,
266 sample_messages,
267 } => {
268 mcap_info(&mcap_file, schemas, sample_messages)?;
269 }
270 }
271
272 Ok(())
273}
274
275#[cfg(not(feature = "mcap"))]
276fn run_cli_inner<P>() -> CuResult<()>
277where
278 P: CopperListTuple + CuPayloadRawBytes,
279{
280 let args = LogReaderCli::parse();
281 let unifiedlog_base = args.unifiedlog_base;
282
283 let mut dl = build_read_logger(&unifiedlog_base)?;
284
285 match args.command {
286 Command::ExtractTextLog { log_index } => {
287 let reader = UnifiedLoggerIOReader::new(dl, UnifiedLogType::StructuredLogLine);
288 textlog_dump(reader, &log_index)?;
289 }
290 Command::ExtractCopperlists { export_format } => {
291 println!("Extracting copperlists with format: {export_format}");
292 let mut reader = UnifiedLoggerIOReader::new(dl, UnifiedLogType::CopperList);
293 let iter = copperlists_reader::<P>(&mut reader);
294
295 match export_format {
296 ExportFormat::Json => {
297 for entry in iter {
298 write_json_pretty(&entry)?;
299 }
300 }
301 ExportFormat::Csv => {
302 let mut first = true;
303 for origin in P::get_all_task_ids() {
304 if !first {
305 print!(", ");
306 } else {
307 print!("id, ");
308 }
309 print!("{origin}_time, {origin}_tov, {origin},");
310 first = false;
311 }
312 println!();
313 for entry in iter {
314 let mut first = true;
315 for msg in entry.cumsgs() {
316 if let Some(payload) = msg.payload() {
317 if !first {
318 print!(", ");
319 } else {
320 print!("{}, ", entry.id);
321 }
322 let metadata = msg.metadata();
323 print!("{}, {}, ", metadata.process_time(), msg.tov());
324 write_json(payload)?;
325 first = false;
326 }
327 }
328 println!();
329 }
330 }
331 }
332 }
333 Command::Fsck { verbose } => {
334 if let Some(value) = check::<P>(&mut dl, verbose) {
335 return value;
336 }
337 }
338 Command::LogStats {
339 output,
340 config,
341 mission,
342 } => {
343 run_logstats::<P>(dl, output, config, mission)?;
344 }
345 }
346
347 Ok(())
348}
349
350fn run_logstats<P>(
351 dl: UnifiedLoggerRead,
352 output: PathBuf,
353 config: PathBuf,
354 mission: Option<String>,
355) -> CuResult<()>
356where
357 P: CopperListTuple + CuPayloadRawBytes,
358{
359 let config_path = config
360 .to_str()
361 .ok_or_else(|| CuError::from("Config path is not valid UTF-8"))?;
362 let cfg = read_configuration(config_path)
363 .map_err(|e| CuError::new_with_cause("Failed to read configuration", e))?;
364 let reader = UnifiedLoggerIOReader::new(dl, UnifiedLogType::CopperList);
365 let stats = compute_logstats::<P>(reader, &cfg, mission.as_deref())?;
366 write_logstats(&stats, &output)
367}
368
369#[cfg(feature = "mcap")]
373fn export_to_mcap_impl<P>(src: impl Read, output: &Path) -> CuResult<McapExportStats>
374where
375 P: CopperListTuple + mcap_export::PayloadSchemas,
376{
377 mcap_export::export_to_mcap::<P, _>(src, output)
378}
379
380#[cfg(feature = "mcap")]
381struct ProgressReader<R> {
382 inner: R,
383 progress: ProgressBar,
384}
385
386#[cfg(feature = "mcap")]
387impl<R> ProgressReader<R> {
388 fn new(inner: R, progress: ProgressBar) -> Self {
389 Self { inner, progress }
390 }
391}
392
393#[cfg(feature = "mcap")]
394impl<R: Read> Read for ProgressReader<R> {
395 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
396 let read = self.inner.read(buf)?;
397 if read > 0 {
398 self.progress.inc(read as u64);
399 }
400 Ok(read)
401 }
402}
403
404#[cfg(feature = "mcap")]
405fn make_progress_bar(total_bytes: u64) -> ProgressBar {
406 let progress_bar = ProgressBar::new(total_bytes);
407 progress_bar.set_draw_target(ProgressDrawTarget::stderr_with_hz(5));
408
409 let style = ProgressStyle::with_template(
410 "[{elapsed_precise}] {bar:40} {bytes}/{total_bytes} ({bytes_per_sec}, ETA {eta})",
411 )
412 .unwrap_or_else(|_| ProgressStyle::default_bar());
413
414 progress_bar.set_style(style.progress_chars("=>-"));
415 progress_bar
416}
417
418#[cfg(feature = "mcap")]
419fn should_show_progress(force_progress: bool, quiet: bool) -> bool {
420 !quiet && (force_progress || std::io::stderr().is_terminal())
421}
422
423#[cfg(feature = "mcap")]
424fn copperlist_total_bytes(log_base: &Path) -> CuResult<u64> {
425 let mut reader = UnifiedLoggerRead::new(log_base)
426 .map_err(|e| CuError::new_with_cause("Failed to open log for progress estimation", e))?;
427 reader
428 .scan_section_bytes(UnifiedLogType::CopperList)
429 .map_err(|e| CuError::new_with_cause("Failed to scan log for progress estimation", e))
430}
431
432fn read_next_entry<T: Decode<()>>(src: &mut impl Read) -> Option<T> {
433 let entry = decode_from_std_read::<T, _, _>(src, standard());
434 match entry {
435 Ok(entry) => Some(entry),
436 Err(DecodeError::UnexpectedEnd { .. }) => None,
437 Err(DecodeError::Io { inner, additional }) => {
438 if inner.kind() == std::io::ErrorKind::UnexpectedEof {
439 None
440 } else {
441 println!("Error {inner:?} additional:{additional}");
442 None
443 }
444 }
445 Err(e) => {
446 println!("Error {e:?}");
447 None
448 }
449 }
450}
451
452pub fn copperlists_reader<P: CopperListTuple>(
455 mut src: impl Read,
456) -> impl Iterator<Item = CopperList<P>> {
457 std::iter::from_fn(move || read_next_entry::<CopperList<P>>(&mut src))
458}
459
460pub fn keyframes_reader(mut src: impl Read) -> impl Iterator<Item = KeyFrame> {
462 std::iter::from_fn(move || read_next_entry::<KeyFrame>(&mut src))
463}
464
465pub fn structlog_reader(mut src: impl Read) -> impl Iterator<Item = CuResult<CuLogEntry>> {
466 std::iter::from_fn(move || {
467 let entry = decode_from_std_read::<CuLogEntry, _, _>(&mut src, standard());
468
469 match entry {
470 Err(DecodeError::UnexpectedEnd { .. }) => None,
471 Err(DecodeError::Io {
472 inner,
473 additional: _,
474 }) => {
475 if inner.kind() == std::io::ErrorKind::UnexpectedEof {
476 None
477 } else {
478 Some(Err(CuError::new_with_cause("Error reading log", inner)))
479 }
480 }
481 Err(e) => Some(Err(CuError::new_with_cause("Error reading log", e))),
482 Ok(entry) => {
483 if entry.msg_index == 0 {
484 None
485 } else {
486 Some(Ok(entry))
487 }
488 }
489 }
490 })
491}
492
493pub fn textlog_dump(src: impl Read, index: &Path) -> CuResult<()> {
498 let all_strings = read_interned_strings(index).map_err(|e| {
499 CuError::new_with_cause(
500 "Failed to read interned strings from index",
501 std::io::Error::other(e),
502 )
503 })?;
504
505 for result in structlog_reader(src) {
506 match result {
507 Ok(entry) => match rebuild_logline(&all_strings, &entry) {
508 Ok(line) => println!("{line}"),
509 Err(e) => println!("Failed to rebuild log line: {e:?}"),
510 },
511 Err(e) => return Err(e),
512 }
513 }
514
515 Ok(())
516}
517
518#[cfg(all(feature = "python", not(target_os = "macos")))]
520mod python {
521 use bincode::config::standard;
522 use bincode::decode_from_std_read;
523 use bincode::error::DecodeError;
524 use cu29::prelude::*;
525 use cu29_intern_strs::read_interned_strings;
526 use pyo3::exceptions::PyIOError;
527 use pyo3::prelude::*;
528 use pyo3::types::{PyDelta, PyDict, PyList};
529 use std::io::Read;
530 use std::path::Path;
531
532 #[pyclass]
533 pub struct PyLogIterator {
534 reader: Box<dyn Read + Send + Sync>,
535 }
536
537 #[pymethods]
538 impl PyLogIterator {
539 fn __iter__(slf: PyRefMut<Self>) -> PyRefMut<Self> {
540 slf
541 }
542
543 fn __next__(mut slf: PyRefMut<Self>) -> Option<PyResult<PyCuLogEntry>> {
544 match decode_from_std_read::<CuLogEntry, _, _>(&mut slf.reader, standard()) {
545 Ok(entry) => {
546 if entry.msg_index == 0 {
547 None
548 } else {
549 Some(Ok(PyCuLogEntry { inner: entry }))
550 }
551 }
552 Err(DecodeError::UnexpectedEnd { .. }) => None,
553 Err(DecodeError::Io { inner, .. })
554 if inner.kind() == std::io::ErrorKind::UnexpectedEof =>
555 {
556 None
557 }
558 Err(e) => Some(Err(PyIOError::new_err(e.to_string()))),
559 }
560 }
561 }
562
563 #[pyfunction]
567 pub fn struct_log_iterator_bare(
568 bare_struct_src_path: &str,
569 index_path: &str,
570 ) -> PyResult<(PyLogIterator, Vec<String>)> {
571 let file = std::fs::File::open(bare_struct_src_path)
572 .map_err(|e| PyIOError::new_err(e.to_string()))?;
573 let all_strings = read_interned_strings(Path::new(index_path))
574 .map_err(|e| PyIOError::new_err(e.to_string()))?;
575 Ok((
576 PyLogIterator {
577 reader: Box::new(file),
578 },
579 all_strings,
580 ))
581 }
582 #[pyfunction]
586 pub fn struct_log_iterator_unified(
587 unified_src_path: &str,
588 index_path: &str,
589 ) -> PyResult<(PyLogIterator, Vec<String>)> {
590 let all_strings = read_interned_strings(Path::new(index_path))
591 .map_err(|e| PyIOError::new_err(e.to_string()))?;
592
593 let logger = UnifiedLoggerBuilder::new()
594 .file_base_name(Path::new(unified_src_path))
595 .build()
596 .map_err(|e| PyIOError::new_err(e.to_string()))?;
597 let dl = match logger {
598 UnifiedLogger::Read(dl) => dl,
599 UnifiedLogger::Write(_) => {
600 return Err(PyIOError::new_err(
601 "Expected read-only unified logger for Python export",
602 ));
603 }
604 };
605
606 let reader = UnifiedLoggerIOReader::new(dl, UnifiedLogType::StructuredLogLine);
607 Ok((
608 PyLogIterator {
609 reader: Box::new(reader),
610 },
611 all_strings,
612 ))
613 }
614
615 #[pyclass]
617 pub struct PyCuLogEntry {
618 pub inner: CuLogEntry,
619 }
620
621 #[pymethods]
622 impl PyCuLogEntry {
623 pub fn ts<'a>(&self, py: Python<'a>) -> PyResult<Bound<'a, PyDelta>> {
625 let nanoseconds: u64 = self.inner.time.into();
626
627 let days = (nanoseconds / 86_400_000_000_000) as i32;
629 let seconds = (nanoseconds / 1_000_000_000) as i32;
630 let microseconds = ((nanoseconds % 1_000_000_000) / 1_000) as i32;
631
632 PyDelta::new(py, days, seconds, microseconds, false)
633 }
634
635 pub fn msg_index(&self) -> u32 {
637 self.inner.msg_index
638 }
639
640 pub fn paramname_indexes(&self) -> Vec<u32> {
642 self.inner.paramname_indexes.iter().copied().collect()
643 }
644
645 pub fn params(&self, py: Python<'_>) -> PyResult<Vec<Py<PyAny>>> {
647 self.inner
648 .params
649 .iter()
650 .map(|value| value_to_py(value, py))
651 .collect()
652 }
653 }
654
655 #[pymodule(name = "libcu29_export")]
657 fn cu29_export(m: &Bound<'_, PyModule>) -> PyResult<()> {
658 m.add_class::<PyCuLogEntry>()?;
659 m.add_class::<PyLogIterator>()?;
660 m.add_function(wrap_pyfunction!(struct_log_iterator_bare, m)?)?;
661 m.add_function(wrap_pyfunction!(struct_log_iterator_unified, m)?)?;
662 Ok(())
663 }
664
665 fn value_to_py(value: &cu29::prelude::Value, py: Python<'_>) -> PyResult<Py<PyAny>> {
666 match value {
667 Value::String(s) => Ok(s.into_pyobject(py)?.into()),
668 Value::U64(u) => Ok(u.into_pyobject(py)?.into()),
669 Value::I64(i) => Ok(i.into_pyobject(py)?.into()),
670 Value::F64(f) => Ok(f.into_pyobject(py)?.into()),
671 Value::Bool(b) => Ok(b.into_pyobject(py)?.to_owned().into()),
672 Value::CuTime(t) => Ok(t.0.into_pyobject(py)?.into()),
673 Value::Bytes(b) => Ok(b.into_pyobject(py)?.into()),
674 Value::Char(c) => Ok(c.into_pyobject(py)?.into()),
675 Value::I8(i) => Ok(i.into_pyobject(py)?.into()),
676 Value::U8(u) => Ok(u.into_pyobject(py)?.into()),
677 Value::I16(i) => Ok(i.into_pyobject(py)?.into()),
678 Value::U16(u) => Ok(u.into_pyobject(py)?.into()),
679 Value::I32(i) => Ok(i.into_pyobject(py)?.into()),
680 Value::U32(u) => Ok(u.into_pyobject(py)?.into()),
681 Value::Map(m) => {
682 let dict = PyDict::new(py);
683 for (k, v) in m.iter() {
684 dict.set_item(value_to_py(k, py)?, value_to_py(v, py)?)?;
685 }
686 Ok(dict.into_pyobject(py)?.into())
687 }
688 Value::F32(f) => Ok(f.into_pyobject(py)?.into()),
689 Value::Option(o) => match o.as_ref() {
690 Some(value) => value_to_py(value, py),
691 None => Ok(py.None()),
692 },
693 Value::Unit => Ok(py.None()),
694 Value::Newtype(v) => value_to_py(v, py),
695 Value::Seq(s) => {
696 let items: Vec<Py<PyAny>> = s
697 .iter()
698 .map(|value| value_to_py(value, py))
699 .collect::<PyResult<_>>()?;
700 let list = PyList::new(py, items)?;
701 Ok(list.into_pyobject(py)?.into())
702 }
703 }
704 }
705}
706
707#[cfg(test)]
708mod tests {
709 use super::*;
710 use bincode::{Decode, Encode, encode_into_slice};
711 use serde::Deserialize;
712 use std::env;
713 use std::fs;
714 use std::io::Cursor;
715 use std::path::PathBuf;
716 use std::sync::{Arc, Mutex};
717 use tempfile::{TempDir, tempdir};
718
719 fn copy_stringindex_to_temp(tmpdir: &TempDir) -> PathBuf {
720 let fake_out_dir = tmpdir.path().join("build").join("out").join("dir");
722 fs::create_dir_all(&fake_out_dir).unwrap();
723 unsafe {
725 env::set_var("LOG_INDEX_DIR", &fake_out_dir);
726 }
727
728 let _ = cu29_intern_strs::intern_string("unused to start counter");
730 let _ = cu29_intern_strs::intern_string("Just a String {}");
731 let _ = cu29_intern_strs::intern_string("Just a String (low level) {}");
732 let _ = cu29_intern_strs::intern_string("Just a String (end to end) {}");
733
734 let index_dir = cu29_intern_strs::default_log_index_dir();
735 cu29_intern_strs::read_interned_strings(&index_dir).unwrap();
736 index_dir
737 }
738
739 #[test]
740 fn test_extract_low_level_cu29_log() {
741 let temp_dir = TempDir::new().unwrap();
742 let temp_path = copy_stringindex_to_temp(&temp_dir);
743 let entry = CuLogEntry::new(3, CuLogLevel::Info);
744 let bytes = bincode::encode_to_vec(&entry, standard()).unwrap();
745 let reader = Cursor::new(bytes.as_slice());
746 textlog_dump(reader, temp_path.as_path()).unwrap();
747 }
748
749 #[test]
750 fn end_to_end_datalogger_and_structlog_test() {
751 let dir = tempdir().expect("Failed to create temp dir");
752 let path = dir
753 .path()
754 .join("end_to_end_datalogger_and_structlog_test.copper");
755 {
756 let UnifiedLogger::Write(logger) = UnifiedLoggerBuilder::new()
758 .write(true)
759 .create(true)
760 .file_base_name(&path)
761 .preallocated_size(100000)
762 .build()
763 .expect("Failed to create logger")
764 else {
765 panic!("Failed to create logger")
766 };
767 let data_logger = Arc::new(Mutex::new(logger));
768 let stream = stream_write(data_logger.clone(), UnifiedLogType::StructuredLogLine, 1024)
769 .expect("Failed to create stream");
770 let rt = LoggerRuntime::init(RobotClock::default(), stream, None::<NullLog>);
771
772 let mut entry = CuLogEntry::new(4, CuLogLevel::Info); entry.add_param(0, Value::String("Parameter for the log line".into()));
774 log(&mut entry).expect("Failed to log");
775 let mut entry = CuLogEntry::new(2, CuLogLevel::Info); entry.add_param(0, Value::String("Parameter for the log line".into()));
777 log(&mut entry).expect("Failed to log");
778
779 drop(rt);
781 }
782 let UnifiedLogger::Read(logger) = UnifiedLoggerBuilder::new()
784 .file_base_name(
785 &dir.path()
786 .join("end_to_end_datalogger_and_structlog_test.copper"),
787 )
788 .build()
789 .expect("Failed to create logger")
790 else {
791 panic!("Failed to create logger")
792 };
793 let reader = UnifiedLoggerIOReader::new(logger, UnifiedLogType::StructuredLogLine);
794 let temp_dir = TempDir::new().unwrap();
795 textlog_dump(
796 reader,
797 Path::new(copy_stringindex_to_temp(&temp_dir).as_path()),
798 )
799 .expect("Failed to dump log");
800 }
801
802 #[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize, Encode, Decode, Default)]
804 struct MyMsgs((u8, i32, f32));
805
806 impl ErasedCuStampedDataSet for MyMsgs {
807 fn cumsgs(&self) -> Vec<&dyn ErasedCuStampedData> {
808 Vec::new()
809 }
810 }
811
812 impl MatchingTasks for MyMsgs {
813 fn get_all_task_ids() -> &'static [&'static str] {
814 &[]
815 }
816 }
817
818 #[test]
820 fn test_copperlists_dump() {
821 let mut data = vec![0u8; 10000];
822 let mypls: [MyMsgs; 4] = [
823 MyMsgs((1, 2, 3.0)),
824 MyMsgs((2, 3, 4.0)),
825 MyMsgs((3, 4, 5.0)),
826 MyMsgs((4, 5, 6.0)),
827 ];
828
829 let mut offset: usize = 0;
830 for pl in mypls.iter() {
831 let cl = CopperList::<MyMsgs>::new(1, *pl);
832 offset +=
833 encode_into_slice(&cl, &mut data.as_mut_slice()[offset..], standard()).unwrap();
834 }
835
836 let reader = Cursor::new(data);
837
838 let mut iter = copperlists_reader::<MyMsgs>(reader);
839 assert_eq!(iter.next().unwrap().msgs, MyMsgs((1, 2, 3.0)));
840 assert_eq!(iter.next().unwrap().msgs, MyMsgs((2, 3, 4.0)));
841 assert_eq!(iter.next().unwrap().msgs, MyMsgs((3, 4, 5.0)));
842 assert_eq!(iter.next().unwrap().msgs, MyMsgs((4, 5, 6.0)));
843 }
844}