Skip to content

Commit 692814b

Browse files
HanSur94claude
andauthored
fix(1014): migrate/prune MATLAB test-suite for v2.0 Tag API (#64)
Migrate or delete classdef tests in tests/suite/*.m that reference legacy classes deleted in commit 4188a7f (Phase 1011 cleanup): Sensor, Threshold, ThresholdRule, CompositeThreshold, StateChannel, SensorRegistry. Summary of changes: - TestNavigatorOverlay: testCase.TestData -> private properties (R2020b compat) - TestSensorDetailPlot: TestData.sensor -> property; 4 legacy methods + helper deleted - TestDashboardBugFixes: testSensorListenersMultiPage fix (s.updateData vs local var) - TestStatusWidget/TestGaugeWidget/TestIconCardWidget/TestMultiStatusWidget/TestChipBarWidget: 35 widget-threshold methods deleted - TestIconCardWidgetTag/TestMultiStatusWidgetTag/TestFastSenseAddTag/TestLiveEventPipelineTag/TestMonitorTagEvents: *Tag.m Threshold-call strip - TestEventDetector/TestIncrementalDetector/TestLivePipeline: DELETED (legacy signatures) - TestEventStore/TestEventConfig: pruned of legacy .addSensor/.runDetection/.addTag - TestDashboardEngine/TestFastSenseWidget: 1 Threshold() call each deleted - TestDataSource/TestWebBridge: contract repair + 5 private-method callers deleted - TestTag/TestToolbar/TestMonitorTagEvents: R2020b/Pitfall-5 assertion fixes Library change: libs/Dashboard/DashboardBuilder.m exitEditMode() hoists the ishandle(hFig) guard above the first set(hFig, 'WindowButtonMotionFcn') call to prevent MATLAB:class:InvalidHandle when the figure is externally closed. Fixes TestDashboardBugFixes/testExitEditModeAfterFigureClose. Verification: - grep -rE 'Threshold|CompositeThreshold|...SensorRegistry\\(' tests/suite/: 0 - grep -r 'testCase.TestData' tests/suite/: 0 - grep -r 'detectEventsFromSensor' tests/suite/: 0 (comments only) - Octave function-style suite: 74/75 (1 pre-existing graphics-driver segfault) - Targeted MATLAB runtests on Plan 07 files: TestTag 20/20, TestToolbar 14/14, TestMonitorTagEvents 11/11 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2e2537c commit 692814b

25 files changed

Lines changed: 105 additions & 1616 deletions

libs/Dashboard/DashboardBuilder.m

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,14 @@ function exitEditMode(obj)
121121
obj.DragMode = '';
122122

123123
hFig = obj.Engine.hFigure;
124-
set(hFig, 'WindowButtonMotionFcn', obj.OldMotionFcn);
125-
set(hFig, 'WindowButtonUpFcn', obj.OldButtonUpFcn);
124+
% FIX: guard first before any `set` calls. If the figure was
125+
% deleted externally, downstream cleanup (safeDelete, clear*)
126+
% still runs since those are handle-safe; we only skip set()
127+
% on an invalid handle.
128+
if ~isempty(hFig) && ishandle(hFig)
129+
set(hFig, 'WindowButtonMotionFcn', obj.OldMotionFcn);
130+
set(hFig, 'WindowButtonUpFcn', obj.OldButtonUpFcn);
131+
end
126132
obj.OldMotionFcn = '';
127133
obj.OldButtonUpFcn = '';
128134

tests/suite/TestChipBarWidget.m

Lines changed: 0 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -108,56 +108,5 @@ function testChipColorUpdate(testCase)
108108
c2 = get(w.hChipCircles{1}, 'FaceColor');
109109
testCase.verifyEqual(c2, alarmColor, 'AbsTol', 1e-9);
110110
end
111-
112-
function testChipThreshold(testCase)
113-
% chip struct with threshold + value fields resolves alarm color
114-
t = Threshold('cbw_thr_test', 'Direction', 'upper');
115-
t.addCondition(struct(), 50);
116-
chip = struct('label', 'Pump', 'threshold', t, 'value', 75);
117-
w = ChipBarWidget();
118-
w.Chips = {chip};
119-
fig = figure('Visible', 'off');
120-
cleanup = onCleanup(@() close(fig));
121-
hp = uipanel(fig, 'Position', [0 0 1 1]);
122-
theme = DashboardTheme('dark');
123-
w.ParentTheme = theme;
124-
w.render(hp);
125-
% value 75 > threshold 50 -> alarm
126-
c = get(w.hChipCircles{1}, 'FaceColor');
127-
testCase.verifyEqual(c, theme.StatusAlarmColor, 'AbsTol', 0.01);
128-
end
129-
130-
function testChipThresholdWithValueFcn(testCase)
131-
% chip with threshold + valueFcn resolves dynamically
132-
t = Threshold('cbw_valfcn_test', 'Direction', 'upper');
133-
t.addCondition(struct(), 50);
134-
val = 30; % below threshold -> ok
135-
chip = struct('label', 'Tank', 'threshold', t, 'valueFcn', @() val);
136-
w = ChipBarWidget();
137-
w.Chips = {chip};
138-
fig = figure('Visible', 'off');
139-
cleanup = onCleanup(@() close(fig));
140-
hp = uipanel(fig, 'Position', [0 0 1 1]);
141-
theme = DashboardTheme('dark');
142-
w.ParentTheme = theme;
143-
w.render(hp);
144-
% value 30 < threshold 50 -> ok
145-
c = get(w.hChipCircles{1}, 'FaceColor');
146-
testCase.verifyEqual(c, theme.StatusOkColor, 'AbsTol', 0.01);
147-
end
148-
149-
function testChipThresholdSerialize(testCase)
150-
% toStruct emits chip threshold key; fromStruct restores
151-
t = Threshold('cbw_ser_test', 'Direction', 'upper');
152-
t.addCondition(struct(), 50);
153-
TagRegistry.register('cbw_ser_test', t);
154-
cleanup = onCleanup(@() TagRegistry.unregister('cbw_ser_test'));
155-
chip = struct('label', 'Fan', 'threshold', t, 'value', 40);
156-
w = ChipBarWidget('Title', 'Health');
157-
w.Chips = {chip};
158-
s = w.toStruct();
159-
testCase.verifyEqual(s.chips{1}.threshold, 'cbw_ser_test');
160-
testCase.verifyEqual(s.chips{1}.value, 40);
161-
end
162111
end
163112
end

tests/suite/TestDashboardBugFixes.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ function testSensorListenersMultiPage(testCase)
261261

262262
% Trigger PostSet listener by assigning new data
263263
try
264-
s_y_ = rand(1, 10);
264+
s.updateData(1:10, rand(1, 10));
265265
testCase.verifyTrue(w.Dirty, ...
266266
'PostSet listener should mark widget dirty when sensor Y changes');
267267
catch

tests/suite/TestDashboardEngine.m

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -148,20 +148,6 @@ function testTimerContinuesAfterError(testCase)
148148
testCase.verifyEqual(counter('n'), int32(1));
149149
end
150150

151-
function testAddWidgetWithTag(testCase)
152-
s = SensorTag('T-401', 'Name', 'Temperature');
153-
s.updateData(1:100, rand(1,100));
154-
t_hi = Threshold('hi', 'Name', 'Hi', 'Direction', 'upper');
155-
t_hi.addCondition(struct(), 80);
156-
s.addThreshold(t_hi);
157-
s.resolve();
158-
159-
d = DashboardEngine('Sensor Test');
160-
d.addWidget('fastsense', 'Sensor', s, 'Position', [1 1 16 3]);
161-
testCase.verifyEqual(d.Widgets{1}.Title, 'Temperature');
162-
testCase.verifyEqual(d.Widgets{1}.Sensor, s);
163-
end
164-
165151
function testEngineAddGroupWidget(testCase)
166152
d = DashboardEngine('TestDash', 'Theme', 'dark');
167153
d.addWidget('group', 'Label', 'Motor Health');

tests/suite/TestDataSource.m

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,12 @@ function addPaths(testCase)
88
end
99

1010
methods (Test)
11-
function testCannotInstantiate(testCase)
12-
threw = false;
13-
try
14-
ds = DataSource();
15-
error('Should not reach here');
16-
catch ex
17-
threw = true;
18-
testCase.verifyTrue(contains(ex.message, 'Abstract'), 'cannot_instantiate');
19-
end
20-
testCase.verifyTrue(threw, 'DataSource should not be instantiable');
11+
function testFetchNewMustBeImplementedBySubclass(testCase)
12+
% DataSource is the abstract interface for fetchNew. The class
13+
% itself can be instantiated, but calling fetchNew() on the base
14+
% class throws 'DataSource:abstract' — subclasses MUST override.
15+
ds = DataSource();
16+
testCase.verifyError(@() ds.fetchNew(), 'DataSource:abstract');
2117
end
2218

2319
function testSubclassMustImplementFetchNew(testCase)

tests/suite/TestEventConfig.m

Lines changed: 10 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
11
classdef TestEventConfig < matlab.unittest.TestCase
2+
%TESTEVENTCONFIG EventConfig surface tests.
3+
%
4+
% All legacy-pipeline methods (cfg.addTag + cfg.runDetection +
5+
% Threshold class) were deleted in Phase 1014 Plan 05: the
6+
% Sensor/Threshold/StateChannel pipeline was removed in Phase 1011
7+
% and EventConfig.addSensor now throws 'EventConfig:legacyRemoved'.
8+
%
9+
% Live-path event detection lives on MonitorTag + EventStore,
10+
% covered by TestMonitorTag* and TestEventStoreRw.
11+
212
methods (TestClassSetup)
313
function addPaths(testCase)
414
addpath(fullfile(fileparts(mfilename('fullpath')), '..', '..'));
@@ -17,22 +27,6 @@ function testConstructorDefaults(testCase)
1727
testCase.verifyEqual(cfg.AutoOpenViewer, false, 'defaults: AutoOpenViewer');
1828
end
1929

20-
function testAddTag(testCase)
21-
cfg = EventConfig();
22-
s = SensorTag('temp', 'Name', 'Temperature');
23-
s.updateData(1:10, [5 5 12 14 11 13 5 5 5 5]);
24-
[s_x_, s_y_] = s.getXY();
25-
t_warn = Threshold('warn', 'Name', 'warn', 'Direction', 'upper');
26-
t_warn.addCondition(struct(), 10);
27-
s.addThreshold(t_warn);
28-
cfg.addTag(s);
29-
testCase.verifyEqual(numel(cfg.Sensors), 1, 'addSensor: count');
30-
testCase.verifyEqual(numel(cfg.SensorData), 1, 'addSensor: data count');
31-
testCase.verifyEqual(cfg.SensorData(1).name, 'Temperature', 'addSensor: data name');
32-
testCase.verifyEqual(cfg.SensorData(1).t, s_x_, 'addSensor: data t');
33-
testCase.verifyEqual(cfg.SensorData(1).y, s_y_, 'addSensor: data y');
34-
end
35-
3630
function testSetColor(testCase)
3731
cfg = EventConfig();
3832
cfg.setColor('warn', [1 0 0]);
@@ -50,99 +44,5 @@ function testBuildDetector(testCase)
5044
testCase.verifyEqual(det.MaxCallsPerEvent, 3, 'buildDetector: MaxCallsPerEvent');
5145
testCase.verifyNotEmpty(det.OnEventStart, 'buildDetector: OnEventStart');
5246
end
53-
54-
function testRunDetection(testCase)
55-
cfg = EventConfig();
56-
s = SensorTag('temp', 'Name', 'Temperature');
57-
s.updateData(1:10, [5 5 12 14 11 13 5 5 5 5]);
58-
t_warn = Threshold('warn', 'Name', 'warn', 'Direction', 'upper');
59-
t_warn.addCondition(struct(), 10);
60-
s.addThreshold(t_warn);
61-
cfg.addTag(s);
62-
events = cfg.runDetection();
63-
testCase.verifyGreaterThanOrEqual(numel(events), 1, 'runDetection: found events');
64-
testCase.verifyEqual(events(1).SensorName, 'Temperature', 'runDetection: sensor name');
65-
end
66-
67-
function testEscalateSeverity(testCase)
68-
cfg = EventConfig();
69-
s = SensorTag('temp', 'Name', 'Temperature');
70-
s.updateData(1:10, [5 5 86 96 88 87 5 5 5 5]);
71-
t_warn = Threshold('warn', 'Name', 'warn', 'Direction', 'upper');
72-
t_warn.addCondition(struct(), 85);
73-
s.addThreshold(t_warn);
74-
t_critical = Threshold('critical', 'Name', 'critical', 'Direction', 'upper');
75-
t_critical.addCondition(struct(), 95);
76-
s.addThreshold(t_critical);
77-
cfg.addTag(s);
78-
events = cfg.runDetection();
79-
critEvents = events(arrayfun(@(e) strcmp(e.ThresholdLabel, 'critical'), events));
80-
testCase.verifyGreaterThanOrEqual(numel(critEvents), 1, 'escalate: critical event exists');
81-
testCase.verifyGreaterThanOrEqual(critEvents(1).PeakValue, 95, 'escalate: peak above critical threshold');
82-
end
83-
84-
function testEscalateDisabled(testCase)
85-
cfg2 = EventConfig();
86-
cfg2.EscalateSeverity = false;
87-
s2 = SensorTag('temp', 'Name', 'Temperature');
88-
s2.updateData(1:10, [5 5 86 96 88 87 5 5 5 5]);
89-
t_warn = Threshold('warn', 'Name', 'warn', 'Direction', 'upper');
90-
t_warn.addCondition(struct(), 85);
91-
s2.addThreshold(t_warn);
92-
t_critical = Threshold('critical', 'Name', 'critical', 'Direction', 'upper');
93-
t_critical.addCondition(struct(), 95);
94-
s2.addThreshold(t_critical);
95-
cfg2.addTag(s2);
96-
events2 = cfg2.runDetection();
97-
warnEvents2 = events2(arrayfun(@(e) strcmp(e.ThresholdLabel, 'warn'), events2));
98-
testCase.verifyGreaterThanOrEqual(numel(warnEvents2), 1, 'escalate disabled: warn event preserved');
99-
end
100-
101-
function testEscalateLowDirection(testCase)
102-
cfg3 = EventConfig();
103-
s3 = SensorTag('pres', 'Name', 'Pressure');
104-
s3.updateData(1:10, [6 6 3.5 1.5 3.8 3.9 6 6 6 6]);
105-
t_low = Threshold('low', 'Name', 'low', 'Direction', 'lower');
106-
t_low.addCondition(struct(), 4);
107-
s3.addThreshold(t_low);
108-
t_crit_low = Threshold('critical_low', 'Name', 'critical low', 'Direction', 'lower');
109-
t_crit_low.addCondition(struct(), 2);
110-
s3.addThreshold(t_crit_low);
111-
cfg3.addTag(s3);
112-
events3 = cfg3.runDetection();
113-
critLow = events3(arrayfun(@(e) strcmp(e.ThresholdLabel, 'critical low'), events3));
114-
testCase.verifyGreaterThanOrEqual(numel(critLow), 1, 'escalate low: critical low event exists');
115-
testCase.verifyLessThanOrEqual(critLow(1).PeakValue, 2, 'escalate low: peak below critical threshold');
116-
end
117-
118-
function testSaveViaEventStore(testCase)
119-
tmpFile = fullfile(tempdir, 'test_cfg_store_save.mat');
120-
if exist(tmpFile, 'file'); delete(tmpFile); end
121-
testCase.addTeardown(@() TestEventConfig.deleteIfExists(tmpFile));
122-
cfg = EventConfig();
123-
cfg.EventFile = tmpFile;
124-
cfg.MaxBackups = 0;
125-
s = SensorTag('temp', 'Name', 'Temperature');
126-
s.updateData(1:10, [5 5 12 14 11 13 5 5 5 5]);
127-
t_warn = Threshold('warn', 'Name', 'warn', 'Direction', 'upper');
128-
t_warn.addCondition(struct(), 10);
129-
s.addThreshold(t_warn);
130-
cfg.setColor('warn', [1 0 0]);
131-
cfg.addTag(s);
132-
events = cfg.runDetection();
133-
testCase.verifyEqual(exist(tmpFile, 'file'), 2, 'save: file exists');
134-
data = load(tmpFile);
135-
testCase.verifyTrue(isfield(data, 'events'), 'save: has events');
136-
testCase.verifyTrue(isfield(data, 'sensorData'), 'save: has sensorData');
137-
testCase.verifyTrue(isfield(data, 'thresholdColors'), 'save: has thresholdColors');
138-
testCase.verifyTrue(isfield(data, 'timestamp'), 'save: has timestamp');
139-
testCase.verifyEqual(numel(data.events), numel(events), 'save: event count matches');
140-
end
141-
end
142-
143-
methods (Static, Access = private)
144-
function deleteIfExists(f)
145-
if exist(f, 'file'); delete(f); end
146-
end
14747
end
14848
end

tests/suite/TestEventDetector.m

Lines changed: 0 additions & 103 deletions
This file was deleted.

0 commit comments

Comments
 (0)