Skip to content

Commit 6ed67ad

Browse files
authored
Merge pull request #63 from OpenMined/madhava/more-coverage
Madhava/more coverage
2 parents 1798dee + 13a325d commit 6ed67ad

30 files changed

Lines changed: 4450 additions & 7 deletions

coverage.sh

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Usage: ./coverage.sh [--full-clean|-c] [--open] [--large] [--all-tests] [--no-li
2525
--no-lint Skip cargo fmt and clippy checks
2626
--focused-test Run one focused integration test target:
2727
file_formats, formats_lib, inspect, prepare, cli, cli_bin, schema, core,
28-
runtime_lib, runtime_security, or runtime_resources
28+
reporting_lib, runtime_lib, runtime_security, or runtime_resources
2929
3030
Environment:
3131
AUTO_INSTALL_LLVM_COV=0 Do not auto-install cargo-llvm-cov
@@ -83,6 +83,7 @@ PACKAGES=(
8383
bioscript-cli
8484
bioscript-core
8585
bioscript-formats
86+
bioscript-reporting
8687
bioscript-runtime
8788
bioscript-schema
8889
)
@@ -176,12 +177,16 @@ if [[ -n "$FOCUSED_TEST" ]]; then
176177
env "${COV_ENV[@]}" cargo llvm-cov --no-report -p bioscript-cli --bin bioscript
177178
;;
178179
schema)
180+
env "${COV_ENV[@]}" cargo llvm-cov --no-report -p bioscript-schema --lib
179181
env "${COV_ENV[@]}" cargo llvm-cov --no-report -p bioscript-schema --test validate_variants -- --nocapture --test-threads="$TEST_THREADS"
180182
;;
181183
core)
182184
env "${COV_ENV[@]}" cargo llvm-cov --no-report -p bioscript-core --lib
183185
env "${COV_ENV[@]}" cargo llvm-cov --no-report -p bioscript-core --test source_size -- --nocapture --test-threads="$TEST_THREADS"
184186
;;
187+
reporting_lib)
188+
env "${COV_ENV[@]}" cargo llvm-cov --no-report -p bioscript-reporting --lib
189+
;;
185190
runtime_lib)
186191
env "${COV_ENV[@]}" cargo llvm-cov --no-report -p bioscript-runtime --lib
187192
;;
@@ -206,9 +211,11 @@ else
206211
env "${COV_ENV[@]}" cargo llvm-cov --no-report -p bioscript-formats --test prepare -- --nocapture --test-threads="$TEST_THREADS"
207212
env "${COV_ENV[@]}" cargo llvm-cov --no-report -p bioscript-cli --test cli -- --nocapture --test-threads="$TEST_THREADS"
208213
env "${COV_ENV[@]}" cargo llvm-cov --no-report -p bioscript-cli --bin bioscript
214+
env "${COV_ENV[@]}" cargo llvm-cov --no-report -p bioscript-schema --lib
209215
env "${COV_ENV[@]}" cargo llvm-cov --no-report -p bioscript-schema --test validate_variants -- --nocapture --test-threads="$TEST_THREADS"
210216
env "${COV_ENV[@]}" cargo llvm-cov --no-report -p bioscript-core --lib
211217
env "${COV_ENV[@]}" cargo llvm-cov --no-report -p bioscript-core --test source_size -- --nocapture --test-threads="$TEST_THREADS"
218+
env "${COV_ENV[@]}" cargo llvm-cov --no-report -p bioscript-reporting --lib
212219
env "${COV_ENV[@]}" cargo llvm-cov --no-report -p bioscript-runtime --lib
213220
env "${COV_ENV[@]}" cargo llvm-cov --no-report -p bioscript-runtime --test security -- --nocapture --test-threads="$TEST_THREADS"
214221
env "${COV_ENV[@]}" cargo llvm-cov --no-report -p bioscript-runtime --test resources_coverage -- --nocapture --test-threads="$TEST_THREADS"

rust/bioscript-cli/src/cli_bootstrap.rs

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,3 +417,162 @@ fn write_timing_report(path: &PathBuf, timings: &[StageTiming]) -> Result<(), St
417417
fs::write(path, output)
418418
.map_err(|err| format!("failed to write timing report {}: {err}", path.display()))
419419
}
420+
421+
#[cfg(test)]
422+
mod cli_bootstrap_tests {
423+
use super::*;
424+
use std::time::{SystemTime, UNIX_EPOCH};
425+
426+
fn temp_dir(name: &str) -> PathBuf {
427+
let unique = SystemTime::now()
428+
.duration_since(UNIX_EPOCH)
429+
.unwrap()
430+
.as_nanos();
431+
let dir = env::temp_dir().join(format!(
432+
"bioscript-cli-bootstrap-{name}-{}-{unique}",
433+
std::process::id()
434+
));
435+
fs::create_dir_all(&dir).unwrap();
436+
dir
437+
}
438+
439+
#[test]
440+
fn parse_cli_options_consumes_paths_loader_limits_and_filters() {
441+
let options = parse_cli_options(vec![
442+
"script.bs".to_owned(),
443+
"--root".to_owned(),
444+
"root".to_owned(),
445+
"--input-file".to_owned(),
446+
"input.txt".to_owned(),
447+
"--output-file".to_owned(),
448+
"output.txt".to_owned(),
449+
"--participant-id".to_owned(),
450+
"p1".to_owned(),
451+
"--trace-report".to_owned(),
452+
"trace.tsv".to_owned(),
453+
"--timing-report".to_owned(),
454+
"timing.tsv".to_owned(),
455+
"--filter".to_owned(),
456+
"tag=pgx".to_owned(),
457+
"--cache-dir".to_owned(),
458+
"cache".to_owned(),
459+
"--input-format".to_owned(),
460+
"text".to_owned(),
461+
"--input-index".to_owned(),
462+
"input.idx".to_owned(),
463+
"--reference-file".to_owned(),
464+
"ref.fa".to_owned(),
465+
"--reference-index".to_owned(),
466+
"ref.fa.fai".to_owned(),
467+
"--max-duration-ms".to_owned(),
468+
"250".to_owned(),
469+
"--max-memory-bytes".to_owned(),
470+
"1024".to_owned(),
471+
"--max-allocations".to_owned(),
472+
"2000".to_owned(),
473+
"--max-recursion-depth".to_owned(),
474+
"50".to_owned(),
475+
"--auto-index".to_owned(),
476+
])
477+
.unwrap();
478+
479+
assert_eq!(options.script_path, Some(PathBuf::from("script.bs")));
480+
assert_eq!(options.root, Some(PathBuf::from("root")));
481+
assert_eq!(options.input_file.as_deref(), Some("input.txt"));
482+
assert_eq!(options.output_file.as_deref(), Some("output.txt"));
483+
assert_eq!(options.participant_id.as_deref(), Some("p1"));
484+
assert_eq!(options.trace_report, Some(PathBuf::from("trace.tsv")));
485+
assert_eq!(options.timing_report, Some(PathBuf::from("timing.tsv")));
486+
assert_eq!(options.filters, vec!["tag=pgx"]);
487+
assert_eq!(options.cache_dir, Some(PathBuf::from("cache")));
488+
assert_eq!(options.loader.format, Some(GenotypeSourceFormat::Text));
489+
assert_eq!(options.loader.input_index, Some(PathBuf::from("input.idx")));
490+
assert_eq!(options.loader.reference_file, Some(PathBuf::from("ref.fa")));
491+
assert_eq!(
492+
options.loader.reference_index,
493+
Some(PathBuf::from("ref.fa.fai"))
494+
);
495+
assert!(options.auto_index);
496+
}
497+
498+
#[test]
499+
fn parse_cli_options_reports_missing_values_and_unexpected_arguments() {
500+
for (flag, message) in [
501+
("--root", "--root requires"),
502+
("--input-file", "--input-file requires"),
503+
("--output-file", "--output-file requires"),
504+
("--participant-id", "--participant-id requires"),
505+
("--trace-report", "--trace-report requires"),
506+
("--timing-report", "--timing-report requires"),
507+
("--filter", "--filter requires"),
508+
("--cache-dir", "--cache-dir requires"),
509+
("--input-format", "--input-format requires"),
510+
("--input-index", "--input-index requires"),
511+
("--reference-file", "--reference-file requires"),
512+
("--reference-index", "--reference-index requires"),
513+
("--max-duration-ms", "--max-duration-ms requires"),
514+
("--max-memory-bytes", "--max-memory-bytes requires"),
515+
("--max-allocations", "--max-allocations requires"),
516+
("--max-recursion-depth", "--max-recursion-depth requires"),
517+
] {
518+
assert!(parse_err(vec![flag.to_owned()]).contains(message));
519+
}
520+
assert!(parse_err(vec![
521+
"script.bs".to_owned(),
522+
"extra.bs".to_owned(),
523+
])
524+
.contains("unexpected argument"));
525+
assert!(parse_err(vec![
526+
"--input-format".to_owned(),
527+
"bad".to_owned(),
528+
])
529+
.contains("invalid --input-format"));
530+
assert!(parse_err(vec![
531+
"--max-duration-ms".to_owned(),
532+
"bad".to_owned(),
533+
])
534+
.contains("invalid --max-duration-ms"));
535+
}
536+
537+
#[test]
538+
fn write_timing_report_creates_parent_and_sanitizes_tabs() {
539+
let dir = temp_dir("timing");
540+
let path = dir.join("nested/timing.tsv");
541+
write_timing_report(
542+
&path,
543+
&[
544+
StageTiming {
545+
stage: "stage1".to_owned(),
546+
duration_ms: 12,
547+
detail: "a\tb".to_owned(),
548+
},
549+
StageTiming {
550+
stage: "stage2".to_owned(),
551+
duration_ms: 0,
552+
detail: "ok".to_owned(),
553+
},
554+
],
555+
)
556+
.unwrap();
557+
let text = fs::read_to_string(&path).unwrap();
558+
assert!(text.starts_with("stage\tduration_ms\tdetail\n"));
559+
assert!(text.contains("stage1\t12\ta b"));
560+
561+
fs::remove_dir_all(dir).unwrap();
562+
}
563+
564+
#[test]
565+
fn prepare_cli_indexes_noops_when_auto_index_is_disabled() {
566+
let mut options = default_cli_options();
567+
let timings = prepare_cli_indexes(Path::new("."), &mut options).unwrap();
568+
assert!(timings.is_empty());
569+
assert!(options.loader.input_index.is_none());
570+
}
571+
572+
fn parse_err(args: Vec<String>) -> String {
573+
match parse_cli_options(args) {
574+
Ok(_) => panic!("expected CLI parse to fail"),
575+
Err(err) => err,
576+
}
577+
}
578+
}

rust/bioscript-cli/src/cli_commands.rs

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,3 +258,178 @@ fn run_validate_assays(args: Vec<String>) -> Result<(), String> {
258258

259259
Ok(())
260260
}
261+
262+
#[cfg(test)]
263+
mod cli_command_tests {
264+
use super::*;
265+
use std::time::{SystemTime, UNIX_EPOCH};
266+
267+
fn temp_dir(name: &str) -> PathBuf {
268+
let unique = SystemTime::now()
269+
.duration_since(UNIX_EPOCH)
270+
.unwrap()
271+
.as_nanos();
272+
let dir = env::temp_dir().join(format!(
273+
"bioscript-cli-command-{name}-{}-{unique}",
274+
std::process::id()
275+
));
276+
fs::create_dir_all(&dir).unwrap();
277+
dir
278+
}
279+
280+
fn valid_variant_yaml() -> &'static str {
281+
r#"
282+
schema: bioscript:variant:1.0
283+
version: "1.0"
284+
name: Test variant
285+
gene: ABC
286+
identifiers:
287+
rsids: [rs1]
288+
coordinates:
289+
grch38:
290+
chrom: "1"
291+
pos: 100
292+
alleles:
293+
kind: snv
294+
ref: G
295+
alts: [A]
296+
"#
297+
}
298+
299+
#[test]
300+
fn prepare_and_inspect_commands_validate_arguments() {
301+
assert!(run_prepare(vec!["--root".to_owned()])
302+
.unwrap_err()
303+
.contains("--root requires"));
304+
assert!(run_prepare(vec!["--input-file".to_owned()])
305+
.unwrap_err()
306+
.contains("--input-file requires"));
307+
assert!(run_prepare(vec![
308+
"--input-format".to_owned(),
309+
"not-a-format".to_owned(),
310+
])
311+
.unwrap_err()
312+
.contains("invalid --input-format"));
313+
assert!(run_prepare(vec!["--unexpected".to_owned()])
314+
.unwrap_err()
315+
.contains("unexpected argument"));
316+
317+
assert!(run_inspect(Vec::new()).unwrap_err().contains("usage"));
318+
assert!(run_inspect(vec!["a.txt".to_owned(), "b.txt".to_owned()])
319+
.unwrap_err()
320+
.contains("unexpected argument"));
321+
assert!(run_inspect(vec!["sample.cram".to_owned(), "--input-index".to_owned()])
322+
.unwrap_err()
323+
.contains("--input-index requires"));
324+
}
325+
326+
#[test]
327+
fn yaml_manifest_extension_matching_is_case_sensitive_by_contract() {
328+
assert!(is_yaml_manifest(Path::new("panel.yaml")));
329+
assert!(is_yaml_manifest(Path::new("panel.yml")));
330+
assert!(!is_yaml_manifest(Path::new("panel.YAML")));
331+
assert!(!is_yaml_manifest(Path::new("panel.json")));
332+
}
333+
334+
#[test]
335+
fn validate_variants_writes_report_and_surfaces_errors() {
336+
let dir = temp_dir("variants");
337+
let valid = dir.join("variant.yaml");
338+
let report = dir.join("reports/variant.txt");
339+
fs::write(&valid, valid_variant_yaml()).unwrap();
340+
341+
run_validate_variants(vec![
342+
valid.display().to_string(),
343+
"--report".to_owned(),
344+
report.display().to_string(),
345+
])
346+
.unwrap();
347+
assert!(fs::read_to_string(&report).unwrap().contains("files_scanned"));
348+
349+
let invalid = dir.join("invalid.yaml");
350+
fs::write(&invalid, "schema: bioscript:variant:1.0\n").unwrap();
351+
let err = run_validate_variants(vec![invalid.display().to_string()]).unwrap_err();
352+
assert!(err.contains("validation found"));
353+
354+
assert!(run_validate_variants(Vec::new()).unwrap_err().contains("usage"));
355+
assert!(run_validate_variants(vec![valid.display().to_string(), "--report".to_owned()])
356+
.unwrap_err()
357+
.contains("--report requires"));
358+
assert!(run_validate_variants(vec![
359+
valid.display().to_string(),
360+
"extra".to_owned(),
361+
])
362+
.unwrap_err()
363+
.contains("unexpected argument"));
364+
365+
fs::remove_dir_all(dir).unwrap();
366+
}
367+
368+
#[test]
369+
fn validate_panels_and_assays_cover_report_and_error_paths() {
370+
let dir = temp_dir("panels-assays");
371+
let variant = dir.join("variant.yaml");
372+
fs::write(&variant, valid_variant_yaml()).unwrap();
373+
374+
let panel = dir.join("panel.yaml");
375+
fs::write(
376+
&panel,
377+
r#"
378+
schema: bioscript:panel:1.0
379+
version: "1.0"
380+
name: Test panel
381+
members:
382+
- kind: variant
383+
path: variant.yaml
384+
"#,
385+
)
386+
.unwrap();
387+
let panel_report = dir.join("reports/panel.txt");
388+
run_validate_panels(vec![
389+
panel.display().to_string(),
390+
"--report".to_owned(),
391+
panel_report.display().to_string(),
392+
])
393+
.unwrap();
394+
assert!(panel_report.exists());
395+
396+
let assay = dir.join("assay.yaml");
397+
fs::write(
398+
&assay,
399+
r#"
400+
schema: bioscript:assay:1.0
401+
version: "1.0"
402+
name: Test assay
403+
members:
404+
- kind: variant
405+
path: variant.yaml
406+
"#,
407+
)
408+
.unwrap();
409+
let assay_report = dir.join("reports/assay.txt");
410+
run_validate_assays(vec![
411+
assay.display().to_string(),
412+
"--report".to_owned(),
413+
assay_report.display().to_string(),
414+
])
415+
.unwrap();
416+
assert!(assay_report.exists());
417+
418+
assert!(run_validate_panels(Vec::new()).unwrap_err().contains("usage"));
419+
assert!(run_validate_panels(vec![panel.display().to_string(), "--report".to_owned()])
420+
.unwrap_err()
421+
.contains("--report requires"));
422+
assert!(run_validate_panels(vec![panel.display().to_string(), "extra".to_owned()])
423+
.unwrap_err()
424+
.contains("unexpected argument"));
425+
assert!(run_validate_assays(Vec::new()).unwrap_err().contains("usage"));
426+
assert!(run_validate_assays(vec![assay.display().to_string(), "--report".to_owned()])
427+
.unwrap_err()
428+
.contains("--report requires"));
429+
assert!(run_validate_assays(vec![assay.display().to_string(), "extra".to_owned()])
430+
.unwrap_err()
431+
.contains("unexpected argument"));
432+
433+
fs::remove_dir_all(dir).unwrap();
434+
}
435+
}

0 commit comments

Comments
 (0)