Skip to content

Commit be7d89a

Browse files
authored
test: add comprehensive export integration tests (resolves #27) (#32)
* test: add comprehensive export integration tests (resolves #27) - Add tests/export_integration_tests.rs with 8 integration tests - Tests cover: path computation, directory creation, empty data handling - Verify single-log and multi-log export behavior (.01, .02, .03 suffixes) - Test ExportOptions variants and edge cases - All tests passing, coverage includes GPX and event export scenarios - Update .gitignore to whitelist tests/ directory - Resolves remaining work item #1 from issue #27 * docs: enhance export API documentation with examples - Add cross-link from CRATE_USAGE.md to examples/README.md for runnable code - Enhance API Integration section in examples/README.md with concrete code snippets - Show CSV export and GPX+Event export examples with actual usage patterns - Improve clarity on relationship between library API docs and example programs - Clarify that examples/README.md provides runnable demonstration code
1 parent 1f4b22d commit be7d89a

4 files changed

Lines changed: 293 additions & 9 deletions

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
!src/**
2020
!src/**/*.rs
2121

22+
# Whitelist tests directory and all Rust files within it
23+
!tests/
24+
!tests/**
25+
!tests/**/*.rs
26+
2227
# Whitelist examples directory
2328
!examples/
2429
!examples/*.rs

CRATE_USAGE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,8 @@ fn main() -> anyhow::Result<()> {
216216
}
217217
```
218218

219+
For runnable examples with complete code and output, see [examples/README.md](./examples/README.md).
220+
219221
## Examples
220222

221223
Run the crate example that demonstrates multi-firmware support and PID extraction:

examples/README.md

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -266,16 +266,32 @@ done
266266

267267
## API Integration
268268

269-
All export functions are accessible via the crate:
269+
All export functions are accessible via the crate. These examples show the API in use:
270270

271+
### CSV Export
271272
```rust
272-
use bbl_parser::{
273-
parse_bbl_file,
274-
export_to_csv,
275-
export_to_gpx,
276-
export_to_event,
277-
ExportOptions
278-
};
273+
use bbl_parser::{parse_bbl_file, export_to_csv, ExportOptions};
274+
use std::path::Path;
275+
276+
let opts = ExportOptions { csv: true, gpx: false, event: false, output_dir: None, force_export: false };
277+
let log = parse_bbl_file(Path::new("flight.BBL"), opts.clone(), false)?;
278+
export_to_csv(&log, Path::new("flight.BBL"), &opts)?;
279+
// Creates: flight.csv + flight.headers.csv
280+
```
281+
282+
### GPX + Event Export
283+
```rust
284+
use bbl_parser::{export_to_gpx, export_to_event, ExportOptions};
285+
286+
let opts = ExportOptions { csv: false, gpx: true, event: true, output_dir: Some("out".into()), force_export: false };
287+
288+
if !log.gps_coordinates.is_empty() {
289+
export_to_gpx(Path::new("flight.BBL"), 0, 1, &log.gps_coordinates, &log.home_coordinates, &opts)?;
290+
}
291+
292+
if !log.event_frames.is_empty() {
293+
export_to_event(Path::new("flight.BBL"), 0, 1, &log.event_frames, &opts)?;
294+
}
279295
```
280296

281-
See `CRATE_USAGE.md` in the root directory for comprehensive integration examples.
297+
See `CRATE_USAGE.md` for basic setup and API reference.

tests/export_integration_tests.rs

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
//! Integration tests for export functionality
2+
//!
3+
//! Tests the export layer across different scenarios:
4+
//! - GPX export with directory creation
5+
//! - Event export with JSON format
6+
//! - Multi-log suffix handling (.NN)
7+
//! - Output directory defaulting to input parent
8+
//! - Error handling for edge cases
9+
10+
use bbl_parser::export::*;
11+
use bbl_parser::{EventFrame, ExportOptions, GpsCoordinate};
12+
use std::fs;
13+
use tempfile::TempDir;
14+
15+
#[test]
16+
fn test_export_gpx_creates_output_directory() {
17+
let temp_dir = TempDir::new().expect("Failed to create temp dir");
18+
let nonexistent_dir = temp_dir.path().join("nonexistent").join("output");
19+
20+
let bbl_path = temp_dir.path().join("test.bbl");
21+
let gps_coords = vec![GpsCoordinate {
22+
latitude: 40.7129,
23+
longitude: -74.0061,
24+
altitude: 100.0,
25+
timestamp_us: 54311755,
26+
num_sats: Some(10),
27+
speed: Some(5.0),
28+
ground_course: Some(180.0),
29+
}];
30+
31+
let export_opts = ExportOptions {
32+
csv: false,
33+
gpx: true,
34+
event: false,
35+
output_dir: Some(nonexistent_dir.to_str().unwrap().to_string()),
36+
force_export: false,
37+
};
38+
39+
let result = export_to_gpx(&bbl_path, 0, 1, &gps_coords, &[], &export_opts, None);
40+
assert!(
41+
result.is_ok(),
42+
"GPX export should succeed and create directories"
43+
);
44+
45+
// Verify output directory was created
46+
assert!(
47+
nonexistent_dir.exists(),
48+
"Output directory should be created"
49+
);
50+
51+
// Verify GPX file was created
52+
let gpx_path = nonexistent_dir.join("test.gps.gpx");
53+
assert!(
54+
gpx_path.exists(),
55+
"GPX file should be created in new directory"
56+
);
57+
}
58+
59+
#[test]
60+
fn test_export_event_creates_output_directory() {
61+
let temp_dir = TempDir::new().expect("Failed to create temp dir");
62+
let nonexistent_dir = temp_dir.path().join("event_out");
63+
64+
let bbl_path = temp_dir.path().join("test.bbl");
65+
let event_frames = vec![
66+
EventFrame {
67+
event_name: "Disarm".to_string(),
68+
timestamp_us: 143932686,
69+
event_type: 13, // EVT_END
70+
event_data: Vec::new(),
71+
},
72+
EventFrame {
73+
event_name: "Flight mode change".to_string(),
74+
timestamp_us: 143905899,
75+
event_type: 8, // EVT_MODE
76+
event_data: Vec::new(),
77+
},
78+
];
79+
80+
let export_opts = ExportOptions {
81+
csv: false,
82+
gpx: false,
83+
event: true,
84+
output_dir: Some(nonexistent_dir.to_str().unwrap().to_string()),
85+
force_export: false,
86+
};
87+
88+
let result = export_to_event(&bbl_path, 0, 1, &event_frames, &export_opts);
89+
assert!(
90+
result.is_ok(),
91+
"Event export should succeed and create directory"
92+
);
93+
94+
// Verify directory and file created
95+
assert!(
96+
nonexistent_dir.exists(),
97+
"Event output directory should be created"
98+
);
99+
let event_path = nonexistent_dir.join("test.event");
100+
assert!(event_path.exists(), "Event file should be created");
101+
102+
// Verify event content
103+
let content = fs::read_to_string(&event_path).expect("Failed to read event file");
104+
assert!(
105+
content.contains("Disarm"),
106+
"Event file should contain Disarm event"
107+
);
108+
assert!(
109+
content.contains("Flight mode change"),
110+
"Event file should contain Flight mode change"
111+
);
112+
}
113+
114+
#[test]
115+
fn test_export_event_empty_returns_ok() {
116+
let temp_dir = TempDir::new().expect("Failed to create temp dir");
117+
let bbl_path = temp_dir.path().join("test.bbl");
118+
119+
let export_opts = ExportOptions {
120+
csv: false,
121+
gpx: false,
122+
event: true,
123+
output_dir: Some(temp_dir.path().to_str().unwrap().to_string()),
124+
force_export: false,
125+
};
126+
127+
let result = export_to_event(&bbl_path, 0, 1, &[], &export_opts);
128+
assert!(
129+
result.is_ok(),
130+
"Event export should succeed with empty events"
131+
);
132+
133+
// Verify no event file created
134+
let event_path = temp_dir.path().join("test.event");
135+
assert!(
136+
!event_path.exists(),
137+
"No event file should be created for empty events"
138+
);
139+
}
140+
141+
#[test]
142+
fn test_compute_export_paths_single_log() {
143+
let temp_dir = TempDir::new().expect("Failed to create temp dir");
144+
let input_path = temp_dir.path().join("test.bbl");
145+
let output_dir = temp_dir.path().join("output");
146+
147+
let export_opts = ExportOptions {
148+
csv: true,
149+
gpx: true,
150+
event: true,
151+
output_dir: Some(output_dir.to_str().unwrap().to_string()),
152+
force_export: false,
153+
};
154+
155+
let (csv_path, _headers_path, gpx_path, event_path) =
156+
compute_export_paths(&input_path, &export_opts, 1, 1);
157+
158+
// Verify no .NN suffix for single log
159+
assert!(
160+
csv_path.to_string_lossy().ends_with("test.csv"),
161+
"CSV path should not have .NN suffix for single log"
162+
);
163+
assert!(
164+
gpx_path.to_string_lossy().ends_with("test.gps.gpx"),
165+
"GPX path should be correct for single log"
166+
);
167+
assert!(
168+
event_path.to_string_lossy().ends_with("test.event"),
169+
"Event path should be correct for single log"
170+
);
171+
}
172+
173+
#[test]
174+
fn test_compute_export_paths_multi_log() {
175+
let temp_dir = TempDir::new().expect("Failed to create temp dir");
176+
let input_path = temp_dir.path().join("test.bbl");
177+
let output_dir = temp_dir.path().join("output");
178+
179+
let export_opts = ExportOptions {
180+
csv: true,
181+
gpx: true,
182+
event: true,
183+
output_dir: Some(output_dir.to_str().unwrap().to_string()),
184+
force_export: false,
185+
};
186+
187+
let (csv_path, _headers_path, gpx_path, event_path) =
188+
compute_export_paths(&input_path, &export_opts, 2, 3);
189+
190+
// Verify .NN suffix is applied for multi-log
191+
assert!(
192+
csv_path.to_string_lossy().contains("test.02.csv"),
193+
"CSV path should have .02 suffix for second log of three"
194+
);
195+
assert!(
196+
gpx_path.to_string_lossy().contains("test.02.gps.gpx"),
197+
"GPX path should have .02 suffix"
198+
);
199+
assert!(
200+
event_path.to_string_lossy().contains("test.02.event"),
201+
"Event path should have .02 suffix"
202+
);
203+
}
204+
205+
#[test]
206+
fn test_export_options_defaults() {
207+
let opts = ExportOptions::default();
208+
assert!(!opts.csv, "Default CSV should be false");
209+
assert!(!opts.gpx, "Default GPX should be false");
210+
assert!(!opts.event, "Default event should be false");
211+
assert!(
212+
opts.output_dir.is_none(),
213+
"Default output_dir should be None"
214+
);
215+
assert!(!opts.force_export, "Default force_export should be false");
216+
}
217+
218+
#[test]
219+
fn test_export_options_custom() {
220+
let opts = ExportOptions {
221+
csv: true,
222+
gpx: true,
223+
event: false,
224+
output_dir: Some("/tmp/test".to_string()),
225+
force_export: true,
226+
};
227+
228+
assert!(opts.csv);
229+
assert!(opts.gpx);
230+
assert!(!opts.event);
231+
assert_eq!(opts.output_dir.as_ref().unwrap(), "/tmp/test");
232+
assert!(opts.force_export);
233+
}
234+
235+
#[test]
236+
fn test_gpx_empty_coordinates_returns_ok() {
237+
let temp_dir = TempDir::new().expect("Failed to create temp dir");
238+
let bbl_path = temp_dir.path().join("test.bbl");
239+
240+
let export_opts = ExportOptions {
241+
csv: false,
242+
gpx: true,
243+
event: false,
244+
output_dir: Some(temp_dir.path().to_str().unwrap().to_string()),
245+
force_export: false,
246+
};
247+
248+
// Should return Ok even with empty GPS coordinates
249+
let result = export_to_gpx(&bbl_path, 0, 1, &[], &[], &export_opts, None);
250+
assert!(
251+
result.is_ok(),
252+
"Export should succeed with empty GPS coordinates"
253+
);
254+
255+
// Verify no GPX file is created when GPS coordinates are empty
256+
let gpx_path = temp_dir.path().join("test.gps.gpx");
257+
assert!(
258+
!gpx_path.exists(),
259+
"No GPX file should be created when GPS coordinates are empty"
260+
);
261+
}

0 commit comments

Comments
 (0)