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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,30 +33,30 @@
end

function t = chronoAlignedTime(item)
if isfield(item, 'tAligned') && ~isempty(item.tAligned)
t = item.tAligned(:);
elseif isfield(item, 'tAligned_s') && ~isempty(item.tAligned_s)
if isfield(item, 'tAligned_s') && ~isempty(item.tAligned_s)
t = item.tAligned_s(:);
elseif isfield(item, 'tAligned') && ~isempty(item.tAligned)
t = item.tAligned(:);
else
t = [];
end
end

function v = chronoVoltage(item)
if isfield(item, 'Vf') && ~isempty(item.Vf)
v = item.Vf(:);
elseif isfield(item, 'Vf_V') && ~isempty(item.Vf_V)
if isfield(item, 'Vf_V') && ~isempty(item.Vf_V)
v = item.Vf_V(:);
elseif isfield(item, 'Vf') && ~isempty(item.Vf)
v = item.Vf(:);
else
v = [];
end
end

function i = chronoCurrent(item)
if isfield(item, 'Im') && ~isempty(item.Im)
i = item.Im(:);
elseif isfield(item, 'Im_A') && ~isempty(item.Im_A)
if isfield(item, 'Im_A') && ~isempty(item.Im_A)
i = item.Im_A(:);
elseif isfield(item, 'Im') && ~isempty(item.Im)
i = item.Im(:);
else
i = [];
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@
end

function t = chronoTime(item)
if isfield(item, 't') && ~isempty(item.t)
t = item.t;
elseif isfield(item, 't_s') && ~isempty(item.t_s)
if isfield(item, 't_s') && ~isempty(item.t_s)
t = item.t_s;
elseif isfield(item, 't') && ~isempty(item.t)
t = item.t;
else
t = [];
end
Expand Down
18 changes: 9 additions & 9 deletions apps/electrochem/chrono_overlay/+chrono_overlay/+view/plotVTIT.m
Original file line number Diff line number Diff line change
Expand Up @@ -74,20 +74,20 @@ function plotVTIT(axV, axI, items, opts)
end

function v = chronoVoltage(item)
if isfield(item, 'Vf') && ~isempty(item.Vf)
v = item.Vf(:);
elseif isfield(item, 'Vf_V') && ~isempty(item.Vf_V)
if isfield(item, 'Vf_V') && ~isempty(item.Vf_V)
v = item.Vf_V(:);
elseif isfield(item, 'Vf') && ~isempty(item.Vf)
v = item.Vf(:);
else
v = [];
end
end

function i = chronoCurrent(item)
if isfield(item, 'Im') && ~isempty(item.Im)
i = item.Im(:);
elseif isfield(item, 'Im_A') && ~isempty(item.Im_A)
if isfield(item, 'Im_A') && ~isempty(item.Im_A)
i = item.Im_A(:);
elseif isfield(item, 'Im') && ~isempty(item.Im)
i = item.Im(:);
else
i = [];
end
Expand All @@ -105,10 +105,10 @@ function plotVTIT(axV, axI, items, opts)
end

function t = chronoAlignedTime(item)
if isfield(item, 'tAligned') && ~isempty(item.tAligned)
t = item.tAligned(:);
elseif isfield(item, 'tAligned_s') && ~isempty(item.tAligned_s)
if isfield(item, 'tAligned_s') && ~isempty(item.tAligned_s)
t = item.tAligned_s(:);
elseif isfield(item, 'tAligned') && ~isempty(item.tAligned)
t = item.tAligned(:);
else
t = [];
end
Expand Down
33 changes: 2 additions & 31 deletions apps/electrochem/eis/+eis/+export/buildExportTable.m
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
end

function [x, y] = filteredXY(item, xName, yName, useLogX, useLogY)
x = valuesForAxis(item, xName);
y = valuesForAxis(item, yName);
x = eis.ops.valuesForAxis(item, xName);
y = eis.ops.valuesForAxis(item, yName);
valid = isfinite(x) & isfinite(y);
x = x(valid);
y = y(valid);
Expand All @@ -49,35 +49,6 @@
end
end

function values = valuesForAxis(item, axisName)
switch axisName
case 'Freq (Hz)'
values = item.Freq;
case 'log10(Freq)'
values = log10(item.Freq);
case 'Time (s)'
values = item.Time;
case 'Point #'
values = item.Pt;
case 'Zreal (ohm)'
values = item.Zreal;
case 'Zimag (ohm)'
values = item.Zimag;
case '-Zimag (ohm)'
values = item.negZimag;
case 'Zmod (ohm)'
values = item.Zmod;
case 'Zphz (deg)'
values = item.Zphz;
case 'Idc (A)'
values = item.Idc;
case 'Vdc (V)'
values = item.Vdc;
otherwise
error('Unsupported axis selection: %s', axisName);
end
end

function padded = padWithNaN(v, n)
padded = NaN(n, 1);
if isempty(v)
Expand Down
32 changes: 21 additions & 11 deletions apps/electrochem/eis/+eis/+ops/valuesForAxis.m
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,38 @@
function values = valuesForAxis(item, axisName)
switch axisName
case 'Freq (Hz)'
values = item.Freq;
values = itemField(item, 'freq_Hz', 'Freq');
case 'log10(Freq)'
values = log10(item.Freq);
values = log10(itemField(item, 'freq_Hz', 'Freq'));
case 'Time (s)'
values = item.Time;
values = itemField(item, 'time_s', 'Time');
case 'Point #'
values = item.Pt;
values = itemField(item, 'point', 'Pt');
case 'Zreal (ohm)'
values = item.Zreal;
values = itemField(item, 'Zreal_ohm', 'Zreal');
case 'Zimag (ohm)'
values = item.Zimag;
values = itemField(item, 'Zimag_ohm', 'Zimag');
case '-Zimag (ohm)'
values = item.negZimag;
values = itemField(item, 'negZimag_ohm', 'negZimag');
case 'Zmod (ohm)'
values = item.Zmod;
values = itemField(item, 'Zmod_ohm', 'Zmod');
case 'Zphz (deg)'
values = item.Zphz;
values = itemField(item, 'Zphz_deg', 'Zphz');
case 'Idc (A)'
values = item.Idc;
values = itemField(item, 'Idc_A', 'Idc');
case 'Vdc (V)'
values = item.Vdc;
values = itemField(item, 'Vdc_V', 'Vdc');
otherwise
error('Unsupported axis selection: %s', axisName);
end
end

function values = itemField(item, canonicalName, legacyName)
if isfield(item, canonicalName) && ~isempty(item.(canonicalName))
values = item.(canonicalName);
elseif isfield(item, legacyName) && ~isempty(item.(legacyName))
values = item.(legacyName);
else
values = [];
end
end
11 changes: 11 additions & 0 deletions apps/electrochem/eis/+eis/+view/axisModeForSelection.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
% Expected caller: EIS app view code and unit tests. Inputs are the selected
% X/Y axis labels. Output is the MATLAB axes mode needed for that pairing.

function mode = axisModeForSelection(xName, yName)
if strcmp(xName, 'Zreal (ohm)') && ...
(strcmp(yName, '-Zimag (ohm)') || strcmp(yName, 'Zimag (ohm)'))
mode = "equal";
else
mode = "normal";
end
end
15 changes: 13 additions & 2 deletions apps/electrochem/eis/+eis/+view/buildSummary.m
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,24 @@
summary = cell(0, 1);
summary{end+1} = sprintf('Loaded files: %d', numel(items));
for i = 1:numel(items)
fmin = min(items(i).Freq, [], 'omitnan');
fmax = max(items(i).Freq, [], 'omitnan');
freq = itemField(items(i), 'freq_Hz', 'Freq');
fmin = min(freq, [], 'omitnan');
fmax = max(freq, [], 'omitnan');
summary{end+1} = sprintf('%s | N=%d | Freq %.4g to %.4g Hz | order: %s', ...
items(i).name, items(i).n, fmin, fmax, ternary(items(i).freqDesc, 'high->low', 'low->high/mixed'));
end
end

function values = itemField(item, canonicalName, legacyName)
if isfield(item, canonicalName) && ~isempty(item.(canonicalName))
values = item.(canonicalName);
elseif isfield(item, legacyName) && ~isempty(item.(legacyName))
values = item.(legacyName);
else
values = [];
end
end

function txt = ternary(cond, a, b)
if cond
txt = a;
Expand Down
40 changes: 3 additions & 37 deletions apps/electrochem/eis/+eis/+view/plotOverlay.m
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
legend(ax, 'off');
end

if isNyquistSelection(opts.xName, opts.yName)
if eis.view.axisModeForSelection(opts.xName, opts.yName) == "equal"
axis(ax, 'equal');
end
end
Expand Down Expand Up @@ -88,8 +88,8 @@
end

function [x, y] = filteredXY(item, xName, yName, useLogX, useLogY)
x = valuesForAxis(item, xName);
y = valuesForAxis(item, yName);
x = eis.ops.valuesForAxis(item, xName);
y = eis.ops.valuesForAxis(item, yName);
valid = isfinite(x) & isfinite(y);
x = x(valid);
y = y(valid);
Expand All @@ -105,40 +105,6 @@
end
end

function values = valuesForAxis(item, axisName)
switch axisName
case 'Freq (Hz)'
values = item.Freq;
case 'log10(Freq)'
values = log10(item.Freq);
case 'Time (s)'
values = item.Time;
case 'Point #'
values = item.Pt;
case 'Zreal (ohm)'
values = item.Zreal;
case 'Zimag (ohm)'
values = item.Zimag;
case '-Zimag (ohm)'
values = item.negZimag;
case 'Zmod (ohm)'
values = item.Zmod;
case 'Zphz (deg)'
values = item.Zphz;
case 'Idc (A)'
values = item.Idc;
case 'Vdc (V)'
values = item.Vdc;
otherwise
error('Unsupported axis selection: %s', axisName);
end
end

function tf = isNyquistSelection(xName, yName)
tf = strcmp(xName, 'Zreal (ohm)') && ...
(strcmp(yName, '-Zimag (ohm)') || strcmp(yName, 'Zimag (ohm)'));
end

function txt = pluralS(n)
if n == 1
txt = '';
Expand Down
11 changes: 11 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,17 @@ Shared setup, structural GUI assertions, and focused support routines live under

Architecture guardrails are split by concern under `tests/integration/project/`: public package surface, reusable package dependency boundaries, app entrypoint boundaries, and app-owned workflow boundaries. These guardrails may require workflow code to remain under the owning app tree, but they should not require GUI-free helpers to stay in the public app entry-point file. App-owned packages are checked by boundary rules rather than exact file-list assertions.

Compatibility bridge behavior should be isolated in named compatibility tests.
For example, DTA legacy bridge fields such as `t`, `Vf`, `Im`, `Freq`, and
`Zreal` are covered by `DtaCompatibilityBridgeTest`; ordinary DTA and app
tests should use canonical unit-explicit fields and direct app-owned package
functions.

Unit app tests should not read app source text to prove behavior. Source-string
scans belong in project guardrails; app behavior tests should call package
functions directly or use GUI structural tests when the behavior is layout or
callback wiring.

When a suite file becomes broad enough that unrelated changes must read hundreds of lines, add a narrower `test_*.m` file in the same suite instead of appending more coverage to the broad file.

## GUI Validation
Expand Down
2 changes: 2 additions & 0 deletions tests/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Tests mirror source ownership. Do not create a parallel runner framework unless
- Use `tests/support/` for official-runner setup, artifact paths, structured
trace capture, GUI fixture setup, and component snapshots.
- Do not move app-specific formulas, expected scientific values, result schemas, or export columns into shared test helpers.
- Keep compatibility bridge assertions isolated in named compatibility tests. Ordinary app and facade tests should prefer current canonical fields and direct package functions.
- Unit app tests should not read app source text to prove behavior. Keep source-string scans in project guardrails.
- Boundary tests may require app-owned logic to stay under the owning app tree, but should not require GUI-free helpers to remain inside the public app entry-point file or assert exact app-private helper file lists.
- Runner-migration tests should not rely only on GUI structural launches. When
migration creates an app-owned package for DIC or wearable apps, add unit
Expand Down
14 changes: 7 additions & 7 deletions tests/integration/project/ProjectDebtGuardrailTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ function legacyTestBackdoorDebtDoesNotGrow(testCase)
testCommandFiles = uniqueMatchedFiles(root, {'apps', '+labkit'}, ...
'__labkit_test__');
testCase.verifyEmpty(testCommandFiles, ...
['legacy app test command references must not remain after Phase 4. Files: ' ...
['legacy app test command references must not remain. Files: ' ...
strjoin(cellstr(testCommandFiles), ', ')]);

handlerFiles = uniqueMatchedFiles(root, {'apps'}, ...
'function\s+handlers\s*=\s*\w*[Aa]ppTestHandlers');
testCase.verifyEmpty(handlerFiles, ...
['legacy app test handler functions must not remain after Phase 4. Files: ' ...
['legacy app test handler functions must not remain. Files: ' ...
strjoin(cellstr(handlerFiles), ', ')]);

diagnosticsFiles = uniqueMatchedFiles(root, {'apps'}, ...
'loadFileDiagnostics|parse\w*LoadDiagnosticsRequest|collectLoadDiagnostics');
testCase.verifyEmpty(diagnosticsFiles, ...
['hidden load diagnostics must not remain after Phase 4. Files: ' ...
['hidden load diagnostics must not remain. Files: ' ...
strjoin(cellstr(diagnosticsFiles), ', ')]);

fprintf('Legacy backdoor inventory: %d test-command files, %d handler files, %d diagnostics files.\n', ...
Expand All @@ -31,7 +31,7 @@ function oversizedAppEntrypointDebtIsRemoved(testCase)
root = setupLabKitTestPath();
actual = collectOversizedEntrypoints(root, 500);
testCase.verifyEmpty(actual, ...
['app entrypoints must remain at or below 500 lines after Phase 5. Files: ' ...
['app entrypoints must remain at or below 500 lines. Files: ' ...
strjoin(cellstr(actual), ', ')]);
fprintf('Entrypoint size debt inventory: %d files over 500 lines.\n', numel(actual));
end
Expand All @@ -55,9 +55,9 @@ function oldRunnerDependenciesAreRemoved(testCase)
root = setupLabKitTestPath();

testCase.verifyFalse(isfolder(fullfile(root, 'tests', 'suites')), ...
'tests/suites must not remain after Phase 6 official-test migration.');
'tests/suites must not remain after the official-test migration.');
testCase.verifyFalse(isfile(fullfile(root, 'tests', 'run_all_tests.m')), ...
'tests/run_all_tests.m must not remain after Phase 6 official-test migration.');
'tests/run_all_tests.m must not remain after the official-test migration.');

dependencyFiles = uniqueMatchedFiles(root, ...
{'.github', 'scripts', 'docs', 'tests', 'buildfile.m', ...
Expand All @@ -66,7 +66,7 @@ function oldRunnerDependenciesAreRemoved(testCase)
dependencyFiles = setdiff(dependencyFiles, ...
"tests/integration/project/ProjectDebtGuardrailTest.m");
testCase.verifyEmpty(dependencyFiles, ...
['old custom-runner dependencies must not remain after Phase 6. Files: ' ...
['old custom-runner dependencies must not remain. Files: ' ...
strjoin(cellstr(dependencyFiles), ', ')]);

fprintf('Old runner dependency inventory: %d files.\n', numel(dependencyFiles));
Expand Down
Loading
Loading