Compare commits

..
Author SHA1 Message Date
skylar taylor-barrick ef24afc10c #618: move the OFFSET canvas test into the ui-egui test binary
Main now builds one integration-test binary per crate (#1989); only tests that install process-wide state stay separate. This test only drives its own session, so it joins tests/it.
2026-10-23 12:18:39 -06:00
skylar taylor-barrick 6528bb564b Merge remote-tracking branch 'origin/main' into test/backfill-regression-tests 2026-10-23 12:09:53 -06:00
skylar taylor-barrick d0281360c2 #618: finish the grid-snapped OFFSET in the canvas test
The test stopped once the source line was picked, so a broken side-point step could leave the drawing unchanged and still pass. It now clicks a side point above the line and checks that the copy lands 5 units up, at y = 7. It also checks that OFFSET asks for the next object and that Enter ends it.
2026-10-22 21:16:48 -06:00
skylar taylor-barrick de5aecfe8b #651: compare small text against the plot build path
The full-detail baseline used build(), which takes the on-screen path; plots take build_plot(), which keeps glyphs at full detail and records them as plot text. Comparing against build_plot() checks what the test's name says. With the screen-detail change reverted it still fails (480 triangles on screen, 480 in the plot).
2026-10-22 16:49:30 -06:00
skylar taylor-barrick 546b5a1cf3 #651: test that small filled text is simplified on screen and keeps full detail in plots
Filled TrueType text was triangulated at full outline detail at every zoom, which froze the canvas on pages of small text. The test builds four O glyphs on screen at 10 px and under 2 px per em and checks the triangle count drops (to one box per glyph at the smallest), while a plot keeps every triangle. Without the fix the on-screen build has the same 1656 triangles as the plot. It skips when no system font is installed, like the other TrueType render tests.
2026-10-22 16:34:32 -06:00
skylar taylor-barrick 2ab09850bb #1336: test that the DXF LAYER table stops at the record cap
The LAYER, LTYPE, STYLE and DIMSTYLE tables had no record cap, unlike VIEW, UCS and VPORT. The test reads a DXF with over a million LAYER records and checks the layer list stays at the cap; without the guard it holds every record (1000102 layers). The other three tables scan their lists linearly per record, so a million-record file is too slow to build in a test and they stay uncovered.
2026-10-22 16:34:32 -06:00
skylar taylor-barrick 4ab8c384b3 #618: test that OFFSET takes a clicked distance and picks its source under grid snap
OFFSET only accepted a typed distance, and with grid snap on a click meant to pick the source object was moved to the nearest grid point. The engine test clicks two points for the distance and checks the guide, the distance and the resulting offset; the canvas test does the same through grid-snapped clicks and then picks the source line. Without the fix the engine test stays at the distance prompt and the canvas test never picks the line.
2026-10-22 16:34:32 -06:00
15 changed files with 1112 additions and 312 deletions
-38
View File
@@ -21,41 +21,3 @@ jobs:
nix:
# Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos.
uses: storytold/craft-common-actions/.github/workflows/nix.yml@main
git-sources:
name: Git dependencies come from storytold
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Git dependencies come from storytold
run: |
set -euo pipefail
test -f Cargo.lock || { echo "Cargo.lock is missing"; exit 1; }
# Source replacement, patches and path overrides in cargo config never show up
# in Cargo.lock. Parsed rather than grepped: TOML has many spellings of one key.
# Cargo still reads the legacy .cargo/config.
python3 - <<'PY'
import os, sys, tomllib
for path in (".cargo/config", ".cargo/config.toml"):
if not os.path.exists(path):
continue
config = tomllib.load(open(path, "rb"))
replaced = [name for name, source in config.get("source", {}).items()
if isinstance(source, dict) and "replace-with" in source]
if replaced or "patch" in config or "paths" in config:
print(f"{path} replaces, patches or overrides a source")
sys.exit(1)
PY
# Fails when Cargo.lock is out of date with the manifests.
cargo metadata --locked --format-version 1 > /dev/null
python3 - <<'PY'
import sys, tomllib
lock = tomllib.load(open("Cargo.lock", "rb"))
bad = sorted({p["source"] for p in lock.get("package", [])
if p.get("source", "").startswith("git+")
and not p["source"].startswith("git+https://github.com/storytold/")})
if bad:
print("Git dependencies outside github.com/storytold:", *bad, sep="\n")
sys.exit(1)
PY
+1 -44
View File
@@ -11,57 +11,14 @@ on:
default: ""
permissions:
contents: read
contents: write
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
git-sources:
name: Git dependencies come from storytold
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Git dependencies come from storytold
run: |
set -euo pipefail
test -f Cargo.lock || { echo "Cargo.lock is missing"; exit 1; }
# Source replacement, patches and path overrides in cargo config never show up
# in Cargo.lock. Parsed rather than grepped: TOML has many spellings of one key.
# Cargo still reads the legacy .cargo/config.
python3 - <<'PY'
import os, sys, tomllib
for path in (".cargo/config", ".cargo/config.toml"):
if not os.path.exists(path):
continue
config = tomllib.load(open(path, "rb"))
replaced = [name for name, source in config.get("source", {}).items()
if isinstance(source, dict) and "replace-with" in source]
if replaced or "patch" in config or "paths" in config:
print(f"{path} replaces, patches or overrides a source")
sys.exit(1)
PY
# Fails when Cargo.lock is out of date with the manifests.
cargo metadata --locked --format-version 1 > /dev/null
python3 - <<'PY'
import sys, tomllib
lock = tomllib.load(open("Cargo.lock", "rb"))
bad = sorted({p["source"] for p in lock.get("package", [])
if p.get("source", "").startswith("git+")
and not p["source"].startswith("git+https://github.com/storytold/")})
if bad:
print("Git dependencies outside github.com/storytold:", *bad, sep="\n")
sys.exit(1)
PY
release:
needs: git-sources
permissions:
# The reusable release workflow tags and uploads the draft release.
contents: write
# Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos.
uses: storytold/craft-common-actions/.github/workflows/release.yml@main
with:
+1 -2
View File
@@ -11,8 +11,7 @@ permissions:
concurrency:
group: sonarcloud-${{ github.ref }}
# A push to main is the run that publishes coverage; superseded runs there would never finish.
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
cancel-in-progress: true
jobs:
sonarcloud:
+2 -1
View File
@@ -18,7 +18,8 @@ pub use craft_docpath::Refused;
/// on other platforms.
pub fn resolve(stored: &str, document: Option<&Path>) -> Result<PathBuf, Refused> {
let document = document.filter(|d| !d.as_os_str().is_empty()).map(|d| std::path::absolute(d).unwrap_or_else(|_| d.to_path_buf()));
craft_docpath::resolve_absolute(stored.trim(), document.as_deref())
let path = craft_docpath::resolve(stored.trim(), document.as_deref())?;
if path.is_absolute() { Ok(path) } else { Err(Refused::Invalid) }
}
#[cfg(test)]
+23
View File
@@ -1,7 +1,9 @@
//! OFFSET Erase/Layer/Multiple/Undo and the MOVE/COPY Displacement option at the command line.
use cadcraft_engine::Input;
use cadcraft_engine::Session;
use cadcraft_engine::doc::EntityKind;
use cadcraft_engine::geom::Vec2;
use serde_json::json;
/// Every line as ((ax, ay), (bx, by), layer), sorted by the first y.
@@ -62,6 +64,27 @@ fn offset_multiple_and_undo() {
assert_eq!(lines(&s), vec![l((0.0, 0.0), (10.0, 0.0), "0"), l((0.0, 2.0), (10.0, 2.0), "0"), l((0.0, 4.0), (10.0, 4.0), "0")]);
}
// Regression test for #618: OFFSET's distance prompt took only a typed number; clicking two points
// (the distance between them) was rejected, and the second click had no distance guide.
#[test]
fn offset_distance_from_two_clicked_points() {
let mut s = one_line();
s.start("offset").unwrap();
s.input(Input::Point(Vec2::new(20.0, 20.0))).unwrap();
let p = s.current_prompt().unwrap();
assert!(p.message.contains("second point"), "{}", s.prompt_text());
assert!(p.base.is_some_and(|b| b.near(Vec2::new(20.0, 20.0), 1e-9)), "the second click is measured from the first");
// The guide from the first click to the cursor is the previewed distance.
let guide = s.preview(Vec2::new(20.0, 22.5));
assert!(matches!(guide.first().map(|e| &e.kind), Some(EntityKind::Line(g)) if g.a.xy().near(Vec2::new(20.0, 20.0), 1e-9)), "{guide:?}");
s.input(Input::Point(Vec2::new(20.0, 22.5))).unwrap();
assert!(s.current_prompt().unwrap().message.contains("Select object"), "{}", s.prompt_text());
assert_eq!(s.doc().unwrap().header.f64("OFFSETDIST", -1.0), 2.5);
s.script("5,0 5,1\n\n").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
assert_eq!(lines(&s), vec![l((0.0, 0.0), (10.0, 0.0), "0"), l((0.0, 2.5), (10.0, 2.5), "0")]);
}
#[test]
fn offset_json_erase_and_layer() {
let mut s = one_line();
+16
View File
@@ -349,6 +349,22 @@ fn hostile_dxf_does_not_panic() {
}
}
// Regression test for #1336: the LAYER table of a DXF had no record cap, so a hostile file could
// grow the layer list without bound (its VIEW/UCS/VPORT peers stop at one million records).
#[test]
fn dxf_layer_table_stops_at_the_record_cap() {
use std::fmt::Write as _;
let records = 1_000_100;
let mut text = String::from("0\nSECTION\n2\nTABLES\n0\nTABLE\n2\nLAYER\n");
for i in 0..records {
let _ = write!(text, "0\nLAYER\n2\nL{i}\n");
}
text.push_str("0\nENDTAB\n0\nENDSEC\n0\nEOF\n");
let d = read(text.as_bytes(), "layers.dxf").unwrap();
assert!(d.layers.len() <= 1_000_000, "{} layers read from {records} records", d.layers.len());
assert!(d.layers.len() > 900_000, "the records below the cap are kept: {}", d.layers.len());
}
#[test]
fn svg_and_png_export() {
let d = sample();
+25
View File
@@ -297,6 +297,31 @@ fn truetype_text_is_filled_or_outlined() {
assert!(l.prims.len() >= 3, "O has two contours, H one");
}
// Regression test for #651: filled TrueType text was triangulated at full outline detail at every
// zoom, so a page of small text on screen produced millions of triangles and froze the canvas.
#[test]
fn small_filled_text_on_screen_is_simplified_but_plots_keep_full_detail() {
let Some(font) = system_font() else { return };
let d = text_doc(font, "OOOO");
let tris = |l: &DisplayList| l.prims.iter().filter(|p| p.kind == Kind::Tris).map(|p| l.points(p).len() / 3).sum::<usize>();
let plot = build_plot(&d, &Space::Model, &Options::default());
let full = tris(&plot);
assert!(full > 100, "full-detail O glyphs are many triangles: {full}");
// One pixel is a tenth of the cap height: the outlines are simplified, the shapes stay.
let reduced = build(&d, &Space::Model, &screen(0.1, None));
assert!(tris(&reduced) < full / 2, "{} triangles at 10 px per cap height, {full} in a plot", tris(&reduced));
assert!(tris(&reduced) > 0 && reduced.bounds.width() > plot.bounds.width() * 0.9, "{:?} vs {:?}", reduced.bounds, plot.bounds);
// An em under two pixels is drawn as the ink's box: two triangles a glyph, same extent.
let tiny = build(&d, &Space::Model, &screen(1.0, None));
assert_eq!(tris(&tiny), 2 * 4, "one box per glyph");
assert!(
tiny.bounds.width() > plot.bounds.width() * 0.9 && tiny.bounds.height() > plot.bounds.height() * 0.9,
"{:?} vs {:?}",
tiny.bounds,
plot.bounds
);
}
#[test]
fn missing_font_falls_back_to_stroke() {
let d = text_doc("NoSuchFontAnywhere.ttf", "OH");
File diff suppressed because it is too large Load Diff
+16
View File
@@ -391,4 +391,20 @@ mod tests {
assert_eq!(parse_hex(bad), None, "{bad}");
}
}
#[test]
fn dialog_labels_have_ukrainian_translations() {
let sources = [include_str!("dialog.rs")];
let mut missing: Vec<String> = dialog::TABS.iter().filter(|(_, l)| crate::i18n::tr("uk", l) == *l).map(|(_, l)| l.to_string()).collect();
for source in sources {
for rest in source.split("crate::tl!(").skip(1) {
let Some(text) = rest.strip_prefix('"').and_then(|r| r.split('"').next()) else { continue };
// File format names read the same in every language.
if crate::i18n::tr("uk", text) == text && !["DXF", "DWG"].contains(&text) {
missing.push(text.to_string());
}
}
}
assert!(missing.is_empty(), "missing translations: {missing:?}");
}
}
+31
View File
@@ -169,6 +169,37 @@ fn a_selected_block_reference_offers_the_block_editor() {
assert_eq!(app.session.state().unwrap().block_editor(), None);
}
#[test]
fn every_menu_label_has_a_ukrainian_translation() {
let mut app = app();
app.cmdline("LINE 0,0 10,0");
app.cmdline("");
app.run("pline", json!({ "vertices": [[0, 0], [10, 0]] })).unwrap();
select_all(&mut app);
let mut all = flatten(&context_menu::entries(&app));
let line = app.session.doc().unwrap().model.handles()[0];
app.session.set_selection(app.session.doc().unwrap().model.handles().into_iter().filter(|h| *h != line).collect());
all.extend(flatten(&context_menu::entries(&app)));
all.extend(flatten(&context_menu::snap_entries(&app)));
type_line(&mut app, "LINE");
type_line(&mut app, "0,0");
all.extend(flatten(&context_menu::command_entries(&app).unwrap()));
let mut missing = Vec::new();
for (label, id, _) in &all {
let segments: Vec<&str> = label.split('/').collect();
// The item itself is user text when it types a value, or a command name (Recent Commands).
let user_text = (id.starts_with(context_menu::TYPE) && !id.contains('\'')) || segments.first() == Some(&"Recent Commands");
let n = segments.len() - usize::from(user_text);
for s in &segments[..n] {
// Point filters (.X) and "Repeat LINE" (a template) aren't catalog keys.
if !s.starts_with('.') && !s.starts_with("Repeat ") && cadcraft_ui_egui::i18n::tr("uk", s) == *s {
missing.push(s.to_string());
}
}
}
assert!(missing.is_empty(), "untranslated menu labels: {missing:?}");
}
/// The text painted in one frame at `time` (seconds).
fn painted(app: &mut CadApp, ctx: &egui::Context, events: Vec<egui::Event>, time: f64) -> String {
fn collect(shape: &egui::Shape, text: &mut String) {
+1
View File
@@ -16,6 +16,7 @@ mod lisp_files;
mod measure_distance_snapping;
mod menubar_hover;
mod mspace_viewport_view;
mod offset_clicks;
mod options_dialog;
mod parameters_rename;
mod pickdrag;
+83
View File
@@ -0,0 +1,83 @@
//! OFFSET on the canvas: the distance can be clicked, and picking the source is not moved by the
//! grid snap (#618).
use cadcraft_engine::Session;
use cadcraft_engine::doc::{Common, Drawing, EntityKind, Line, Space};
use cadcraft_engine::geom::Vec2;
use cadcraft_ui_egui::{CadApp, Services};
use egui::{Event, Modifiers, PointerButton, Pos2, vec2};
use egui_kittest::Harness;
use serde_json::json;
fn harness() -> Harness<'static, CadApp> {
let mut d = Drawing::new_imperial();
d.add(&Space::Model, Common::default(), EntityKind::Line(Line { a: Vec2::new(0.0, 2.0).to3(0.0), b: Vec2::new(10.0, 2.0).to3(0.0) })).unwrap();
let mut session = Session::empty();
session.open_drawing(d, "Offset clicks", None);
session.execute("view.set", &json!({"center":[5,5],"height":20})).unwrap();
session.settings.osmode = 0;
session.settings.snapmode = true;
session.execute("setvar", &json!({"name":"SNAPUNIT","value":[5,5]})).unwrap();
session.settings.orthomode = false;
let mut app = CadApp::new(session, Services::default());
app.canvas.zoom_pending = false;
let mut h = Harness::builder().with_size(vec2(1600.0, 1000.0)).build_ui_state(
|ui, app: &mut CadApp| {
app.logic(ui.ctx());
app.ui(ui);
},
app,
);
h.run_steps(5);
h
}
fn click(h: &mut Harness<'_, CadApp>, world: Vec2) {
let pos: Pos2 = h.state().canvas.xf.unwrap().to_screen(world);
h.event(Event::PointerMoved(pos));
h.run_steps(2);
for pressed in [true, false] {
h.event(Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE });
h.run_steps(2);
}
}
// Regression test for #618: OFFSET's distance could not be clicked, and with grid snap on a click
// meant to pick the source object was moved to the nearest grid point, missing the object.
#[test]
fn offset_takes_a_clicked_distance_and_picks_its_source_under_grid_snap() {
let mut h = harness();
h.state_mut().start("offset");
h.run_steps(2);
// Two clicks, snapped to the grid points (5, 5) and (10, 5): a distance of 5.
click(&mut h, Vec2::new(5.1, 4.9));
assert!(h.state().session.prompt_text().contains("second point"), "{}", h.state().session.prompt_text());
click(&mut h, Vec2::new(9.9, 5.1));
assert_eq!(h.state().session.doc().unwrap().header.f64("OFFSETDIST", -1.0), 5.0);
assert!(h.state().session.prompt_text().contains("Select object"), "{}", h.state().session.prompt_text());
// The source line is at y = 2, two units from the grid point (5, 0) the click would snap to.
click(&mut h, Vec2::new(7.0, 2.04));
let prompt = h.state().session.prompt_text();
assert!(prompt.contains("side to offset"), "the line was not picked: {prompt}");
// A side click above the line (snapped to (5, 5)) makes the copy 5 units up, at y = 7.
click(&mut h, Vec2::new(7.0, 7.0));
let lines: Vec<(f64, f64)> = h
.state()
.session
.doc()
.unwrap()
.model
.iter()
.filter_map(|e| match &e.kind {
EntityKind::Line(line) => Some((line.a.y, line.b.y)),
_ => None,
})
.collect();
assert_eq!(lines.len(), 2, "OFFSET added no line: {lines:?}");
assert!(lines.iter().any(|&(a, b)| (a - 7.0).abs() < 1e-9 && (b - 7.0).abs() < 1e-9), "no copy at y = 7: {lines:?}");
// OFFSET asks for the next object; Enter ends it.
assert!(h.state().session.prompt_text().contains("Select object to offset"), "{}", h.state().session.prompt_text());
h.key_press(egui::Key::Enter);
h.run_steps(2);
assert_eq!(h.state().session.prompt_text(), "Command:", "Enter ends OFFSET");
}
+11 -1
View File
@@ -136,7 +136,7 @@ fn layer_and_properties_palette_menus() {
}
#[test]
fn every_id_is_a_command() {
fn every_label_has_a_ukrainian_translation_and_every_id_a_command() {
let mut app = app();
app.cmdline("LINE 0,0 10,0");
app.cmdline("");
@@ -146,7 +146,16 @@ fn every_id_is_a_command() {
all.extend(flatten(&sm::layout_tab_entries(&app, "Layout1")));
all.extend(flatten(&sm::layer_entries(&app, "0")));
all.extend(flatten(&sm::properties_entries(&app)));
let mut missing = Vec::new();
for (label, id, _) in &all {
let segments: Vec<&str> = label.split('/').collect();
// Command names (Recent Commands) and line counts aren't translated.
let n = segments.len() - usize::from(segments.len() > 1);
for s in &segments[..n] {
if cadcraft_ui_egui::i18n::tr("uk", s) == *s {
missing.push(s.to_string());
}
}
let command = id.strip_prefix("run:").and_then(|r| r.split_once(':')).map_or(id.as_str(), |(c, _)| c);
let special = ["drawing:", "layout:", "cmdline:"].iter().any(|p| id.starts_with(p));
assert!(
@@ -154,4 +163,5 @@ fn every_id_is_a_command() {
"{label}: {id} isn't a command"
);
}
assert!(missing.is_empty(), "untranslated menu labels: {missing:?}");
}
+8 -8
View File
@@ -30,8 +30,8 @@ selection. Resetting palettes keeps the language choice.
The catalogs cover command and menu labels, tool groups and tooltips, the Start page,
file and model-space tabs, status controls, object snaps, drafting prompts, property labels,
dialogs, and the About window's controls. The command reference can be searched by English
command ID, English label, or translated label. A missing entry falls back to English and
never fails a build; `cargo xtask i18n` (a step of `cargo xtask ci`) lists each language's.
command ID, English label, or translated label. Ukrainian has enforced coverage;
missing entries in the other catalogs are reported by tests and fall back to English.
Czech has 1,202 catalog rows, covering the reference catalog and every marked UI
literal, including Options. Four `menu` entries distinguish menu headings from object/command
@@ -139,14 +139,14 @@ The implementation follows the display-time catalog pattern used by
- Windows retain explicit IDs based on their English source, and tool groups retain their
source names as keys, so switching languages preserves their state.
- Add a translation attribution row in `ATTRIBUTION.md`, update the language lists, and add
the language's locale tags and plural rule. The rendering, font-glyph and live-switch tests
run for every catalog; no test checks one language's wording, and adding a command needs
only its English label.
locale, plural, font-glyph and live-switch tests. Add the catalog to `REPORTED` in
`i18n/tests.rs`: only Ukrainian, the reference translation, is in `REQUIRED`, so adding a
command needs a row in one language, not in all of them.
- Catalogs are embedded and parsed once. Missing or malformed rows cannot crash the app;
tests reject invalid rows, duplicate keys, changed placeholders, and mismatched ellipses.
- `cargo test -p cadcraft-ui-egui i18n` checks the catalogs, locale resolution, preferences,
integer plurals, bundled glyphs and actual egui renders with language switching.
`cargo xtask i18n` prints the strings each language shows in English. `cargo xtask assets` checks asset attribution;
- `cargo test -p cadcraft-ui-egui i18n` checks menu/command/tool/snap coverage, every marked
UI literal, locale resolution, preferences, integer plurals, bundled glyphs and actual
egui renders with language switching. `cargo xtask assets` checks asset attribution;
`cargo xtask ci` runs the workspace quality gates.
Translations are original work under `MIT OR Apache-2.0`, written from the meaning of
+1 -16
View File
@@ -21,8 +21,7 @@ commands:
layers enforce the crate dependency layering (plan/architecture.md §3)
wasm cargo check --target wasm32-unknown-unknown for the wasm-safe crates
test cargo nextest run (cargo test without nextest) plus the doctests
i18n print the strings each interface language shows in English (never fails)
ci fmt --check, clippy -D warnings, test, i18n, assets, layers, wasm (stops at first failure)
ci fmt --check, clippy -D warnings, test, assets, layers, wasm (stops at first failure)
corpus [--download]
show where test corpora live; --download fetches PngSuite into corpus/pngsuite
stats [--exact] count tests and lines per crate (--exact: ask the test harness via `-- --list`)
@@ -39,7 +38,6 @@ fn main() -> ExitCode {
Some("layers") => cmd_layers(),
Some("wasm") => cmd_wasm(),
Some("test") => cmd_test(),
Some("i18n") => cmd_i18n(),
Some("ci") => cmd_ci(),
Some("corpus") => cmd_corpus(rest.contains(&"--download")),
Some("stats") => stats::run(&root(), rest.contains(&"--exact")),
@@ -169,18 +167,6 @@ fn cmd_test() -> Result<(), String> {
run(c, "cargo test --workspace --doc")
}
/// The translation gap report (`cadcraft-ui-egui`'s ignored `i18n::tests::translation_gaps`).
/// A missing translation shows in English and never fails a build, so neither does this step.
fn cmd_i18n() -> Result<(), String> {
let mut c = cargo();
// `--workspace` builds the same test binaries as the test step; `-p` alone would rebuild them.
c.args(["test", "--workspace", "--lib", "-q", "--", "--ignored", "--exact", "i18n::tests::translation_gaps", "--nocapture"]);
if let Err(e) = run(c, "cargo test --workspace --lib -q -- --ignored --exact i18n::tests::translation_gaps --nocapture") {
eprintln!("note: the translation gap report did not run: {e}");
}
Ok(())
}
fn cmd_ci() -> Result<(), String> {
type Step = (&'static str, Box<dyn Fn() -> Result<(), String>>);
let steps: Vec<Step> = vec![
@@ -201,7 +187,6 @@ fn cmd_ci() -> Result<(), String> {
}),
),
("test", Box::new(cmd_test)),
("i18n", Box::new(cmd_i18n)),
("assets", Box::new(|| assets::run(&root()))),
("layers", Box::new(cmd_layers)),
("wasm", Box::new(cmd_wasm)),