From dac53da0626a2c03ff84e8856d8247b3f4687ab7 Mon Sep 17 00:00:00 2001 From: Steve Van Hooser Date: Sat, 11 Apr 2026 13:22:23 -0400 Subject: [PATCH 1/7] Add INSTRUCTIONS.md for make_artifacts symmetry tests --- tests/symmetry/make_artifacts/INSTRUCTIONS.md | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tests/symmetry/make_artifacts/INSTRUCTIONS.md diff --git a/tests/symmetry/make_artifacts/INSTRUCTIONS.md b/tests/symmetry/make_artifacts/INSTRUCTIONS.md new file mode 100644 index 0000000..0c8ee0f --- /dev/null +++ b/tests/symmetry/make_artifacts/INSTRUCTIONS.md @@ -0,0 +1,57 @@ +# NDR Symmetry Artifacts Instructions (Python — make_artifacts) + +This folder contains Python tests whose purpose is to generate standard NDR artifacts for symmetry testing with other NDR language ports (e.g., MATLAB). + +## Rules for `make_artifacts` tests: + +1. **Artifact Location**: Tests must store their generated artifacts in the system's temporary directory (`tempfile.gettempdir()`). +2. **Directory Structure**: Inside the temporary directory, artifacts must be placed in a specific nested folder structure: + `NDR/symmetryTest/pythonArtifacts////` + + - ``: The sub-package name under `make_artifacts`. For example, for a test located at `tests/symmetry/make_artifacts/reader/`, the namespace is `reader`. + - ``: The name of the test class (e.g., `readData`), written in camelCase to match MATLAB conventions. + - ``: The specific name of the test method being executed (e.g., `testReadDataArtifacts`), also in camelCase. + +3. **Persistent Teardown**: The generated artifact files **must persist** in the temporary directory so that the MATLAB test suite can read them. Do **not** use `tmp_path` for the artifact output directory — only use it for intermediate scratch state that can be discarded. + +4. **Artifact Contents**: Every `make_artifacts` test should produce at minimum: + - A `metadata.json` file describing the channels, sample rates, `t0`/`t1` boundaries, + and epoch clock types returned by the reader. + - A `readData.json` file containing a small, reproducible sample of data read via + `readchannels_epochsamples(...)` so the MATLAB suite can verify numerical parity. + +5. **Deterministic Input**: Tests should read from the checked-in example data files shared + between NDR-matlab and NDR-python so that both language ports operate on byte-identical + inputs. + +6. **Imports**: Use the shared constant `PYTHON_ARTIFACTS` from `tests/symmetry/conftest.py` + to build the artifact path. The base directory is: + `/NDR/symmetryTest/pythonArtifacts/`. + +## Example: + +For a test class `TestReadData` in `tests/symmetry/make_artifacts/reader/test_read_data.py` +with a test method `test_read_data_artifacts`, the artifacts should be saved to: + +``` +/NDR/symmetryTest/pythonArtifacts/reader/readData/testReadDataArtifacts/ +``` + +## Running + +```bash +# Generate artifacts +pytest tests/symmetry/make_artifacts/ -v +``` + +## Adding a new symmetry test: + +1. Create a sub-package under `make_artifacts/` named after the NDR domain (e.g., `reader/`, + `format/`, `time/`). +2. Add a `test_.py` file with a test class that exercises the reader (or other API) + and writes `metadata.json` and `readData.json` (plus any other JSON blobs you need) to + the artifact path described above. +3. Mirror the directory naming in MATLAB: + `tools/tests/+ndr/+symmetry/+makeArtifacts/+/.m`. +4. Add a corresponding `read_artifacts` test that can verify the generated artifacts (see + `tests/symmetry/read_artifacts/INSTRUCTIONS.md`). From da0bb393767530cec885860de545da3e8d28476f Mon Sep 17 00:00:00 2001 From: Steve Van Hooser Date: Sat, 11 Apr 2026 13:22:43 -0400 Subject: [PATCH 2/7] Add INSTRUCTIONS.md for read_artifacts symmetry tests --- tests/symmetry/read_artifacts/INSTRUCTIONS.md | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 tests/symmetry/read_artifacts/INSTRUCTIONS.md diff --git a/tests/symmetry/read_artifacts/INSTRUCTIONS.md b/tests/symmetry/read_artifacts/INSTRUCTIONS.md new file mode 100644 index 0000000..a454060 --- /dev/null +++ b/tests/symmetry/read_artifacts/INSTRUCTIONS.md @@ -0,0 +1,83 @@ +# NDR Read Artifacts Instructions (Python — read_artifacts) + +This folder contains Python tests whose purpose is to read standard NDR artifacts generated by both the Python NDR test suite and the MATLAB test suite, comparing them against expected values. + +## Process overview: + +1. **Artifact Location**: The test suites place their generated artifacts in the system's temporary directory (`tempfile.gettempdir()`). +2. **Directory Structure**: Inside the temporary directory, artifacts can be found in a specific nested folder structure: + `NDR/symmetryTest/////` + + - ``: Either `matlabArtifacts` or `pythonArtifacts`. + - ``: The module/package location of the corresponding test (e.g., `reader`). + - ``: The name of the test class (e.g., `readData`). + - ``: The specific name of the test method that was executed (e.g., `testReadDataArtifacts`). + +3. **Testing Goals**: The Python tests located in this `read_artifacts` package should define assertions that: + - Load the JSON representations created by the target suite (e.g., `metadata.json`, + `readData.json`). + - Re-read the same example data files through the corresponding NDR reader in Python. + - Assert that the numeric values (channel lists, sample rates, `t0`/`t1`, sample data) + match what the target suite stored. + - Run across both `pythonArtifacts` and `matlabArtifacts` using parameterized testing so + a single test verifies parity in *both* directions. + +4. **Skipping**: When the artifact directory for a given `SourceType` does not exist, the + test must **skip** (not fail). This allows the suite to run on machines that only have + one language installed. + +## Example: + +Use pytest's `@pytest.fixture(params=...)` to dynamically pass the `SourceType` to your tests: + +```python +import json +import numpy as np +import pytest +from pathlib import Path + +from tests.symmetry.conftest import SOURCE_TYPES, SYMMETRY_BASE + +EXAMPLE_DATA = Path(__file__).parents[4] / "example_data" + + +@pytest.fixture(params=SOURCE_TYPES) +def source_type(request): + return request.param + + +class TestReadData: + def _artifact_dir(self, source_type): + return SYMMETRY_BASE / source_type / "reader" / "readData" / "testReadDataArtifacts" + + def test_read_data_metadata(self, source_type): + artifact_dir = self._artifact_dir(source_type) + if not artifact_dir.exists(): + pytest.skip(f"No artifacts from {source_type}") + + metadata = json.loads((artifact_dir / "metadata.json").read_text()) + + from ndr.reader.intan_rhd import IntanRHD + reader = IntanRHD() + epochfiles = [str(EXAMPLE_DATA / "example.rhd")] + + actual_sr = reader.samplerate(epochfiles, 1, "ai", 1) + assert actual_sr == metadata["samplerate"] +``` + +## Running + +```bash +# Verify artifacts (skips missing sources) +pytest tests/symmetry/read_artifacts/ -v +``` + +## Adding a new symmetry test: + +1. Create a sub-package under `read_artifacts/` matching the namespace used in + `make_artifacts/` (e.g., `reader/`, `format/`). +2. Add a `test_.py` file with a parameterized test class that reads from both + `matlabArtifacts` and `pythonArtifacts`. +3. Always use `pytest.skip()` when the expected artifact directory does not exist. +4. Mirror the MATLAB counterpart: + `tools/tests/+ndr/+symmetry/+readArtifacts/+/.m`. From 6b368eeda0f6d3f5e4bb498bd6aa4e0ac4030c95 Mon Sep 17 00:00:00 2001 From: Steve Van Hooser Date: Sat, 11 Apr 2026 13:23:32 -0400 Subject: [PATCH 3/7] Expand bridge YAML spec with full build instructions Mirror the comprehensive NDI-python bridge spec so NDR developers have step-by-step guidance for authoring new bridge files, including the active-maintenance rules, upstream-change detection via matlab_last_sync_hash, and a section on how to bootstrap a new bridge YAML file for a new sub-package. --- .../ndr_matlab_python_bridge.yaml | 133 ++++++++++++++++-- 1 file changed, 123 insertions(+), 10 deletions(-) diff --git a/docs/developer_notes/ndr_matlab_python_bridge.yaml b/docs/developer_notes/ndr_matlab_python_bridge.yaml index dfa3985..d8fa97a 100644 --- a/docs/developer_notes/ndr_matlab_python_bridge.yaml +++ b/docs/developer_notes/ndr_matlab_python_bridge.yaml @@ -1,40 +1,153 @@ # The NDR Bridge Protocol: YAML Specification -# + +# ============================================================================= +# 1. File Purpose & Placement +# ============================================================================= # Name: ndr_matlab_python_bridge.yaml # Location: One file per sub-package directory # (e.g., src/ndr/reader/ndr_matlab_python_bridge.yaml). -# Role: Primary Contract. Defines how MATLAB names and types map to Python. +# Role: This is the Primary Contract. It defines how MATLAB names and types +# map to Python. If a function is not here, it does not officially +# exist in the Python port. + +# ============================================================================= +# 2. Standard Header +# ============================================================================= +# Every bridge file must begin with this metadata to orient the agent: project_metadata: bridge_version: "1.1" naming_policy: "Strict MATLAB Mirror" indexing_policy: "Semantic Parity (1-based for user concepts, 0-based for internal data)" -# When porting a function: +# ============================================================================= +# 3. The "Active Maintenance" Instruction +# ============================================================================= +# When an agent or developer works on a function: +# # 1. Check: Does the function/class exist in the YAML? -# 2. Add/Update: If missing or changed, update the YAML first. -# 3. Record Hash: git log -1 --format="%h" -- -# 4. Notify: Tell the user what was added/changed. +# 2. Add/Update: If it is missing or the MATLAB signature has changed, +# update the YAML first. +# 3. Record Hash: Store the short git hash of the MATLAB .m file being +# ported in the `matlab_last_sync_hash` field. Obtain the hash with: +# git log -1 --format="%h" -- +# 4. Notify: The agent MUST explicitly tell the user: +# "I have updated the ndr_matlab_python_bridge.yaml to include +# [Function Name]. Please review the interface contract." +# +# ============================================================================= +# 3a. Detecting Upstream MATLAB Changes +# ============================================================================= +# The `matlab_last_sync_hash` field enables efficient change detection: +# +# 1. For each entry, compare `matlab_last_sync_hash` against the current +# HEAD hash of the corresponding MATLAB .m file: +# git log -1 --format="%h" -- +# 2. If the hashes differ, the MATLAB file has changed since the last sync +# and the Python port may need to be updated. +# 3. After re-syncing, update `matlab_last_sync_hash` to the new hash and +# record the sync date in `decision_log`. + +# ============================================================================= +# 4. Structure for Classes and Functions +# ============================================================================= # --- Example: Class --- -# - name: base +# - name: base # Exact MATLAB Name # type: class # matlab_path: "+ndr/+reader/base.m" # python_path: "ndr/reader/base.py" -# matlab_last_sync_hash: "a4c9e07" +# python_class: "ndr_reader_base" +# matlab_last_sync_hash: "a4c9e07" # Git hash of base.m when last ported/synced +# +# properties: +# - name: MightHaveTimeGaps +# type_matlab: "logical" +# type_python: "bool" +# decision_log: "Mirroring property name exactly." +# # methods: # - name: readchannels_epochsamples # input_arguments: # - name: channeltype +# type_matlab: "char" # type_python: "str" # - name: channel +# type_matlab: "numeric" # type_python: "int | list[int]" -# - name: epoch -# type_python: "str | int" +# - name: epochstreams +# type_matlab: "cell array of char" +# type_python: "list[str]" +# - name: epoch_select +# type_matlab: "numeric" +# type_python: "int" +# decision_log: "Semantic Parity: User provides 1-based ID." # - name: s0 # type_python: "int" # - name: s1 # type_python: "int" # output_arguments: # - name: data +# type_matlab: "double | int16 | int32" # type_python: "numpy.ndarray" + +# --- Example: Standalone Function --- +# - name: ndrpath +# type: function +# matlab_path: "+ndr/+fun/ndrpath.m" +# python_path: "ndr/fun/ndrpath.py" +# matlab_last_sync_hash: "8f3a2b1" +# input_arguments: [] +# output_arguments: +# - name: p +# type_matlab: "char" +# type_python: "str" +# decision_log: "Synchronized with MATLAB main as of 2026-04-11." + +# ============================================================================= +# 5. Building a New Bridge YAML File +# ============================================================================= +# When adding a brand-new sub-package to NDR-python (for example +# `src/ndr/newformat/`), create a fresh `ndr_matlab_python_bridge.yaml` +# next to the Python source using the following procedure: +# +# 1. Create the file at `src/ndr//ndr_matlab_python_bridge.yaml`. +# 2. Start with the Standard Header from Section 2 (copy verbatim). +# 3. For each MATLAB class in `+ndr/+/`, add a `classes:` entry with: +# - `name` (MATLAB class name, e.g. `intan_rhd`) +# - `type: class` +# - `matlab_path` (e.g. `"+ndr/+reader/intan_rhd.m"`) +# - `python_path` (e.g. `"ndr/reader/intan_rhd.py"`) +# - `python_class` (derived using the Class Name Mirror Rule from +# `docs/developer_notes/PYTHON_PORTING_GUIDE.md`, e.g. +# `ndr_reader_intan__rhd`) +# - `inherits` (if applicable) +# - `matlab_last_sync_hash` (from `git log -1 --format="%h" -- `) +# - `properties:` and `methods:` blocks with `input_arguments` / +# `output_arguments`, each argument carrying `name`, `type_matlab`, and +# `type_python`. +# 4. For each standalone MATLAB function, add an entry under `functions:` with +# the same fields (minus `properties` and `methods`). +# 5. Run `python -c "import yaml, pathlib; yaml.safe_load(pathlib.Path('src/ndr//ndr_matlab_python_bridge.yaml').read_text())"` +# to validate the YAML parses. +# 6. Notify the user with a one-line summary of the new contract so they can +# review it before any Python implementation is written. +# +# Refer to `src/ndr/reader/ndr_matlab_python_bridge.yaml` for a concrete, +# non-trivial example covering both `classes:` with inherited methods and +# `static_methods:` blocks. + +# ============================================================================= +# 6. Summary of Field Rules +# ============================================================================= +# Field | Rule +# ---------------------|---------------------------------------------------------- +# name | Strict Match. Case-sensitive match to the MATLAB .m file. +# matlab_last_sync_hash| The short git hash of the MATLAB .m file at the time it +# | was last examined/ported. Used to detect upstream changes. +# input_arguments | Used to generate Pydantic @validate_call. +# output_arguments | Defines the order of the Return Tuple. +# type_python | Use Python 3.10+ syntax (e.g., str | int). +# decision_log | Mandatory for any divergence. Should include the sync +# | date (e.g., "Synchronized with MATLAB main as of +# | 2026-04-11."). From 5882cce9dd5caddb8e43b5f8e10f9b2f487d9613 Mon Sep 17 00:00:00 2001 From: Steve Van Hooser Date: Sat, 11 Apr 2026 13:24:13 -0400 Subject: [PATCH 4/7] Expand symmetry_tests.md with full workflow and bridge cross-reference --- docs/developer_notes/symmetry_tests.md | 170 ++++++++++++++++++++++--- 1 file changed, 153 insertions(+), 17 deletions(-) diff --git a/docs/developer_notes/symmetry_tests.md b/docs/developer_notes/symmetry_tests.md index fbab568..32bd89d 100644 --- a/docs/developer_notes/symmetry_tests.md +++ b/docs/developer_notes/symmetry_tests.md @@ -1,53 +1,189 @@ # Cross-Language Symmetry Test Framework **Status:** Active -**Scope:** NDR-python <-> NDR-matlab parity +**Scope:** NDR-python ↔ NDR-matlab parity ## Purpose -Symmetry tests verify that data read by one language implementation matches -the other. This ensures the Python and MATLAB NDR stacks remain interoperable. + +Symmetry tests verify that data read by one language implementation matches the +other. This ensures the Python and MATLAB NDR stacks remain interoperable as +both codebases evolve, and mirrors the same framework that NDI-python and +NDI-matlab already use. ## Architecture +The framework has two halves, each existing in both languages: + | Phase | Python location | MATLAB location | |-------|----------------|-----------------| -| **makeArtifacts** | `tests/symmetry/make_artifacts/` | `tests/+ndr/+symmetry/+makeArtifacts/` | -| **readArtifacts** | `tests/symmetry/read_artifacts/` | `tests/+ndr/+symmetry/+readArtifacts/` | +| **makeArtifacts** | `tests/symmetry/make_artifacts/` | `tools/tests/+ndr/+symmetry/+makeArtifacts/` | +| **readArtifacts** | `tests/symmetry/read_artifacts/` | `tools/tests/+ndr/+symmetry/+readArtifacts/` | ### Artifact Directory Layout +All artifacts are written to the OS temporary directory under a fixed path: + ``` /NDR/symmetryTest/ ├── pythonArtifacts/ │ └── /// -│ ├── readData.json # Channel data, timestamps, etc. -│ └── metadata.json # Channel list, sample rates, epoch info +│ ├── metadata.json # Channel list, sample rates, t0/t1, epoch clocks +│ └── readData.json # Short reproducible sample of reader output └── matlabArtifacts/ └── /// └── ... (same structure) ``` +- **``** — the NDR domain being tested (e.g., `reader`). +- **``** — the test class name, in camelCase (e.g., `readData`). +- **``** — the test method name, in camelCase (e.g., `testReadDataArtifacts`). + ### Workflow -1. **makeArtifacts** (Python or MATLAB) reads example data files and writes - JSON artifacts containing: channel lists, sample rates, epoch clocks, - t0/t1 boundaries, and actual data samples. +``` +┌──────────────────────────┐ ┌──────────────────────────┐ +│ Python makeArtifacts │ │ MATLAB makeArtifacts │ +│ pytest tests/symmetry/ │ │ runtests('ndr.symmetry. │ +│ make_artifacts/ -v │ │ makeArtifacts') │ +└──────────┬───────────────┘ └──────────┬───────────────┘ + │ writes │ writes + ▼ ▼ + pythonArtifacts/ matlabArtifacts/ + │ │ + └────────────┬────────────────────┘ + │ reads + ┌────────────┴────────────────────┐ + │ │ + ▼ ▼ +┌──────────────────────────┐ ┌──────────────────────────┐ +│ Python readArtifacts │ │ MATLAB readArtifacts │ +│ pytest tests/symmetry/ │ │ runtests('ndr.symmetry. │ +│ read_artifacts/ -v │ │ readArtifacts') │ +└──────────────────────────┘ └──────────────────────────┘ +``` -2. **readArtifacts** (the other language) loads the same example data files, - reads the same channels/epochs, and compares against the stored artifacts. +Each `readArtifacts` test is parameterized over `{matlabArtifacts, pythonArtifacts}` +so a single test class validates both directions of compatibility. -Each `readArtifacts` test is parameterized over `{matlabArtifacts, pythonArtifacts}`. +## Running the Tests -## Running +### From Python ```bash # Generate artifacts pytest tests/symmetry/make_artifacts/ -v -# Verify artifacts +# Verify artifacts (skips missing sources) pytest tests/symmetry/read_artifacts/ -v + +# Both phases at once +pytest tests/symmetry/ -v +``` + +### From MATLAB + +```matlab +% Generate artifacts +results = runtests('ndr.symmetry.makeArtifacts', 'IncludeSubpackages', true); + +% Verify artifacts +results = runtests('ndr.symmetry.readArtifacts', 'IncludeSubpackages', true); ``` +### Why Separate from Regular Tests? + +Symmetry tests are **excluded from the default `pytest` run** (via +`--ignore=tests/symmetry` in `pyproject.toml`) because: + +1. **readArtifacts** tests will mostly just skip unless the user has previously + run MATLAB's `makeArtifacts` suite on the same machine. +2. **makeArtifacts** tests write to the system temp directory, which is a + side-effect that doesn't belong in routine CI. +3. The full cross-language cycle requires both runtimes and is better suited to + integration / nightly CI pipelines. + ## Writing a New Symmetry Test -See NDI-python's `docs/developer_notes/symmetry_tests.md` for the full template. -Adapt the pattern for NDR's reader-centric API. + +### 1. Choose a namespace + +Pick the NDR domain being tested (e.g., `reader`, `format`, `time`). + +### 2. Create the makeArtifacts test + +**Python:** `tests/symmetry/make_artifacts//test_.py` + +```python +import json +import shutil +from pathlib import Path +import pytest + +from tests.symmetry.conftest import PYTHON_ARTIFACTS + +ARTIFACT_DIR = PYTHON_ARTIFACTS / "" / "" / "" +EXAMPLE_DATA = Path(__file__).parents[4] / "example_data" + + +class TestMyFeature: + def test_my_feature_artifacts(self): + if ARTIFACT_DIR.exists(): + shutil.rmtree(ARTIFACT_DIR) + ARTIFACT_DIR.mkdir(parents=True) + + # ... build reader, collect metadata, dump metadata.json + readData.json +``` + +**MATLAB:** `tools/tests/+ndr/+symmetry/+makeArtifacts/+/.m` + +Follow the `INSTRUCTIONS.md` in the MATLAB `+makeArtifacts` folder. + +### 3. Create the readArtifacts test + +**Python:** `tests/symmetry/read_artifacts//test_.py` + +```python +import json +import pytest +from tests.symmetry.conftest import SOURCE_TYPES, SYMMETRY_BASE + + +@pytest.fixture(params=SOURCE_TYPES) +def source_type(request): + return request.param + + +class TestMyFeature: + def test_my_feature_artifacts(self, source_type): + artifact_dir = SYMMETRY_BASE / source_type / "" / "" / "" + if not artifact_dir.exists(): + pytest.skip(f"No artifacts from {source_type}") + # ... load metadata.json / readData.json and assert against re-read values +``` + +**MATLAB:** `tools/tests/+ndr/+symmetry/+readArtifacts/+/.m` + +Follow the `INSTRUCTIONS.md` in the MATLAB `+readArtifacts` folder. + +### 4. Naming Conventions + +| Concept | Python | MATLAB | +|---------|--------|--------| +| Test directory | `tests/symmetry/make_artifacts/reader/` | `tools/tests/+ndr/+symmetry/+makeArtifacts/+reader/` | +| Test file | `test_read_data.py` | `readData.m` | +| Test class | `TestReadData` | `readData` (classdef) | +| Artifact className | `readData` (camelCase) | `readData` | +| Artifact testName | `testReadDataArtifacts` (camelCase) | `testReadDataArtifacts` | + +Use **camelCase** for the artifact directory components (`className`, `testName`) +so that both languages write to and read from the exact same paths. + +## Related: Building the Bridge YAML Files + +Symmetry tests confirm **behavioural** parity at runtime. The bridge YAML files +under `src/ndr/*/ndr_matlab_python_bridge.yaml` describe **interface** parity at +the signature level. See +`docs/developer_notes/ndr_matlab_python_bridge.yaml` for the full spec, and +Section 5 of that file ("Building a New Bridge YAML File") for the step-by-step +procedure to add or update a contract. The `PYTHON_PORTING_GUIDE.md` also +documents the porting workflow, including how to compute and record +`matlab_last_sync_hash`. From 65875b25a39626ab7269384c46ef25cece43b82d Mon Sep 17 00:00:00 2001 From: Steve Van Hooser Date: Sat, 11 Apr 2026 13:26:25 -0400 Subject: [PATCH 5/7] Add cross-language symmetry test CI workflow Runs the full 4-stage NDR cross-language symmetry cycle on pushes to main, pull requests targeting main, and manual dispatch: 1. MATLAB makeArtifacts (writes matlabArtifacts/) 2. Python readArtifacts (reads matlabArtifacts/) 3. Python makeArtifacts (writes pythonArtifacts/) 4. MATLAB readArtifacts (reads pythonArtifacts/) This runs separately from the regular ci.yml Python test job so that it can evolve independently and only executes when the symmetry suite changes or a full cross-language cycle is explicitly needed. --- .github/workflows/test-symmetry.yml | 120 ++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 .github/workflows/test-symmetry.yml diff --git a/.github/workflows/test-symmetry.yml b/.github/workflows/test-symmetry.yml new file mode 100644 index 0000000..3bedde9 --- /dev/null +++ b/.github/workflows/test-symmetry.yml @@ -0,0 +1,120 @@ +name: Test Cross-Language Symmetry + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + symmetry: + name: MATLAB <-> Python symmetry tests + runs-on: ubuntu-latest + + steps: + # -- Checkout both repos ---------------------------------------------- + - name: Check out NDR-python + uses: actions/checkout@v4 + with: + path: NDR-python + + - name: Check out NDR-matlab + uses: actions/checkout@v4 + with: + repository: VH-Lab/NDR-matlab + path: NDR-matlab + + # -- Runtime setup ---------------------------------------------------- + - name: Start virtual display server + run: | + sudo apt-get install -y xvfb + Xvfb :99 & + echo "DISPLAY=:99" >> $GITHUB_ENV + + - name: Set up MATLAB + uses: matlab-actions/setup-matlab@v2 + with: + release: latest + cache: true + products: | + Signal_Processing_Toolbox + Statistics_and_Machine_Learning_Toolbox + + - name: Install MatBox + uses: ehennestad/matbox-actions/install-matbox@v1 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install NDR-python (with dev extras) + working-directory: NDR-python + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" + + # -- Stage 1: MATLAB makeArtifacts ------------------------------------ + - name: "Stage 1: MATLAB makeArtifacts" + uses: matlab-actions/run-command@v2 + with: + command: | + cd("NDR-matlab"); + addpath(genpath("+ndr")); + addpath(genpath("tools")); + ndr_Init; + + import matlab.unittest.TestSuite + import matlab.unittest.TestRunner + + suite = TestSuite.fromPackage("ndr.symmetry.makeArtifacts", "IncludingSubpackages", true); + fprintf("\n=== MATLAB makeArtifacts: %d test(s) ===\n", numel(suite)); + assert(~isempty(suite), "No MATLAB makeArtifacts tests found.") + + runner = TestRunner.withTextOutput("Verbosity", "Detailed"); + results = runner.run(suite); + + fprintf("\n=== MATLAB makeArtifacts: %d passed, %d failed ===\n", ... + nnz([results.Passed]), nnz([results.Failed])); + assert(all(~[results.Failed]), "MATLAB makeArtifacts failed") + + # -- Stage 2: Python readArtifacts (reads MATLAB artifacts) ----------- + - name: "Stage 2: Python readArtifacts (reads MATLAB artifacts)" + working-directory: NDR-python + run: | + pytest tests/symmetry/read_artifacts/ -v --tb=short + + # -- Stage 3: Python makeArtifacts ------------------------------------ + - name: "Stage 3: Python makeArtifacts" + working-directory: NDR-python + run: | + pytest tests/symmetry/make_artifacts/ -v --tb=short + + # -- Stage 4: MATLAB readArtifacts (reads Python artifacts) ----------- + - name: "Stage 4: MATLAB readArtifacts (reads Python artifacts)" + uses: matlab-actions/run-command@v2 + with: + command: | + cd("NDR-matlab"); + addpath(genpath("+ndr")); + addpath(genpath("tools")); + ndr_Init; + + import matlab.unittest.TestSuite + import matlab.unittest.TestRunner + + suite = TestSuite.fromPackage("ndr.symmetry.readArtifacts", "IncludingSubpackages", true); + fprintf("\n=== MATLAB readArtifacts: %d test(s) ===\n", numel(suite)); + assert(~isempty(suite), "No MATLAB readArtifacts tests found.") + + runner = TestRunner.withTextOutput("Verbosity", "Detailed"); + results = runner.run(suite); + + fprintf("\n=== MATLAB readArtifacts: %d passed, %d failed ===\n", ... + nnz([results.Passed]), nnz([results.Failed])); + assert(all(~[results.Failed]), "MATLAB readArtifacts failed") From 5a1b98be9621af1296df7cd80ff46a27da5602c6 Mon Sep 17 00:00:00 2001 From: Steve Van Hooser Date: Sat, 11 Apr 2026 17:31:41 -0400 Subject: [PATCH 6/7] Fix MATLAB path setup in cross-language symmetry CI Both MATLAB stages were failing before any tests ran because: 1. `addpath(genpath("+ndr"))` is invalid: MATLAB namespace (+foo) directories must not appear on the path themselves, only their parents. Replace with `addpath(ndrRoot)` plus `addpath(ndrRoot/tools/tests)` so both the top-level `+ndr` and the test `+ndr/+symmetry` packages are discoverable. 2. `ndr_Init` tried to `mkdir` a filecache path under `userpath` which isn't writable on the CI runner. The symmetry tests only need `ndr.fun.ndrpath()` (self-contained), so skip `ndr_Init`. Also use an explicit `ndrRoot` variable so the `cd` indirection is gone and `pwd` stays at the workspace root throughout the job. --- .github/workflows/test-symmetry.yml | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test-symmetry.yml b/.github/workflows/test-symmetry.yml index 3bedde9..ef04ced 100644 --- a/.github/workflows/test-symmetry.yml +++ b/.github/workflows/test-symmetry.yml @@ -64,10 +64,18 @@ jobs: uses: matlab-actions/run-command@v2 with: command: | - cd("NDR-matlab"); - addpath(genpath("+ndr")); - addpath(genpath("tools")); - ndr_Init; + % Add NDR-matlab repo root (for +ndr) and tools/tests + % (for +ndr/+symmetry). Note: MATLAB namespace (+foo) + % directories must NOT themselves appear on the path -- + % only their parent directories should. + ndrRoot = fullfile(pwd, "NDR-matlab"); + addpath(ndrRoot); + addpath(fullfile(ndrRoot, "tools", "tests")); + + % Skip ndr_Init -- it tries to mkdir filecache paths under + % userpath which are not writable on the CI runner. + % The symmetry tests only use ndr.fun.ndrpath() which is + % self-contained. import matlab.unittest.TestSuite import matlab.unittest.TestRunner @@ -100,10 +108,9 @@ jobs: uses: matlab-actions/run-command@v2 with: command: | - cd("NDR-matlab"); - addpath(genpath("+ndr")); - addpath(genpath("tools")); - ndr_Init; + ndrRoot = fullfile(pwd, "NDR-matlab"); + addpath(ndrRoot); + addpath(fullfile(ndrRoot, "tools", "tests")); import matlab.unittest.TestSuite import matlab.unittest.TestRunner From d496908f5d7b299ae8d8ab3381b97dd307893fe9 Mon Sep 17 00:00:00 2001 From: Steve Van Hooser Date: Sat, 11 Apr 2026 17:40:13 -0400 Subject: [PATCH 7/7] Check out NDR-matlab at matching branch and install vlt deps Two fixes to get the cross-language symmetry CI actually running: 1. The NDR-matlab checkout was defaulting to `main`, which does not contain the `+ndr/+symmetry` package until the feature branch is merged. Pass `ref: ${{ github.head_ref || github.ref_name }}` so NDR-matlab is checked out at the same branch name as the NDR-python change under test (developers are expected to keep branch names in sync across the two repos, the same way this feature branch does). After both repos merge to main, the same expression resolves to `main`. 2. Install NDR-matlab's tools/requirements.txt via matbox.installRequirements so vhlab-toolbox-matlab (which provides vlt.string.*) is available to the reader.intan_rhd symmetry tests in both MATLAB stages. --- .github/workflows/test-symmetry.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-symmetry.yml b/.github/workflows/test-symmetry.yml index ef04ced..36ade01 100644 --- a/.github/workflows/test-symmetry.yml +++ b/.github/workflows/test-symmetry.yml @@ -23,11 +23,18 @@ jobs: with: path: NDR-python - - name: Check out NDR-matlab + # Check out NDR-matlab at the same branch name as this workflow run + # (e.g. the PR's head branch or the branch being pushed to main). + # Developers are expected to keep matching branch names across the + # two repos when making cross-language changes, the same way the + # symmetry branches work. Falls back to main via the default so + # `main` pushes also work. + - name: Check out NDR-matlab (matching branch) uses: actions/checkout@v4 with: repository: VH-Lab/NDR-matlab path: NDR-matlab + ref: ${{ github.head_ref || github.ref_name }} # -- Runtime setup ---------------------------------------------------- - name: Start virtual display server @@ -72,6 +79,10 @@ jobs: addpath(ndrRoot); addpath(fullfile(ndrRoot, "tools", "tests")); + % Install MATLAB dependencies declared in tools/requirements.txt + % (e.g. vhlab-toolbox-matlab, which provides vlt.string.*). + matbox.installRequirements(fullfile(ndrRoot, "tools")); + % Skip ndr_Init -- it tries to mkdir filecache paths under % userpath which are not writable on the CI runner. % The symmetry tests only use ndr.fun.ndrpath() which is @@ -111,6 +122,7 @@ jobs: ndrRoot = fullfile(pwd, "NDR-matlab"); addpath(ndrRoot); addpath(fullfile(ndrRoot, "tools", "tests")); + matbox.installRequirements(fullfile(ndrRoot, "tools")); import matlab.unittest.TestSuite import matlab.unittest.TestRunner