mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-20 01:56:37 +03:00
bundle: the page JS and the bundler become a library crate downstream packs can reuse
The in-page bundle, the extension pieces, the registry JSON and the wasm-pack call were reachable only through `cargo xtask bundle`, which read `browser-bundle/*.js` from the repo root. A downstream crate that links impeccable-core + impeccable-wasm with its own rule pack had to copy the page JS to produce a detector bundle for its module. They move to `impeccable-bundle` (crates/bundle), which embeds every `browser-bundle/*.js` with `include_str!` and exposes `in_page_bundle`, `extension_pieces`, `registry_json`, `check_capture_contract` and `wasm_pack_build`. Nothing writes files or exits the process; the caller places the bytes. `registry_json` now reads `all_antipatterns()`, so an installed pack's rows land in `antipatterns.json` too (no built-in change). xtask becomes the workspace's caller and writes the same files to the same places; `cargo xtask bundle` is byte-identical, tracked live asset included. `IMPECCABLE_BUNDLE_SKIP_WASM_PACK` is the skip switch's new name, the old `IMPECCABLE_XTASK_SKIP_WASM_PACK` still works. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
410626e5de
commit
453a6e1500
@@ -0,0 +1,441 @@
|
||||
//! impeccable-bundle: the page JS and the bundler behind `cargo xtask bundle`.
|
||||
//!
|
||||
//! Every browser artifact the detector ships is this crate's output: the
|
||||
//! in-page bundle (`dist/detect-antipatterns-browser.js`, also the tracked
|
||||
//! `crates/live/assets/` copy the engine embeds and serves as `/detect.js`)
|
||||
//! and the five `extension/detector/` pieces. The page JS is embedded with
|
||||
//! `include_str!`, so a crate that depends on this one gets it without
|
||||
//! copying `browser-bundle/*.js` anywhere.
|
||||
//!
|
||||
//! A downstream crate that links `impeccable-core` + `impeccable-wasm` plus
|
||||
//! its own [`RulePack`](impeccable_core::rule_pack::RulePack) into one wasm
|
||||
//! module builds the same artifacts for its own module:
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use std::path::Path;
|
||||
//! let (glue, wasm) = impeccable_bundle::wasm_pack_build(
|
||||
//! Path::new("crates/my-wasm"),
|
||||
//! Path::new("target/wasm-bundle"),
|
||||
//! &[],
|
||||
//! )?;
|
||||
//! let js = impeccable_bundle::in_page_bundle(&glue, &wasm);
|
||||
//! let registry = impeccable_bundle::registry_json();
|
||||
//! let ext = impeccable_bundle::extension_pieces(&glue, &wasm, ®istry);
|
||||
//! # Ok::<(), String>(())
|
||||
//! ```
|
||||
//!
|
||||
//! The pack's rows reach [`registry_json`] once the pack is installed
|
||||
//! (`impeccable_core::rule_pack::install`), because the registry reads
|
||||
//! built-ins plus every registered slice.
|
||||
//!
|
||||
//! Nothing here writes files or exits the process: the caller decides where
|
||||
//! the bytes go and how a failure is reported.
|
||||
|
||||
use base64::Engine as _;
|
||||
use std::borrow::Cow;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
macro_rules! page_js {
|
||||
($($name:literal),* $(,)?) => {
|
||||
/// The page JS, in the order [`in_page_bundle`] concatenates it:
|
||||
/// `(file name, source)`. Embedded at compile time from
|
||||
/// `browser-bundle/`.
|
||||
pub const PAGE_JS: &[(&str, &str)] = &[
|
||||
$(($name, include_str!(concat!("../../../browser-bundle/", $name))),)*
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
page_js![
|
||||
"00-header.js",
|
||||
"10-probe.js",
|
||||
"15-snapshot.js",
|
||||
"30-scan-common.js",
|
||||
"35-visual.js",
|
||||
"40-overlay.js",
|
||||
"50-scan.js",
|
||||
"60-offscreen.js",
|
||||
"99-footer.js",
|
||||
];
|
||||
|
||||
/// One embedded page file by name.
|
||||
pub fn page_js(name: &str) -> Option<&'static str> {
|
||||
PAGE_JS.iter().find(|(n, _)| *n == name).map(|(_, src)| *src)
|
||||
}
|
||||
|
||||
fn src(name: &str) -> Cow<'static, str> {
|
||||
let s = page_js(name).unwrap_or_else(|| panic!("browser-bundle/{name}: not embedded"));
|
||||
if s.ends_with('\n') {
|
||||
Cow::Borrowed(s)
|
||||
} else {
|
||||
Cow::Owned(format!("{s}\n"))
|
||||
}
|
||||
}
|
||||
|
||||
/// In-page bundle concatenation order. `@@GLUE@@` is the wasm-bindgen glue,
|
||||
/// `@@WASM@@` the embedded module + synchronous instantiation.
|
||||
const ORDER: &[&str] = &[
|
||||
"00-header.js",
|
||||
"10-probe.js",
|
||||
"15-snapshot.js",
|
||||
"@@GLUE@@",
|
||||
"@@WASM@@",
|
||||
"30-scan-common.js",
|
||||
"35-visual.js",
|
||||
"40-overlay.js",
|
||||
"50-scan.js",
|
||||
"99-footer.js",
|
||||
];
|
||||
|
||||
/// The extension pieces, each an IIFE over a subset of the same sources plus
|
||||
/// a `window.*` export line. `core.js` is the wasm glue + an async loader
|
||||
/// (the module ships beside it as `core_bg.wasm`; no base64: the offscreen
|
||||
/// document fetches it) + the scan plumbing and visual-contrast orchestration
|
||||
/// + the offscreen session protocol.
|
||||
const EXT_SNAPSHOT: &[&str] = &["15-snapshot.js"];
|
||||
const EXT_OVERLAY: &[&str] = &["40-overlay.js"];
|
||||
const EXT_CORE: &[&str] = &["@@GLUE@@", "@@LOADER@@", "30-scan-common.js", "35-visual.js", "60-offscreen.js"];
|
||||
|
||||
fn push_glue(out: &mut String, glue_js: &str) {
|
||||
out.push_str("// --- wasm-bindgen glue (generated by cargo xtask bundle) ---\n");
|
||||
out.push_str(glue_js);
|
||||
if !glue_js.ends_with('\n') {
|
||||
out.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
/// The in-page bundle: `dist/detect-antipatterns-browser.js` and the tracked
|
||||
/// `crates/live/assets/` copy. `glue_js` and `wasm` are one wasm-pack
|
||||
/// `--target no-modules` build (see [`wasm_pack_build`]); the module is
|
||||
/// embedded as base64 and instantiated synchronously at load.
|
||||
///
|
||||
/// Deterministic: the same glue and module produce the same bytes.
|
||||
pub fn in_page_bundle(glue_js: &str, wasm: &[u8]) -> String {
|
||||
let b64 = base64::engine::general_purpose::STANDARD.encode(wasm);
|
||||
let mut out = String::new();
|
||||
for part in ORDER {
|
||||
match *part {
|
||||
"@@GLUE@@" => push_glue(&mut out, glue_js),
|
||||
"@@WASM@@" => {
|
||||
out.push_str("// --- impeccable_wasm module (generated by cargo xtask bundle) ---\n");
|
||||
out.push_str(&format!("const __IMPECCABLE_WASM_BYTES = {};\n", wasm.len()));
|
||||
out.push_str("const __IMPECCABLE_WASM_B64 = \"");
|
||||
out.push_str(&b64);
|
||||
out.push_str("\";\n");
|
||||
out.push_str(WASM_INIT);
|
||||
}
|
||||
name => out.push_str(&src(name)),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// What goes into `extension/detector/`, from [`extension_pieces`].
|
||||
pub struct ExtensionPieces {
|
||||
/// `snapshot.js`: the content-script page snapshot producer.
|
||||
pub snapshot_js: String,
|
||||
/// `overlay.js`: the content-script overlay UI.
|
||||
pub overlay_js: String,
|
||||
/// `core.js`: the offscreen-document wasm loader and scan session.
|
||||
pub core_js: String,
|
||||
/// `core_bg.wasm`: the module `core.js` fetches beside itself.
|
||||
pub core_bg_wasm: Vec<u8>,
|
||||
/// `antipatterns.json`: the registry slice the extension panel reads.
|
||||
pub antipatterns_json: String,
|
||||
}
|
||||
|
||||
/// The extension pieces for one wasm build. `registry_json` is written
|
||||
/// through unchanged, so a caller can pass [`registry_json()`] or its own.
|
||||
pub fn extension_pieces(glue_js: &str, wasm: &[u8], registry_json: &str) -> ExtensionPieces {
|
||||
let piece = |parts: &[&str], exports: &str, what: &str| -> String {
|
||||
let mut p = format!(
|
||||
"/**\n * Impeccable extension: {what}\n * Copyright (c) 2026 Paul Bakaus\n *\n * GENERATED -- do not edit. Source: browser-bundle/*.js (+ crates/core, crates/wasm for core.js).\n * Rebuild: cargo xtask bundle\n */\n"
|
||||
);
|
||||
p.push_str("(function () {\n");
|
||||
for part in parts {
|
||||
match *part {
|
||||
"@@GLUE@@" => push_glue(&mut p, glue_js),
|
||||
"@@LOADER@@" => p.push_str(EXT_CORE_LOADER),
|
||||
name => p.push_str(&src(name)),
|
||||
}
|
||||
}
|
||||
p.push_str(exports);
|
||||
p.push_str("})();\n");
|
||||
p
|
||||
};
|
||||
ExtensionPieces {
|
||||
snapshot_js: piece(
|
||||
EXT_SNAPSHOT,
|
||||
"window.__impeccableSnapshot = __impeccableSnapshot;\nwindow.__impeccableCreateDrawableIO = __createDrawableIO;\n",
|
||||
"snapshot.js, the content-script page snapshot producer (measurement only)",
|
||||
),
|
||||
overlay_js: piece(
|
||||
EXT_OVERLAY,
|
||||
"window.__impeccableCreateOverlay = createImpeccableOverlay;\n",
|
||||
"overlay.js, the content-script overlay UI (draws a findings list; no rules)",
|
||||
),
|
||||
core_js: piece(
|
||||
EXT_CORE,
|
||||
"",
|
||||
"core.js, the offscreen-document WASM core loader + scan session (rules run in core_bg.wasm)",
|
||||
),
|
||||
core_bg_wasm: wasm.to_vec(),
|
||||
antipatterns_json: registry_json.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The `atob` + synchronous instantiation that runs at bundle load. Chrome
|
||||
/// (verified on 151, headless and headed) compiles multi-MB modules
|
||||
/// synchronously on the main thread; the historical 4 KB limit no longer
|
||||
/// applies to `new WebAssembly.Module`. A CSP without 'wasm-unsafe-eval'
|
||||
/// throws here; consumers handle that per docs/WASM-BUNDLE.md.
|
||||
const WASM_INIT: &str = r#"function __impeccableWasmBytes() {
|
||||
const bin = atob(__IMPECCABLE_WASM_B64);
|
||||
const bytes = new Uint8Array(bin.length);
|
||||
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
||||
return bytes;
|
||||
}
|
||||
let __impeccable = null;
|
||||
let __impeccableInitError = null;
|
||||
try {
|
||||
wasm_bindgen.initSync({ module: __impeccableWasmBytes() });
|
||||
__impeccable = wasm_bindgen;
|
||||
} catch (e) {
|
||||
__impeccableInitError = e;
|
||||
}
|
||||
"#;
|
||||
|
||||
/// The offscreen document's core loader: fetch the module beside the script
|
||||
/// and instantiate asynchronously (the extension's own CSP carries
|
||||
/// 'wasm-unsafe-eval'). Also stubs the live-page probe namespace the glue
|
||||
/// imports, so a call that would need a page fails loudly instead of with a
|
||||
/// ReferenceError.
|
||||
const EXT_CORE_LOADER: &str = r#"const __impeccableDom = new Proxy({}, {
|
||||
get() { throw new Error('[impeccable] the offscreen document has no live page; load a snapshot first'); },
|
||||
});
|
||||
async function __impeccableLoadCore() {
|
||||
await wasm_bindgen({ module_or_path: chrome.runtime.getURL('detector/core_bg.wasm') });
|
||||
return wasm_bindgen;
|
||||
}
|
||||
"#;
|
||||
|
||||
/// The capture contract: the property and state lists in `15-snapshot.js`
|
||||
/// must equal the core's (`STYLE_PROPS`, `PSEUDO_PROPS` in snapshot.rs;
|
||||
/// `STATE_PSEUDOS` in selector.rs), or a rule reads a column the capture did
|
||||
/// not write. Returns the mismatch report on drift.
|
||||
pub fn check_capture_contract() -> Result<(), String> {
|
||||
let snapshot_js = page_js("15-snapshot.js").expect("15-snapshot.js embedded");
|
||||
fn js_list(src: &str, name: &str) -> Vec<String> {
|
||||
let start = src
|
||||
.find(&format!("const {name} = ["))
|
||||
.unwrap_or_else(|| panic!("15-snapshot.js: {name} not found"));
|
||||
let rest = &src[start..];
|
||||
let end = rest.find("];").expect("list end");
|
||||
rest[..end]
|
||||
.split('"')
|
||||
.skip(1)
|
||||
.step_by(2)
|
||||
.map(|s| s.to_string())
|
||||
.collect()
|
||||
}
|
||||
let pairs: [(&str, Vec<String>); 3] = [
|
||||
("__SNAP_STYLE_PROPS", impeccable_core::browser::snapshot::STYLE_PROPS.iter().map(|s| s.to_string()).collect()),
|
||||
("__SNAP_PSEUDO_PROPS", impeccable_core::browser::snapshot::PSEUDO_PROPS.iter().map(|s| s.to_string()).collect()),
|
||||
("__SNAP_STATE_PSEUDOS", impeccable_core::browser::selector::STATE_PSEUDOS.iter().map(|s| s.to_string()).collect()),
|
||||
];
|
||||
for (name, want) in pairs {
|
||||
let have = js_list(snapshot_js, name);
|
||||
if have != want {
|
||||
return Err(format!(
|
||||
"browser-bundle/15-snapshot.js {name} differs from the core's list\n js: {have:?}\n core: {want:?}"
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `antipatterns.json`: `{ id, name, category, description }` per rule, in
|
||||
/// registry order (built-ins first, then any installed rule pack's rows), as
|
||||
/// 2-space JSON with a trailing newline. This is the shape `bun run build`
|
||||
/// and the extension panel read.
|
||||
pub fn registry_json() -> String {
|
||||
let rows: Vec<serde_json::Value> = impeccable_core::registry::all_antipatterns()
|
||||
.map(|ap| {
|
||||
serde_json::json!({
|
||||
"id": ap.id,
|
||||
"name": ap.name,
|
||||
"category": ap.category,
|
||||
"description": ap.description,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let mut s = serde_json::to_string_pretty(&rows).expect("registry json");
|
||||
s.push('\n');
|
||||
s
|
||||
}
|
||||
|
||||
/// Build the wasm module for `crate_dir` and read back the wasm-bindgen glue
|
||||
/// and the `.wasm`:
|
||||
///
|
||||
/// ```text
|
||||
/// wasm-pack build <crate_dir> --target no-modules --release \
|
||||
/// --no-typescript --no-pack --out-dir <out_dir> --out-name impeccable
|
||||
/// ```
|
||||
///
|
||||
/// `extra_cargo_args` are passed to cargo after `--` (the workspace uses this
|
||||
/// for `--features pure-exports`). `WASM_PACK` names the binary when it is
|
||||
/// not on PATH; `IMPECCABLE_BUNDLE_SKIP_WASM_PACK=1` (or the older
|
||||
/// `IMPECCABLE_XTASK_SKIP_WASM_PACK=1`) reuses whatever `out_dir` already
|
||||
/// holds, for iterating on the page JS alone.
|
||||
///
|
||||
/// A downstream crate passes its own crate dir, so the module it gets back is
|
||||
/// the engine plus its rule pack.
|
||||
pub fn wasm_pack_build(
|
||||
crate_dir: &Path,
|
||||
out_dir: &Path,
|
||||
extra_cargo_args: &[&str],
|
||||
) -> Result<(String, Vec<u8>), String> {
|
||||
let skip = std::env::var_os("IMPECCABLE_BUNDLE_SKIP_WASM_PACK").is_some()
|
||||
|| std::env::var_os("IMPECCABLE_XTASK_SKIP_WASM_PACK").is_some();
|
||||
if !skip {
|
||||
let wasm_pack = std::env::var("WASM_PACK").unwrap_or_else(|_| "wasm-pack".to_string());
|
||||
let mut cmd = Command::new(&wasm_pack);
|
||||
cmd.arg("build")
|
||||
.arg(crate_dir)
|
||||
.arg("--target")
|
||||
.arg("no-modules")
|
||||
.arg("--release")
|
||||
.arg("--no-typescript")
|
||||
.arg("--no-pack")
|
||||
.arg("--out-dir")
|
||||
.arg(out_dir)
|
||||
.arg("--out-name")
|
||||
.arg("impeccable")
|
||||
// Size profile for the module; native builds keep opt-level 3.
|
||||
.env("CARGO_PROFILE_RELEASE_OPT_LEVEL", "z")
|
||||
// wasm-pack refuses to build a `cdylib` whose Cargo.toml lives in
|
||||
// a workspace with `[profile.*]` overrides only when it cannot
|
||||
// find the target dir; keep it explicit.
|
||||
.env("CARGO_TARGET_DIR", target_dir_for(crate_dir));
|
||||
if !extra_cargo_args.is_empty() {
|
||||
cmd.arg("--");
|
||||
for arg in extra_cargo_args {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
}
|
||||
let status = cmd
|
||||
.status()
|
||||
.map_err(|e| format!("wasm-pack build: failed to spawn {wasm_pack}: {e}"))?;
|
||||
if !status.success() {
|
||||
return Err(format!("wasm-pack build failed ({status})"));
|
||||
}
|
||||
}
|
||||
let glue_path = out_dir.join("impeccable.js");
|
||||
let wasm_path = out_dir.join("impeccable_bg.wasm");
|
||||
let glue = std::fs::read_to_string(&glue_path)
|
||||
.map_err(|e| format!("{}: {e}", glue_path.display()))?;
|
||||
let wasm = std::fs::read(&wasm_path).map_err(|e| format!("{}: {e}", wasm_path.display()))?;
|
||||
Ok((glue, wasm))
|
||||
}
|
||||
|
||||
/// `CARGO_TARGET_DIR` for a wasm-pack run: the caller's, else the target dir
|
||||
/// of the workspace `crate_dir` sits in (nearest ancestor with a
|
||||
/// `Cargo.lock`), else the crate's own.
|
||||
fn target_dir_for(crate_dir: &Path) -> PathBuf {
|
||||
if let Some(dir) = std::env::var_os("CARGO_TARGET_DIR") {
|
||||
return PathBuf::from(dir);
|
||||
}
|
||||
let abs = std::fs::canonicalize(crate_dir).unwrap_or_else(|_| crate_dir.to_path_buf());
|
||||
abs.ancestors()
|
||||
.find(|dir| dir.join("Cargo.lock").exists())
|
||||
.unwrap_or(&abs)
|
||||
.join("target")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn bundle_dir() -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../browser-bundle")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_files_match_the_directory() {
|
||||
let mut on_disk: Vec<String> = std::fs::read_dir(bundle_dir())
|
||||
.expect("browser-bundle/")
|
||||
.map(|e| e.expect("dir entry").file_name().to_string_lossy().into_owned())
|
||||
.filter(|name| name.ends_with(".js"))
|
||||
.collect();
|
||||
on_disk.sort();
|
||||
let mut embedded: Vec<String> = PAGE_JS.iter().map(|(n, _)| n.to_string()).collect();
|
||||
embedded.sort();
|
||||
assert_eq!(
|
||||
embedded, on_disk,
|
||||
"browser-bundle/*.js changed: update PAGE_JS (and ORDER, if the file belongs in the page bundle)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_sources_match_the_files_on_disk() {
|
||||
for (name, src) in PAGE_JS {
|
||||
let disk = std::fs::read_to_string(bundle_dir().join(name)).expect(name);
|
||||
assert_eq!(*src, disk, "{name} drifted from disk");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn in_page_bundle_has_the_expected_skeleton() {
|
||||
let out = in_page_bundle("const glue = 1;\n", &[0, 97, 115, 109]);
|
||||
assert!(out.starts_with(page_js("00-header.js").unwrap()));
|
||||
assert!(out.ends_with(page_js("99-footer.js").unwrap()));
|
||||
assert!(out.contains("// --- wasm-bindgen glue (generated by cargo xtask bundle) ---\nconst glue = 1;\n"));
|
||||
assert!(out.contains("const __IMPECCABLE_WASM_BYTES = 4;\n"));
|
||||
// base64 of the four-byte wasm preamble.
|
||||
assert!(out.contains("const __IMPECCABLE_WASM_B64 = \"AGFzbQ==\";\n"));
|
||||
assert!(out.contains("wasm_bindgen.initSync({ module: __impeccableWasmBytes() });"));
|
||||
// 60-offscreen.js is an extension-only piece.
|
||||
assert!(!out.contains(page_js("60-offscreen.js").unwrap()));
|
||||
// Order: probe and snapshot before the glue, scan after it.
|
||||
let glue_at = out.find("const glue = 1;").unwrap();
|
||||
assert!(out.find("__SNAP_STYLE_PROPS").unwrap() < glue_at);
|
||||
assert!(out.rfind("createImpeccableOverlay").unwrap() > glue_at);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extension_pieces_are_iifes_over_their_sources() {
|
||||
let ext = extension_pieces("const glue = 1;\n", &[0, 97, 115, 109], "[]\n");
|
||||
for (what, js) in [("snapshot", &ext.snapshot_js), ("overlay", &ext.overlay_js), ("core", &ext.core_js)] {
|
||||
assert!(js.starts_with("/**\n * Impeccable extension: "), "{what}");
|
||||
assert!(js.contains("(function () {\n"), "{what}");
|
||||
assert!(js.ends_with("})();\n"), "{what}");
|
||||
}
|
||||
assert!(ext.snapshot_js.contains("window.__impeccableSnapshot = __impeccableSnapshot;"));
|
||||
assert!(ext.overlay_js.contains("window.__impeccableCreateOverlay = createImpeccableOverlay;"));
|
||||
assert!(ext.core_js.contains("chrome.runtime.getURL('detector/core_bg.wasm')"));
|
||||
// The offscreen core fetches its module; nothing is base64 in there.
|
||||
assert!(!ext.core_js.contains("__IMPECCABLE_WASM_B64"));
|
||||
assert_eq!(ext.core_bg_wasm, vec![0, 97, 115, 109]);
|
||||
assert_eq!(ext.antipatterns_json, "[]\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capture_contract_holds() {
|
||||
check_capture_contract().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_json_is_two_space_rows() {
|
||||
let json = registry_json();
|
||||
assert!(json.ends_with("]\n"));
|
||||
assert!(json.contains("\n {\n \"id\": "));
|
||||
let rows: Vec<serde_json::Value> = serde_json::from_str(&json).expect("valid json");
|
||||
assert_eq!(rows.len(), impeccable_core::registry::all_antipatterns().count());
|
||||
for row in &rows {
|
||||
for key in ["id", "name", "category", "description"] {
|
||||
assert!(row.get(key).is_some(), "row missing {key}: {row}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user