Test Anything Protocol
TAP streams read and written, version 14 and older ones: plans first or last, SKIP and TODO in the spellings producers use, indented and buffered subtests, YAML diagnostics, Bail out!, escaped hashes. A summary across subtests that passes or fails by TAP's own rules: a failing TODO does not count, a wrong plan does.
Version 1.0.0 · 3 functions · 1 crate
Functions
What use tap_results.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use tap_parse, tap_summary from tap_results (Quillo 0.6).
tap_parse(text: str) → anyor an errorReads a TAP stream: { version, plan, tests, comments, bail_out, problems, unknown_lines, passed }, each test with its directive, YAML diagnostics and subtest.
tap_summary(text: str) → anyor an errorTotals across subtests: { total, passed, failed, skipped, todo, unexpected_passes, failures, ok, problems, bail_out }; a failing TODO is not a failure, and a wrong plan is.
tap_write(tests: list) → stror an errorWrites TAP version 14 from tests { ok, description, directive, reason, diagnostics, subtest }; subtest is a list of tests. The numbers and the plan are made here.
Get it
$ quillo install tap_results # tap_results.qwer into this folder$ quillo install tap_results --example # and its example, tap_results.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/tap_results.qwer$ curl -O https://quillolang.org/plugins/files/tap_results.qwe$ curl -O https://quillolang.org/plugins/files/tap_results_sample.tapThen, in your program, all of it or only what you call:
use tap_results.qweruse tap_parse, tap_summary from tap_results // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
serde_json = { version = "1", features = ["preserve_order"] }Example
// A test run's TAP read for a dashboard: what passed by TAP's own rules, what failed and where,// and a program's checks written as TAP for a harness to read.//// quillo run tap_results.qwe then open http://localhost:8080/read, /summary, /yaml, /write, /old and /brokenuse "tap_results.qwer"let TEXT = read_file("tap_results_sample.tap")route GET /read { let t = tap_parse(TEXT)? { version: t.version, plan: t.plan, passed: t.passed, comments: t.comments, problems: t.problems, tests: t.tests | map((x) => [x.number, x.ok, x.description, x.directive, x.reason]), subtest: [t.tests[5].subtest.name, t.tests[5].subtest.plan.end, t.tests[5].subtest.tests[2].subtest.name], buffered: t.tests[7].subtest.tests[0].description, }}route GET /summary = tap_summary(TEXT)?route GET /yaml = tap_parse(TEXT)?.tests[1].diagnosticslet CHECKS = [ { ok: true, description: "boots" }, { ok: false, description: "pays # twice", diagnostics: { message: "two: charges", wanted: 1, found: 2, lines: "first\nsecond\n", tags: ["a", "b"], nested: { deep: true, none: null } } }, { ok: true, description: "mail", directive: "skip", reason: "no sink" }, { ok: false, description: "search", directive: "todo", reason: "soon" }, { description: "group", subtest: [{ ok: true, description: "one" }, { ok: false, description: "two" }] }, { ok: true },]route GET /write { let text = tap_write(CHECKS)? let back = tap_parse(text)? { text: text, plan: back.plan.end, passed: back.passed, problems: back.problems, hash: back.tests[1].description, yaml: back.tests[1].diagnostics, group: [back.tests[4].ok, back.tests[4].subtest.passed], same: tap_write(back.tests | map((t) => t.subtest == null ? t : merge(t, { subtest: t.subtest.tests })))? == text, bad: tap_write([{ ok: true, directive: "later" }]).error, newline: tap_write([{ ok: true, description: "a\nb" }]).error, }}route GET /old = { plan_last: tap_parse("ok 1\nok\nnot ok - third # TODO later\n1..3\n")?.passed, skip_all: tap_summary("1..0 # SKIP no database\n")?, plan_reason: tap_parse("1..0 # skipped: none\n")?.plan, case: tap_parse("ok 1 - x # skipped because\nok 2 - y # ToDo soon\n1..2\n")?.tests | map((t) => [t.directive, t.reason]), no_number: tap_parse("ok - x\nok - y\n1..2")?.tests | map((t) => t.number), crlf: tap_parse("ok 1\r\n1..1\r\n")?.passed, junk: tap_parse("hello\nok 1\n1..1\n")?.unknown_lines, bad_yaml: tap_parse("not ok 1\n ---\n : [\n ...\n1..1\n")?.tests[0].diagnostics,}route GET /broken = { no_plan: tap_summary("ok 1\n")?, too_few: tap_parse("1..3\nok 1\n")?.problems, two_plans: tap_parse("1..1\nok 1\n1..1\n")?.problems, middle: tap_parse("ok 1\n1..2\nok 2\n")?.problems, order: tap_parse("ok 2\nok 1\n1..2\n")?.problems, bail: tap_summary("1..3\nok 1\nBail out! database down\nok 2\n")?, open_yaml: tap_parse("ok 1\n ---\n a: 1\n1..1\n")?.problems, bad_sub: tap_summary(" not ok 1 - inner\n 1..1\nok 1 - outer\n1..1\n")?.ok,}$ quillo run tap_results.qwe$ curl 'localhost:8080/read'$ curl 'localhost:8080/summary'$ curl 'localhost:8080/yaml'$ curl 'localhost:8080/write'$ curl 'localhost:8080/old'$ curl 'localhost:8080/broken'Source
//! Test Anything Protocol read and written: plans, directives, subtests, YAML diagnostics, Bail out!, and a summary that says whether a run passed by TAP's own rules.//! version: 1.0.0//!//! [dependencies]//! serde_json = { version = "1", features = ["preserve_order"] }use serde_json::{json, Map, Value};const MAX_DEPTH: usize = 32;fn indent_of(l: &str) -> usize { l.len() - l.trim_start_matches(' ').len()}/// `# SKIP why`, `# TODO why`, in any case, and SKIPPED and the like, as older producers write; a `\#` is not a directive.fn split_directive(text: &str) -> (String, Option<String>, Option<String>) { let b = text.as_bytes(); let mut i = 0; while i < b.len() { if b[i] == b'\\' { i += 2; continue; } if b[i] == b'#' { let rest = text[i + 1..].trim_start(); let word: String = rest.chars().take_while(|c| c.is_alphabetic()).collect(); let u = word.to_uppercase(); if u.starts_with("SKIP") || u == "TODO" { let reason = rest[word.len()..].trim().trim_start_matches([':', '-']).trim(); let kind = if u.starts_with("SKIP") { "skip" } else { "todo" }; return (text[..i].trim_end().to_string(), Some(kind.to_string()), Some(reason.to_string()).filter(|r| !r.is_empty())); } } i += 1; } (text.trim_end().to_string(), None, None)}fn unescape(d: &str) -> String { d.replace("\\#", "#").replace("\\\\", "\\")}fn scalar(s: &str) -> Value { let t = s.trim(); if t.len() >= 2 && (t.starts_with('"') && t.ends_with('"') || t.starts_with('\'') && t.ends_with('\'')) { if t.starts_with('"') { if let Ok(v) = serde_json::from_str::<Value>(t) { return v; } } return json!(t[1..t.len() - 1].replace("''", "'")); } match t { "" | "~" | "null" => Value::Null, "true" => json!(true), "false" => json!(false), "[]" => json!([]), "{}" => json!({}), _ => { if let Ok(i) = t.parse::<i64>() { return json!(i); } if t.bytes().any(|b| b.is_ascii_digit()) { if let Ok(f) = t.parse::<f64>() { if f.is_finite() { return json!(f); } } } json!(t) } }}/// The YAML TAP uses: maps, lists, scalars and `|` blocks. Whatever is outside that comes back as text, under `raw`.fn yaml_block(lines: &[&str], at: &mut usize, indent: usize, depth: usize) -> Result<Value, String> { if depth > MAX_DEPTH { return Err("too deep".to_string()); } let first = lines.get(*at).ok_or("empty")?; if first.trim_start().starts_with("- ") || first.trim() == "-" { let mut list = vec![]; while *at < lines.len() && indent_of(lines[*at]) == indent && (lines[*at].trim_start().starts_with("- ") || lines[*at].trim() == "-") { let rest = lines[*at].trim_start().trim_start_matches('-').trim_start().to_string(); *at += 1; if rest.is_empty() { let next = lines.get(*at).map_or(0, |l| indent_of(l)); list.push(if next > indent { yaml_block(lines, at, next, depth + 1)? } else { Value::Null }); } else if rest.contains(": ") || rest.ends_with(':') { // `- a: 1` opens a map at the indent of `a`. let inner = indent + 2; let mut m = Map::new(); let (k, v) = rest.split_once(':').ok_or("bad")?; m.insert(k.trim().to_string(), scalar(v)); if *at < lines.len() && indent_of(lines[*at]) == inner { if let Value::Object(o) = yaml_block(lines, at, inner, depth + 1)? { m.extend(o); } } list.push(Value::Object(m)); } else { list.push(scalar(&rest)); } } return Ok(Value::Array(list)); } let mut m = Map::new(); while *at < lines.len() { let l = lines[*at]; if l.trim().is_empty() { *at += 1; continue; } let ind = indent_of(l); if ind < indent { break; } if ind > indent { return Err(format!("unexpected indent: {}", l.trim())); } let t = l.trim(); let (k, v) = t.split_once(':').filter(|(k, v)| !k.is_empty() && (v.is_empty() || v.starts_with(' '))).ok_or(format!("not a mapping: {}", t))?; let (k, v) = (k.trim().trim_matches(|c| c == '"' || c == '\'').to_string(), v.trim()); *at += 1; if v == "|" || v == ">" || v == "|-" || v == ">-" { let mut body = vec![]; while *at < lines.len() && (lines[*at].trim().is_empty() || indent_of(lines[*at]) > indent) { body.push(lines[*at]); *at += 1; } let pad = body.iter().filter(|b| !b.trim().is_empty()).map(|b| indent_of(b)).min().unwrap_or(0); let joined: Vec<&str> = body.iter().map(|b| b.get(pad..).unwrap_or("")).collect(); let mut text = joined.join(if v.starts_with('>') { " " } else { "\n" }); if !v.ends_with('-') { text.push('\n'); } m.insert(k, json!(text)); } else if v.is_empty() { let next = lines.get(*at).map_or(0, |n| indent_of(n)); let list_same = lines.get(*at).map_or(false, |n| next == indent && n.trim_start().starts_with("- ")); if *at < lines.len() && (next > indent || list_same) { m.insert(k, yaml_block(lines, at, next, depth + 1)?); } else { m.insert(k, Value::Null); } } else { m.insert(k, scalar(v)); } } Ok(Value::Object(m))}fn yaml(lines: &[&str]) -> Value { let mut at = 0; let indent = lines.iter().find(|l| !l.trim().is_empty()).map_or(0, |l| indent_of(l)); if lines.iter().all(|l| l.trim().is_empty()) { return json!({}); } match yaml_block(lines, &mut at, indent, 0) { Ok(v) if at >= lines.len() => v, _ => json!({ "raw": lines.iter().map(|l| l.trim_start_matches(' ')).collect::<Vec<_>>().join("\n") }), }}struct Doc { version: Option<i64>, plan: Option<Value>, plan_at: Option<usize>, tests: Vec<Value>, comments: Vec<String>, bail: Option<String>, problems: Vec<String>, unknown: Vec<String>,}fn test_point(line: &str) -> Option<(bool, Option<i64>, String)> { let (ok, rest) = if let Some(r) = line.strip_prefix("not ok") { (false, r) } else if let Some(r) = line.strip_prefix("ok") { (true, r) } else { return None; }; if !(rest.is_empty() || rest.starts_with(' ') || rest.starts_with('\t')) { return None; } let rest = rest.trim_start(); let digits: String = rest.chars().take_while(char::is_ascii_digit).collect(); let (num, rest) = if digits.is_empty() { (None, rest) } else { (digits.parse::<i64>().ok(), rest[digits.len()..].trim_start()) }; let rest = rest.strip_prefix("- ").or(rest.strip_prefix('-')).unwrap_or(rest); Some((ok, num, rest.to_string()))}/// Reads one level, starting at `*i`, with lines at `pad` spaces; stops when a line is less indented.fn level(lines: &[&str], i: &mut usize, pad: usize, depth: usize) -> Result<Doc, String> { if depth > MAX_DEPTH { return Err("subtests are nested too deep".to_string()); } let mut d = Doc { version: None, plan: None, plan_at: None, tests: vec![], comments: vec![], bail: None, problems: vec![], unknown: vec![] }; let mut pending: Option<(Doc, Option<String>)> = None; let mut seen = 0; while *i < lines.len() { let raw = lines[*i]; let n = *i + 1; if raw.trim().is_empty() { *i += 1; continue; } let ind = indent_of(raw); if ind < pad { break; } if ind >= pad + 4 { // A subtest's lines, with or without `# Subtest:` before them. let doc = level(lines, i, pad + 4, depth + 1)?; let name = pending.take().and_then(|p| p.1); pending = Some((doc, name)); continue; } let line = raw[pad..].trim_end(); *i += 1; if let Some(v) = line.strip_prefix("TAP version ") { match v.trim().parse::<i64>() { Ok(x) if n == 1 || pad > 0 || d.tests.is_empty() => d.version = Some(x), _ => d.problems.push(format!("line {}: a TAP version must come first", n)), } } else if line.starts_with("1..") || line.starts_with("0..") { let (body, _, reason) = split_directive(line); let (from, to) = body.split_once("..").unwrap_or(("1", "0")); let to: i64 = to.trim().parse().map_err(|_| format!("line {}: `{}` is not a plan", n, line))?; if d.plan.is_some() { d.problems.push(format!("line {}: a second plan", n)); } else { d.plan = Some(json!({ "start": from.parse::<i64>().unwrap_or(1), "end": to, "skip": if to == 0 { reason } else { None } })); d.plan_at = Some(d.tests.len()); } } else if let Some((ok, num, text)) = test_point(line) { seen += 1; if let Some(x) = num { if x != seen { d.problems.push(format!("line {}: test {} is out of order, expected {}", n, x, seen)); } } let buffered = text.trim_end().ends_with('{'); let (desc, dir, reason) = split_directive(text.trim_end().trim_end_matches('{').trim_end()); let mut t = Map::new(); t.insert("number".into(), json!(num.unwrap_or(seen))); t.insert("ok".into(), json!(ok)); t.insert("description".into(), json!(unescape(&desc))); t.insert("directive".into(), json!(dir)); t.insert("reason".into(), json!(reason)); t.insert("line".into(), json!(n)); // A YAML block belongs to the test before it, two spaces in. if lines.get(*i).map_or(false, |l| indent_of(l) == pad + 2 && l.trim() == "---") { let start = *i + 1; let mut j = start; while j < lines.len() && !(indent_of(lines[j]) == pad + 2 && lines[j].trim() == "...") { j += 1; } if j >= lines.len() { d.problems.push(format!("line {}: a YAML block is not closed with ...", *i + 1)); } else { t.insert("diagnostics".into(), yaml(&lines[start..j])); *i = j + 1; } } if buffered { let doc = level(lines, i, pad + 4, depth + 1)?; if lines.get(*i).map(|l| l.trim()) == Some("}") { *i += 1; } else { d.problems.push(format!("line {}: a buffered subtest is not closed with }}", n)); } t.insert("subtest".into(), doc_json(doc, Some(unescape(&desc)))); } else if let Some((doc, name)) = pending.take() { t.insert("subtest".into(), doc_json(doc, name.or(Some(unescape(&desc))))); } d.tests.push(Value::Object(t)); } else if let Some(r) = line.strip_prefix("Bail out!") { d.bail = Some(r.trim().to_string()); *i = lines.len(); break; } else if let Some(c) = line.strip_prefix('#') { let c = c.trim(); match c.strip_prefix("Subtest:").or(c.strip_prefix("Subtest")) { Some(name) => pending = Some((level_empty(), Some(name.trim().to_string()))), None => d.comments.push(c.to_string()), } } else if line.starts_with("pragma ") { // Kept apart from unknown lines, and otherwise not acted on. } else { d.unknown.push(line.to_string()); } } Ok(d)}fn level_empty() -> Doc { Doc { version: None, plan: None, plan_at: None, tests: vec![], comments: vec![], bail: None, problems: vec![], unknown: vec![] }}fn doc_json(mut d: Doc, name: Option<String>) -> Value { let count = d.tests.len() as i64; match d.plan.as_ref() { None if d.bail.is_none() => d.problems.push("no plan".to_string()), Some(p) => { let want = p["end"].as_i64().unwrap_or(0) - p["start"].as_i64().unwrap_or(1) + 1; if want != count && d.bail.is_none() { d.problems.push(format!("the plan says {} tests and {} ran", want.max(0), count)); } // The plan goes first or last: never between tests. if let Some(at) = d.plan_at { if at != 0 && at != d.tests.len() { d.problems.push("the plan is in the middle of the tests".to_string()); } } } None => {} } let failed = |t: &&Value| t["ok"] == json!(false) && t["directive"] != json!("todo"); let sub_failed = d.tests.iter().any(|t| t.get("subtest").map_or(false, |s| s["passed"] == json!(false)) && t["directive"] != json!("todo")); let passed = !d.tests.iter().any(|t| failed(&t)) && d.problems.is_empty() && d.bail.is_none() && !sub_failed; json!({ "name": name, "version": d.version, "plan": d.plan, "tests": d.tests, "comments": d.comments, "bail_out": d.bail, "problems": d.problems, "unknown_lines": d.unknown, "passed": passed, })}fn read(text: &str) -> Result<Value, String> { let text = text.trim_start_matches('\u{feff}').replace("\r\n", "\n"); let lines: Vec<&str> = text.lines().collect(); let mut i = 0; let d = level(&lines, &mut i, 0, 0)?; Ok(doc_json(d, None))}/// Reads a TAP stream: `{ version, plan, tests, comments, bail_out, problems, unknown_lines, passed }`, each test with its directive, YAML `diagnostics` and `subtest`.pub fn tap_parse(text: &str) -> Result<Value, String> { read(text)}#[derive(Default)]struct Count { total: i64, pass: i64, fail: i64, skip: i64, todo: i64, bonus: i64, failures: Vec<Value>,}fn count(doc: &Value, trail: &str, c: &mut Count, depth: usize) { if depth > MAX_DEPTH { return; } for t in doc["tests"].as_array().into_iter().flatten() { let name = t["description"].as_str().unwrap_or(""); let path = if trail.is_empty() { name.to_string() } else { format!("{} > {}", trail, name) }; if let Some(s) = t.get("subtest") { count(s, &path, c, depth + 1); continue; } c.total += 1; let ok = t["ok"] == json!(true); match t["directive"].as_str() { Some("skip") => c.skip += 1, Some("todo") if ok => c.bonus += 1, Some("todo") => c.todo += 1, _ if ok => c.pass += 1, _ => { c.fail += 1; c.failures.push(json!({ "name": path, "line": t["line"], "diagnostics": t.get("diagnostics") })); } } }}/// Totals across subtests: `{ total, passed, failed, skipped, todo, unexpected_passes, failures, ok, problems, bail_out }`; a failing TODO is not a failure, and a wrong plan is.pub fn tap_summary(text: &str) -> Result<Value, String> { let d = read(text)?; let mut c = Count::default(); count(&d, "", &mut c, 0); Ok(json!({ "total": c.total, "passed": c.pass, "failed": c.fail, "skipped": c.skip, "todo": c.todo, "unexpected_passes": c.bonus, "failures": c.failures, "ok": d["passed"], "problems": d["problems"], "bail_out": d["bail_out"], }))}fn yaml_scalar(v: &Value) -> String { match v { Value::Null => "~".to_string(), Value::String(s) => { let plain = !s.is_empty() && !s.contains(['\n', ':', '#', '"', '\'', '{', '}', '[', ']', '&', '*', '!', '|', '>', '%', '@', '`']) && s.trim() == s && !s.starts_with('-') && scalar(s).is_string(); if plain { s.clone() } else { serde_json::to_string(s).unwrap_or_default() } } x => x.to_string(), }}fn yaml_write(v: &Value, pad: &str, out: &mut String, depth: usize) -> Result<(), String> { if depth > MAX_DEPTH { return Err("diagnostics are nested too deep".to_string()); } match v { Value::Object(m) => { for (k, x) in m { match x { Value::Object(o) if !o.is_empty() => { out.push_str(&format!("{}{}:\n", pad, k)); yaml_write(x, &format!("{} ", pad), out, depth + 1)?; } Value::Array(a) if !a.is_empty() => { out.push_str(&format!("{}{}:\n", pad, k)); yaml_write(x, &format!("{} ", pad), out, depth + 1)?; } Value::String(s) if s.contains('\n') => { out.push_str(&format!("{}{}: |{}\n", pad, k, if s.ends_with('\n') { "" } else { "-" })); for l in s.trim_end_matches('\n').split('\n') { out.push_str(&format!("{} {}\n", pad, l)); } } Value::Object(_) => out.push_str(&format!("{}{}: {{}}\n", pad, k)), Value::Array(_) => out.push_str(&format!("{}{}: []\n", pad, k)), x => out.push_str(&format!("{}{}: {}\n", pad, k, yaml_scalar(x))), } } } Value::Array(a) => { for x in a { if x.is_object() || x.is_array() { out.push_str(&format!("{}-\n", pad)); yaml_write(x, &format!("{} ", pad), out, depth + 1)?; } else { out.push_str(&format!("{}- {}\n", pad, yaml_scalar(x))); } } } x => out.push_str(&format!("{}{}\n", pad, yaml_scalar(x))), } Ok(())}fn write_level(tests: &[Value], pad: &str, out: &mut String, depth: usize) -> Result<(), String> { if depth > MAX_DEPTH { return Err("subtests are nested too deep".to_string()); } for (i, t) in tests.iter().enumerate() { let n = i + 1; let desc = t.get("description").and_then(Value::as_str).unwrap_or(""); if desc.contains('\n') { return Err(format!("test {}: a description cannot hold a newline", n)); } let sub = t.get("subtest"); let list = sub.and_then(|s| s.get("tests")).and_then(Value::as_array).or(sub.and_then(Value::as_array)); if let Some(list) = list { out.push_str(&format!("{}# Subtest: {}\n", pad, desc)); let inner = format!("{} ", pad); write_level(list, &inner, out, depth + 1)?; out.push_str(&format!("{}1..{}\n", inner, list.len())); } let failed_inside = list.map_or(false, |l| l.iter().any(|x| x.get("ok") == Some(&json!(false)))); let ok = t.get("ok").and_then(Value::as_bool).unwrap_or(!failed_inside); let safe = desc.replace('\\', "\\\\").replace('#', "\\#"); out.push_str(&format!("{}{} {}", pad, if ok { "ok" } else { "not ok" }, n)); if !safe.is_empty() { out.push_str(&format!(" - {}", safe)); } match t.get("directive").and_then(Value::as_str) { Some(d) if d.eq_ignore_ascii_case("skip") || d.eq_ignore_ascii_case("todo") => { out.push_str(&format!(" # {}", d.to_uppercase())); if let Some(r) = t.get("reason").and_then(Value::as_str) { out.push_str(&format!(" {}", r)); } } Some(d) => return Err(format!("test {}: `{}` is not a directive: skip or todo", n, d)), None => {} } out.push('\n'); if let Some(y) = t.get("diagnostics").filter(|y| !y.is_null()) { out.push_str(&format!("{} ---\n", pad)); yaml_write(y, &format!("{} ", pad), out, 0)?; out.push_str(&format!("{} ...\n", pad)); } } Ok(())}/// Writes TAP version 14 from tests `{ ok, description, directive, reason, diagnostics, subtest }`; `subtest` is a list of tests. The numbers and the plan are made here.pub fn tap_write(tests: Vec<serde_json::Value>) -> Result<String, String> { let mut out = String::from("TAP version 14\n"); write_level(&tests, "", &mut out, 0)?; out.push_str(&format!("1..{}\n", tests.len())); Ok(out)}Related
junit_xml.qwer · 1.0.0JUnit XML test reports from Surefire, pytest, Jest and the rest read and written: nested suites, failures, errors and skips, Surefire's flaky and rerun elements, times with commas in them, totals that do not match the file noted, the slowest tests, shards merged, and several runs compared to find what is flaky.4 views · 8 downloadsSARIF reports sarif.qwer · 1.0.0SARIF 2.1 reports from code scanners read into flat results, with rules found by index or id, messages filled in, severities defaulted as the spec says and places followed through artifacts. Reports built and merged by tool with their indexes redone, a summary by rule and file, and what is new since a base report.3 views · 8 downloadsFake data fake_data.qwer · 1.0.0Realistic fake people, addresses, companies and whole tables by locale and seed, the same for the same seed: demo data, seed scripts and tests without real customers.7 views · 10 downloadsjq jq.qwer · 1.0.0jq filters over JSON with jaq: pick, reshape, filter and sum API answers, webhook bodies and JSON lines in the language people already write in jq, with variables and a check for typed-in filters.7 views · 8 downloadsSQL parsing sql_parse.qwer · 1.0.0SQL read as a database would: whether it parses in a dialect, its statement kind, the tables it reads and writes, and a fingerprint for grouping logs.9 views · 13 downloadsThread 0 posts
On Test Anything Protocol: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.