Merge main into fix/ai-os23-2831

This commit is contained in:
dotCooCoo
2026-11-08 18:52:59 -08:00
1346 changed files with 251226 additions and 13067 deletions
+71 -23
View File
@@ -1,10 +1,11 @@
//! `vectorcraft-cli`: VectorCraft from the command line.
//!
//! ```text
//! vectorcraft-cli --safe-mode SUBCOMMAND ...
//! vectorcraft-cli mcp [--connect 127.0.0.1:7979 | --headless] [--automation-read-root DIR]... [--automation-write-root DIR]...
//! vectorcraft-cli run [--in FILE] [--cmd id [--params '{json}'] | --commands FILE|-]... [--export out.svg]... [--scale 2]
//! vectorcraft-cli run [--in FILE] [--cmd id [--params '{json}'] | --commands FILE|-]... [--export out.svg]... [--scale 2] [--ppi 300]
//! vectorcraft-cli commands
//! vectorcraft-cli convert IN OUT [--scale 2] [--artboard 0 | --range 1-3,5] [--outline-text] [--decimals N] [--replace-lossy] [--editing-data] [--legacy-name-encoding ENC] [--preserve-editing] [--ai-version 7|pdf] [--hidden-objects layer|omit|visible] [--transparent-images flatten|omit]
//! vectorcraft-cli convert IN OUT [--scale 2] [--ppi 300] [--artboard 0 | --range 1-3,5] [--outline-text] [--decimals N] [--replace-lossy] [--editing-data] [--legacy-name-encoding ENC] [--preserve-editing] [--ai-version 7|pdf] [--hidden-objects layer|omit|visible] [--transparent-images flatten|omit]
//! vectorcraft-cli info FILE
//! vectorcraft-cli bench FILE [--size 2880x1800] [--iters 5]
//! vectorcraft-cli perf [--paths 50000] [--deep]
@@ -72,6 +73,11 @@ const USAGE: &str = "\
vectorcraft-cli — VectorCraft automation
USAGE:
vectorcraft-cli --safe-mode SUBCOMMAND ...
Run any subcommand without plug-ins: none loads or runs, and plug-in commands report that
Safe Mode is on. Plug-ins stay installed. `--safe-mode mcp` serves a headless session and
does not take --connect.
vectorcraft-cli mcp [--connect ADDR | --headless] [--automation-read-root DIR]... [--automation-write-root DIR]...
Run the MCP server on stdio. Default: connect to a running app at 127.0.0.1:7979
(vectorcraft --control 7979), falling back to a headless in-process session.
@@ -81,11 +87,12 @@ USAGE:
allowed, and a kind left out grants none of its access. They imply --headless: a running app
enforces its own (vectorcraft --control PORT --automation-read-root DIR …).
vectorcraft-cli run [--in FILE] [--cmd ID [--params JSON] | --commands FILE|-]... [--export FILE]... [--scale N]
vectorcraft-cli run [--in FILE] [--cmd ID [--params JSON] | --commands FILE|-]... [--export FILE]... [--scale N] [--ppi N]
Headless batch: open FILE (any readable format) or start a new document, run commands in
order, export each FILE in the format its extension picks (see Writable formats; a .ait FILE
is a template, written as file.saveAsTemplate writes it). --scale sizes raster exports
(pixels per point, default 1); other formats leave it out. Prints one JSON result per step.
is a template, written as file.saveAsTemplate writes it). Raster exports use the document's
raster effects resolution unless --scale (pixels per point) or --ppi (pixels per inch, wins
over --scale) is given; other formats leave them out. Prints one JSON result per step.
--commands reads steps from FILE, or from standard input for -, where it stands among the
--cmd and --export steps: a JSON array of {\"command\": ID, \"params\": {...}} (or \"id\" for
\"command\"), or one such object per line (JSON Lines), at most 64 MiB and 100000 steps.
@@ -94,10 +101,10 @@ USAGE:
vectorcraft-cli commands
Print the command catalogue as JSON.
vectorcraft-cli convert IN OUT [--scale N] [--artboard I | --range R] [--outline-text] [--decimals N] [--replace-lossy] [--editing-data] [--legacy-name-encoding ENC] [--preserve-editing] [--ai-version 7|pdf] [--hidden-objects layer|omit|visible] [--transparent-images flatten|omit]
vectorcraft-cli convert IN OUT [--scale N] [--ppi N] [--artboard I | --range R] [--outline-text] [--decimals N] [--replace-lossy] [--editing-data] [--legacy-name-encoding ENC] [--preserve-editing] [--ai-version 7|pdf] [--hidden-objects layer|omit|visible] [--transparent-images flatten|omit]
Open IN (any readable format) and export OUT in the format its extension picks (see Writable
formats). --scale sizes a raster OUT (pixels per point, default 1); other formats leave it
out. --artboard is 0-based, --range 1-based (\"1-3,5\"); a PDF gets every artboard
formats). Raster resolution: --ppi, else 72 times --scale, else the document's raster effects
resolution; other formats leave them out. --artboard is 0-based, --range 1-based (\"1-3,5\"); a PDF gets every artboard
unless one of them is given, EPS the bounds of the art, the other formats the first artboard.
A .ait OUT is a template of every artboard, written as file.saveAsTemplate writes it (not with
--artboard or --range).
@@ -129,12 +136,17 @@ USAGE:
Render FILE (any readable format) fitted to WxH (default 2880x1800) and print ms per frame
(warm), multithreaded and single-threaded.
vectorcraft-cli perf [--paths N] [--deep]
vectorcraft-cli perf [--paths N] [--deep] [--native-io [--artboards 1..4]]
Check the performance budgets (render, pan, hit test, save/load, SVG, Pathfinder, handle
and segment drags) on a synthetic N-path document (default 50000). Exits non-zero if a
budget is exceeded, unless the report says the machine is busy. --deep also times work with
no budget yet and prints memory use (the working set on Windows), with the peak so far on
Windows and Linux.
--native-io instead measures the application's native save/open commands (one warmup,
five samples, no disk I/O), including preview, PDF, compression and API base64 costs.
It reports the default and explicit PDF opt-out, with and without preview/compression;
--artboards repeats the same artwork
on 1 to 4 overlapping artboards. This mode allows at most 50000 paths.
";
/// The usage text plus the formats `document.open` reads and `document.export` writes.
@@ -176,12 +188,18 @@ fn main() -> ExitCode {
#[cfg(target_os = "macos")]
mac_fonts::install();
prefs_dir::install_fonts();
let args: Vec<String> = std::env::args().skip(1).collect();
let mut args: Vec<String> = std::env::args().skip(1).collect();
// Before any subcommand attaches the plug-in folder.
let safe_mode = args.first().is_some_and(|a| a == "--safe-mode");
if safe_mode {
args.remove(0);
vectorcraft_engine::cmd::plugin::start_in_safe_mode();
}
let r = match args.first().map(String::as_str) {
// `commands` and `info` take no options, so `--help` or `-h` anywhere after them prints the
// usage. The other subcommands check for it themselves.
Some("commands" | "info") if help_requested(&args[1..], &[]) => help(),
Some("mcp") => mcp(&args[1..]),
Some("mcp") => mcp(&args[1..], safe_mode),
Some("run") => run(&args[1..]),
Some("commands") => commands(),
Some("convert") => convert(&args[1..]),
@@ -214,7 +232,9 @@ fn with_stores(mut h: Headless) -> Headless {
h
}
fn mcp(args: &[String]) -> Result<(), String> {
/// `mcp`. With `--safe-mode` (`safe_mode`) it serves a headless session: a running app loads its own
/// plug-ins, so `--connect` is refused and the default does not connect.
fn mcp(args: &[String], safe_mode: bool) -> Result<(), String> {
if help_requested(args, &["--connect", "--automation-read-root", "--automation-write-root"]) {
return help();
}
@@ -241,6 +261,9 @@ fn mcp(args: &[String]) -> Result<(), String> {
if headless && connect.is_some() {
return Err("use either --connect or --headless".into());
}
if safe_mode && connect.is_some() {
return Err("--safe-mode runs a headless session without plug-ins; a running app loads its own: start it with `vectorcraft --safe-mode --control <port>`".into());
}
let read: Vec<&Path> = read_roots.iter().map(Path::new).collect();
let write: Vec<&Path> = write_roots.iter().map(Path::new).collect();
let roots = AutomationRoots::new(&read, &write)?;
@@ -253,7 +276,7 @@ fn mcp(args: &[String]) -> Result<(), String> {
file_access::confine_process(roots.clone());
eprintln!("vectorcraft-cli: files confined: {}", roots.describe());
Box::new(Headless::with_document().with_automation_roots(Some(roots)))
} else if headless {
} else if headless || safe_mode {
Box::new(with_stores(Headless::with_document()))
} else if let Some(addr) = connect {
// Explicit address: fail loudly if the app isn't there.
@@ -284,6 +307,7 @@ fn convert(args: &[String]) -> Result<(), String> {
let valued = [
"--scale",
"-s",
"--ppi",
"--artboard",
"-a",
"--range",
@@ -299,7 +323,8 @@ fn convert(args: &[String]) -> Result<(), String> {
}
let mut files = vec![];
let (mut scale, mut artboard, mut range, mut outline_text, mut replace_lossy, mut editing_data, mut preserve_editing) =
(1.0f64, None::<u64>, None::<String>, false, false, false, false);
(None::<f64>, None::<u64>, None::<String>, false, false, false, false);
let mut ppi = None::<f64>;
let mut decimals = None::<u64>;
// The encoding of a legacy `.ai`'s names that aren't UTF-8, passed on as `document.open`'s
// `legacyNameEncoding`; the engine checks it.
@@ -310,7 +335,15 @@ fn convert(args: &[String]) -> Result<(), String> {
let mut it = args.iter();
while let Some(a) = it.next() {
match a.as_str() {
"--scale" | "-s" => scale = it.next().and_then(|v| v.parse().ok()).ok_or("--scale needs a number")?,
"--scale" | "-s" => scale = Some(it.next().and_then(|v| v.parse().ok()).ok_or("--scale needs a number")?),
"--ppi" => {
ppi = Some(
it.next()
.and_then(|v| v.parse().ok())
.filter(|n: &f64| n.is_finite() && *n > 0.0)
.ok_or("--ppi needs a positive finite number")?,
);
}
"--artboard" | "-a" => artboard = Some(it.next().and_then(|v| v.parse().ok()).ok_or("--artboard needs an index")?),
"--range" | "-r" => range = Some(it.next().cloned().ok_or("--range needs artboards such as 1-3,5")?),
"--outline-text" => outline_text = true,
@@ -352,7 +385,7 @@ fn convert(args: &[String]) -> Result<(), String> {
// A conversion keeps hidden layers and objects, written hidden, where the format can (SVG,
// PSD).
let params = json!({"path": output, "artboard": artboard, "range": range, "outlineText": outline_text, "hiddenLayers": true, "acknowledgeLoss": replace_lossy});
let mut params = with_pixel_scale(params, &output, scale);
let mut params = with_pixel_scale(params, &output, scale, ppi);
if let Some(decimals) = decimals {
params["decimals"] = json!(decimals);
}
@@ -398,12 +431,17 @@ fn is_template(path: &str) -> bool {
vectorcraft_engine::cmd::fileio::format_for_name(path).is_some_and(|f| f.id == "ait")
}
/// The export `params` for `path`, with `--scale` (pixels per point) when its format is raster.
/// The export `params` for `path`, with explicit raster resolution options only for raster formats.
/// DXF has a `scale` of its own (drawing units per unit), which would resize the drawing and leave
/// its units unset.
fn with_pixel_scale(mut params: Value, path: &str, scale: f64) -> Value {
fn with_pixel_scale(mut params: Value, path: &str, scale: Option<f64>, ppi: Option<f64>) -> Value {
if vectorcraft_engine::cmd::fileio::writable_format(None, Some(path)).is_ok_and(|f| f.raster) {
params["scale"] = json!(scale);
if let Some(scale) = scale {
params["scale"] = json!(scale);
}
if let Some(ppi) = ppi {
params["ppi"] = json!(ppi);
}
}
params
}
@@ -511,7 +549,7 @@ fn command_step(v: Value) -> Result<Step, String> {
}
fn run(args: &[String]) -> Result<(), String> {
let valued = ["--in", "-i", "--cmd", "-c", "--params", "-p", "--commands", "--export", "-o", "--scale", "-s"];
let valued = ["--in", "-i", "--cmd", "-c", "--params", "-p", "--commands", "--export", "-o", "--scale", "-s", "--ppi"];
if help_requested(args, &valued) {
return help();
}
@@ -520,7 +558,8 @@ fn run(args: &[String]) -> Result<(), String> {
// The step of the `--cmd` just given, which a `--params` sets.
let mut params_for: Option<usize> = None;
let mut stdin_read = false;
let mut scale = 1.0;
let mut scale = None::<f64>;
let mut ppi = None::<f64>;
let mut it = args.iter();
while let Some(a) = it.next() {
let mut val = |what: &str| it.next().cloned().ok_or_else(|| format!("{a} needs {what}"));
@@ -557,7 +596,16 @@ fn run(args: &[String]) -> Result<(), String> {
}
"--scale" | "-s" => {
let raw = val("a number")?;
scale = raw.parse::<f64>().map_err(|_| format!("--scale {raw}: not a number"))?;
scale = Some(raw.parse::<f64>().map_err(|_| format!("--scale {raw}: not a number"))?);
}
"--ppi" => {
let raw = val("a number")?;
ppi = Some(
raw.parse::<f64>()
.ok()
.filter(|n| n.is_finite() && *n > 0.0)
.ok_or_else(|| format!("--ppi {raw}: needs a positive finite number"))?,
);
}
other => return Err(format!("unknown run option `{other}`")),
}
@@ -602,7 +650,7 @@ fn run(args: &[String]) -> Result<(), String> {
let r = if is_template(&path) {
h.call("engine.execute", json!({"command": "file.saveAsTemplate", "params": {"path": path, "format": "ait"}}))
} else {
h.call("app.export", with_pixel_scale(json!({"path": path}), &path, scale))
h.call("app.export", with_pixel_scale(json!({"path": path}), &path, scale, ppi))
};
let r = r.map_err(|e| format!("step {n}: export {path}: {e}"))?;
emit(json!({"step": "export", "result": r}))?;
+158 -4
View File
@@ -64,6 +64,61 @@ fn median_ms(runs: usize, mut f: impl FnMut()) -> f64 {
v.get(v.len() / 2).copied().unwrap_or(0.0)
}
/// Exercise the application's native save/open commands, including previews, PDF output,
/// compression and API base64 conversion. Session setup and validation are outside timing.
/// Five measured runs follow one warmup; no filesystem or GPU window is involved.
fn native_io(doc: &Document) -> Result<(), String> {
for (label, compatible, preview) in [
("default options", None, false),
("PDF disabled", Some(false), false),
("preview+gzip", None, true),
("preview+gzip, PDF disabled", Some(false), true),
] {
let mut session = vectorcraft_engine::Session::new();
session.add_document(doc.clone(), None);
let mut params = json!({"format":"vectorcraft","modified":null,"preview":preview,"compress":preview});
if let Some(value) = compatible {
params["pdfCompatible"] = json!(value);
}
let (mut saves, mut opens) = (Vec::new(), Vec::new());
let (mut size, mut has_pdf, mut compressed) = (0, false, false);
for run in 0..6 {
let start = Instant::now();
let saved = session.execute("file.saveAs", &params).map_err(|e| e.to_string())?;
let save_ms = start.elapsed().as_secs_f64() * 1000.0;
let data = saved["dataBase64"].as_str().ok_or("native save returned no dataBase64")?;
let bytes = vectorcraft_format::base64_decode(data).ok_or("native save returned invalid base64")?;
size = bytes.len();
has_pdf = vectorcraft_format::pdf_content(&bytes).is_some();
if compatible == Some(false) && has_pdf {
return Err("native save ignored the explicit PDF opt-out".into());
}
compressed = vectorcraft_format::is_compressed(&bytes);
if compressed != preview || vectorcraft_format::preview(&bytes).is_some() != preview {
return Err("native save did not apply the requested preview/compression options".into());
}
let args = json!({"name":"perf.vectorcraft","dataBase64":data});
let mut opened = vectorcraft_engine::Session::new();
let start = Instant::now();
opened.execute("document.open", &args).map_err(|e| e.to_string())?;
let open_ms = start.elapsed().as_secs_f64() * 1000.0;
if opened.doc().map_err(|e| e.to_string())?.doc.artboards.len() != doc.artboards.len() {
return Err("native round trip changed the artboard count".into());
}
if run > 0 {
saves.push(save_ms);
opens.push(open_ms);
}
}
saves.sort_by(f64::total_cmp);
opens.sort_by(f64::total_cmp);
let save = saves.get(2).copied().ok_or("no save measurements")?;
let open = opens.get(2).copied().ok_or("no open measurements")?;
outln!(" native {label}: save {save:.2} ms, open {open:.2} ms, {size} bytes, PDF {has_pdf}, compressed {compressed}");
}
Ok(())
}
/// 1-minute load average (macOS / Linux), if available.
#[cfg(not(windows))]
fn load_average() -> Option<f64> {
@@ -116,7 +171,8 @@ fn edit_drags(n: usize) -> Result<(f64, f64), String> {
let view = vectorcraft_engine::ViewInfo { zoom: 8.0, ..Default::default() };
let err = |e: vectorcraft_engine::EngineError| e.to_string();
let mut drag = |down: Point| -> Result<f64, String> {
let ev = |kind, y: f64| PointerEvent { kind, pos: down + Vec2::new(0.0, y), mods: Mods::default(), backquote: false, pressure: 1.0 };
let ev =
|kind, y: f64| PointerEvent { kind, pos: down + Vec2::new(0.0, y), mods: Mods::default(), backquote: false, pressure: 1.0, pen: None };
s.execute("select.anchors", &json!({"id": id.0, "anchors": [[0, 400]]})).map_err(err)?;
s.select_tool("directSelection", view).map_err(err)?;
let undo = s.doc().map_err(err)?.history.undo.len();
@@ -186,6 +242,48 @@ fn budget_row(name: &str, ms: f64, budget: f64, ok: bool) -> String {
format!(" {name:<42} {ms:>8.2} ms {budget:>7.0} ms {}", if ok { "ok" } else { "OVER" })
}
/// Tracked rows for pointing at things in an n-path document: what hovering, clicking, marquee
/// selection and snapping cost when nothing narrows the search.
fn pointing(n: usize, rows: &mut Vec<(String, f64)>) -> Result<(), String> {
use vectorcraft_engine::tools::{Mods, PointerEvent, PointerKind};
let err = |e: vectorcraft_engine::EngineError| e.to_string();
let mut s = vectorcraft_engine::Session::new();
s.add_document(synthetic(n)?, None);
let view = vectorcraft_engine::ViewInfo { zoom: 1.0, ..Default::default() };
s.select_tool("selection", view).map_err(err)?;
// Off the art (nothing under the pointer), the case every hover over empty canvas pays.
let off = Point::new(-200.0, -200.0);
let hover = |s: &mut vectorcraft_engine::Session| {
let _ = s.pointer(&PointerEvent::new(PointerKind::Move, off.x, off.y), view);
let _ = s.cursor(off, Mods::default(), view);
};
rows.push((format!("hover over empty canvas ({n} paths)"), median_ms(5, || hover(&mut s))));
s.execute("select.all", &json!({})).map_err(err)?;
rows.push((format!("hover over empty canvas, all selected ({n} paths)"), median_ms(5, || hover(&mut s))));
let doc = s.doc().map_err(err)?.doc.clone();
rows.push((format!("hit test, a miss ({n} paths)"), median_ms(5, || drop(hit::hit_test(&doc, off, hit::HitOptions::default())))));
let r = Rect::new(600.0, 450.0, 1000.0, 750.0);
rows.push((format!("marquee 400×300 ({n} paths)"), median_ms(5, || drop(hit::marquee(&doc, r, None, false)))));
let view_rect = Rect::new(0.0, 0.0, 1600.0, 1200.0);
rows.push((
format!("smart guide targets in view ({n} paths)"),
median_ms(5, || drop(vectorcraft_engine::tools::guides::Targets::collect(&doc, &[], Some(view_rect)))),
));
// Adding one more object to a selection of half the document: the command a shift-click ends
// in, without the pointer handling and hit test before it.
let ids: Vec<u64> = doc.layers[0].children().map(|c| c.iter().map(|n| n.id.0).collect()).unwrap_or_default();
s.execute("select.set", &json!({"ids": ids[..ids.len() / 2]})).map_err(err)?;
let mut k = ids.len() / 2;
rows.push((
format!("select.toggle in a {}-object selection", ids.len() / 2),
median_ms(5, || {
k += 1;
let _ = s.execute("select.toggle", &json!({"id": ids[k % ids.len()]}));
}),
));
Ok(())
}
/// `--deep`: tracked rows (no budget yet) for work the budgets above don't reach: editing a whole
/// selection, live effects, every Pathfinder operation, typing in a long story, and memory, with
/// the peak so far where `rss_mb` reads one.
@@ -215,7 +313,14 @@ fn deep(n: usize) -> Result<(), String> {
let first = s.doc().map_err(err)?.selection.objects.first().copied();
let at = first.and_then(|id| s.doc().ok()?.doc.node(id)?.geometric_bounds()).map(|b| b.center());
if let Some(down) = at {
let ev = |kind, dx: f64| PointerEvent { kind, pos: down + Vec2::new(dx, 0.0), mods: Mods::default(), backquote: false, pressure: 1.0 };
let ev = |kind, dx: f64| PointerEvent {
kind,
pos: down + Vec2::new(dx, 0.0),
mods: Mods::default(),
backquote: false,
pressure: 1.0,
pen: None,
};
s.select_tool("selection", view).map_err(err)?;
s.pointer(&ev(PointerKind::Down, 0.0), view).map_err(err)?;
let mut dx = 0.0;
@@ -274,6 +379,8 @@ fn deep(n: usize) -> Result<(), String> {
}),
));
note("after typing");
pointing(n, &mut rows)?;
note("after pointing");
// Four embedded 24 MP photos drawn at fit page by three renderers, as the app's canvas, render
// worker and a panel each draw them.
{
@@ -341,19 +448,36 @@ fn deep(n: usize) -> Result<(), String> {
}
pub fn run(args: &[String]) -> Result<(), String> {
if crate::help_requested(args, &["--paths"]) {
if crate::help_requested(args, &["--paths", "--artboards"]) {
return crate::help();
}
let mut n = 50_000usize;
let mut want_deep = false;
let mut want_native = false;
let mut artboards = None::<usize>;
let mut it = args.iter();
while let Some(a) = it.next() {
match a.as_str() {
"--deep" => want_deep = true,
"--native-io" => want_native = true,
"--artboards" => {
artboards =
Some(it.next().and_then(|v| v.parse().ok()).filter(|n| (1..=4).contains(n)).ok_or("--artboards needs a number from 1 to 4")?)
}
"--paths" => n = it.next().and_then(|v| v.parse().ok()).ok_or("--paths needs a number")?,
other => return Err(format!("unknown perf option `{other}`")),
}
}
if want_native && n > 50_000 {
return Err("--native-io allows at most 50000 paths".into());
}
if want_native && want_deep {
return Err("--native-io and --deep are separate measurement modes".into());
}
if artboards.is_some() && !want_native {
return Err("--artboards requires --native-io".into());
}
let artboards = artboards.unwrap_or(1);
let cores = std::thread::available_parallelism().map_or(1, |c| c.get());
// The figure is the 1-minute load average, or on Windows the cores busy over one second.
#[cfg(not(windows))]
@@ -366,8 +490,20 @@ pub fn run(args: &[String]) -> Result<(), String> {
outln!("WARNING: the machine is busy; wall-clock timings below are not trustworthy.");
}
let t = Instant::now();
let doc = synthetic(n)?;
let mut doc = synthetic(n)?;
outln!(" (built in {:.0} ms)", t.elapsed().as_secs_f64() * 1000.0);
if want_native {
let first = doc.artboards.first().cloned().ok_or("synthetic document has no artboard")?;
// Overlapping artboards intentionally repeat the same paths on every PDF page.
for index in 1..artboards {
let mut page = first.clone();
page.id = first.id.checked_add(index as u32).ok_or("artboard id overflow")?;
page.name = format!("Perf {}", index + 1);
doc.artboards.push(page);
}
outln!(" application native I/O: {artboards} overlapping artboard(s), one warmup + five samples; medians; no disk I/O");
return native_io(&doc);
}
let mut rows: Vec<(&str, f64, f64)> = vec![];
let opts = RenderOptions::default();
@@ -461,6 +597,24 @@ pub fn run(args: &[String]) -> Result<(), String> {
mod tests {
use super::*;
#[test]
fn native_io_exercises_the_application_save_and_open_commands() {
native_io(&synthetic(3).unwrap()).unwrap();
}
#[test]
fn native_io_rejects_conflicting_modes_and_excessive_workloads() {
for (args, expected) in [
(vec!["--native-io", "--deep"], "separate measurement modes"),
(vec!["--native-io", "--paths", "50001"], "at most 50000"),
(vec!["--native-io", "--artboards", "5"], "1 to 4"),
(vec!["--artboards", "1"], "requires --native-io"),
(vec!["--artboards", "2"], "requires --native-io"),
] {
assert!(run(&args.into_iter().map(str::to_owned).collect::<Vec<_>>()).unwrap_err().contains(expected));
}
}
#[test]
fn busy_share_leaves_out_idle_time() {
// In one second four processors log 40,000,000 ticks of 100 ns of kernel and user time,