Adds .forgejo/workflows/cad.yml owning cad-core, cad-ui and shared tooling: non-empty source-root gates, exact ignored-test budget (20), empty-fixture proof, locked cargo check/test/clippy/fmt. Guards every stale nigig-build/.../workspace/cad scan and both coverage harnesses to fail closed (exit 1 with move pointer) until BUILD-00 removes/retargets them; an empty grep scan is never green. Verified: sun 14/14 + measure 19/19 standalone (DVec3 shim); coverage harnesses exit 1; stale-guard loop fail=0; git diff --check clean.
626 lines
29 KiB
YAML
626 lines
29 KiB
YAML
name: nigig-build (CAD)
|
|
|
|
# Phase 0.3 of the CAD remediation plan.
|
|
#
|
|
# Enforces:
|
|
# 1. Dependency resolution is pinned and reproducible (--locked).
|
|
# 2. Every git dependency is pinned to a rev, not a branch.
|
|
# 3. `nigig-build` compiles (lib + tests).
|
|
# 4. The test suite is green.
|
|
#
|
|
# History: the crate had 44 compile errors when this file was added, and
|
|
# then 17 failing tests once it built. Both are now fixed, so the test step
|
|
# asserts a plain pass instead of a "no worse than baseline" threshold.
|
|
|
|
on:
|
|
push:
|
|
paths:
|
|
- 'crates/apps/nigig-build/**'
|
|
- 'crates/apps/doc/**'
|
|
- 'crates/apps/spreadsheet/**'
|
|
- 'crates/nigig-core/**'
|
|
- 'crates/nigig-uikit/**'
|
|
- 'crates/matrix_client/**'
|
|
- 'tools/test-cad-coverage.sh'
|
|
- 'tools/test-doc-workspace-coverage.sh'
|
|
- 'Cargo.lock'
|
|
- 'Cargo.toml'
|
|
- 'rust-toolchain.toml'
|
|
- '.forgejo/workflows/nigig-build.yml'
|
|
pull_request:
|
|
paths:
|
|
- 'crates/apps/nigig-build/**'
|
|
- 'crates/apps/doc/**'
|
|
- 'crates/apps/spreadsheet/**'
|
|
- 'crates/nigig-core/**'
|
|
- 'crates/nigig-uikit/**'
|
|
- 'crates/matrix_client/**'
|
|
- 'tools/test-cad-coverage.sh'
|
|
- 'tools/test-doc-workspace-coverage.sh'
|
|
- 'Cargo.lock'
|
|
- 'Cargo.toml'
|
|
- 'rust-toolchain.toml'
|
|
- '.forgejo/workflows/nigig-build.yml'
|
|
|
|
jobs:
|
|
supply-chain:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
# A git dependency on a *branch* re-resolves on every build and is a
|
|
# direct code-execution path into CI if the branch is force-pushed.
|
|
# Phase 0.1 pinned all 33 manifests to an explicit rev; this keeps
|
|
# them pinned.
|
|
- name: Every git dependency must be pinned to a rev
|
|
run: |
|
|
set -euo pipefail
|
|
# Strip commented-out lines before checking; only live
|
|
# dependency declarations are in scope.
|
|
if grep -rn 'git = ' --include=Cargo.toml . \
|
|
| grep -v ':[0-9]*:[[:space:]]*#' \
|
|
| grep -v 'rev = '; then
|
|
echo
|
|
echo "ERROR: the git dependencies above are not pinned to a rev."
|
|
echo "Add rev = \"<full-40-char-sha>\" to each."
|
|
exit 1
|
|
fi
|
|
# ...and the rev must be the full 40-character SHA. An
|
|
# abbreviated rev resolves only while no other object shares
|
|
# its prefix; that is a property of the repository's current
|
|
# object count, not a guarantee. Git's own abbreviation length
|
|
# grows as a repo grows, so a short pin silently becomes
|
|
# ambiguous -- and an attacker who can push to the fork can
|
|
# try to manufacture a colliding prefix. This message has
|
|
# claimed "full-40-char-sha" since it was written without
|
|
# actually checking it.
|
|
if grep -rn 'rev = ' --include=Cargo.toml . \
|
|
| grep -v ':[0-9]*:[[:space:]]*#' \
|
|
| grep -vE 'rev = "[0-9a-f]{40}"'; then
|
|
echo
|
|
echo "ERROR: the rev(s) above are abbreviated. Use the full"
|
|
echo "40-character SHA so the pin cannot become ambiguous."
|
|
exit 1
|
|
fi
|
|
echo "OK: all git dependencies are pinned to a full SHA."
|
|
|
|
# A lockfile that changes during CI means the committed one was stale.
|
|
# An unused dependency is not cosmetic here. robius-sms was declared
|
|
# by nigig-build (and transitively by nigig-core and nigig-uikit)
|
|
# and never called once -- zero references in any of their sources.
|
|
# It dragged in polkit -> gio -> glib, which is the ONLY reason this
|
|
# crate needed RUSTSEC-2024-0370 and RUSTSEC-2024-0429 exemptions
|
|
# and carried an unanswered LGPL-2.1 distribution question. Deleting
|
|
# three manifest lines removed all of it.
|
|
#
|
|
# Scoped to the specific packages rather than a blanket
|
|
# `cargo machete`: five other unused dependencies exist across these
|
|
# crates (chrono, futures, postcard, rand, serde_json) and a gate
|
|
# that fails on day one gets switched off. Widen this list as those
|
|
# are cleared.
|
|
- name: The removed platform deps must not come back
|
|
run: |
|
|
set -euo pipefail
|
|
bad=0
|
|
for manifest in \
|
|
crates/apps/nigig-build/Cargo.toml \
|
|
crates/apps/nigig-email/Cargo.toml \
|
|
crates/nigig-core/Cargo.toml \
|
|
crates/nigig-uikit/Cargo.toml; do
|
|
crate_dir="$(dirname "$manifest")"
|
|
for dep in robius-sms robius-location; do
|
|
declared=$(grep -cE "^[[:space:]]*${dep}[[:space:]]*=" "$manifest" || true)
|
|
[ "$declared" -eq 0 ] && continue
|
|
underscored="${dep//-/_}"
|
|
used=$(grep -rl "$underscored" "$crate_dir/src" 2>/dev/null | wc -l)
|
|
if [ "$used" -eq 0 ]; then
|
|
echo "ERROR: $manifest declares $dep but $crate_dir/src never uses it."
|
|
bad=1
|
|
fi
|
|
done
|
|
done
|
|
if [ "$bad" -ne 0 ]; then
|
|
echo
|
|
echo "These pull polkit/gio/glib into the graph and reintroduce"
|
|
echo "RUSTSEC-2024-0370, RUSTSEC-2024-0429 and an LGPL-2.1"
|
|
echo "distribution question, for code that is never called."
|
|
exit 1
|
|
fi
|
|
echo "OK"
|
|
|
|
- name: Lockfile must be committed and current
|
|
run: |
|
|
set -euo pipefail
|
|
test -f Cargo.lock || { echo "missing Cargo.lock at workspace root"; exit 1; }
|
|
cargo metadata --locked --format-version 1 > /dev/null
|
|
git diff --exit-code -- Cargo.lock
|
|
|
|
# Phase 2: these two classes of defect are easy to reintroduce by
|
|
# copy-paste and invisible in review.
|
|
- name: No build-time paths used at runtime in the CAD module
|
|
run: |
|
|
set -euo pipefail
|
|
cad=crates/apps/nigig-build/src/construction_frame/pages/workspace/cad
|
|
# CORE-00: fail closed when the scan target is empty. This tree
|
|
# was removed (CAD now lives under crates/apps/cad); an
|
|
# unguarded grep over a missing dir reports "OK" while scanning
|
|
# nothing. Removal of these stale gates is tracked under BUILD-00.
|
|
test -d "$cad" || { echo "ERROR: CAD scan root $cad does not exist."; exit 1; }
|
|
# Rust sources only. ARCHITECTURE.md documents this rule and so
|
|
# necessarily names the macro; scanning Markdown made the gate
|
|
# fail on its own documentation.
|
|
#
|
|
# Then strip the "file:line:" prefix and drop any line whose code
|
|
# starts with a comment marker. Doc/inline comments mentioning the
|
|
# macro are fine; a real call is not.
|
|
if grep -rn --include='*.rs' 'env!("CARGO_MANIFEST_DIR")' "$cad" \
|
|
| sed 's/^[^:]*:[0-9]*://' \
|
|
| grep -vE '^[[:space:]]*(//|/\*|\*)'; then
|
|
echo
|
|
echo "ERROR: CARGO_MANIFEST_DIR is a BUILD-time path. Using it at"
|
|
echo "runtime bakes the build machine's source tree into the"
|
|
echo "binary. Use persistence::cad_data_dir() instead."
|
|
exit 1
|
|
fi
|
|
echo "OK"
|
|
|
|
- name: No hardcoded network endpoints in the CAD module
|
|
run: |
|
|
set -euo pipefail
|
|
cad=crates/apps/nigig-build/src/construction_frame/pages/workspace/cad
|
|
# CORE-00: fail closed when the scan target is empty. This tree
|
|
# was removed (CAD now lives under crates/apps/cad); an
|
|
# unguarded grep over a missing dir reports "OK" while scanning
|
|
# nothing. Removal of these stale gates is tracked under BUILD-00.
|
|
test -d "$cad" || { echo "ERROR: CAD scan root $cad does not exist."; exit 1; }
|
|
# RFC1918 literals outside comments. constants.rs is excluded
|
|
# wholesale: its only matches are the endpoint_tests that assert
|
|
# such addresses are REJECTED.
|
|
if grep -rnE --include='*.rs' \
|
|
'"https?://(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.)' "$cad" \
|
|
--exclude=constants.rs \
|
|
| sed 's/^[^:]*:[0-9]*://' \
|
|
| grep -vE '^[[:space:]]*(//|/\*|\*)'; then
|
|
echo
|
|
echo "ERROR: a private-network endpoint is hardcoded above."
|
|
echo "Read it from the environment (see constants::local_openai_url)."
|
|
exit 1
|
|
fi
|
|
echo "OK"
|
|
|
|
# CadNode::pos/rot/size return Vec3f BY VALUE. `node.pos().x = v`
|
|
# compiles, mutates a temporary and throws it away. This silently
|
|
# broke all nine properties-panel inputs and the Extend tool; it
|
|
# produces no warning and no runtime error, only a control that
|
|
# does nothing. Mutation must go through set_pos/set_rot/set_size.
|
|
- name: No writes through the by-value Vec3f getters
|
|
run: |
|
|
set -euo pipefail
|
|
cad=crates/apps/nigig-build/src/construction_frame/pages/workspace/cad
|
|
# CORE-00: fail closed when the scan target is empty. This tree
|
|
# was removed (CAD now lives under crates/apps/cad); an
|
|
# unguarded grep over a missing dir reports "OK" while scanning
|
|
# nothing. Removal of these stale gates is tracked under BUILD-00.
|
|
test -d "$cad" || { echo "ERROR: CAD scan root $cad does not exist."; exit 1; }
|
|
# Exclude the two test modules that demonstrate the trap.
|
|
if grep -rnE --include='*.rs' \
|
|
'\.(pos|rot|size)\(\)\.[xyz][[:space:]]*[-+*/]?=[^=]' "$cad" \
|
|
| grep -v 'setters_write_through_but_getters_are_copies' \
|
|
| sed 's/^[^:]*:[0-9]*://' \
|
|
| grep -vE '^[[:space:]]*(//|/\*|\*)' \
|
|
| grep -v 'n\.pos()\.x = 42\.0' \
|
|
| grep -v 'n\.rot()\.y = 42\.0' \
|
|
| grep -v 'p\.size()\.y = v'; then
|
|
echo
|
|
echo "ERROR: the assignment(s) above write to a temporary copy"
|
|
echo "and are discarded. CadNode::pos/rot/size return Vec3f by"
|
|
echo "value. Read into a local, mutate it, then call set_pos /"
|
|
echo "set_rot / set_size."
|
|
exit 1
|
|
fi
|
|
echo "OK"
|
|
|
|
# eval_cad_script_in_vm is the single chokepoint where user- and
|
|
# AI-authored source reaches the script VM, and vm.eval is one
|
|
# blocking call. Without a run budget an unterminated loop wedges the
|
|
# rebuild worker permanently -- reproduced before the budget existed:
|
|
# still running after 90 seconds. Losing this line would reintroduce
|
|
# a hang that no test failure announces, only a frozen app.
|
|
- name: The CAD script evaluator must set a run budget
|
|
run: |
|
|
set -euo pipefail
|
|
f=crates/apps/nigig-build/src/construction_frame/pages/workspace/cad/script_bindings.rs
|
|
# CORE-00: fail closed when the scan target is empty (see above).
|
|
test -f "$f" || { echo "ERROR: CAD scan file $f does not exist."; exit 1; }
|
|
if ! grep -q 'vm.bx.run_budget = Some(' "$f"; then
|
|
echo "ERROR: eval_cad_script_in_vm no longer installs a"
|
|
echo "ScriptRunBudget. An unterminated CAD script would hang the"
|
|
echo "rebuild worker forever. See CAD_SCRIPT_TIME_BUDGET."
|
|
exit 1
|
|
fi
|
|
echo "OK"
|
|
|
|
# Phase 0.4. Blocked until Cargo.lock was committed in Phase 0.2,
|
|
# because cargo-deny resolves the graph from the lockfile.
|
|
#
|
|
# Gates security advisories and yanked crates. The licence, bans and
|
|
# source checks also run, but are configured to reflect what this
|
|
# graph actually is -- see deny-nigig-build.toml, where every
|
|
# exception is named and justified rather than blanket-disabled. A
|
|
# NEW unlicensed crate, or a new advisory, still fails.
|
|
- name: Dependency audit, licences, bans and sources
|
|
run: |
|
|
set -euo pipefail
|
|
version=0.18.6
|
|
url="https://github.com/EmbarkStudios/cargo-deny/releases/download"
|
|
curl -sSLf -o /tmp/cargo-deny.tar.gz \
|
|
"$url/$version/cargo-deny-$version-x86_64-unknown-linux-musl.tar.gz"
|
|
tar xzf /tmp/cargo-deny.tar.gz -C /tmp
|
|
install -m 0755 \
|
|
"/tmp/cargo-deny-$version-x86_64-unknown-linux-musl/cargo-deny" \
|
|
/usr/local/bin/cargo-deny
|
|
# --config is resolved relative to the manifest, not the working
|
|
# directory, so it must be absolute.
|
|
cargo-deny --manifest-path crates/apps/nigig-build/Cargo.toml \
|
|
--all-features check --config "$PWD/deny-nigig-build.toml"
|
|
|
|
# A bare identifier in a match pattern that is NOT a known variant
|
|
# is parsed as a new binding that matches everything. Four such
|
|
# names (KeyEnter, KeyBackspace, BracketLeft, BracketRight) plus
|
|
# Digit0-9/Equal/LeftBracket/RightBracket/Apostrophe silently turned
|
|
# keyboard handlers into catch-alls: the whole direct-distance-entry
|
|
# feature was unreachable, and typing any digit produced '0'.
|
|
#
|
|
# It compiles, and no test catches it. rustc reports it as
|
|
# `unreachable_pattern` plus an `unused_variables` warning on a
|
|
# capitalised name -- which is the signature grepped for here.
|
|
- name: No match arms binding a non-existent enum variant
|
|
run: |
|
|
set -euo pipefail
|
|
# A capitalised "unused variable" is almost always a mistyped
|
|
# variant used as a pattern.
|
|
out=$(cargo build --locked -p nigig-build --lib --message-format=short 2>&1 \
|
|
| grep -E 'unused variable: `[A-Z]' || true)
|
|
if [ -n "$out" ]; then
|
|
echo "$out"
|
|
echo
|
|
echo "ERROR: the pattern(s) above bind a new variable instead of"
|
|
echo "matching an enum variant -- check the spelling against the"
|
|
echo "enum definition. These silently swallow every input."
|
|
exit 1
|
|
fi
|
|
echo "OK"
|
|
|
|
# `save_cad_script(..).ok();` compiles, discards a real io::Error,
|
|
# and used to be followed by an unconditional "Saved" label -- the
|
|
# user was told their work was safe when the write had failed. A
|
|
# save path is the one place a dropped Result is data loss.
|
|
- name: No discarded Results on CAD save/write paths
|
|
run: |
|
|
set -euo pipefail
|
|
cad=crates/apps/nigig-build/src/construction_frame/pages/workspace/cad
|
|
# CORE-00: fail closed when the scan target is empty. This tree
|
|
# was removed (CAD now lives under crates/apps/cad); an
|
|
# unguarded grep over a missing dir reports "OK" while scanning
|
|
# nothing. Removal of these stale gates is tracked under BUILD-00.
|
|
test -d "$cad" || { echo "ERROR: CAD scan root $cad does not exist."; exit 1; }
|
|
if grep -rnE --include='*.rs' \
|
|
'(save|write|persist|store|export)[A-Za-z_]*\([^;]*\)\.ok\(\);' \
|
|
"$cad" \
|
|
| sed 's/^[^:]*:[0-9]*://' \
|
|
| grep -vE '^[[:space:]]*(//|/\*|\*)'; then
|
|
echo
|
|
echo "ERROR: the call(s) above throw away a Result from a write"
|
|
echo "path. Surface it -- see save_status_message()."
|
|
exit 1
|
|
fi
|
|
echo "OK"
|
|
|
|
# A CAD editor holds unsaved work. A panic on a UI path takes the
|
|
# whole model with it, which is strictly worse than the mistake
|
|
# being reported. `Command::merge`'s default panicked on a
|
|
# reachable path -- a command overriding can_merge but not merge --
|
|
# while the code five lines away already refused to panic on an
|
|
# unreachable one. Use debug_assert!, or return.
|
|
- name: No panicking macros in CAD production code
|
|
run: |
|
|
set -euo pipefail
|
|
cad=crates/apps/nigig-build/src/construction_frame/pages/workspace/cad
|
|
# CORE-00: fail closed when the scan target is empty. This tree
|
|
# was removed (CAD now lives under crates/apps/cad); an
|
|
# unguarded grep over a missing dir reports "OK" while scanning
|
|
# nothing. Removal of these stale gates is tracked under BUILD-00.
|
|
test -d "$cad" || { echo "ERROR: CAD scan root $cad does not exist."; exit 1; }
|
|
hits=0
|
|
for f in "$cad"/*.rs; do
|
|
# Stop at the first #[cfg(test)]: test code may panic freely.
|
|
out=$(awk '/^#\[cfg\(test\)\]/{exit}
|
|
/unreachable!\(|panic!\(|todo!\(|unimplemented!\(/{
|
|
printf "%d: %s\n", NR, $0 }' "$f" \
|
|
| grep -vE '^[0-9]+:[[:space:]]*(//|/\*|\*)' || true)
|
|
if [ -n "$out" ]; then
|
|
echo "$f"; echo "$out"; hits=1
|
|
fi
|
|
done
|
|
if [ "$hits" -ne 0 ]; then
|
|
echo
|
|
echo "ERROR: the panicking macro(s) above are in production code."
|
|
echo "Prefer debug_assert! (loud in dev, survivable in release)"
|
|
echo "or an early return."
|
|
exit 1
|
|
fi
|
|
echo "OK"
|
|
|
|
# unwrap()/expect() in CAD production code. A panic here kills the
|
|
# whole editor and loses unsaved work, so each surviving one must be
|
|
# a real constructor invariant whose message says which.
|
|
#
|
|
# Skips #[cfg(test)] modules by BRACE DEPTH, not by stopping at the
|
|
# first one. arch_gltf.rs has production code after two test
|
|
# modules, so a "stop at the first #[cfg(test)]" scan -- which is
|
|
# what the panicking-macro gate below does -- silently misses it.
|
|
# The allowlist is deliberate: a bare count drifts upward quietly
|
|
# and a blanket ban just gets #[allow]-ed.
|
|
- name: No new unwrap/expect in CAD production code
|
|
run: |
|
|
set -euo pipefail
|
|
python3 - <<'EOF'
|
|
import re, glob, sys
|
|
CAD = "crates/apps/nigig-build/src/construction_frame/pages/workspace/cad"
|
|
# CORE-00: fail closed when the scan target is empty (see above).
|
|
import os
|
|
if not os.path.isdir(CAD):
|
|
print(f"ERROR: CAD scan root {CAD} does not exist.")
|
|
sys.exit(1)
|
|
# Known-good, each a documented invariant:
|
|
# cad_scene.rs x4 -- SceneBuilder::new always inserts the
|
|
# "default" material and layer (see ~line 850).
|
|
# arch_gltf.rs x1 -- serde_json::to_vec over a Value built in
|
|
# this file; it has no non-serialisable branch.
|
|
ALLOWED = 5
|
|
def production(path):
|
|
depth, td, pending = 0, None, False
|
|
for i, line in enumerate(open(path), 1):
|
|
if re.match(r"#\[cfg\(test\)\]", line.strip()):
|
|
pending = True
|
|
o, c = line.count("{"), line.count("}")
|
|
if pending and o:
|
|
td, pending = depth, False
|
|
if td is None:
|
|
yield i, line.rstrip()
|
|
depth += o - c
|
|
if td is not None and depth <= td:
|
|
td = None
|
|
hits = []
|
|
for f in sorted(glob.glob(CAD + "/*.rs")):
|
|
for n, line in production(f):
|
|
s = line.strip()
|
|
if s.startswith(("//", "/*", "*")):
|
|
continue
|
|
if ".unwrap()" in line or ".expect(" in line:
|
|
hits.append(f"{f}:{n}: {s[:100]}")
|
|
if len(hits) > ALLOWED:
|
|
print(f"ERROR: {len(hits)} unwrap/expect in CAD production code, allowed {ALLOWED}.")
|
|
print("A panic on a UI path kills the editor and loses unsaved work.")
|
|
print("Use Option/Result or an early return. If it really is an")
|
|
print("invariant, raise ALLOWED here and name the invariant above.")
|
|
print()
|
|
for h in hits:
|
|
print(" " + h)
|
|
sys.exit(1)
|
|
if len(hits) < ALLOWED:
|
|
print(f"NOTE: {len(hits)} found, allowance {ALLOWED}. Lower it to lock the win in.")
|
|
print(f"OK ({len(hits)}/{ALLOWED})")
|
|
EOF
|
|
|
|
# BufWriter flushes on drop and DISCARDS any error it hits. Every
|
|
# CAD export buffered its output, so a write that failed only at
|
|
# flush time -- full disk, revoked permission, network mount gone --
|
|
# returned Ok(()) and the status label said the file was written.
|
|
# Route file exports through exporters::export_to_file, which
|
|
# flushes and reports.
|
|
- name: No unflushed BufWriter in CAD export paths
|
|
run: |
|
|
set -euo pipefail
|
|
cad=crates/apps/nigig-build/src/construction_frame/pages/workspace/cad
|
|
# CORE-00: fail closed when the scan target is empty. This tree
|
|
# was removed (CAD now lives under crates/apps/cad); an
|
|
# unguarded grep over a missing dir reports "OK" while scanning
|
|
# nothing. Removal of these stale gates is tracked under BUILD-00.
|
|
test -d "$cad" || { echo "ERROR: CAD scan root $cad does not exist."; exit 1; }
|
|
# exporters.rs owns the one legitimate BufWriter (inside
|
|
# export_to_file, which flushes). arch_pdf writes into a Vec,
|
|
# where flush cannot fail.
|
|
if grep -rnE --include='*.rs' 'BufWriter::new' "$cad" \
|
|
--exclude=exporters.rs --exclude=arch_pdf.rs \
|
|
| sed 's/^[^:]*:[0-9]*://' \
|
|
| grep -vE '^[[:space:]]*(//|/\*|\*)'; then
|
|
echo
|
|
echo "ERROR: the BufWriter(s) above are outside the flushing"
|
|
echo "helper. Use exporters::export_to_file so a failed flush"
|
|
echo "is reported instead of silently truncating the file."
|
|
exit 1
|
|
fi
|
|
echo "OK"
|
|
|
|
- name: Reject whitespace errors
|
|
run: git diff --check
|
|
|
|
cad-module:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 45
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
# Makepad needs a desktop/GL stack even for a check build.
|
|
- name: Install native dependencies
|
|
run: |
|
|
sudo apt-get update -qq
|
|
sudo apt-get install -y -qq \
|
|
pkg-config libwayland-dev libxcursor-dev libxrandr-dev \
|
|
libxi-dev libx11-dev libgl1-mesa-dev libasound2-dev \
|
|
libpolkit-gobject-1-dev libpolkit-agent-1-dev \
|
|
libglib2.0-dev libssl-dev libsqlite3-dev libudev-dev
|
|
|
|
# Named for the whole crate, not "CAD module": `-p nigig-build`
|
|
# covers doc/, project_management/, cost_estimator/ and tests/ as
|
|
# well. When this gate was first executed it reported 1,559 diffs
|
|
# across 89 files, and the three largest were all outside cad/ --
|
|
# so the old name sent anyone reading the failure to the wrong
|
|
# directory.
|
|
- name: Formatting (nigig-build crate)
|
|
run: |
|
|
cargo fmt --version
|
|
cargo fmt -p nigig-build -- --check \
|
|
|| { echo "run: cargo fmt -p nigig-build"; exit 1; }
|
|
|
|
full-crate-check:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 60
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
# libpulse/libxkbcommon are link-time-only: `cargo check` passes
|
|
# without them, `cargo test` does not.
|
|
- name: Install native dependencies
|
|
run: |
|
|
sudo apt-get update -qq
|
|
sudo apt-get install -y -qq \
|
|
pkg-config libwayland-dev libxcursor-dev libxrandr-dev \
|
|
libxi-dev libx11-dev libgl1-mesa-dev libasound2-dev \
|
|
libpolkit-gobject-1-dev libpolkit-agent-1-dev \
|
|
libglib2.0-dev libssl-dev libsqlite3-dev libudev-dev \
|
|
libpulse-dev libxkbcommon-dev
|
|
|
|
- name: Check
|
|
run: cargo check --locked -p nigig-build --lib
|
|
|
|
- name: Test (lib)
|
|
run: cargo test --locked -p nigig-build --lib
|
|
|
|
# Phase 4.7 moved the CAD integration suite out of src/ into its own
|
|
# test target. `--lib` does not build it, so without this step the
|
|
# 154 tests it contains would run in no pipeline at all.
|
|
#
|
|
# This names the target explicitly rather than running the whole
|
|
# crate's tests, because `--test cost_estimator` and
|
|
# `--test cost_estimator_ui` do not currently compile. That is
|
|
# pre-existing breakage, not this workflow's to hide -- but gating
|
|
# on it would make this job red for reasons unrelated to the CAD
|
|
# module, and a step that is always red gets ignored. Add those
|
|
# targets here the moment they build.
|
|
- name: Test (CAD integration suite)
|
|
run: cargo test --locked -p nigig-build --test cad_integration
|
|
|
|
# NOTE: `cargo clippy -- -D warnings` is not enabled yet -- the crate
|
|
# currently emits ~290 warnings. Enable it once that backlog is
|
|
# cleared; a step that cannot fail is worse than no step.
|
|
|
|
# ---------------------------------------------------------------------
|
|
# Source coverage for the CAD engine.
|
|
#
|
|
# `cargo test -p nigig-build` needs wayland/X11/GL/alsa/polkit, so the
|
|
# module had never been run under instrumentation and "well tested" was
|
|
# an assertion rather than a measurement. tools/test-cad-coverage.sh
|
|
# copies the twenty pure engine files into a host-only crate with the
|
|
# same module path, runs them plus tests/cad_integration.rs under
|
|
# -C instrument-coverage, and enforces a total floor plus a per-file
|
|
# floor. The per-file floors are the point: losing every test in one
|
|
# file moves the total by a point or two and a single number would let
|
|
# that through.
|
|
#
|
|
# No native dependencies and no `runs-on` GPU: the harness pulls only
|
|
# makepad-math and makepad-csg, both dependency-free Rust. It installs
|
|
# its own toolchain into a temp dir and deletes everything -- toolchain,
|
|
# cargo home, target dir, profraw data, fetched Makepad tree -- through
|
|
# a shell trap, so nothing is cached between runs and nothing is left
|
|
# in the workspace.
|
|
#
|
|
# This does NOT cover the widget layer (mod.rs, viewport*.rs,
|
|
# workspace*.rs, script_bindings.rs, cad_editor_sheet.rs,
|
|
# code_editor.rs). Those need live_design!, Cx and an event loop;
|
|
# full-crate-check above is what gates them. `tools.rs` is pure and is
|
|
# included by the harness; `profile_benchmarks.rs` is included only by
|
|
# its explicit CAD_BENCH=1 mode.
|
|
cad-engine-coverage:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 45
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Engine coverage, with floors
|
|
run: ./tools/test-cad-coverage.sh
|
|
|
|
# ---------------------------------------------------------------------
|
|
# Source coverage for the document workspace's pure layer.
|
|
#
|
|
# Same shape as the CAD gate above: tools/test-doc-workspace-coverage.sh
|
|
# copies the doc module's dependency-free sources (model, layout,
|
|
# editing, collaboration, advanced JSON, CRDT bridge, projection layout/
|
|
# session, mobile gestures, persistence seams) plus tests_pure.rs into a
|
|
# host-only crate with a makepad-math shim, runs them under
|
|
# -C instrument-coverage, and enforces a total floor plus a per-file
|
|
# floor for every instrumented file. Everything -- toolchain, cargo
|
|
# home, target dir, fetched Makepad tree, profraw data -- lives in a
|
|
# mktemp dir removed by a shell trap on every exit path.
|
|
#
|
|
# This does NOT cover the widget layer (mod.rs, crdt_widget.rs,
|
|
# widgets/, render/, projection_renderer.rs): those need live_design!,
|
|
# Cx and an event loop, and are gated by the full-crate build and test
|
|
# jobs above. persistence.rs keeps a lower floor on purpose: three
|
|
# write-path entry points save into the host's real application-data
|
|
# directory and are covered only through their path-injected seams
|
|
# (save_doc_state_to / load_saved_doc_state_with); see the doc module's
|
|
# COVERAGE.md for the honest exclusion list.
|
|
doc-workspace-coverage:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 45
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Doc workspace coverage, with floors
|
|
run: ./tools/test-doc-workspace-coverage.sh
|
|
|
|
# ---------------------------------------------------------------------
|
|
# The CAD widget layer: viewport*.rs, workspace*.rs, mod.rs and
|
|
# friends. The engine gate above is structurally blind to them, so
|
|
# "97% covered" has always been a statement about the smaller half.
|
|
#
|
|
# Measured when this job was added: 13.25% of 10,637 lines, with
|
|
# viewport_input.rs, viewport_render.rs, workspace_actions.rs,
|
|
# cad_editor_sheet.rs, viewport_2d.rs and code_editor.rs at exactly
|
|
# zero. 9,228 lines that no test has ever executed.
|
|
#
|
|
# REPORT-ONLY, deliberately, and this is the whole argument for the
|
|
# job existing: a floor at 13% reads as a blessing rather than a debt.
|
|
# What it buys is that the number is printed on every push instead of
|
|
# being rediscovered in six months. Per Phase 0 of
|
|
# REVIEWS/REPO_COVERAGE_100_PLAN.md the first real input test should
|
|
# set a floor behind it -- at measured-minus-one, in this job.
|
|
#
|
|
# Unlike the engine harness this builds the real crate, so it needs
|
|
# Makepad's Linux packages and a toolchain.
|
|
cad-widget-coverage:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 45
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Install Makepad's native dependencies
|
|
run: bash tools/makepad-native-libs.sh --install
|
|
|
|
# NOT actions/setup-rust@v1 -- see .forgejo/RUNNER.md.
|
|
- name: Install the declared toolchain
|
|
run: |
|
|
set -e
|
|
version="$(sed -n 's/^[[:space:]]*channel[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' \
|
|
rust-toolchain.toml | head -n 1)"
|
|
curl --fail --location --proto '=https' --tlsv1.2 https://sh.rustup.rs -o /tmp/rustup-init
|
|
chmod 700 /tmp/rustup-init
|
|
/tmp/rustup-init -y --profile minimal --default-toolchain "$version" \
|
|
--component llvm-tools-preview --no-modify-path
|
|
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
|
|
|
- name: Widget layer coverage (report-only)
|
|
run: ./tools/test-cad-widget-coverage.sh
|