From c5b84536838464a82735b07233c6a50e099d1d63 Mon Sep 17 00:00:00 2001 From: IonutMuthi Date: Thu, 2 Apr 2026 12:25:53 +0300 Subject: [PATCH] claude: added JS unit test skills Signed-off-by: IonutMuthi --- .../.claude-plugin/plugin.json | 2 +- tools/scopy_dev_plugin/README.md | 4 + .../commands/create-unit-tests.md | 179 ++++++++++++++++++ .../commands/validate-unit-tests.md | 148 +++++++++++++++ .../skills/unit-test-patterns/SKILL.md | 56 ++++++ .../complex-calibration-flow.md | 61 ++++++ .../complex-channel-independence.md | 82 ++++++++ .../complex-dpd-operations.md | 97 ++++++++++ .../complex-frequency-tuning.md | 66 +++++++ .../complex-gain-mode-interaction.md | 65 +++++++ .../complex-phase-rotation.md | 70 +++++++ .../complex-profile-loading.md | 64 +++++++ .../complex-refresh-cycle.md | 57 ++++++ .../complex-state-transitions.md | 60 ++++++ .../complex-udc-lo-splitting.md | 63 ++++++ .../unit-test-patterns/data-driven-tests.md | 81 ++++++++ .../unit-test-patterns/file-structure.md | 147 ++++++++++++++ .../test-bad-value-combo.md | 62 ++++++ .../test-bad-value-range.md | 81 ++++++++ .../test-calibration-flag.md | 99 ++++++++++ .../unit-test-patterns/test-checkbox.md | 76 ++++++++ .../skills/unit-test-patterns/test-combo.md | 66 +++++++ .../unit-test-patterns/test-conversion.md | 122 ++++++++++++ .../skills/unit-test-patterns/test-range.md | 94 +++++++++ .../unit-test-patterns/test-readonly.md | 53 ++++++ .../skills/unit-test-quality-checks/SKILL.md | 84 ++++++++ 26 files changed, 2038 insertions(+), 1 deletion(-) create mode 100644 tools/scopy_dev_plugin/commands/create-unit-tests.md create mode 100644 tools/scopy_dev_plugin/commands/validate-unit-tests.md create mode 100644 tools/scopy_dev_plugin/skills/unit-test-patterns/SKILL.md create mode 100644 tools/scopy_dev_plugin/skills/unit-test-patterns/complex-calibration-flow.md create mode 100644 tools/scopy_dev_plugin/skills/unit-test-patterns/complex-channel-independence.md create mode 100644 tools/scopy_dev_plugin/skills/unit-test-patterns/complex-dpd-operations.md create mode 100644 tools/scopy_dev_plugin/skills/unit-test-patterns/complex-frequency-tuning.md create mode 100644 tools/scopy_dev_plugin/skills/unit-test-patterns/complex-gain-mode-interaction.md create mode 100644 tools/scopy_dev_plugin/skills/unit-test-patterns/complex-phase-rotation.md create mode 100644 tools/scopy_dev_plugin/skills/unit-test-patterns/complex-profile-loading.md create mode 100644 tools/scopy_dev_plugin/skills/unit-test-patterns/complex-refresh-cycle.md create mode 100644 tools/scopy_dev_plugin/skills/unit-test-patterns/complex-state-transitions.md create mode 100644 tools/scopy_dev_plugin/skills/unit-test-patterns/complex-udc-lo-splitting.md create mode 100644 tools/scopy_dev_plugin/skills/unit-test-patterns/data-driven-tests.md create mode 100644 tools/scopy_dev_plugin/skills/unit-test-patterns/file-structure.md create mode 100644 tools/scopy_dev_plugin/skills/unit-test-patterns/test-bad-value-combo.md create mode 100644 tools/scopy_dev_plugin/skills/unit-test-patterns/test-bad-value-range.md create mode 100644 tools/scopy_dev_plugin/skills/unit-test-patterns/test-calibration-flag.md create mode 100644 tools/scopy_dev_plugin/skills/unit-test-patterns/test-checkbox.md create mode 100644 tools/scopy_dev_plugin/skills/unit-test-patterns/test-combo.md create mode 100644 tools/scopy_dev_plugin/skills/unit-test-patterns/test-conversion.md create mode 100644 tools/scopy_dev_plugin/skills/unit-test-patterns/test-range.md create mode 100644 tools/scopy_dev_plugin/skills/unit-test-patterns/test-readonly.md create mode 100644 tools/scopy_dev_plugin/skills/unit-test-quality-checks/SKILL.md diff --git a/tools/scopy_dev_plugin/.claude-plugin/plugin.json b/tools/scopy_dev_plugin/.claude-plugin/plugin.json index f5257168fb..788fe6e6ef 100644 --- a/tools/scopy_dev_plugin/.claude-plugin/plugin.json +++ b/tools/scopy_dev_plugin/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "scopy_dev_plugin", - "version": "1.0.0", + "version": "1.1.0", "description": "Scopy development tools: code generation, documentation, testing, quality checks, and styling for IIO plugin development.", "author": { "name": "Muthi Ionut Adrian" } } diff --git a/tools/scopy_dev_plugin/README.md b/tools/scopy_dev_plugin/README.md index 1995ac3e9e..08ba0ec9e2 100644 --- a/tools/scopy_dev_plugin/README.md +++ b/tools/scopy_dev_plugin/README.md @@ -40,6 +40,8 @@ Both must be on your `PATH`. On Ubuntu: `sudo apt install clang-format` and `pip | `/scopy_dev_plugin:verify-package ` | CI pre-flight validation (format + license) | | `/scopy_dev_plugin:validate-api ` | Validate API class implementation (checks A1–A7) | | `/scopy_dev_plugin:validate-automated-tests ` | Validate JS automated test scripts (checks T1–T7) | +| `/scopy_dev_plugin:create-unit-tests ` | Generate JS unit test scripts for IIOWidget coverage | +| `/scopy_dev_plugin:validate-unit-tests ` | Validate JS unit test scripts (checks U1–U7) | ## Knowledge Skills (auto-load) @@ -51,6 +53,8 @@ These skills are loaded automatically when relevant context is detected: - **scopy-doc-format** — RST documentation conventions - **scopy-test-format** — Test case UID and RBP conventions - **scopy-api-patterns** — API class structure and Q_INVOKABLE patterns +- **unit-test-quality-checks** — Unit test validation rules (U1–U7) for IIOWidget coverage tests +- **unit-test-patterns** — Code patterns for unit test helpers and complex test scenarios ## Hooks diff --git a/tools/scopy_dev_plugin/commands/create-unit-tests.md b/tools/scopy_dev_plugin/commands/create-unit-tests.md new file mode 100644 index 0000000000..9a7a05eaa9 --- /dev/null +++ b/tools/scopy_dev_plugin/commands/create-unit-tests.md @@ -0,0 +1,179 @@ +# /create-unit-tests — Create JS unit test scripts for IIOWidget coverage + +You are creating JavaScript unit test scripts for a Scopy plugin that test every IIOWidget attribute via `readWidget`/`writeWidget` and API getter/setter methods. + +**Plugin:** `$ARGUMENTS` + +## Step 0: Load context + +Use the Read tool to check if a port state file exists: +- Path: `tasks/$ARGUMENTS-port-state.md` +- If the file does not exist, note "No state file — will discover from source files directly." and continue. + +## Prerequisites check + +Before writing tests, verify this input exists: +1. **Plugin API class** — use the Glob tool to search for `*_api.h` in `scopy/packages/$ARGUMENTS/` + +If the API class does not exist, stop and tell the user to run `/create-api $ARGUMENTS` first. + +## Step 1: Discovery + +Read these specific files to discover all testable widgets and API methods: + +1. **Plugin API header** (all `Q_INVOKABLE` methods): + - Use Glob: `scopy/packages/$ARGUMENTS/plugins/*/include/**/*_api.h` + - Extract every getter/setter pair, standalone getters, and utility methods (`calibrate()`, `refresh()`, `loadProfile()`, etc.) + +2. **Widget factory source** (all IIOWidgetBuilder calls): + - Use Glob: `scopy/packages/$ARGUMENTS/plugins/*/src/**/*.cpp` + - Identify files that use `IIOWidgetBuilder` + - Extract: widget keys, UI strategy type (EditableUi, ComboUi, CheckBoxUi, RangeUi), range bounds, combo options, conversion functions + +3. **EMU XML** (device structure and attribute defaults): + - Use Glob: `scopy/packages/$ARGUMENTS/emu-xml/*.xml` + - Extract: device name (for PHY prefix), channel structure, attribute names, default values, `_available` options + +4. **Tool source files** (for tool names and advanced tool detection): + - Search for `switchToTool()`, `getAdvancedTabs()`, `switchAdvancedTab()` in the API header + - Check for `advanced/` subdirectory in the plugin source + +5. **Existing test files** (avoid duplication): + - Use Glob: `scopy/js/testAutomations/$ARGUMENTS/` + +6. **Test framework API**: + - `scopy/js/testAutomations/common/testFramework.js` + +## Step 2: Classification — WAIT FOR APPROVAL + +After reading all source material, present a structured classification report: + +### Widget Key Prefix +``` +var PHY = "/"; // from EMU XML +``` + +### Basic Tool Widgets + +| Section | Widget Key | Type | Min | Max | Mid | Options | Test Helper | UID | +|---------|-----------|------|-----|-----|-----|---------|-------------|-----| +| Global | ensm_mode | combo | - | - | - | ["radio_on", "radio_off"] | testCombo + API | UNIT.GLOBAL.ENSM_MODE | +| RX | voltage0_in/hardwaregain | range | 0 | 30 | 15 | - | testRange + testConversion | UNIT.RX.CH0_HARDWARE_GAIN | +| ... | ... | ... | ... | ... | ... | ... | ... | ... | + +### Advanced Tool Widgets (if applicable) + +Group by tab name: + +| Tab | Widget Key | Type | Min | Max | Mid | Options | Test Helper | UID | +|-----|-----------|------|-----|-----|-----|---------|-------------|-----| +| CLK Settings | adi,clocks-device-clock_khz | range | 30720 | 320000 | 122880 | - | testRange | UNIT.CLK.DEVICE_CLOCK_KHZ | +| ... | ... | ... | ... | ... | ... | ... | ... | ... | + +### API-Only Methods (no widget, tested via getter/setter) + +| Method | Test Type | UID | +|--------|-----------|-----| +| getRxRssi(channel) (readonly) | testReadOnly via API | UNIT.RX.CH0_RSSI | +| calibrate() | complex test | UNIT.CAL.CALIBRATE_TRIGGER | +| ... | ... | ... | + +### Widget Counts +- Basic tool: X widgets +- Advanced tool: Y widgets across Z tabs +- API-only: W methods +- Total attribute test cases: N + +### Proposed Complex Tests + +Scan the API header for these method signature triggers and list matching complex tests: + +| # | Pattern | API Trigger | UID | +|---|---------|-------------|-----| +| C1 | Calibration Flow | `calibrate()` found | UNIT.CAL.FULL_CALIBRATION_FLOW | +| C2 | Profile Loading | `loadProfile()` found | UNIT.PROFILE.LOAD_AND_VERIFY | +| C3 | Gain Mode Interaction | `getXxxGainControlMode()` + `setXxxHardwareGain()` found | UNIT.RX.GAIN_MODE_INTERACTION | +| C4 | State Transitions | `getEnsmMode()`/`setEnsmMode()` found | UNIT.GLOBAL.ENSM_STATE_TRANSITIONS | +| C5 | DPD Operations | `dpdReset()` + `getDpdStatus()` found | UNIT.DPD.RESET_AND_STATUS_CH0 | +| C6 | Channel Independence | Setter with `int channel`, 2+ channels | UNIT.TX.CHANNEL_INDEPENDENCE | +| C7 | Phase Rotation | `getPhaseRotation()`/`setPhaseRotation()` found | UNIT.FPGA.PHASE_ROTATION_CH0 | +| C8 | Frequency Tuning | Hz-to-MHz conversion in getter/setter | UNIT.RX.LO_FREQUENCY | +| C9 | UDC LO Splitting | `hasUdc()`/`getUdcEnabled()` found | UNIT.UDC.LO_SPLITTING | +| C10 | Refresh Cycle | `refresh()` found | UNIT.UTIL.REFRESH_ALL | + +Only list patterns where the API trigger was actually found. + +### Proposed File Structure + +Apply adaptive splitting: +- Always: `_Basic_Unit_test.js` (or `_Unit_test.js` if single file) +- If advanced tool detected: `+ _Advanced_Unit_test.js` +- If complex tests approved: `+ _Complex_Unit_test.js` +- If multiple files: `+ _Unit_test.js` (combined runner) + +### Variant Detection + +If the plugin supports device variants (e.g., AD9371 vs AD9375), describe: +- How to detect the variant at runtime +- Which widgets/tests need skip guards + +**Wait for user approval before writing any JavaScript.** + +## Step 3: Interactive Complex Test Discovery + +After presenting the plan, ask the user: +1. "Which complex tests should I include?" (present the matched list) +2. "Are there any plugin-specific complex scenarios not in the standard patterns?" +3. "Are there variant-specific features that need skip guards?" + +## Step 4: Generate Files + +Generate each file following the `unit-test-patterns` skill. Use the `file-structure.md` pattern for boilerplate. + +**File locations:** +- `scopy/js/testAutomations/$ARGUMENTS/_Basic_Unit_test.js` +- `scopy/js/testAutomations/$ARGUMENTS/_Advanced_Unit_test.js` (if applicable) +- `scopy/js/testAutomations/$ARGUMENTS/_Complex_Unit_test.js` (if applicable) +- `scopy/js/testAutomations/$ARGUMENTS/_Unit_test.js` (combined runner or single file) + +**Critical generation rules (non-negotiable):** + +1. Every `writeWidget()` and setter call is followed by `msleep(500)` +2. Every test saves original value before modifying, and restores it in ALL code paths (normal, early return, catch) +3. Standard widget types use canonical helper functions (`testRange`, `testCombo`, `testCheckbox`, `testReadOnly`, `testConversion`) +4. Sections with 3+ widgets of the same type use `runDataDrivenTests()` with test descriptor arrays +5. Every range widget gets both `testRange()` and `testBadValueRange()` tests +6. Every combo widget gets both `testCombo()` and `testBadValueCombo()` tests +7. API getter/setter pairs with unit conversion get `testConversion()` tests +8. Files end with `TestFramework.disconnectFromDevice()`, `TestFramework.printSummary()`, `scopy.exit()` +9. UID format: `UNIT.
.` (uppercase, dots as separators) +10. Never invent API methods — only use what's in the `*_api.h` header +11. Never invent widget keys — only use keys discoverable from source code or EMU XML +12. Variant-specific tests wrapped in skip guards (e.g., `if (!isAd9375) return "SKIP"`) + +## Step 5: Validate + +Run the `unit-test-quality-checks` skill rules (U1-U7) against the generated files: +- [ ] [U1] Every discoverable widget has a test +- [ ] [U2] Standard helpers used (no ad-hoc logic for standard types) +- [ ] [U3] Every setter has `msleep(500)` after it +- [ ] [U4] Original values saved and restored in all code paths +- [ ] [U5] Bad value tests present for range and combo widgets +- [ ] [U6] Complex tests isolated in marked section +- [ ] [U7] File naming, license header, termination sequence correct + +## Step 6: Update state file (if it exists) + +```markdown +## Status +- Phase: UNIT_TESTS_COMPLETE +``` + +## Rules + +- Do NOT modify any C++ source code +- Do NOT invent API methods that don't exist in the `*_api.h` header +- Do NOT invent widget keys — discover them from source code and EMU XML +- Getter return values are always strings — compare with `===` against string values +- Every test must restore original hardware state +- Use `"SKIP"` return for features not available on current hardware variant diff --git a/tools/scopy_dev_plugin/commands/validate-unit-tests.md b/tools/scopy_dev_plugin/commands/validate-unit-tests.md new file mode 100644 index 0000000000..598b686f8a --- /dev/null +++ b/tools/scopy_dev_plugin/commands/validate-unit-tests.md @@ -0,0 +1,148 @@ +# /validate-unit-tests — Validate JS unit test scripts for a Scopy plugin + +You are validating the JavaScript unit test scripts for the Scopy plugin: `$ARGUMENTS` + +The `unit-test-quality-checks` skill rules (checks U1–U7) govern this analysis. + +## Step 1: Discover files + +Use the Glob tool to locate: +- Unit test files: `js/testAutomations/$ARGUMENTS/*_Unit_test.js`, `js/testAutomations/$ARGUMENTS/*_Basic_Unit_test.js`, `js/testAutomations/$ARGUMENTS/*_Advanced_Unit_test.js`, `js/testAutomations/$ARGUMENTS/*_Complex_Unit_test.js` +- Plugin API header: `scopy/packages/$ARGUMENTS/plugins/*/include/**/*_api.h` +- Widget factory sources: `scopy/packages/$ARGUMENTS/plugins/*/src/**/*.cpp` (files using `IIOWidgetBuilder`) +- EMU XML: `scopy/packages/$ARGUMENTS/emu-xml/*.xml` +- Test framework: `js/testAutomations/common/testFramework.js` + +If no JS unit test files are found, report "No unit test files found for `$ARGUMENTS`" and stop. + +Read **all** discovered files before starting analysis. + +## Step 2: Build expected widget set + +From the source files, build the complete set of testable widgets and API methods: + +1. **From widget factory sources**: Extract all IIOWidgetBuilder calls to get widget keys and their types (EditableUi → range, ComboUi → combo, CheckBoxUi → checkbox, read-only patterns) +2. **From API header**: Extract all `Q_INVOKABLE` getter/setter pairs and standalone getters +3. **From EMU XML**: Extract device name prefix, channel structure, attribute names + +This is the "expected" set — every item should have at least one test. + +## Step 3: Build actual test set + +From the JS unit test files, extract: + +1. **Widget keys tested**: All keys passed to `readWidget()`, `writeWidget()`, `testRange()`, `testCombo()`, `testCheckbox()`, `testReadOnly()`, `testConversion()`, `testBadValueRange()`, `testBadValueCombo()`, and `runDataDrivenTests()` calls +2. **API methods exercised**: All `.()` calls +3. **Test UIDs**: All `TestFramework.runTest("", ...)` UIDs + +## Step 4: Run checks U1–U7 + +### CRITICAL + +**[U1] Widget Coverage** +- Compare expected widget set against actual test set +- Flag any widget key with no corresponding test +- Flag any `Q_INVOKABLE` getter/setter pair with no test exercising it +- Flag any test referencing a widget key not in the expected set +- Report coverage: `X/Y widgets tested (Z%)` + +**[U2] Helper Function Usage** +- Scan each `TestFramework.runTest()` body +- For non-complex tests: if the body manually writes/reads/compares widget values when a standard helper exists for that widget type, flag it +- Verify `runDataDrivenTests()` is used for sections with 3+ widgets of the same type +- Complex tests (in sections marked `// SECTION: Complex`) are exempt + +**[U3] Sleep After Setters** +- Identify every `writeWidget()`, `set*()`, or state-mutating call (`calibrate()`, `dpdReset()`, `refresh()`, `loadProfile()`) +- Check that the very next non-empty line is `msleep(500)` or longer +- Flag any setter not followed immediately by msleep +- Check both in helper definitions and in individual test bodies + +**[U4] State Restoration** +- Identify every test that modifies state (calls a setter or writeWidget) +- Check that it saves the original value before modification +- Check that it restores the original value in: normal completion, early return, and catch block +- Flag any test that modifies state without full restoration + +### WARNING + +**[U5] Bad Value Tests** +- Count range widgets with `testRange()` calls vs those with `testBadValueRange()` calls +- Count combo widgets with `testCombo()` calls vs those with `testBadValueCombo()` calls +- Report coverage ratio for each +- Warn if bad value test coverage drops below 80% + +**[U6] Complex Test Isolation** +- Check that complex multi-step tests are in a clearly marked section +- Check each complex test has a descriptive comment (e.g., `// C1: Full Calibration Flow`) +- Check complex test UIDs follow `UNIT.
.` format +- Flag any undocumented complex logic mixed into attribute test sections + +### INFO + +**[U7] File Structure** +- Verify file naming convention (`*_Basic_Unit_test.js`, `*_Advanced_Unit_test.js`, etc.) +- Verify GPL license header present in every file +- Verify termination sequence: `disconnectFromDevice()` → `printSummary()` → `scopy.exit()` +- Verify combined runner uses `evaluateFile()` and does not duplicate helpers or tests +- Report file count and structure + +## Step 5: Generate report + +``` +## Unit Test Validation Report: $ARGUMENTS + +### Summary +| Check | Severity | Result | +|-------|----------|--------| +| [U1] Widget Coverage | CRITICAL | PASS/FAIL | +| [U2] Helper Function Usage | CRITICAL | PASS/FAIL | +| [U3] Sleep After Setters | CRITICAL | PASS/FAIL | +| [U4] State Restoration | CRITICAL | PASS/FAIL | +| [U5] Bad Value Tests | WARNING | PASS/WARN | +| [U6] Complex Test Isolation | WARNING | PASS/WARN | +| [U7] File Structure | INFO | PASS/INFO | + +### Critical Issues +**[U1] Missing widget coverage** +`voltage0_in/rf_bandwidth` — no test found in any unit test file. +> **Fix:** Add a testReadOnly() call for this widget. + +**[U3] Missing sleep after setter** +`ad9371_Basic_Unit_test.js:142` — `writeWidget()` not followed by `msleep(500)`. +> **Fix:** Add `msleep(500);` on the next line. + +### Warnings +**[U5] Bad value test coverage** +- Range widgets: 12/15 have testBadValueRange() (80%) +- Combo widgets: 3/5 have testBadValueCombo() (60%) — below 80% threshold + +### Info +**[U7] File structure** +- Files found: _Basic_Unit_test.js, _Advanced_Unit_test.js, _Unit_test.js +- License headers: OK +- Termination sequence: OK + +### Widget Coverage Detail +| Widget Key | Type | Has Test | Has Bad Value Test | +|-----------|------|----------|-------------------| +| voltage0_in/hardwaregain | range | YES | YES | +| voltage0_in/rf_bandwidth | readonly | NO | N/A | +| ensm_mode | combo | YES | YES | +| ... | ... | ... | ... | +Coverage: X/Y widgets tested (Z%) + +### API Method Coverage +| Method | Has Test | +|--------|----------| +| getRxHardwareGain(channel) | YES | +| setRxHardwareGain(channel, val) | YES | +| getRxRssi(channel) | NO | +| ... | ... | +Coverage: X/Y methods tested (Z%) + +### Verdict +[PASS/FAIL] — [one sentence summary] +``` + +PASS = zero critical issues. FAIL = one or more critical issues. diff --git a/tools/scopy_dev_plugin/skills/unit-test-patterns/SKILL.md b/tools/scopy_dev_plugin/skills/unit-test-patterns/SKILL.md new file mode 100644 index 0000000000..a20d43e4e6 --- /dev/null +++ b/tools/scopy_dev_plugin/skills/unit-test-patterns/SKILL.md @@ -0,0 +1,56 @@ +--- +name: unit-test-patterns +description: Code patterns and examples for Scopy IIOWidget unit tests. Covers standard helpers, data-driven testing, and complex multi-step scenarios. Auto-loads when creating or reviewing `*_Unit_test.js` files. +--- + +# Unit Test Patterns for Scopy IIOWidget Tests + +This skill provides code patterns for generating and reviewing JS unit tests that validate IIOWidget attributes in Scopy plugins. All examples are from the AD9371 reference implementation. + +## Standard Helper Functions + +These helpers test individual widget attributes. Every unit test file must define and use them. + +| Pattern | File | Widget Type | Description | +|---------|------|-------------|-------------| +| [testRange](test-range.md) | `test-range.md` | Range/Spinbox | Write min/max/mid, verify readback with tolerance | +| [testCombo](test-combo.md) | `test-combo.md` | Combo/Dropdown | Iterate valid options, verify exact match | +| [testCheckbox](test-checkbox.md) | `test-checkbox.md` | Checkbox/Toggle | Toggle 0/1, verify readback | +| [testReadOnly](test-readonly.md) | `test-readonly.md` | Read-Only | Verify value is non-empty | +| [testConversion](test-conversion.md) | `test-conversion.md` | API + Widget | Validate API scaling vs raw widget value | +| [testBadValueRange](test-bad-value-range.md) | `test-bad-value-range.md` | Range boundary | Verify above-max/below-min clamping | +| [testBadValueCombo](test-bad-value-combo.md) | `test-bad-value-combo.md` | Combo boundary | Verify invalid option rejection | +| [testCalibrationFlag](test-calibration-flag.md) | `test-calibration-flag.md` | Calibration toggle | Test enable/disable via API getter/setter | +| [runDataDrivenTests](data-driven-tests.md) | `data-driven-tests.md` | Batch dispatch | Run array of test descriptors through helpers | + +## Complex Test Patterns + +Multi-step scenarios that test functionality beyond single-widget read/write. Each pattern includes: when to use, API signature trigger, and complete code example. + +| Pattern | File | Trigger | +|---------|------|---------| +| [Calibration Flow](complex-calibration-flow.md) | `complex-calibration-flow.md` | `calibrate()` exists in API | +| [Profile Loading](complex-profile-loading.md) | `complex-profile-loading.md` | `loadProfile()` exists | +| [Gain Mode Interaction](complex-gain-mode-interaction.md) | `complex-gain-mode-interaction.md` | `getXxxGainControlMode()` + `setXxxHardwareGain()` | +| [State Transitions](complex-state-transitions.md) | `complex-state-transitions.md` | `getEnsmMode()`/`setEnsmMode()` | +| [DPD Operations](complex-dpd-operations.md) | `complex-dpd-operations.md` | `dpdReset()` + `getDpdStatus()` | +| [Channel Independence](complex-channel-independence.md) | `complex-channel-independence.md` | Setter with `int channel`, count > 1 | +| [Phase Rotation](complex-phase-rotation.md) | `complex-phase-rotation.md` | `getPhaseRotation()`/`setPhaseRotation()` | +| [Frequency Tuning](complex-frequency-tuning.md) | `complex-frequency-tuning.md` | Getter/setter with Hz-to-MHz conversion | +| [UDC LO Splitting](complex-udc-lo-splitting.md) | `complex-udc-lo-splitting.md` | `hasUdc()`/`getUdcEnabled()` | +| [Refresh Cycle](complex-refresh-cycle.md) | `complex-refresh-cycle.md` | `refresh()` exists | + +## File Structure + +| Reference | File | +|-----------|------| +| [File Structure & Boilerplate](file-structure.md) | `file-structure.md` | + +## Key Rules + +1. **Every setter must be followed by `msleep(500)`** — no exceptions +2. **Every test must save and restore original values** — in all exit paths +3. **Use standard helpers** — do not write ad-hoc read/write/compare for standard widget types +4. **Use `runDataDrivenTests()`** — for sections with 3+ widgets of the same type +5. **UID format:** `UNIT.
.` — uppercase, dots as separators +6. **Widget key format:** `//` or `/` diff --git a/tools/scopy_dev_plugin/skills/unit-test-patterns/complex-calibration-flow.md b/tools/scopy_dev_plugin/skills/unit-test-patterns/complex-calibration-flow.md new file mode 100644 index 0000000000..85bdc9aef5 --- /dev/null +++ b/tools/scopy_dev_plugin/skills/unit-test-patterns/complex-calibration-flow.md @@ -0,0 +1,61 @@ +# Complex: Calibration Flow + +## Description + +Tests the full calibration trigger flow: enable calibration flags, trigger calibration, verify flags remain set, restore originals. + +## API Signature Trigger + +Generate this test when the plugin's `*_api.h` contains a `calibrate()` method along with `getCalibrateXxxEn()`/`setCalibrateXxxEn()` getter/setter pairs. + +## Code Example + +From `ad9371_Unit_test.js`: + +```javascript +// C1: Full Calibration Flow +TestFramework.runTest("UNIT.CAL.FULL_CALIBRATION_FLOW", function() { + try { + // Save originals + var origRxQec = ad9371.getCalibrateRxQecEn(); + var origTxQec = ad9371.getCalibrateTxQecEn(); + var origTxLol = ad9371.getCalibrateTxLolEn(); + var origTxLolExt = ad9371.getCalibrateTxLolExtEn(); + + // Enable all calibration flags + ad9371.setCalibrateRxQecEn("1"); + ad9371.setCalibrateTxQecEn("1"); + ad9371.setCalibrateTxLolEn("1"); + ad9371.setCalibrateTxLolExtEn("1"); + msleep(500); + + // Trigger calibration + ad9371.calibrate(); + msleep(2000); + + // Verify flags still set + var rxQec = ad9371.getCalibrateRxQecEn(); + var txQec = ad9371.getCalibrateTxQecEn(); + printToConsole(" After calibrate: rx_qec=" + rxQec + " tx_qec=" + txQec); + + // Restore + ad9371.setCalibrateRxQecEn(origRxQec); + ad9371.setCalibrateTxQecEn(origTxQec); + ad9371.setCalibrateTxLolEn(origTxLol); + ad9371.setCalibrateTxLolExtEn(origTxLolExt); + msleep(500); + + return true; + } catch (e) { + printToConsole(" Error: " + e); + return false; + } +}); +``` + +## Key Requirements + +- Save ALL calibration flags before modifying any +- Use longer sleep after `calibrate()` (2000ms) — hardware needs time +- Restore all flags in all exit paths +- This test validates the calibration mechanism, not calibration accuracy diff --git a/tools/scopy_dev_plugin/skills/unit-test-patterns/complex-channel-independence.md b/tools/scopy_dev_plugin/skills/unit-test-patterns/complex-channel-independence.md new file mode 100644 index 0000000000..5ac4b83c2f --- /dev/null +++ b/tools/scopy_dev_plugin/skills/unit-test-patterns/complex-channel-independence.md @@ -0,0 +1,82 @@ +# Complex: Channel Independence + +## Description + +Tests that per-channel attributes are truly independent — changing channel 0 does not affect channel 1 and vice versa. + +## API Signature Trigger + +Generate this test when the plugin's `*_api.h` has setter methods taking an `int channel` parameter with 2+ channels. + +## Code Example + +From `ad9371_Unit_test.js`: + +```javascript +// C7: Per-Channel Independence +TestFramework.runTest("UNIT.TX.CHANNEL_INDEPENDENCE", function() { + try { + var origCh0 = ad9371.getTxAttenuation(0); + var origCh1 = ad9371.getTxAttenuation(1); + printToConsole(" Original ch0=" + origCh0 + " ch1=" + origCh1); + + // Set ch0 to 5 dB + ad9371.setTxAttenuation(0, "5"); + msleep(1000); + var ch0After = parseFloat(ad9371.getTxAttenuation(0)); + var ch1After = parseFloat(ad9371.getTxAttenuation(1)); + printToConsole(" After setting ch0=5: ch0=" + ch0After + " ch1=" + ch1After); + + if (Math.abs(ch0After - 5.0) > 0.5) { + printToConsole(" FAIL: ch0 should be ~5"); + ad9371.setTxAttenuation(0, origCh0); + msleep(500); + return false; + } + if (Math.abs(ch1After - parseFloat(origCh1)) > 0.5) { + printToConsole(" FAIL: ch1 changed unexpectedly from " + origCh1 + " to " + ch1After); + ad9371.setTxAttenuation(0, origCh0); + msleep(500); + return false; + } + + // Set ch1 to 10 dB, verify ch0 unchanged + ad9371.setTxAttenuation(1, "10"); + msleep(1000); + var ch0Final = parseFloat(ad9371.getTxAttenuation(0)); + var ch1Final = parseFloat(ad9371.getTxAttenuation(1)); + printToConsole(" After setting ch1=10: ch0=" + ch0Final + " ch1=" + ch1Final); + + if (Math.abs(ch0Final - 5.0) > 0.5) { + printToConsole(" FAIL: ch0 changed when setting ch1"); + ad9371.setTxAttenuation(0, origCh0); + ad9371.setTxAttenuation(1, origCh1); + msleep(500); + return false; + } + if (Math.abs(ch1Final - 10.0) > 0.5) { + printToConsole(" FAIL: ch1 should be ~10"); + ad9371.setTxAttenuation(0, origCh0); + ad9371.setTxAttenuation(1, origCh1); + msleep(500); + return false; + } + + // Restore + ad9371.setTxAttenuation(0, origCh0); + ad9371.setTxAttenuation(1, origCh1); + msleep(500); + return true; + } catch (e) { + printToConsole(" Error: " + e); + return false; + } +}); +``` + +## Key Requirements + +- Save and restore BOTH channels +- Test in both directions: set ch0 verify ch1 unchanged, then set ch1 verify ch0 unchanged +- Use wider tolerance (0.5) for channel independence checks +- Restore both channels in all exit paths diff --git a/tools/scopy_dev_plugin/skills/unit-test-patterns/complex-dpd-operations.md b/tools/scopy_dev_plugin/skills/unit-test-patterns/complex-dpd-operations.md new file mode 100644 index 0000000000..acc64a6f1a --- /dev/null +++ b/tools/scopy_dev_plugin/skills/unit-test-patterns/complex-dpd-operations.md @@ -0,0 +1,97 @@ +# Complex: DPD Operations + +## Description + +Tests Digital Pre-Distortion (DPD) reset and status verification. Resets DPD, checks status string is valid, and verifies track count is readable. Typically AD9375-only. + +## API Signature Trigger + +Generate this test when the plugin's `*_api.h` contains `dpdReset()` and `getDpdStatus()` methods. + +## Skip Guard + +```javascript +if (!isAd9375) { + printToConsole(" SKIP: Not AD9375"); + return "SKIP"; +} +``` + +## Code Example + +From `ad9371_Unit_test.js`: + +```javascript +// C6: DPD Reset + Status Check (AD9375 only) +TestFramework.runTest("UNIT.DPD.RESET_AND_STATUS_CH0", function() { + try { + if (!isAd9375) { + printToConsole(" SKIP: Not AD9375"); + return "SKIP"; + } + + var statusBefore = ad9371.getDpdStatus(0); + printToConsole(" DPD status before reset: " + statusBefore); + + ad9371.dpdReset(0); + msleep(1000); + + var statusAfter = ad9371.getDpdStatus(0); + printToConsole(" DPD status after reset: " + statusAfter); + + // Verify status is a valid human-readable string + if (!statusAfter || statusAfter === "") { + printToConsole(" FAIL: status empty after reset"); + return false; + } + if (statusAfter.indexOf("No Error") === -1 && statusAfter.indexOf("Error:") === -1) { + printToConsole(" FAIL: status not a valid string: " + statusAfter); + return false; + } + + // Verify track count is readable + var trackCount = ad9371.getDpdTrackCount(0); + printToConsole(" DPD track count: " + trackCount); + if (trackCount === null || trackCount === "") { + printToConsole(" FAIL: track count unreadable after reset"); + return false; + } + + return true; + } catch (e) { + printToConsole(" Error: " + e); + return false; + } +}); +``` + +## Known Status Strings Pattern + +For validating status strings, use a known-list pattern: + +```javascript +var KNOWN_DPD_STATUS = [ + "No Error", "Error: ORx disabled", "Error: Tx disabled", + "Error: DPD initialization not run", "Error: Path delay not setup", + "Error: ORx signal too low", "Error: ORx signal saturated", + "Error: Tx signal too low", "Error: Tx signal saturated", + "Error: Model error high", "Error: AM AM outliers", + "Error: Invalid Tx profile", "Error: ORx QEC Disabled" +]; + +function isKnownStatus(val, knownList) { + for (var i = 0; i < knownList.length; i++) { + if (val === knownList[i]) return true; + } + if (val.indexOf("Error: Unknown status") === 0) return true; + return false; +} +``` + +## Key Requirements + +- Skip guard for variant-specific features (e.g., `isAd9375`) +- Longer sleep after reset (1000ms) +- Validate status is a recognized string, not just non-empty +- Accept "Unknown status" patterns for undocumented firmware codes +- Same pattern applies to CLGC and VSWR subsystems diff --git a/tools/scopy_dev_plugin/skills/unit-test-patterns/complex-frequency-tuning.md b/tools/scopy_dev_plugin/skills/unit-test-patterns/complex-frequency-tuning.md new file mode 100644 index 0000000000..21963cbf32 --- /dev/null +++ b/tools/scopy_dev_plugin/skills/unit-test-patterns/complex-frequency-tuning.md @@ -0,0 +1,66 @@ +# Complex: Frequency Tuning + +## Description + +Tests LO frequency write/readback with unit conversion validation. API typically works in MHz while hardware stores Hz. Uses `testConversion()` helper. + +## API Signature Trigger + +Generate this test when the plugin's `*_api.h` has getter/setter pairs where the getter returns a scaled value (e.g., MHz) and the widget stores raw values (e.g., Hz). + +## Code Example + +From `ad9371_Basic_Unit_test.js`: + +```javascript +TestFramework.runTest("UNIT.RX.LO_FREQUENCY", function() { + try { + return testConversion( + function() { return ad9371.getRxLoFrequency(); }, + function(v) { ad9371.setRxLoFrequency(v); }, + PHY + "altvoltage0_out/frequency", + "2500", // API value: 2500 MHz + "2500000000", // Raw value: 2500000000 Hz + 1.0 // tolerance + ); + } catch (e) { + printToConsole(" Error: " + e); + return false; + } +}); +``` + +## Read-Only Frequency Verification + +For read-only frequency attributes (RF bandwidth, sampling rate), verify the conversion directly: + +```javascript +TestFramework.runTest("UNIT.RX.RF_BANDWIDTH", function() { + try { + var raw = ad9371.readWidget(PHY + "voltage0_in/rf_bandwidth"); + if (!raw || raw === "") { + printToConsole(" SKIP: rf_bandwidth not readable on this hardware"); + return "SKIP"; + } + var rawHz = parseFloat(raw); + var apiMHz = parseFloat(ad9371.getRxRfBandwidth()); + var expectedMHz = rawHz / 1e6; + var diff = Math.abs(apiMHz - expectedMHz); + if (diff > 0.001) { + printToConsole(" FAIL: MHz conversion mismatch"); + return false; + } + return true; + } catch (e) { + printToConsole(" Error: " + e); + return false; + } +}); +``` + +## Key Requirements + +- Use `testConversion()` helper for writable frequency attributes +- For read-only frequencies, verify conversion math directly +- Common conversion factor: `1e6` (MHz to Hz) +- Tolerance: 1.0 for frequency values (Hz rounding) diff --git a/tools/scopy_dev_plugin/skills/unit-test-patterns/complex-gain-mode-interaction.md b/tools/scopy_dev_plugin/skills/unit-test-patterns/complex-gain-mode-interaction.md new file mode 100644 index 0000000000..c199f10d4a --- /dev/null +++ b/tools/scopy_dev_plugin/skills/unit-test-patterns/complex-gain-mode-interaction.md @@ -0,0 +1,65 @@ +# Complex: Gain Mode Interaction + +## Description + +Tests the interaction between gain control mode (manual/automatic/hybrid) and hardware gain. Verifies that gain is writable in manual mode and readable in AGC mode. + +## API Signature Trigger + +Generate this test when the plugin's `*_api.h` contains both `getXxxGainControlMode()`/`setXxxGainControlMode()` and `getXxxHardwareGain()`/`setXxxHardwareGain()` methods. + +## Code Example + +From `ad9371_Unit_test.js`: + +```javascript +// C4: Gain Mode Interaction +TestFramework.runTest("UNIT.RX.GAIN_MODE_INTERACTION", function() { + try { + var origMode = ad9371.getRxGainControlMode(); + var origGain = ad9371.getRxHardwareGain(0); + printToConsole(" Original mode: " + origMode + ", gain: " + origGain); + + // Set to manual and set a specific gain + ad9371.setRxGainControlMode("manual"); + msleep(1000); + ad9371.setRxHardwareGain(0, "20"); + msleep(1000); + var manualGain = ad9371.getRxHardwareGain(0); + printToConsole(" Manual mode gain set to 20, read: " + manualGain); + if (Math.abs(parseFloat(manualGain) - 20.0) > 1.0) { + printToConsole(" FAIL: gain should be ~20 in manual mode"); + ad9371.setRxGainControlMode(origMode); + msleep(500); + return false; + } + + // Switch to automatic (AGC) + ad9371.setRxGainControlMode("automatic"); + msleep(1000); + var agcGain = ad9371.getRxHardwareGain(0); + printToConsole(" AGC mode gain: " + agcGain); + if (agcGain === null || agcGain === "") { + printToConsole(" FAIL: gain unreadable in AGC mode"); + ad9371.setRxGainControlMode(origMode); + msleep(500); + return false; + } + + // Restore + ad9371.setRxGainControlMode(origMode); + msleep(500); + return true; + } catch (e) { + printToConsole(" Error: " + e); + return false; + } +}); +``` + +## Key Requirements + +- Use longer sleep (1000ms) after mode changes — hardware needs settling time +- Verify gain is controllable in manual mode AND readable in AGC mode +- Restore original mode in all exit paths +- Tolerance for gain comparison is wider (1.0 dB) due to AGC adjustments diff --git a/tools/scopy_dev_plugin/skills/unit-test-patterns/complex-phase-rotation.md b/tools/scopy_dev_plugin/skills/unit-test-patterns/complex-phase-rotation.md new file mode 100644 index 0000000000..6ae84477e0 --- /dev/null +++ b/tools/scopy_dev_plugin/skills/unit-test-patterns/complex-phase-rotation.md @@ -0,0 +1,70 @@ +# Complex: Phase Rotation + +## Description + +Tests FPGA phase rotation write/readback with tolerance. Includes detection of hardware support — some FPGA configurations don't support phase rotation writes. + +## API Signature Trigger + +Generate this test when the plugin's `*_api.h` contains `getPhaseRotation(int channel)` / `setPhaseRotation(int channel, double angle)`. + +## Code Example + +From `ad9371_Unit_test.js`: + +```javascript +// C3: Phase Rotation Roundtrip +TestFramework.runTest("UNIT.FPGA.PHASE_ROTATION_CH0", function() { + try { + var orig = ad9371.getPhaseRotation(0); + printToConsole(" Original phase ch0: " + orig); + if (orig === null || orig === "") { + printToConsole(" SKIP: Phase rotation not available on this hardware"); + return "SKIP"; + } + + // Set to 0 degrees first as baseline + ad9371.setPhaseRotation(0, 0.0); + msleep(1000); + var read0 = parseFloat(ad9371.getPhaseRotation(0)); + printToConsole(" Set 0, read: " + read0); + + // Set to 45 degrees + ad9371.setPhaseRotation(0, 45.0); + msleep(1000); + var read45 = parseFloat(ad9371.getPhaseRotation(0)); + printToConsole(" Set 45, read: " + read45); + + // Check if writes are taking effect + if (Math.abs(read0) < 2.0 && Math.abs(read45 - 45.0) > 5.0) { + printToConsole(" SKIP: Phase rotation writes not supported on this FPGA configuration"); + ad9371.setPhaseRotation(0, parseFloat(orig)); + msleep(500); + return "SKIP"; + } + + if (Math.abs(read45 - 45.0) > 2.0) { + printToConsole(" FAIL: phase mismatch, expected ~45 got " + read45); + ad9371.setPhaseRotation(0, parseFloat(orig)); + msleep(500); + return false; + } + + // Restore + ad9371.setPhaseRotation(0, parseFloat(orig)); + msleep(500); + return true; + } catch (e) { + printToConsole(" Error: " + e); + return false; + } +}); +``` + +## Key Requirements + +- Use `return "SKIP"` when feature is not available on hardware +- Wider tolerance for phase angles (2.0 degrees) +- Detect write-not-supported pattern (value doesn't change after write) +- Test per channel — generate one test per channel +- Longer sleep (1000ms) for FPGA operations diff --git a/tools/scopy_dev_plugin/skills/unit-test-patterns/complex-profile-loading.md b/tools/scopy_dev_plugin/skills/unit-test-patterns/complex-profile-loading.md new file mode 100644 index 0000000000..8b3e9b7af1 --- /dev/null +++ b/tools/scopy_dev_plugin/skills/unit-test-patterns/complex-profile-loading.md @@ -0,0 +1,64 @@ +# Complex: Profile Loading + +## Description + +Tests loading a device profile and verifying the device state is valid afterwards. Reads representative values before and after profile load. + +## API Signature Trigger + +Generate this test when the plugin's `*_api.h` contains `loadProfile(QString)` and `getDefaultProfilePath()` methods. + +## Code Example + +From `ad9371_Unit_test.js`: + +```javascript +// C2: Profile Loading +TestFramework.runTest("UNIT.PROFILE.LOAD_AND_VERIFY", function() { + try { + var profilePath = ad9371.getDefaultProfilePath(); + if (!profilePath || profilePath === "") { + printToConsole(" SKIP: No profile file available"); + return "SKIP"; + } + printToConsole(" Profile path: " + profilePath); + + // Read current state + var rxBwBefore = ad9371.getRxRfBandwidth(); + var txBwBefore = ad9371.getTxRfBandwidth(); + printToConsole(" Before: RX BW=" + rxBwBefore + " TX BW=" + txBwBefore); + + // Load profile + ad9371.loadProfile(profilePath); + msleep(5000); + + // Refresh widgets + ad9371.refresh(); + msleep(2000); + + // Read after - verify sampling frequency is readable + var rxSfAfter = ad9371.getRxSamplingFrequency(); + var txSfAfter = ad9371.getTxSamplingFrequency(); + printToConsole(" After: RX SF=" + rxSfAfter + " TX SF=" + txSfAfter); + + // Verify at least sampling frequency is readable after profile load + if (!rxSfAfter || rxSfAfter === "" || rxSfAfter === "0.000000") { + printToConsole(" FAIL: RX sampling frequency invalid after profile load"); + return false; + } + + printToConsole(" PASS: profile loaded successfully"); + return true; + } catch (e) { + printToConsole(" Error: " + e); + return false; + } +}); +``` + +## Key Requirements + +- Use `return "SKIP"` when no profile file is available +- Long sleep after `loadProfile()` (5000ms) — hardware reconfiguration takes time +- Call `refresh()` after profile load to update widget values +- Verify representative values are readable (not necessarily unchanged) diff --git a/tools/scopy_dev_plugin/skills/unit-test-patterns/complex-refresh-cycle.md b/tools/scopy_dev_plugin/skills/unit-test-patterns/complex-refresh-cycle.md new file mode 100644 index 0000000000..5e17540f9b --- /dev/null +++ b/tools/scopy_dev_plugin/skills/unit-test-patterns/complex-refresh-cycle.md @@ -0,0 +1,57 @@ +# Complex: Refresh Cycle + +## Description + +Tests the `refresh()` method by reading representative values before and after refresh, verifying they remain valid and consistent. + +## API Signature Trigger + +Generate this test when the plugin's `*_api.h` contains a `refresh()` method. + +## Code Example + +From `ad9371_Unit_test.js`: + +```javascript +// C10: Refresh Cycle +TestFramework.runTest("UNIT.UTIL.REFRESH_ALL", function() { + try { + // Read representative values before refresh + var ensmBefore = ad9371.getEnsmMode(); + var rxGainBefore = ad9371.getRxHardwareGain(0); + printToConsole(" Before refresh: ensm=" + ensmBefore + " rxGain=" + rxGainBefore); + + // Trigger refresh + ad9371.refresh(); + msleep(2000); + + // Read again after refresh + var ensmAfter = ad9371.getEnsmMode(); + var rxGainAfter = ad9371.getRxHardwareGain(0); + printToConsole(" After refresh: ensm=" + ensmAfter + " rxGain=" + rxGainAfter); + + // Values should be non-empty and consistent + if (!ensmAfter || ensmAfter === "") { + printToConsole(" FAIL: ENSM mode empty after refresh"); + return false; + } + if (!rxGainAfter || rxGainAfter === "") { + printToConsole(" FAIL: RX gain empty after refresh"); + return false; + } + + printToConsole(" PASS: refresh completed, values consistent"); + return true; + } catch (e) { + printToConsole(" Error: " + e); + return false; + } +}); +``` + +## Key Requirements + +- No state modification needed — this is a read-verify test +- Longer sleep after `refresh()` (2000ms) to allow all widgets to update +- Pick 2-3 representative values from different sections +- Verify values are non-empty, not necessarily identical to before diff --git a/tools/scopy_dev_plugin/skills/unit-test-patterns/complex-state-transitions.md b/tools/scopy_dev_plugin/skills/unit-test-patterns/complex-state-transitions.md new file mode 100644 index 0000000000..134dee5059 --- /dev/null +++ b/tools/scopy_dev_plugin/skills/unit-test-patterns/complex-state-transitions.md @@ -0,0 +1,60 @@ +# Complex: State Transitions + +## Description + +Tests device state machine transitions (e.g., ENSM mode cycling between radio_off and radio_on) and verifies each state is properly entered. + +## API Signature Trigger + +Generate this test when the plugin's `*_api.h` contains `getEnsmMode()`/`setEnsmMode()` or similar state machine getter/setter with multiple valid states. + +## Code Example + +From `ad9371_Unit_test.js`: + +```javascript +// C5: ENSM Mode State Transitions +TestFramework.runTest("UNIT.GLOBAL.ENSM_STATE_TRANSITIONS", function() { + try { + var orig = ad9371.getEnsmMode(); + printToConsole(" Original ENSM mode: " + orig); + + ad9371.setEnsmMode("radio_off"); + msleep(500); + var readOff = ad9371.getEnsmMode(); + printToConsole(" Set radio_off, read: " + readOff); + if (readOff !== "radio_off") { + printToConsole(" FAIL: expected 'radio_off', got '" + readOff + "'"); + ad9371.setEnsmMode(orig); + msleep(500); + return false; + } + + ad9371.setEnsmMode("radio_on"); + msleep(500); + var readOn = ad9371.getEnsmMode(); + printToConsole(" Set radio_on, read: " + readOn); + if (readOn !== "radio_on") { + printToConsole(" FAIL: expected 'radio_on', got '" + readOn + "'"); + ad9371.setEnsmMode(orig); + msleep(500); + return false; + } + + // Restore + ad9371.setEnsmMode(orig); + msleep(500); + return true; + } catch (e) { + printToConsole(" Error: " + e); + return false; + } +}); +``` + +## Key Requirements + +- Test transitions in both directions (off -> on, on -> off) +- Verify each state with exact string comparison +- Restore original state in all exit paths +- Different from `testCombo` — this tests the transition sequence, not just valid values diff --git a/tools/scopy_dev_plugin/skills/unit-test-patterns/complex-udc-lo-splitting.md b/tools/scopy_dev_plugin/skills/unit-test-patterns/complex-udc-lo-splitting.md new file mode 100644 index 0000000000..3ce1b14f14 --- /dev/null +++ b/tools/scopy_dev_plugin/skills/unit-test-patterns/complex-udc-lo-splitting.md @@ -0,0 +1,63 @@ +# Complex: UDC LO Splitting + +## Description + +Tests Up/Down Converter (UDC) enable/disable with LO frequency interaction. Verifies that UDC can be toggled and LO frequency remains settable. + +## API Signature Trigger + +Generate this test when the plugin's `*_api.h` contains `hasUdc()`, `getUdcEnabled()`/`setUdcEnabled()`. + +## Code Example + +From `ad9371_Unit_test.js`: + +```javascript +// C8: Up/Down Converter LO Splitting +TestFramework.runTest("UNIT.UDC.LO_SPLITTING", function() { + try { + if (!ad9371.hasUdc()) { + printToConsole(" SKIP: UDC hardware not present"); + return "SKIP"; + } + + var origUdc = ad9371.getUdcEnabled(); + var origRxLo = ad9371.getRxLoFrequency(); + printToConsole(" Original UDC=" + origUdc + " RX LO=" + origRxLo); + + // Enable UDC + ad9371.setUdcEnabled(true); + msleep(500); + if (!ad9371.getUdcEnabled()) { + printToConsole(" FAIL: UDC did not enable"); + return false; + } + + // Set RX LO + ad9371.setRxLoFrequency("1000"); + msleep(500); + var udcLo = ad9371.getRxLoFrequency(); + printToConsole(" UDC enabled, RX LO set to 1000, read: " + udcLo); + + // Disable UDC + ad9371.setUdcEnabled(false); + msleep(500); + + // Restore + ad9371.setRxLoFrequency(origRxLo); + msleep(500); + ad9371.setUdcEnabled(origUdc); + msleep(500); + return true; + } catch (e) { + printToConsole(" Error: " + e); + return false; + } +}); +``` + +## Key Requirements + +- Skip when `hasUdc()` returns false +- Save and restore both UDC state and LO frequency +- Test enable, LO set, disable sequence diff --git a/tools/scopy_dev_plugin/skills/unit-test-patterns/data-driven-tests.md b/tools/scopy_dev_plugin/skills/unit-test-patterns/data-driven-tests.md new file mode 100644 index 0000000000..9226499028 --- /dev/null +++ b/tools/scopy_dev_plugin/skills/unit-test-patterns/data-driven-tests.md @@ -0,0 +1,81 @@ +# runDataDrivenTests — Batch Data-Driven Test Dispatch + +## Description + +Runs an array of test descriptor objects through the appropriate standard helper function based on the `type` field. This is the preferred way to test sections with 3+ widgets of the same type, reducing boilerplate and ensuring consistency. + +## When to Use + +- A section has 3 or more widgets that can be tested with standard helpers +- Advanced tool tabs with many similar attributes (e.g., AGC parameters, profile settings) +- Any batch of widgets where individual `TestFramework.runTest()` calls would be repetitive + +## Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `tests` | object[] | Array of test descriptor objects | + +### Test Descriptor Object Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `uid` | string | Yes | Test UID (e.g., `"UNIT.CLK.DEVICE_CLOCK_KHZ"`) | +| `attr` | string | Yes | Attribute suffix (appended to PHY prefix) | +| `type` | string | Yes | One of: `"range"`, `"checkbox"`, `"combo"`, `"readonly"` | +| `min` | number | For range | Minimum value | +| `max` | number | For range | Maximum value | +| `mid` | number | For range | Midpoint value | +| `options` | string[] | For combo | Array of valid option strings | + +## Code Example + +From `ad9371_Basic_Unit_test.js`: + +```javascript +function runDataDrivenTests(tests) { + for (var i = 0; i < tests.length; i++) { + (function(t) { + TestFramework.runTest(t.uid, function() { + if (t.type === "range") return testRange(PHY + t.attr, t.min, t.max, t.mid); + if (t.type === "checkbox") return testCheckbox(PHY + t.attr); + if (t.type === "combo") return testCombo(PHY + t.attr, t.options); + if (t.type === "readonly") return testReadOnly(PHY + t.attr); + return false; + }); + })(tests[i]); + } +} +``` + +## Usage Example + +```javascript +// Advanced tab: CLK Settings +var clkTests = [ + {uid: "UNIT.CLK.DEVICE_CLOCK_KHZ", attr: "adi,clocks-device-clock_khz", type: "range", min: 30720, max: 320000, mid: 122880}, + {uid: "UNIT.CLK.CLKPLL_HS_DIV", attr: "adi,clocks-clkpll-hs-div", type: "combo", options: ["4.0", "5.0"]}, + {uid: "UNIT.CLK.CLKPLL_VCO_DIV", attr: "adi,clocks-clkpll-vco-div", type: "combo", options: ["1.0", "1.5", "2.0", "3.0"]} +]; +runDataDrivenTests(clkTests); + +// With conditional skip guard for AD9375 +if (isAd9375) { + var dpdTests = [ + {uid: "UNIT.DPD.DESIRED_GAIN", attr: "adi,dpd-desired-gain", type: "range", min: -100, max: 100, mid: 0}, + {uid: "UNIT.DPD.MODEL_VERSION", attr: "adi,dpd-model-version", type: "readonly"}, + {uid: "UNIT.DPD.TRACKING_EN", attr: "adi,dpd-tracking-config-enable", type: "checkbox"} + ]; + runDataDrivenTests(dpdTests); +} else { + printToConsole(" SKIP: DPD tests (not AD9375)"); +} +``` + +## Key Requirements + +- Use IIFE `(function(t) { ... })(tests[i])` to capture loop variable correctly +- The `PHY` prefix is prepended inside the dispatcher, so `attr` should NOT include it +- Type must exactly match one of: `"range"`, `"checkbox"`, `"combo"`, `"readonly"` +- All standard helper requirements (msleep, state restoration) are enforced by the helpers themselves +- Unrecognized type returns `false` (test failure) diff --git a/tools/scopy_dev_plugin/skills/unit-test-patterns/file-structure.md b/tools/scopy_dev_plugin/skills/unit-test-patterns/file-structure.md new file mode 100644 index 0000000000..3ddfb151f1 --- /dev/null +++ b/tools/scopy_dev_plugin/skills/unit-test-patterns/file-structure.md @@ -0,0 +1,147 @@ +# Unit Test File Structure & Boilerplate + +## File Naming Convention + +| File | Purpose | When to create | +|------|---------|----------------| +| `_Basic_Unit_test.js` | Main/basic tool widget tests | Always (if multiple files) | +| `_Advanced_Unit_test.js` | Advanced tab widget tests | Plugin has advanced tool | +| `_Complex_Unit_test.js` | Multi-step functionality tests | Plugin has complex scenarios | +| `_Unit_test.js` | Combined runner OR single file | Always | + +## Standalone File Template + +Every standalone unit test file follows this structure: + +```javascript +/* + * Copyright (c) 2025 Analog Devices Inc. + * + * This file is part of Scopy + * (see https://www.github.com/analogdevicesinc/scopy). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +// Unit Test Automation +// Tests + +// Load test framework +evaluateFile("../js/testAutomations/common/testFramework.js"); + +// Test Suite +TestFramework.init(" Unit Tests"); + +// Connect to device +if (!TestFramework.connectToDevice("ip:127.0.0.0")) { + printToConsole("ERROR: Cannot proceed without device connection"); + scopy.exit(); +} + +// Widget key prefix +var PHY = "/"; + +// Initial setup (e.g., set device to active mode) +.setEnsmMode("radio_on"); +msleep(1000); + +// Feature detection (if needed) +var hasFeatureX = (.readWidget(PHY + "feature_attribute") !== null); + +// ============================================ +// Helper Functions +// ============================================ + +// ... paste all standard helpers here ... +// testRange, testCombo, testCheckbox, testReadOnly, +// testConversion, testBadValueRange, testBadValueCombo, +// runDataDrivenTests + +// ============================================ +// SECTION:
+// ============================================ + +// Switch to tool +switchToTool(""); +msleep(500); + +// --- --- +TestFramework.runTest("UNIT.
.", function() { + // ... test logic ... +}); + +// ... more tests ... + +// ============================================ +// Cleanup +// ============================================ +TestFramework.disconnectFromDevice(); +TestFramework.printSummary(); +scopy.exit(); +``` + +## Combined Runner Template + +When multiple sub-files exist, the combined runner is a thin wrapper: + +```javascript +/* + * Copyright (c) 2025 Analog Devices Inc. + * ... (same license header) ... + */ + +// Combined Unit Test Runner +// Runs all unit test sub-files in sequence + +evaluateFile("../js/testAutomations//_Basic_Unit_test.js"); +evaluateFile("../js/testAutomations//_Advanced_Unit_test.js"); +evaluateFile("../js/testAutomations//_Complex_Unit_test.js"); +``` + +## UID Format + +``` +UNIT.
. +``` + +- `UNIT` — always the prefix for unit tests +- `SECTION` — uppercase section name (e.g., `RX`, `TX`, `OBS`, `CLK`, `DPD`, `CAL`, `GLOBAL`, `FPGA`, `UTIL`, `BAD`) +- `ATTRIBUTE_NAME` — uppercase snake_case derived from the attribute (e.g., `CH0_HARDWARE_GAIN`, `LO_FREQUENCY`) + +Examples: +- `UNIT.RX.CH0_HARDWARE_GAIN` +- `UNIT.TX.LO_FREQUENCY` +- `UNIT.DPD.TRACKING_EN` +- `UNIT.BAD.RX_HARDWARE_GAIN_RANGE` +- `UNIT.CAL.FULL_CALIBRATION_FLOW` + +## Widget Key Prefix Convention + +```javascript +var PHY = "/"; // e.g., "ad9371-phy/" +``` + +The device name comes from the EMU XML `` tag's `id` attribute. + +## Termination Sequence (non-negotiable) + +Every standalone file must end with exactly: + +```javascript +TestFramework.disconnectFromDevice(); +TestFramework.printSummary(); +scopy.exit(); +``` + +In this exact order. No code after `scopy.exit()`. diff --git a/tools/scopy_dev_plugin/skills/unit-test-patterns/test-bad-value-combo.md b/tools/scopy_dev_plugin/skills/unit-test-patterns/test-bad-value-combo.md new file mode 100644 index 0000000000..1390c5d64a --- /dev/null +++ b/tools/scopy_dev_plugin/skills/unit-test-patterns/test-bad-value-combo.md @@ -0,0 +1,62 @@ +# testBadValueCombo — Invalid Combo Option Test + +## Description + +Tests that a combo widget rejects an invalid option by verifying the value remains unchanged after writing a bad key. + +## When to Use + +- Pair with every `testCombo()` call to verify invalid option rejection +- Use a clearly invalid string that cannot match any valid option + +## Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `key` | string | Full widget key | +| `badKey` | string | An invalid option string (e.g., `"INVALID_OPTION_XYZ"`) | + +## Code Example + +From `ad9371_Basic_Unit_test.js`: + +```javascript +function testBadValueCombo(key, badKey) { + try { + var orig = ad9371.readWidget(key); + printToConsole(" testBadValueCombo: key=" + key + " orig=" + orig + " badKey=" + badKey); + + // Write invalid key -- expect value unchanged + ad9371.writeWidget(key, badKey); + msleep(500); + var readBack = ad9371.readWidget(key); + printToConsole(" Wrote badKey=" + badKey + ", read=" + readBack); + if (readBack !== orig) { + printToConsole(" FAIL: invalid combo key was accepted, orig=" + orig + " got=" + readBack); + ad9371.writeWidget(key, orig); + msleep(500); + return false; + } + + return true; + } catch (e) { + printToConsole(" Error in testBadValueCombo: " + e); + return false; + } +} +``` + +## Usage Example + +```javascript +TestFramework.runTest("UNIT.BAD.GAIN_CONTROL_MODE_COMBO", function() { + return testBadValueCombo(PHY + "voltage0_in/gain_control_mode", "INVALID_OPTION_XYZ"); +}); +``` + +## Key Requirements + +- The bad key should be clearly invalid (use `"INVALID_OPTION_XYZ"`) +- Verify value is **unchanged** (equals original), not just "not equal to bad key" +- `msleep(500)` after `writeWidget()` call +- No explicit restore needed (value should be unchanged), but restore if it was accepted diff --git a/tools/scopy_dev_plugin/skills/unit-test-patterns/test-bad-value-range.md b/tools/scopy_dev_plugin/skills/unit-test-patterns/test-bad-value-range.md new file mode 100644 index 0000000000..8f469600e9 --- /dev/null +++ b/tools/scopy_dev_plugin/skills/unit-test-patterns/test-bad-value-range.md @@ -0,0 +1,81 @@ +# testBadValueRange — Range Boundary Test + +## Description + +Tests that a range widget properly clamps out-of-bound values. Writes above the max and below the min, verifying the widget clamps to the boundary value. + +## When to Use + +- Pair with every `testRange()` call to verify boundary enforcement +- Widget has defined min/max bounds + +## Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `key` | string | Full widget key | +| `min` | number | Minimum valid value | +| `max` | number | Maximum valid value | + +## Code Example + +From `ad9371_Basic_Unit_test.js`: + +```javascript +function testBadValueRange(key, min, max) { + try { + var orig = ad9371.readWidget(key); + printToConsole(" testBadValueRange: key=" + key + " orig=" + orig + " validRange=[" + min + "," + max + "]"); + var tolerance = 0.01; + + // Write above max -- expect clamped to max + var aboveMax = String(parseFloat(max) + 1); + ad9371.writeWidget(key, aboveMax); + msleep(500); + var readAbove = ad9371.readWidget(key); + printToConsole(" Wrote aboveMax=" + aboveMax + ", read=" + readAbove); + if (Math.abs(parseFloat(readAbove) - parseFloat(max)) > tolerance) { + printToConsole(" FAIL: above-max not clamped, expected=" + max + " got=" + readAbove); + ad9371.writeWidget(key, String(parseFloat(orig))); + msleep(500); + return false; + } + + // Write below min -- expect clamped to min + var belowMin = String(parseFloat(min) - 1); + ad9371.writeWidget(key, belowMin); + msleep(500); + var readBelow = ad9371.readWidget(key); + printToConsole(" Wrote belowMin=" + belowMin + ", read=" + readBelow); + if (Math.abs(parseFloat(readBelow) - parseFloat(min)) > tolerance) { + printToConsole(" FAIL: below-min not clamped, expected=" + min + " got=" + readBelow); + ad9371.writeWidget(key, String(parseFloat(orig))); + msleep(500); + return false; + } + + // Restore + ad9371.writeWidget(key, String(parseFloat(orig))); + msleep(500); + return true; + } catch (e) { + printToConsole(" Error in testBadValueRange: " + e); + return false; + } +} +``` + +## Usage Example + +```javascript +TestFramework.runTest("UNIT.BAD.RX_HARDWARE_GAIN_RANGE", function() { + return testBadValueRange(PHY + "voltage0_in/hardwaregain", 0, 30); +}); +``` + +## Key Requirements + +- Test value above max uses `max + 1`, below min uses `min - 1` +- Verify the widget clamps (not rejects) — readback should equal the boundary +- `msleep(500)` after every `writeWidget()` call +- Restore original value after test diff --git a/tools/scopy_dev_plugin/skills/unit-test-patterns/test-calibration-flag.md b/tools/scopy_dev_plugin/skills/unit-test-patterns/test-calibration-flag.md new file mode 100644 index 0000000000..a1937cd0c6 --- /dev/null +++ b/tools/scopy_dev_plugin/skills/unit-test-patterns/test-calibration-flag.md @@ -0,0 +1,99 @@ +# testCalibrationFlag — Calibration Enable Flag Test + +## Description + +Tests calibration enable/disable flags that use API getter/setter pairs (not IIOWidget read/write). These flags control calibration subsystems (RX QEC, TX QEC, TX LOL, DPD, CLGC, VSWR) and are toggled via dedicated API methods. + +## When to Use + +- Plugin has `getCalibrateXxxEn()` / `setCalibrateXxxEn()` API methods +- Calibration flags use `MenuOnOffSwitch` (not standard IIOWidget), so `readWidget`/`writeWidget` may not work +- DPD/CLGC/VSWR calibration enable flags (AD9375-specific) + +## Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `uid` | string | Test UID (e.g., `"UNIT.CAL.RX_QEC_EN"`) | +| `getter` | function | API getter (e.g., `function() { return api.getCalibrateRxQecEn(); }`) | +| `setter` | function | API setter (e.g., `function(v) { api.setCalibrateRxQecEn(v); }`) | + +## Code Example + +From `ad9371_Unit_test.js`: + +```javascript +function testCalibrationFlag(uid, getter, setter) { + TestFramework.runTest(uid, function() { + try { + var orig = getter(); + printToConsole(" Original value: " + orig); + if (orig === null || orig === "") { + printToConsole(" FAIL: getter returned empty"); + return false; + } + + setter("1"); + msleep(500); + var read1 = getter(); + printToConsole(" Set to '1', read back: " + read1); + if (read1 !== "1") { + printToConsole(" FAIL: expected '1', got '" + read1 + "'"); + setter(orig); + msleep(500); + return false; + } + + setter("0"); + msleep(500); + var read0 = getter(); + printToConsole(" Set to '0', read back: " + read0); + if (read0 !== "0") { + printToConsole(" FAIL: expected '0', got '" + read0 + "'"); + setter(orig); + msleep(500); + return false; + } + + // Restore + setter(orig); + msleep(500); + return true; + } catch (e) { + printToConsole(" Error: " + e); + return false; + } + }); +} +``` + +## Usage Example + +```javascript +// Standard calibration flags +testCalibrationFlag("UNIT.CAL.RX_QEC_EN", + function() { return ad9371.getCalibrateRxQecEn(); }, + function(v) { ad9371.setCalibrateRxQecEn(v); } +); + +testCalibrationFlag("UNIT.CAL.TX_QEC_EN", + function() { return ad9371.getCalibrateTxQecEn(); }, + function(v) { ad9371.setCalibrateTxQecEn(v); } +); + +// AD9375-specific (wrap in isAd9375 guard) +if (isAd9375) { + testCalibrationFlag("UNIT.CAL.DPD_EN", + function() { return ad9371.getCalibrateDpdEn(); }, + function(v) { ad9371.setCalibrateDpdEn(v); } + ); +} +``` + +## Key Requirements + +- This helper calls `TestFramework.runTest()` internally (unlike other helpers that return bool) +- Values are strings `"0"` and `"1"`, not booleans +- `msleep(500)` after every setter call +- Restore original value in all exit paths +- Wrap variant-specific flags in skip guards (e.g., `if (isAd9375)`) diff --git a/tools/scopy_dev_plugin/skills/unit-test-patterns/test-checkbox.md b/tools/scopy_dev_plugin/skills/unit-test-patterns/test-checkbox.md new file mode 100644 index 0000000000..a6f5415f1b --- /dev/null +++ b/tools/scopy_dev_plugin/skills/unit-test-patterns/test-checkbox.md @@ -0,0 +1,76 @@ +# testCheckbox — Checkbox/Toggle Widget Test + +## Description + +Tests a checkbox (boolean toggle) widget by writing "0" and "1" and verifying exact readback. Restores the original value after testing. + +## When to Use + +- Widget created with `IIOWidgetBuilder::CheckBoxUi` +- Attribute is a binary enable/disable flag + +## Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `key` | string | Full widget key (e.g., `PHY + "adi,rx1-rx2-phase-inversion-en"`) | + +## Code Example + +From `ad9371_Basic_Unit_test.js`: + +```javascript +function testCheckbox(key) { + try { + var orig = ad9371.readWidget(key); + printToConsole(" testCheckbox: key=" + key + " orig=" + orig); + + // Write "0" + ad9371.writeWidget(key, "0"); + msleep(500); + var read0 = ad9371.readWidget(key); + printToConsole(" Wrote=0, read=" + read0); + if (read0 !== "0") { + printToConsole(" FAIL: checkbox mismatch, expected=0 got=" + read0); + ad9371.writeWidget(key, orig); + msleep(500); + return false; + } + + // Write "1" + ad9371.writeWidget(key, "1"); + msleep(500); + var read1 = ad9371.readWidget(key); + printToConsole(" Wrote=1, read=" + read1); + if (read1 !== "1") { + printToConsole(" FAIL: checkbox mismatch, expected=1 got=" + read1); + ad9371.writeWidget(key, orig); + msleep(500); + return false; + } + + // Restore + ad9371.writeWidget(key, orig); + msleep(500); + return true; + } catch (e) { + printToConsole(" Error in testCheckbox: " + e); + return false; + } +} +``` + +## Usage Example + +```javascript +TestFramework.runTest("UNIT.RX.QEC_TRACKING_EN", function() { + return testCheckbox(PHY + "voltage0_in/quadrature_tracking_en"); +}); +``` + +## Key Requirements + +- Values are always strings `"0"` and `"1"`, not booleans +- Exact string comparison with `!==` +- `msleep(500)` after every `writeWidget()` call +- Restore original value on failure and success diff --git a/tools/scopy_dev_plugin/skills/unit-test-patterns/test-combo.md b/tools/scopy_dev_plugin/skills/unit-test-patterns/test-combo.md new file mode 100644 index 0000000000..32fbfd4742 --- /dev/null +++ b/tools/scopy_dev_plugin/skills/unit-test-patterns/test-combo.md @@ -0,0 +1,66 @@ +# testCombo — Combo/Dropdown Widget Test + +## Description + +Tests a combo (dropdown) widget by iterating all valid options, writing each one, and verifying exact string match on readback. Restores the original selection after testing. + +## When to Use + +- Widget created with `IIOWidgetBuilder::ComboUi` +- Attribute has enumerated options (from `_available` attribute or hardcoded list) + +## Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `key` | string | Full widget key (e.g., `PHY + "ensm_mode"`) | +| `validKeys` | string[] | Array of all valid option strings | + +## Code Example + +From `ad9371_Basic_Unit_test.js`: + +```javascript +function testCombo(key, validKeys) { + try { + var orig = ad9371.readWidget(key); + printToConsole(" testCombo: key=" + key + " orig=" + orig); + + for (var i = 0; i < validKeys.length; i++) { + ad9371.writeWidget(key, validKeys[i]); + msleep(500); + var readBack = ad9371.readWidget(key); + printToConsole(" Wrote=" + validKeys[i] + ", read=" + readBack); + if (readBack !== validKeys[i]) { + printToConsole(" FAIL: combo mismatch, expected=" + validKeys[i] + " got=" + readBack); + ad9371.writeWidget(key, orig); + msleep(500); + return false; + } + } + + // Restore + ad9371.writeWidget(key, orig); + msleep(500); + return true; + } catch (e) { + printToConsole(" Error in testCombo: " + e); + return false; + } +} +``` + +## Usage Example + +```javascript +TestFramework.runTest("UNIT.RX.GAIN_CONTROL_MODE", function() { + return testCombo(PHY + "voltage0_in/gain_control_mode", ["manual", "automatic", "hybrid"]); +}); +``` + +## Key Requirements + +- Use **exact string comparison** (`!==`), not numeric +- Valid options come from EMU XML `_available` attribute or API header +- `msleep(500)` after every `writeWidget()` call +- Restore original value on failure and success diff --git a/tools/scopy_dev_plugin/skills/unit-test-patterns/test-conversion.md b/tools/scopy_dev_plugin/skills/unit-test-patterns/test-conversion.md new file mode 100644 index 0000000000..8913ccf5ad --- /dev/null +++ b/tools/scopy_dev_plugin/skills/unit-test-patterns/test-conversion.md @@ -0,0 +1,122 @@ +# testConversion — API Conversion Test + +## Description + +Tests that an API getter/setter pair and the raw widget readback are consistent, validating unit conversion logic (e.g., Hz to MHz, raw to dB). Writes via the API setter, reads back via both the API getter and `readWidget()`, and compares both against expected values. + +## When to Use + +- API getter returns a scaled/converted value (e.g., MHz) while the widget stores raw values (e.g., Hz) +- API has both getter and setter methods for the same attribute +- Common conversions: Hz/MHz, dB/raw, dBFS/linear, suffix stripping + +## Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `getter` | function | API getter function (e.g., `function() { return api.getRxLoFrequency(); }`) | +| `setter` | function | API setter function (e.g., `function(v) { api.setRxLoFrequency(v); }`) | +| `widgetKey` | string | Full widget key for raw readback | +| `apiVal` | string/number | Value to write via setter (in API units, e.g., MHz) | +| `rawVal` | string/number | Expected raw widget value (in hardware units, e.g., Hz) | +| `tolerance` | number | Acceptable difference for floating-point comparisons (null for exact match) | + +## Code Example + +From `ad9371_Basic_Unit_test.js`: + +```javascript +function testConversion(getter, setter, widgetKey, apiVal, rawVal, tolerance) { + try { + var orig = getter(); + printToConsole(" testConversion: orig=" + orig + " apiVal=" + apiVal + " rawVal=" + rawVal); + + // Write via setter + setter(apiVal); + msleep(500); + + // Read via getter + var apiRead = getter(); + printToConsole(" API read=" + apiRead); + if (tolerance !== undefined && tolerance !== null) { + var apiDiff = Math.abs(parseFloat(apiRead) - parseFloat(apiVal)); + if (apiDiff > tolerance) { + printToConsole(" FAIL: API value mismatch, expected=" + apiVal + " got=" + apiRead + " tolerance=" + tolerance); + setter(orig); + msleep(500); + return false; + } + } else { + if (apiRead !== String(apiVal)) { + printToConsole(" FAIL: API value mismatch, expected=" + apiVal + " got=" + apiRead); + setter(orig); + msleep(500); + return false; + } + } + + // Read via readWidget + var rawRead = ad9371.readWidget(widgetKey); + printToConsole(" Raw widget read=" + rawRead); + if (tolerance !== undefined && tolerance !== null) { + var rawDiff = Math.abs(parseFloat(rawRead) - parseFloat(rawVal)); + if (rawDiff > tolerance) { + printToConsole(" FAIL: Raw value mismatch, expected=" + rawVal + " got=" + rawRead + " tolerance=" + tolerance); + setter(orig); + msleep(500); + return false; + } + } else { + if (rawRead !== String(rawVal)) { + printToConsole(" FAIL: Raw value mismatch, expected=" + rawVal + " got=" + rawRead); + setter(orig); + msleep(500); + return false; + } + } + + // Restore + setter(orig); + msleep(500); + return true; + } catch (e) { + printToConsole(" Error in testConversion: " + e); + return false; + } +} +``` + +## Usage Example + +```javascript +// LO frequency: API in MHz, widget in Hz +TestFramework.runTest("UNIT.RX.LO_FREQUENCY", function() { + return testConversion( + function() { return ad9371.getRxLoFrequency(); }, + function(v) { ad9371.setRxLoFrequency(v); }, + PHY + "altvoltage0_out/frequency", + "2500", // API value: 2500 MHz + "2500000000", // Raw value: 2500000000 Hz + 1.0 // tolerance + ); +}); + +// TX attenuation: API in positive dB, widget in negative dB +TestFramework.runTest("UNIT.TX.CH0_ATTENUATION", function() { + return testConversion( + function() { return ad9371.getTxAttenuation(0); }, + function(v) { ad9371.setTxAttenuation(0, v); }, + PHY + "voltage0_out/hardwaregain", + "10", // API value: 10 dB + "-10", // Raw value: -10 dB (hardware uses negative) + 0.1 // tolerance + ); +}); +``` + +## Key Requirements + +- Both API getter and raw widget are validated in the same test +- `msleep(500)` after every setter call +- Restore original value via the setter (not writeWidget) to maintain conversion consistency +- Use `tolerance` for floating-point values, `null` for exact string match diff --git a/tools/scopy_dev_plugin/skills/unit-test-patterns/test-range.md b/tools/scopy_dev_plugin/skills/unit-test-patterns/test-range.md new file mode 100644 index 0000000000..17c4fc3a04 --- /dev/null +++ b/tools/scopy_dev_plugin/skills/unit-test-patterns/test-range.md @@ -0,0 +1,94 @@ +# testRange — Range/Spinbox Widget Test + +## Description + +Tests a range (spinbox) widget by writing min, max, and mid values and verifying readback with tolerance. Restores the original value after testing. + +## When to Use + +- Widget created with `IIOWidgetBuilder::EditableUi` or `IIOWidgetBuilder::RangeUi` +- Widget has numeric min/max bounds +- Attribute is writable (not read-only) + +## Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `key` | string | Full widget key (e.g., `PHY + "voltage0_in/hardwaregain"`) | +| `min` | number | Minimum valid value | +| `max` | number | Maximum valid value | +| `mid` | number | Midpoint value for additional verification | + +## Code Example + +From `ad9371_Basic_Unit_test.js`: + +```javascript +function testRange(key, min, max, mid) { + try { + var orig = ad9371.readWidget(key); + printToConsole(" testRange: key=" + key + " orig=" + orig); + + var tolerance = 0.01; + + // Write min + ad9371.writeWidget(key, String(min)); + msleep(500); + var readMin = ad9371.readWidget(key); + printToConsole(" Wrote min=" + min + ", read=" + readMin); + if (Math.abs(parseFloat(readMin) - parseFloat(min)) > tolerance) { + printToConsole(" FAIL: min mismatch, expected=" + String(min) + " got=" + readMin); + ad9371.writeWidget(key, String(parseFloat(orig))); + msleep(500); + return false; + } + + // Write max + ad9371.writeWidget(key, String(max)); + msleep(500); + var readMax = ad9371.readWidget(key); + printToConsole(" Wrote max=" + max + ", read=" + readMax); + if (Math.abs(parseFloat(readMax) - parseFloat(max)) > tolerance) { + printToConsole(" FAIL: max mismatch, expected=" + String(max) + " got=" + readMax); + ad9371.writeWidget(key, String(parseFloat(orig))); + msleep(500); + return false; + } + + // Write mid + ad9371.writeWidget(key, String(mid)); + msleep(500); + var readMid = ad9371.readWidget(key); + printToConsole(" Wrote mid=" + mid + ", read=" + readMid); + if (Math.abs(parseFloat(readMid) - parseFloat(mid)) > tolerance) { + printToConsole(" FAIL: mid mismatch, expected=" + String(mid) + " got=" + readMid); + ad9371.writeWidget(key, String(parseFloat(orig))); + msleep(500); + return false; + } + + // Restore + ad9371.writeWidget(key, String(parseFloat(orig))); + msleep(500); + return true; + } catch (e) { + printToConsole(" Error in testRange: " + e); + return false; + } +} +``` + +## Usage Example + +```javascript +TestFramework.runTest("UNIT.RX.CH0_HARDWARE_GAIN", function() { + return testRange(PHY + "voltage0_in/hardwaregain", 0, 30, 15); +}); +``` + +## Key Requirements + +- Tolerance is 0.01 by default — adjust if widget has coarser step size +- Always restore original value, even on failure +- `msleep(500)` after every `writeWidget()` call +- Use `String()` conversion for write values, `parseFloat()` for comparisons diff --git a/tools/scopy_dev_plugin/skills/unit-test-patterns/test-readonly.md b/tools/scopy_dev_plugin/skills/unit-test-patterns/test-readonly.md new file mode 100644 index 0000000000..ffce659222 --- /dev/null +++ b/tools/scopy_dev_plugin/skills/unit-test-patterns/test-readonly.md @@ -0,0 +1,53 @@ +# testReadOnly — Read-Only Widget Test + +## Description + +Tests a read-only widget by reading its value and verifying it is non-null and non-empty. Does not write any value. + +## When to Use + +- Widget is display-only (sampling frequency, RSSI, status strings) +- Attribute has no setter or is marked read-only +- Getter-only API methods + +## Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `key` | string | Full widget key (e.g., `PHY + "voltage0_in/sampling_frequency"`) | + +## Code Example + +From `ad9371_Basic_Unit_test.js`: + +```javascript +function testReadOnly(key) { + try { + var val = ad9371.readWidget(key); + printToConsole(" testReadOnly: key=" + key + " val=" + val); + if (val === null || val === "") { + printToConsole(" FAIL: readOnly value is empty or null"); + return false; + } + printToConsole(" PASS: readOnly value is non-empty: " + val); + return true; + } catch (e) { + printToConsole(" Error in testReadOnly: " + e); + return false; + } +} +``` + +## Usage Example + +```javascript +TestFramework.runTest("UNIT.RX.SAMPLING_FREQUENCY", function() { + return testReadOnly(PHY + "voltage0_in/sampling_frequency"); +}); +``` + +## Key Requirements + +- No `writeWidget()` call — this is read-only +- Check for both `null` and `""` (empty string) +- No state restoration needed (nothing was modified) diff --git a/tools/scopy_dev_plugin/skills/unit-test-quality-checks/SKILL.md b/tools/scopy_dev_plugin/skills/unit-test-quality-checks/SKILL.md new file mode 100644 index 0000000000..8a3f6b8bdc --- /dev/null +++ b/tools/scopy_dev_plugin/skills/unit-test-quality-checks/SKILL.md @@ -0,0 +1,84 @@ +--- +name: unit-test-quality-checks +description: Unit test validation rules for Scopy plugin IIOWidget tests. Auto-loads when reviewing or writing `*_Unit_test.js` files. +--- + +# JS Unit Test Quality Check Rules + +Apply these 7 validation categories when reviewing or generating JS unit test scripts for Scopy plugins. + +--- + +## CRITICAL + +### [U1] Widget Coverage + +- Extract all widget keys from the plugin's widget factory source (IIOWidgetBuilder calls) or `getWidgetKeys()` return +- Extract all `Q_INVOKABLE` getter/setter pairs from the plugin's `*_api.h` +- Compare against widget keys and API methods exercised in the JS unit test files (from `readWidget()`, `writeWidget()`, `testRange()`, `testCombo()`, `testCheckbox()`, `testReadOnly()`, `testConversion()`, and `runDataDrivenTests()` calls) +- Flag any widget key that has no corresponding test +- Flag any `Q_INVOKABLE` getter/setter pair with no test exercising it +- Flag any test that references a widget key not in the discoverable set + +### [U2] Helper Function Usage + +- Tests for standard widget types MUST use canonical helper functions: + - Range/spinbox widgets: `testRange()` + - Combo/dropdown widgets: `testCombo()` + - Checkbox/toggle widgets: `testCheckbox()` + - Read-only widgets: `testReadOnly()` + - API conversion tests: `testConversion()` + - Bad value boundary tests: `testBadValueRange()`, `testBadValueCombo()` +- Data-driven sections with 3+ widgets of the same type MUST use `runDataDrivenTests()` +- Flag any test that manually writes/reads/compares widget values when a standard helper exists for that widget type +- **Exception:** complex tests (in the Complex section) are allowed custom logic + +### [U3] Sleep After Setters + +- Every `writeWidget()`, `set*()`, or any state-mutating API call (e.g., `calibrate()`, `dpdReset()`, `refresh()`, `loadProfile()`) must be followed **immediately** by `msleep(500)` on the next non-empty line +- `msleep(1000)` or longer is acceptable (minimum is `msleep(500)`) +- This applies both inside helper function definitions and in individual test bodies +- No state-mutating call may be left without a subsequent sleep + +### [U4] State Restoration + +- Every test that modifies device state must save the original value before changing it +- The original value must be restored in **all** exit paths: normal completion, early return, and catch block +- For tests that modify multiple values (e.g., ENSM mode + gain control mode), all modified values must be restored +- Complex tests modifying multiple widgets must restore every widget they changed + +--- + +## WARNING + +### [U5] Bad Value Tests + +- Every range widget with a `testRange()` call SHOULD also have a `testBadValueRange()` call +- Every combo widget with a `testCombo()` call SHOULD also have a `testBadValueCombo()` call +- Report the coverage ratio (e.g., "12/15 range widgets have bad value tests, 80%") +- Warn if bad value test coverage drops below 80% + +### [U6] Complex Test Isolation + +- Complex multi-step tests must be in a clearly marked section (e.g., `// SECTION: Complex Functionality Tests`) +- Each complex test must have a comment documenting what it tests (e.g., `// C1: Full Calibration Flow`) +- Complex test UIDs must follow `UNIT.
.` format +- Flag any undocumented complex test mixed into attribute test sections + +--- + +## INFO + +### [U7] File Structure + +- File naming must follow the convention: + - `_Basic_Unit_test.js` — basic/main tool widget tests + - `_Advanced_Unit_test.js` — advanced tab widget tests + - `_Complex_Unit_test.js` — multi-step functionality tests + - `_Unit_test.js` — combined runner (or single file if only one) +- Each standalone file must end with the termination sequence in order: + 1. `TestFramework.disconnectFromDevice()` + 2. `TestFramework.printSummary()` + 3. `scopy.exit()` +- The combined runner file uses `evaluateFile()` to include sub-files and must NOT duplicate helper functions or test cases +- The GPL license header must be present at the top of every file