Skip to content

Commit 049fbee

Browse files
committed
add more tests and cli output options. fix why
1 parent 2462846 commit 049fbee

File tree

6 files changed

+370
-50
lines changed

6 files changed

+370
-50
lines changed

cli/src/main.rs

Lines changed: 145 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use oxigraph::model::NamedNode;
1010
use serde_json;
1111
use std::env::current_dir;
1212
use std::path::PathBuf;
13+
use std::collections::{BTreeMap, BTreeSet};
1314

1415
#[derive(Debug, Parser)]
1516
#[command(name = "ontoenv")]
@@ -49,7 +50,7 @@ struct Cli {
4950
#[clap(long = "no-search", short = 'n', action, global = true)]
5051
no_search: bool,
5152
/// Directories to search for ontologies. If not provided, the current directory is used.
52-
#[clap(global = true)]
53+
#[clap(global = true, last = true)]
5354
locations: Option<Vec<PathBuf>>,
5455
}
5556

@@ -111,7 +112,11 @@ enum Commands {
111112
/// Prints the version of the ontoenv binary
112113
Version,
113114
/// Prints the status of the ontology environment
114-
Status,
115+
Status {
116+
/// Output JSON instead of text
117+
#[clap(long, action, default_value = "false")]
118+
json: bool,
119+
},
115120
/// Update the ontology environment
116121
Update {
117122
/// Suppress per-ontology update output
@@ -120,6 +125,9 @@ enum Commands {
120125
/// Update all ontologies, ignoring modification times
121126
#[clap(long, short = 'a', action)]
122127
all: bool,
128+
/// Output JSON instead of text
129+
#[clap(long, action, default_value = "false")]
130+
json: bool,
123131
},
124132
/// Compute the owl:imports closure of an ontology and write it to a file
125133
Closure {
@@ -147,8 +155,14 @@ enum Commands {
147155
no_imports: bool,
148156
},
149157
/// List various properties of the environment
150-
#[command(subcommand)]
151-
List(ListCommands),
158+
/// List various properties of the environment
159+
List {
160+
#[command(subcommand)]
161+
list_cmd: ListCommands,
162+
/// Output JSON instead of text
163+
#[clap(long, action, default_value = "false")]
164+
json: bool,
165+
},
152166
// TODO: dump all ontologies; nest by ontology name (sorted), w/n each ontology name list all
153167
// the places where that graph can be found. List basic stats: the metadata field in the
154168
// Ontology struct and # of triples in the graph; last updated; etc
@@ -170,9 +184,16 @@ enum Commands {
170184
Why {
171185
/// The name (URI) of the ontology to find importers for
172186
ontologies: Vec<String>,
187+
/// Output JSON instead of text
188+
#[clap(long, action, default_value = "false")]
189+
json: bool,
173190
},
174191
/// Run the doctor to check the environment for issues
175-
Doctor,
192+
Doctor {
193+
/// Output JSON instead of text
194+
#[clap(long, action, default_value = "false")]
195+
json: bool,
196+
},
176197
/// Reset the ontology environment by removing the .ontoenv directory
177198
Reset {
178199
#[clap(long, short, action = clap::ArgAction::SetTrue, default_value = "false")]
@@ -188,15 +209,15 @@ impl ToString for Commands {
188209
match self {
189210
Commands::Init { .. } => "Init".to_string(),
190211
Commands::Version => "Version".to_string(),
191-
Commands::Status => "Status".to_string(),
212+
Commands::Status { .. } => "Status".to_string(),
192213
Commands::Update { .. } => "Update".to_string(),
193214
Commands::Closure { .. } => "Closure".to_string(),
194215
Commands::Add { .. } => "Add".to_string(),
195-
Commands::List(..) => "List".to_string(),
216+
Commands::List { .. } => "List".to_string(),
196217
Commands::Dump { .. } => "Dump".to_string(),
197218
Commands::DepGraph { .. } => "DepGraph".to_string(),
198219
Commands::Why { .. } => "Why".to_string(),
199-
Commands::Doctor => "Doctor".to_string(),
220+
Commands::Doctor { .. } => "Doctor".to_string(),
200221
Commands::Reset { .. } => "Reset".to_string(),
201222
Commands::Config { .. } => "Config".to_string(),
202223
}
@@ -482,17 +503,49 @@ fn main() -> Result<()> {
482503
env!("GIT_HASH")
483504
);
484505
}
485-
Commands::Status => {
506+
Commands::Status { json } => {
486507
let env = require_ontoenv(env)?;
487-
// load env from .ontoenv/ontoenv.json
488-
let status = env.status()?;
489-
// pretty print the status
490-
println!("{status}");
508+
if json {
509+
// Recompute status details similar to env.status()
510+
let ontoenv_dir = current_dir()?.join(".ontoenv");
511+
let last_updated = if ontoenv_dir.exists() {
512+
Some(std::fs::metadata(&ontoenv_dir)?.modified()?.into()) as Option<std::time::SystemTime>
513+
} else { None };
514+
let size: u64 = if ontoenv_dir.exists() {
515+
walkdir::WalkDir::new(&ontoenv_dir)
516+
.into_iter()
517+
.filter_map(Result::ok)
518+
.filter(|e| e.file_type().is_file())
519+
.filter_map(|e| e.metadata().ok())
520+
.map(|m| m.len())
521+
.sum()
522+
} else { 0 };
523+
let missing: Vec<String> = env
524+
.missing_imports()
525+
.into_iter()
526+
.map(|n| n.to_uri_string())
527+
.collect();
528+
let last_str = last_updated.map(|t| chrono::DateTime::<chrono::Utc>::from(t).to_rfc3339());
529+
let obj = serde_json::json!({
530+
"exists": true,
531+
"num_ontologies": env.ontologies().len(),
532+
"last_updated": last_str,
533+
"store_size_bytes": size,
534+
"missing_imports": missing,
535+
});
536+
println!("{}", serde_json::to_string_pretty(&obj)?);
537+
} else {
538+
let status = env.status()?;
539+
println!("{status}");
540+
}
491541
}
492-
Commands::Update { quiet, all } => {
542+
Commands::Update { quiet, all, json } => {
493543
let mut env = require_ontoenv(env)?;
494544
let updated = env.update_all(all)?;
495-
if !quiet {
545+
if json {
546+
let arr: Vec<String> = updated.iter().map(|id| id.to_uri_string()).collect();
547+
println!("{}", serde_json::to_string_pretty(&arr)?);
548+
} else if !quiet {
496549
for id in updated {
497550
if let Some(ont) = env.ontologies().get(&id) {
498551
let name = ont.name().to_string();
@@ -546,30 +599,46 @@ fn main() -> Result<()> {
546599
let _ = env.add(location, true)?;
547600
}
548601
}
549-
Commands::List(list_cmd) => {
602+
Commands::List { list_cmd, json } => {
550603
let env = require_ontoenv(env)?;
551604
match list_cmd {
552605
ListCommands::Locations => {
553606
let mut locations = env.find_files()?;
554607
locations.sort_by(|a, b| a.as_str().cmp(b.as_str()));
555-
for loc in locations {
556-
println!("{}", loc);
608+
if json {
609+
println!("{}", serde_json::to_string_pretty(&locations)?);
610+
} else {
611+
for loc in locations {
612+
println!("{}", loc);
613+
}
557614
}
558615
}
559616
ListCommands::Ontologies => {
560617
// print list of ontology URLs from env.ontologies.values() sorted alphabetically
561618
let mut ontologies: Vec<&GraphIdentifier> = env.ontologies().keys().collect();
562619
ontologies.sort_by(|a, b| a.name().cmp(&b.name()));
563620
ontologies.dedup_by(|a, b| a.name() == b.name());
564-
for ont in ontologies {
565-
println!("{}", ont.to_uri_string());
621+
if json {
622+
let out: Vec<String> =
623+
ontologies.into_iter().map(|o| o.to_uri_string()).collect();
624+
println!("{}", serde_json::to_string_pretty(&out)?);
625+
} else {
626+
for ont in ontologies {
627+
println!("{}", ont.to_uri_string());
628+
}
566629
}
567630
}
568631
ListCommands::Missing => {
569632
let mut missing_imports = env.missing_imports();
570633
missing_imports.sort();
571-
for import in missing_imports {
572-
println!("{}", import.to_uri_string());
634+
if json {
635+
let out: Vec<String> =
636+
missing_imports.into_iter().map(|n| n.to_uri_string()).collect();
637+
println!("{}", serde_json::to_string_pretty(&out)?);
638+
} else {
639+
for import in missing_imports {
640+
println!("{}", import.to_uri_string());
641+
}
573642
}
574643
}
575644
}
@@ -607,28 +676,67 @@ fn main() -> Result<()> {
607676
));
608677
}
609678
}
610-
Commands::Why { ontologies } => {
679+
Commands::Why { ontologies, json } => {
611680
let env = require_ontoenv(env)?;
612-
for ont in ontologies {
613-
let iri = NamedNode::new(ont).map_err(|e| anyhow::anyhow!(e.to_string()))?;
614-
let importers = env.get_importers(&iri)?;
615-
println!("Imported by {}: ", iri.to_uri_string());
616-
for dep in importers {
617-
println!("{}", dep.to_uri_string());
681+
if json {
682+
let mut all: BTreeMap<String, Vec<Vec<String>>> = BTreeMap::new();
683+
for ont in ontologies {
684+
let iri = NamedNode::new(ont).map_err(|e| anyhow::anyhow!(e.to_string()))?;
685+
let paths = env.get_import_paths(&iri)?;
686+
let mut unique: BTreeSet<Vec<String>> = BTreeSet::new();
687+
for p in paths {
688+
unique.insert(p.into_iter().map(|id| id.to_uri_string()).collect());
689+
}
690+
let arr: Vec<Vec<String>> = unique.into_iter().collect();
691+
all.insert(iri.to_uri_string(), arr);
692+
}
693+
println!("{}", serde_json::to_string_pretty(&all)?);
694+
} else {
695+
for ont in ontologies {
696+
let iri = NamedNode::new(ont).map_err(|e| anyhow::anyhow!(e.to_string()))?;
697+
let paths = env.get_import_paths(&iri)?;
698+
if paths.is_empty() {
699+
println!("No importers found for {}", iri.to_uri_string());
700+
continue;
701+
}
702+
println!("Why {}:", iri.to_uri_string());
703+
let mut lines: BTreeSet<String> = BTreeSet::new();
704+
for p in paths {
705+
let line = p
706+
.into_iter()
707+
.map(|id| id.to_uri_string())
708+
.collect::<Vec<_>>()
709+
.join(" -> ");
710+
lines.insert(line);
711+
}
712+
for line in lines {
713+
println!("{}", line);
714+
}
618715
}
619716
}
620717
}
621-
Commands::Doctor => {
718+
Commands::Doctor { json } => {
622719
let env = require_ontoenv(env)?;
623720
let problems = env.doctor()?;
624-
if problems.is_empty() {
625-
println!("No issues found.");
721+
if json {
722+
let out: Vec<serde_json::Value> = problems
723+
.into_iter()
724+
.map(|p| serde_json::json!({
725+
"message": p.message,
726+
"locations": p.locations.into_iter().map(|loc| loc.to_string()).collect::<Vec<_>>()
727+
}))
728+
.collect();
729+
println!("{}", serde_json::to_string_pretty(&out)?);
626730
} else {
627-
println!("Found {} issues:", problems.len());
628-
for problem in problems {
629-
println!("- {}", problem.message);
630-
for location in problem.locations {
631-
println!(" - {location}");
731+
if problems.is_empty() {
732+
println!("No issues found.");
733+
} else {
734+
println!("Found {} issues:", problems.len());
735+
for problem in problems {
736+
println!("- {}", problem.message);
737+
for location in problem.locations {
738+
println!(" - {location}");
739+
}
632740
}
633741
}
634742
}

cli/tests/test_why.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
use std::fs;
2+
use std::path::PathBuf;
3+
use std::process::Command;
4+
5+
fn write_ttl(path: &PathBuf, ontology_uri: &str, extra: &str) {
6+
let content = format!(
7+
"@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .\n\
8+
@prefix owl: <http://www.w3.org/2002/07/owl#> .\n\
9+
<{uri}> a owl:Ontology .\n\
10+
{extra}\n",
11+
uri = ontology_uri,
12+
extra = extra
13+
);
14+
fs::write(path, content).expect("write ttl");
15+
}
16+
17+
#[test]
18+
fn why_lists_importers() {
19+
// temp dir
20+
let mut root = std::env::current_dir().expect("cwd");
21+
root.push(format!("target/test_cli_why_{}", std::process::id()));
22+
if root.exists() { let _ = fs::remove_dir_all(&root); }
23+
fs::create_dir_all(&root).expect("mkdir");
24+
25+
// three ontologies: C imports A; A imports B
26+
let a_uri = "http://example.org/ont/A";
27+
let b_uri = "http://example.org/ont/B";
28+
let c_uri = "http://example.org/ont/C";
29+
let a_path = root.join("A.ttl");
30+
let b_path = root.join("B.ttl");
31+
let c_path = root.join("C.ttl");
32+
write_ttl(&b_path, b_uri, "");
33+
write_ttl(&a_path, a_uri, &format!("<{}> owl:imports <{}> .", a_uri, b_uri));
34+
write_ttl(&c_path, c_uri, &format!("<{}> owl:imports <{}> .", c_uri, a_uri));
35+
36+
// Locate built binary (debug or release)
37+
let mut bin_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..").join("target").join("debug").join(if cfg!(windows) { "ontoenv.exe" } else { "ontoenv" });
38+
if !bin_path.exists() {
39+
bin_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..").join("target").join("release").join(if cfg!(windows) { "ontoenv.exe" } else { "ontoenv" });
40+
}
41+
assert!(bin_path.exists(), "ontoenv binary not found at {:?}", bin_path);
42+
let exe = bin_path.to_string_lossy().to_string();
43+
44+
// init with offline & include root as search dir
45+
let out = Command::new(&exe)
46+
.current_dir(&root)
47+
.arg("init")
48+
.arg("--overwrite")
49+
.arg("--offline")
50+
.output()
51+
.expect("run init");
52+
assert!(out.status.success(), "init failed: {}", String::from_utf8_lossy(&out.stderr));
53+
54+
// run why for B; expect A -> B and C -> A -> B
55+
let out = Command::new(&exe)
56+
.current_dir(&root)
57+
.arg("why")
58+
.arg(b_uri)
59+
.output()
60+
.expect("run why");
61+
assert!(out.status.success(), "why failed: {}", String::from_utf8_lossy(&out.stderr));
62+
let stdout = String::from_utf8_lossy(&out.stdout);
63+
assert!(stdout.contains("Why"), "missing header: {stdout}");
64+
assert!(stdout.contains(&format!("{} -> {}", a_uri, b_uri)), "did not show A->B: {stdout}");
65+
assert!(
66+
stdout.contains(&format!("{} -> {} -> {}", c_uri, a_uri, b_uri)),
67+
"did not show C->A->B: {stdout}"
68+
);
69+
70+
// clean up
71+
let _ = fs::remove_dir_all(&root);
72+
}

0 commit comments

Comments
 (0)