Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
929693c
test: add regression tests for BagTransportReader panic detection
zhexuany Feb 26, 2026
745399e
test: add S3 authenticated upload helpers and BAG panic regression test
zhexuany Feb 26, 2026
186f835
fix: S3 URL format detection and BagTransportReader async handling
zhexuany Feb 26, 2026
74b3dc4
refactor: modularize s3_tests.rs into smaller files
zhexuany Feb 26, 2026
4eabf67
fmt code
zhexuany Feb 26, 2026
c0f3a41
fix: race condition in S3 RoboReader tests cleanup
zhexuany Feb 26, 2026
cdbf4a2
fix: MCAP streaming parser now handles CHUNK records with compression
zhexuany Feb 26, 2026
720add7
fix: lazy credential loading for S3 client
zhexuany Feb 26, 2026
e68f38b
fix: track per-channel message counts in BAG parser
zhexuany Feb 26, 2026
04e7898
feat: add StreamingRoboReader API for high-performance streaming
zhexuany Feb 27, 2026
31e7722
test: add format-specific tests for MCAP, BAG, and RRD
zhexuany Feb 27, 2026
4a85f2d
feat: finalize fail-fast S3 streaming correctness and guardrails
zhexuany Feb 27, 2026
d72b5e9
docs: fix streaming rustdoc examples to current API
zhexuany Feb 27, 2026
1dbb3a3
fix: address review feedback and clippy regressions
zhexuany Feb 27, 2026
7bffb62
test: gate strict S3 suites behind explicit CI flag
zhexuany Feb 27, 2026
dcf11f5
refactor: remove legacy transport readers, unify S3 gating under sing…
zhexuany Feb 28, 2026
f294749
test: add S3 streaming tests for MCAP and BAG formats
zhexuany Feb 28, 2026
ece7e82
test: add S3 frame alignment tests for AlignedFrame
zhexuany Feb 28, 2026
7eb9f70
ci: fix MinIO credential env var names; test: add comprehensive Align…
zhexuany Feb 28, 2026
2278ed5
fix: add MINIO_USER/MINIO_PASSWORD support to AwsCredentials::from_env
zhexuany Feb 28, 2026
fd91193
ci: add missing MINIO_USER and MINIO_PASSWORD env vars to S3 test step
zhexuany Feb 28, 2026
dcb3f75
lint: fix len_zero clippy warning
zhexuany Feb 28, 2026
9102280
ci: exclude S3 tests from general Rust test jobs (no MinIO)
zhexuany Feb 28, 2026
28a3c77
fix: gate remote-dependent tests by feature flag
zhexuany Feb 28, 2026
2c292ea
ci: merge S3 test workflow into main CI
zhexuany Feb 28, 2026
10ab53f
fix(tests): eliminate race condition in S3 streaming reader tests
zhexuany Feb 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 141 additions & 4 deletions src/io/formats/bag/transport_reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,10 +282,10 @@ impl FormatReader for BagTransportReader {
));
}
Poll::Pending => {
return Err(CodecError::encode(
"Transport",
"Unexpected pending from non-async transport".to_string(),
));
// Async transport returned pending - yield and retry
// This happens with S3Transport which performs network I/O
std::thread::yield_now();
continue;
}
}
}
Expand Down Expand Up @@ -656,4 +656,141 @@ mod tests {
let any_ref = reader.as_any_mut();
assert!(any_ref.downcast_ref::<BagTransportReader>().is_some());
}

/// Regression test: BagTransportReader::open_from_transport should not panic
///
/// This test verifies that opening a BAG file via the transport trait
/// does not panic. Previously, there was a panic in std::ops::function
/// when using certain transports.
#[test]
#[cfg(feature = "remote")]
fn test_bag_transport_reader_open_from_transport_no_panic() {
use crate::io::traits::FormatReader;
use crate::io::transport::memory::MemoryTransport;

// Get test fixture
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let fixture_path = manifest_dir.join("tests/fixtures/robocodec_test_15.bag");

if !fixture_path.exists() {
eprintln!("Skipping test: fixture not found");
return;
}

let data = std::fs::read(&fixture_path).unwrap();
let transport =
Box::new(MemoryTransport::new(data)) as Box<dyn crate::io::transport::Transport>;

// This should NOT panic - previously panicked at std::ops::function.rs:250:5
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
BagTransportReader::open_from_transport(transport, "test.bag".to_string())
}));

match result {
Ok(Ok(reader)) => {
assert_eq!(reader.format(), FileFormat::Bag);
assert!(reader.message_count() > 0, "Should have messages");
assert!(!reader.channels().is_empty(), "Should have channels");
}
Ok(Err(e)) => {
// Error is acceptable, panic is not
println!("Got expected error (not panic): {}", e);
}
Err(panic_info) => {
let panic_msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
(*s).to_string()
} else if let Some(s) = panic_info.downcast_ref::<String>() {
s.clone()
} else {
"Unknown panic".to_string()
};
panic!(
"BagTransportReader::open_from_transport panicked: {}",
panic_msg
);
}
}
}

/// Regression test: BagTransportReader::open_from_transport with empty data
///
/// Verifies that empty data is handled gracefully without panic.
#[test]
#[cfg(feature = "remote")]
fn test_bag_transport_reader_open_from_transport_empty_data() {
use crate::io::traits::FormatReader;
use crate::io::transport::memory::MemoryTransport;

let transport =
Box::new(MemoryTransport::new(vec![])) as Box<dyn crate::io::transport::Transport>;

// Should not panic with empty data
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
BagTransportReader::open_from_transport(transport, "empty.bag".to_string())
}));

match result {
Ok(Ok(reader)) => {
assert_eq!(reader.message_count(), 0);
}
Ok(Err(_)) => {
// Error is acceptable for empty data
}
Err(panic_info) => {
let panic_msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
(*s).to_string()
} else if let Some(s) = panic_info.downcast_ref::<String>() {
s.clone()
} else {
"Unknown panic".to_string()
};
panic!(
"BagTransportReader::open_from_transport panicked with empty data: {}",
panic_msg
);
}
}
}

/// Regression test: BagTransportReader::open_from_transport with invalid data
///
/// Verifies that invalid data is handled gracefully without panic.
#[test]
#[cfg(feature = "remote")]
fn test_bag_transport_reader_open_from_transport_invalid_data() {
use crate::io::traits::FormatReader;
use crate::io::transport::memory::MemoryTransport;

// Invalid data that is not a valid BAG file
let invalid_data = b"NOT_A_BAG_FILE".to_vec();
let transport = Box::new(MemoryTransport::new(invalid_data))
as Box<dyn crate::io::transport::Transport>;

// Should not panic with invalid data
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
BagTransportReader::open_from_transport(transport, "invalid.bag".to_string())
}));

match result {
Ok(Ok(_)) => {
// Unexpected success, but not a failure
}
Ok(Err(_)) => {
// Error is expected for invalid data
}
Err(panic_info) => {
let panic_msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
(*s).to_string()
} else if let Some(s) = panic_info.downcast_ref::<String>() {
s.clone()
} else {
"Unknown panic".to_string()
};
panic!(
"BagTransportReader::open_from_transport panicked with invalid data: {}",
panic_msg
);
}
}
}
}
7 changes: 3 additions & 4 deletions src/io/formats/mcap/transport_reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,10 +229,9 @@ impl FormatReader for McapTransportReader {
));
}
Poll::Pending => {
return Err(CodecError::encode(
"Transport",
"Unexpected pending from non-async transport".to_string(),
));
// Async transport returned pending - yield and retry
std::thread::yield_now();
continue;
}
}
}
Expand Down
7 changes: 3 additions & 4 deletions src/io/formats/rrd/transport_reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,10 +268,9 @@ impl FormatReader for RrdTransportReader {
));
}
Poll::Pending => {
return Err(CodecError::encode(
"Transport",
"Unexpected pending from non-async transport".to_string(),
));
// Async transport returned pending - yield and retry
std::thread::yield_now();
continue;
}
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/io/reader/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,9 @@ impl RoboReader {
{
if let Some(transport) = Self::parse_url_to_transport(path)? {
// Use transport-based reading
// Detect format from path extension
let path_obj = std::path::Path::new(path);
// Detect format from path extension (strip query params for S3 URLs)
let path_for_detection = path.split('?').next().unwrap_or(path);
let path_obj = std::path::Path::new(path_for_detection);
let format = detect_format(path_obj)?;

// MCAP, BAG, and RRD formats support transport-based reading
Expand Down
Loading