use std::{io::Write, path::PathBuf, process::ExitCode};
use serde_json::Value;
const SHARED: &[&str] = &["--metric", "--filter", "--kind", "--runner"];
const ALL: &str = "1000000000";
pub fn command(arguments: &[String]) -> ExitCode {
let mut passed = Vec::new();
let mut output = None;
let mut format = None;
let mut index = 0;
while index < arguments.len() {
match arguments[index].as_str() {
"--format" => {
index += 1;
format = arguments.get(index).cloned();
}
"--output" => {
index += 1;
output = arguments.get(index).map(PathBuf::from);
}
"--offset" | "--limit" => index += 1,
"--json" => {}
other => passed.push(other.to_owned()),
}
index += 1;
}
if format.as_deref() != Some("jsonl") {
eprintln!("[supercov] gaps --format takes jsonl");
return ExitCode::from(2);
}
let Some(run) = passed.first().cloned() else {
eprintln!(
"[supercov] usage: supercov runs <run-id> gaps --format jsonl [--metric <metric>] [--output <file>]"
);
return ExitCode::from(2);
};
let shared = shared_options(&passed);
let root = match std::env::current_dir() {
Ok(root) => root,
Err(error) => {
eprintln!("[supercov] could not resolve the current directory: {error}");
return ExitCode::from(2);
}
};
let query = |arguments: Vec<String>| -> Result<Value, String> {
let invocation = super::parse_public_query("runs", &arguments)
.map_err(|error| error.error.message.clone())?;
let output = super::execute_public_query(&root, &invocation)
.map_err(|failure| failure.error.message.clone())?;
let text = output.agent_json().map_err(|error| error.message.clone())?;
serde_json::from_str::<Value>(&text).map_err(|error| error.to_string())
};
let mut gaps_arguments = passed.clone();
gaps_arguments.extend(["--json", "--limit", ALL].map(str::to_owned));
let gaps = match query(gaps_arguments) {
Ok(gaps) => gaps,
Err(error) => {
eprintln!("[supercov] {error}");
return ExitCode::from(2);
}
};
let files = gaps["data"]["gaps"]
.as_array()
.into_iter()
.flatten()
.filter_map(|gap| gap["file"].as_str().map(str::to_owned))
.collect::<Vec<_>>();
let mut lines = String::new();
let mut written = 0usize;
for file in &files {
let mut file_arguments = vec![run.clone(), "file".into(), file.clone()];
file_arguments.extend(shared.iter().cloned());
file_arguments.extend(["--json", "--limit", ALL].map(str::to_owned));
let detail = match query(file_arguments) {
Ok(detail) => detail,
Err(error) => {
eprintln!("[supercov] {file}: {error}");
return ExitCode::from(2);
}
};
for gap_line in detail["data"]["gapLines"].as_array().into_iter().flatten() {
for obligation in gap_line["obligations"].as_array().into_iter().flatten() {
lines.push_str(&row(file, gap_line, obligation).to_string());
lines.push('\n');
written += 1;
}
}
}
match output {
Some(path) => {
if let Err(error) = std::fs::write(&path, &lines) {
eprintln!("[supercov] {}: {error}", path.display());
return ExitCode::from(2);
}
eprintln!(
"[supercov] wrote {written} uncovered obligation(s) in {} file(s) to {}",
files.len(),
path.display()
);
}
None => {
let mut stdout = std::io::stdout().lock();
if stdout.write_all(lines.as_bytes()).is_err() {
return ExitCode::from(2);
}
}
}
ExitCode::SUCCESS
}
fn shared_options(arguments: &[String]) -> Vec<String> {
let mut shared = Vec::new();
let mut index = 0;
while index < arguments.len() {
if SHARED.contains(&arguments[index].as_str())
&& let Some(value) = arguments.get(index + 1)
{
shared.push(arguments[index].clone());
shared.push(value.clone());
index += 1;
}
index += 1;
}
shared
}
fn row(file: &str, gap_line: &Value, obligation: &Value) -> Value {
let mut row = serde_json::Map::new();
row.insert("file".into(), Value::String(file.to_owned()));
if let Some(fields) = obligation.as_object() {
for (key, value) in fields {
row.insert(key.clone(), value.clone());
}
}
if let Some(source) = gap_line["source"].as_str() {
row.insert("lineSource".into(), Value::String(source.to_owned()));
}
Value::Object(row)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_exported_obligation_names_its_file_and_line_source() {
let gap_line = serde_json::json!({ "line": 28, "source": "if (!context) {" });
let obligation =
serde_json::json!({ "kind": "mcdc", "line": 28, "missingCondition": "!context" });
let exported = row("src/chart.tsx", &gap_line, &obligation);
assert_eq!(exported["file"], "src/chart.tsx");
assert_eq!(exported["kind"], "mcdc");
assert_eq!(exported["missingCondition"], "!context");
assert_eq!(exported["lineSource"], "if (!context) {");
let options = shared_options(&[
"run_1".into(),
"--metric".into(),
"mcdc".into(),
"--kind".into(),
"e2e".into(),
]);
assert_eq!(options, ["--metric", "mcdc", "--kind", "e2e"]);
}
}