Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
255 changes: 123 additions & 132 deletions .github/workflows/ci.yml

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,11 @@ being opened, or another agent also working in the repository is not by itself a
Concurrent full runs are allowed; the discipline is to start one only when its coverage is needed.

`obc test full` runs fast binaries, captured-fixture binaries and doctests in separate commands.
`obc check test` runs the fast tier, doctests and the render contract. These workspace commands
need the CI-pinned runner (`cargo install cargo-nextest --version 0.9.143 --locked`). Focused
`obc test -p <crate>` commands continue to use Cargo's test harness.
`obc check test` runs `tools/ci/test.sh`, the same file the CI `test` job runs: the fast tier,
doctests, the default-feature formats shape and the render contract. Every command here, focused
ones included, needs the CI-pinned runner (`cargo install cargo-nextest --version 0.9.143
--locked`). `obc test -p <crate>` runs that scope on nextest and then its doctests; add `--lib`
for the library target alone.

## Reclaiming stale development state

Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions apps/obc-sim/tests/present_fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ fn app_scenarios_idle_is_free_tick_is_small_pan_is_most() {
// The device resolution is the single ls021 authority, not a re-declared literal.
const W: u32 = FRAME_W as u32;
const H: u32 = FRAME_H as u32;
let bytes = obc_fixtures::read("sim-grimsel", "grimsel.obcm").expect("full fixture suite requires map");
let bytes = obc_fixtures::read("sim-grimsel", "grimsel.obcm");
let tables = MapTables::parse(&SliceSource(&bytes)).expect("valid demo map");
let cache = MapCache::new();
let src = SliceSource(&bytes);
Expand Down Expand Up @@ -93,7 +93,7 @@ fn app_scenarios_idle_is_free_tick_is_small_pan_is_most() {
fn monaco_fixture_parses_populated_poi_and_nav_sections() {
use obc_reader::{MapCache, MapTables, Reader, SliceSource};

let bytes = obc_fixtures::read("sim-monaco", "monaco.obcm").expect("full fixture suite requires map");
let bytes = obc_fixtures::read("sim-monaco", "monaco.obcm");
let src = SliceSource(&bytes);
let tables = MapTables::parse(&src).expect("monaco.obcm parses as a valid current map");
let cache = MapCache::new();
Expand Down Expand Up @@ -261,7 +261,7 @@ fn tour_screens_dwell_with_no_present_miss() {

const W: u32 = FRAME_W as u32;
const H: u32 = FRAME_H as u32;
let bytes = obc_fixtures::read("sim-grimsel", "grimsel.obcm").expect("full fixture suite requires map");
let bytes = obc_fixtures::read("sim-grimsel", "grimsel.obcm");
let tables = MapTables::parse(&SliceSource(&bytes)).expect("valid demo map");
let owner = HostStore::memory().unwrap();
let map = obc_host_core::flat_map::FlatMap::from_bytes_in(&owner, &bytes).unwrap();
Expand Down Expand Up @@ -484,7 +484,7 @@ fn demo_reset_rebuild_and_seek_present_clean() {

const W: u32 = FRAME_W as u32;
const H: u32 = FRAME_H as u32;
let bytes = obc_fixtures::read("sim-grimsel", "grimsel.obcm").expect("full fixture suite requires map");
let bytes = obc_fixtures::read("sim-grimsel", "grimsel.obcm");
let tables = MapTables::parse(&SliceSource(&bytes)).expect("valid demo map");
let owner = HostStore::memory().unwrap();
let map = obc_host_core::flat_map::FlatMap::from_bytes_in(&owner, &bytes).unwrap();
Expand Down
32 changes: 29 additions & 3 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,20 @@ Vector and assembly writers are Cargo examples. They run only through the manual
`manual.obc-display` runs the row-hash timing probe. These commands can write fixtures or print
measurements; they are not required test passes.

There is no expensive tier. Every test that is not on the ordinary route has one explicit route,
and this table is the complete list:

| Item | Route |
| --- | --- |
| `host/obc-dem` `decode` and `real_tile` (ignored, live Copernicus download) | `live.copernicus`, explicit command only |
| `host/obc-pack` `assistant_places` (ignored, pinned 549 MB source) | `fixtures.assistant-places`, through `fixtures/verify-assistant-places.py` |
| `manual.obc-display`, `manual.obc-vectors`, `manual.obc-link`, `manual.obc-web-assemble` | their own explicit commands; a generator writes, it never verifies |
| `ios.application-weekly`, the weekly `rust.obc-storage` run | `test-weekly.yml` |

A captured fixture is not on this list. Bounded fixture suites are ordinary work: they run in CI
after an explicit sync, and a missing package fails with the exact `obc fixtures sync` command
rather than skipping. No environment variable turns that failure on.

The required iOS suite owns `WebsiteScreenshotTests.swift`. The separate weekly application suite
owns the other XCUITest classes. `test-weekly.yml` runs the registered weekly iOS and storage
suites each Monday and on manual dispatch. Run the same cadence locally with:
Expand Down Expand Up @@ -88,9 +102,10 @@ fails the upload; setup failure can leave no bundle. The screenshot script also
`OBC_XCRESULT_PATH` for local retention. A workflow declaration alone does not establish a passing
run. TS6 still owns the remaining critical application journeys.

The UI snapshot sweep runs only when its registry input triggers match the change. A broad
coverage or workflow change does not cause an unrelated sweep. Its existing rendering, screen
and snapshot-input triggers remain the single selection source.
The UI snapshot sweep is its own `ui-snapshots` job, off the `test` job's serial path. The
sweep step runs only when `ci.ui-snapshots` is selected, and that suite still selects only on
its own rendering, screen and snapshot-input triggers. A broad coverage or policy change does
not run a sweep.

## Suite registry fields

Expand Down Expand Up @@ -217,6 +232,7 @@ selected suites without an executable CI route are errors; a selection error nev
obc test affected --base origin/develop [--head REF] [--dry-run]
obc test unit|component|contract|fixtures|e2e [--surface NAME] [--dry-run]
obc test -p obc-app # focused package work, no registry involved
obc test -p obc-app --lib # the library target alone, no doctests
obc test fixtures -p obc-route
obc test full # cross-cutting changes only
```
Expand All @@ -229,6 +245,10 @@ current host is reported as skipped with that restriction, never as passed. `obc
keeps its scoped meaning whenever a Cargo scope is present, and that path needs neither the
registry, Git, nor Cargo metadata.

A focused `obc test -p PACKAGE` runs that scope on nextest, the runner CI uses, and then the
same scope's doctests. `--lib` runs the library test target alone and no doctests; Cargo rejects
it on a package with no library.

### Reproducing CI gates locally

`obc check <gates>` runs the primitive commands of the named gates and prints the registry suites
Expand All @@ -238,6 +258,12 @@ request that the run did not reproduce, with the reason. It makes no unqualified

## Rust CI result artifacts

`tools/ci/test.sh` is the body of the `test` job, one section per CI step, and `obc check test`
runs the same file. Compilation is workspace-wide. The per-pull-request coverage ratchet reads
one LCOV report over the whole workspace and fails any critical file it never compiled, so a
narrowed package set would fail the ratchet instead of saving time. Only the two nextest
sections run under llvm-cov instrumentation, because their report is that evidence.

The two `cargo nextest run` commands in `test` use `NEXTEST_PROFILE=ci` for fast binaries
and `NEXTEST_PROFILE=fixtures` for captured fixtures. The profiles in `.config/nextest.toml` write
native JUnit XML to `target/nextest/ci/junit.xml` and `target/nextest/fixtures/junit.xml`. Each test case retains its binary and test identity, result, and
Expand Down
2 changes: 1 addition & 1 deletion firmware/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ contract**, pinned by a digest test. A source void becomes `NODATA`, an uncovere
lattice point becomes `NODATA`, and nothing is ever inpainted.

Anything derived from GLO-30 must carry the Copernicus credit; `bake` prints it,
and `obc_dem::COPERNICUS_ATTRIBUTION` is its single copy in the repo.
and `obc_elevation::COPERNICUS_ATTRIBUTION` is its single copy in the repo.

The two committed terrain sidecars are regenerated by
[`fixtures/build-map-package.sh terrain`](../fixtures/build-map-package.sh), the
Expand Down
5 changes: 0 additions & 5 deletions firmware/obc-app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,3 @@ libm = { version = "0.2", default-features = false } # no arch asm: identical co
# The hand-written OBCM v6 byte builder shared by the reader/renderer tests — the POIs-browser
# integration tests (#425) use `build_poi_map` to synthesise a v6 map with real POIs to snapshot.
obcm-testkit = { path = "../../host/obcm-testkit" }
# The About page (#1149) hand-wraps the Copernicus attribution into on-glass lines; the parity
# test re-joins them and compares against `obc_dem::COPERNICUS_ATTRIBUTION` — the one copy of the
# wording in the repository — so the device text cannot drift from the host's. Dev-only: the
# `no_std` device build never sees the host crate.
obc-dem = { path = "../../host/obc-dem" }
17 changes: 8 additions & 9 deletions firmware/obc-app/src/screen/settings/about.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@
//! The copy is **hand-wrapped constant lines**, not runtime-wrapped text: the legal formulas are
//! fixed strings, so pre-wrapping makes the exact on-glass layout reviewable in the source, and
//! the tests below enforce the two properties that matter — every line fits the panel width
//! (the never-ellipsize rule), and the Copernicus lines re-join to `obc_dem`'s canonical
//! [`COPERNICUS_ATTRIBUTION`] word for word (a dev-dependency, so the device build never sees the
//! host crate).
//! (the never-ellipsize rule), and the Copernicus lines re-join to `obc_elevation`'s canonical
//! [`COPERNICUS_ATTRIBUTION`] word for word.
//!
//! The page is taller than the panel, so it **scrolls by line**: Rotate moves the window, Back
//! climbs out, and a right-edge scrollbar shows where you are. Press does nothing — there is
Expand Down Expand Up @@ -53,7 +52,7 @@ const LINE_CHARS: usize = 17;
const OSM_LINES: &[&str] =
&["\u{00a9} OpenStreetMap", "contributors", "Open Database", "License (ODbL)", "openstreetmap", ".org/copyright"];

/// `obc_dem::COPERNICUS_ATTRIBUTION`, pre-wrapped. The parity test re-joins these with single
/// `obc_elevation::COPERNICUS_ATTRIBUTION`, pre-wrapped. The parity test re-joins these with single
/// spaces and compares against the host crate's const, so the wording cannot drift and the wraps
/// can only fall on word boundaries.
const COPERNICUS_LINES: &[&str] = &[
Expand Down Expand Up @@ -196,19 +195,19 @@ mod tests {
assert!(matches!(run(&mut scr, Gesture::Back), Transition::Pop));
}

/// The Copernicus lines re-join to `obc_dem`'s canonical attribution word for word — the
/// single-copy-of-the-wording rule, held across the firmware/host boundary by a dev-dep the
/// device build never sees. If the wording ever changes in `obc-dem`, this fails here.
/// The Copernicus lines re-join to `obc_elevation`'s canonical attribution word for word —
/// the single-copy-of-the-wording rule. The credit lives in the elevation leaf this crate
/// already depends on, so the device text cannot drift from the one the bakery stamps.
#[test]
fn copernicus_wording_matches_obc_dem() {
fn copernicus_wording_matches_obc_elevation() {
let mut joined = std::string::String::new();
for (i, line) in COPERNICUS_LINES.iter().enumerate() {
if i > 0 {
joined.push(' ');
}
joined.push_str(line);
}
assert_eq!(joined, obc_dem::COPERNICUS_ATTRIBUTION);
assert_eq!(joined, obc_elevation::COPERNICUS_ATTRIBUTION);
}

/// Every pre-wrapped content line fits the Label-width budget, and every caption does so in
Expand Down
33 changes: 33 additions & 0 deletions firmware/obc-elevation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,40 @@ pub use integrator::ProfileIntegrator;
pub use reader::{TerrainHeader, TerrainReader, TerrainTables};
pub use source::{ElevationSource, NullElevation, TerrainElevation};

/// The credit the Copernicus DEM licence requires on any product derived from the dataset, verbatim.
///
/// The licence ("Copernicus DEM Instance COP-DEM-GLO-30-F") requires this exact notice wherever the
/// data have been adapted or modified — which a resample to a different lattice certainly is. It is
/// not a courtesy and it is not paraphrasable: the baker stamps it into the catalog, the builder
/// surfaces it in the browser, and the About page shows it on glass. It lives in this leaf crate
/// because every one of those consumers already depends on it, and none of them should have to
/// take a host DEM tool just to read a string.
pub const COPERNICUS_ATTRIBUTION: &str = "produced using Copernicus WorldDEM-30 © DLR e.V. 2010-2014 \
and © Airbus Defence and Space GmbH 2014-2018 provided under COPERNICUS by the European Union and \
ESA; all rights reserved";

/// The dataset the terrain tiles are derived from, as the catalog names it.
pub const SOURCE_DATASET: &str = "Copernicus DEM GLO-30";

/// The v1 tile-cache depth: four 512 B tiles ≈ 2.1 KB. Four because a single bilinear query can
/// straddle a tile corner and touch exactly four tiles — anything less would thrash on the one
/// access pattern the sampler is guaranteed to make.
pub const DEFAULT_TILE_SLOTS: usize = 4;

#[cfg(test)]
mod tests {
use super::COPERNICUS_ATTRIBUTION;

#[test]
fn the_attribution_is_the_wording_the_licence_names() {
// Pinned as one line: the `const` is written with continuations, and a stray newline in it
// would travel into the catalog and the builder.
assert_eq!(
COPERNICUS_ATTRIBUTION,
"produced using Copernicus WorldDEM-30 © DLR e.V. 2010-2014 and © Airbus Defence and \
Space GmbH 2014-2018 provided under COPERNICUS by the European Union and ESA; all \
rights reserved"
);
assert!(!COPERNICUS_ATTRIBUTION.contains('\n'));
}
}
2 changes: 1 addition & 1 deletion firmware/obc-reader/tests/poi_fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use obc_reader::{MapCache, MapTables, Poi, PoiCategory, Reader, SliceSource, MAX
/// nearest-16 query fills and the results are the closest 16.
#[test]
fn monaco_water_query_smoke() {
let bytes = obc_fixtures::read("sim-monaco", "monaco.obcm").expect("full fixture suite requires map");
let bytes = obc_fixtures::read("sim-monaco", "monaco.obcm");
let src = SliceSource(&bytes);
let tables = MapTables::parse(&src).unwrap();
let cache = MapCache::new();
Expand Down
12 changes: 6 additions & 6 deletions firmware/obc-route/tests/nav_fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ use obc_route::nav::{plan_route, NavScratch};
#[cfg_attr(miri, ignore)]
#[test]
fn road_vs_mtb_diverge_over_grimsel() {
let bytes = obc_fixtures::read("sim-grimsel", "grimsel.obcm").expect("full fixture suite requires map");
let bytes = obc_fixtures::read("sim-grimsel", "grimsel.obcm");
let from = (8_169_610, 46_694_536);
let to = (8_217_309, 46_706_261);

Expand All @@ -54,8 +54,8 @@ fn road_vs_mtb_diverge_over_grimsel() {
#[cfg_attr(miri, ignore)]
#[test]
fn a_real_grimsel_plan_carries_the_pass_road_profile() {
let map = obc_fixtures::read("sim-grimsel", "grimsel.obcm").expect("full fixture suite requires map");
let dem = obc_fixtures::read("sim-grimsel", "grimsel.obcd").expect("full fixture suite requires terrain");
let map = obc_fixtures::read("sim-grimsel", "grimsel.obcm");
let dem = obc_fixtures::read("sim-grimsel", "grimsel.obcd");
let terrain_src = SliceSource(&dem);
let mut terrain = obc_elevation::TerrainElevation::<{ obc_elevation::DEFAULT_TILE_SLOTS }>::parse(&terrain_src)
.expect("the baked terrain parses");
Expand Down Expand Up @@ -95,8 +95,8 @@ fn a_real_grimsel_plan_carries_the_pass_road_profile() {
#[cfg_attr(miri, ignore)] // reads the committed fixtures from disk — see the note above
#[test]
fn a_planned_route_exported_to_gpx_and_reimported_keeps_its_climb() {
let map = obc_fixtures::read("sim-grimsel", "grimsel.obcm").expect("full fixture suite requires map");
let dem = obc_fixtures::read("sim-grimsel", "grimsel.obcd").expect("full fixture suite requires terrain");
let map = obc_fixtures::read("sim-grimsel", "grimsel.obcm");
let dem = obc_fixtures::read("sim-grimsel", "grimsel.obcd");
let terrain_src = SliceSource(&dem);
let mut terrain =
obc_elevation::TerrainElevation::<{ obc_elevation::DEFAULT_TILE_SLOTS }>::parse(&terrain_src).unwrap();
Expand Down Expand Up @@ -140,7 +140,7 @@ fn a_planned_route_exported_to_gpx_and_reimported_keeps_its_climb() {
#[cfg_attr(miri, ignore)]
#[test]
fn the_registered_grimsel_fixture_routes_byte_identically_on_every_profile() {
let bytes = obc_fixtures::read("sim-grimsel", "grimsel.obcm").expect("full fixture suite requires map");
let bytes = obc_fixtures::read("sim-grimsel", "grimsel.obcm");
let (from, to) = ((8_169_610, 46_694_536), (8_217_309, 46_706_261));
let actual = core::array::from_fn::<_, 4, _>(|idx| {
let (res, obcr, _) = plan_p(&bytes, from, to, "Grimsel", idx as u8);
Expand Down
2 changes: 1 addition & 1 deletion fixtures/catalog.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ scenarios = ["grimsel", "monaco-upahead", "assistant-out-and-back", "assistant-f
[profiles.test]
summary = "All external fixture packages exercised by the full repository test suite"
scenarios = ["grimsel", "monaco-upahead"]
packages = ["sim-peak-view"]
packages = []

[packages.assistant-osm]
summary = "Pinned OSM sources for Ride Assistant: Switzerland, Monaco and regional West Cork"
Expand Down
4 changes: 2 additions & 2 deletions fixtures/sources/peak-view/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ reconstructed from them. Production map summit records retain exact OSM coordina

## Obtain

Run `obc fixtures sync sim-peak-view`. The package is also part of the `sim` and `test`
profiles. The simulator resolves the package through the standard fixture cache.
Run `obc fixtures sync sim-peak-view`. The package is also part of the `sim`
profile. The simulator resolves the package through the standard fixture cache.
`OBC_PEAK_TERRAIN_DIR` overrides it for a local bake.

## Generate
Expand Down
3 changes: 1 addition & 2 deletions fixtures/verify-assistant-places.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
#!/usr/bin/env python3
"""Verify captured Ride Assistant places through the production OBCM path."""
import os
from pathlib import Path
import subprocess

ROOT = Path(__file__).resolve().parent.parent
subprocess.run([
"cargo", "test", "--locked", "-p", "obc-pack", "--features", "external-fixtures",
"--test", "assistant_places", "--", "--ignored", "--nocapture",
], cwd=ROOT, env={**os.environ, "OBC_REQUIRE_FIXTURES": "1"}, check=True)
], cwd=ROOT, check=True)
3 changes: 3 additions & 0 deletions host/obc-bake/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ obc-formats = { path = "../../firmware/obc-formats" }
# into this tree. Declared here rather than inherited by workspace feature unification, so a
# standalone `cargo check -p obc-bake` matches what the workspace builds.
obc-dem = { path = "../obc-dem", default-features = false, features = ["geotiff", "fetch"] }
# The Copernicus credit the terrain stage stamps into every catalog entry. It is a `const` in the
# elevation leaf, which this crate reads through rather than copying the wording.
obc-elevation = { path = "../../firmware/obc-elevation" }
# Skin previews are not screenshots of a second renderer. The bakery restamps a
# canonical tiny OBCM through the assembly engine's production skin resolver and
# renders it through the same `RenderScratch` the device runs.
Expand Down
2 changes: 1 addition & 1 deletion host/obc-bake/assets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ parses and covers the published camera centre.
carry *"produced using Copernicus WorldDEM-30 © DLR e.V. 2010-2014 and © Airbus
Defence and Space GmbH 2014-2018 provided under COPERNICUS by the European Union
and ESA; all rights reserved"*. The string lives once, in
`obc_dem::COPERNICUS_ATTRIBUTION`.
`obc_elevation::COPERNICUS_ATTRIBUTION`.

Refresh this fixture whenever the schema's style-id assignment or OBCM version
changes. `obc-bake` checks the assignment before starting a region bake and
Expand Down
Loading
Loading