Skip to content

Commit 8f6ce9f

Browse files
authored
Mpep bind server test (#209)
* Started expServer tests * Added more tests; bug fix for older versions * Removed validation functions for old MATLAB versions; added documentation * More coverage, documentation * Draws text to screen during calibration #128 * Fix for #4 * Added structAssign test; rewardId tests * Typo fix in structAssign_test * Finished tests for expServer * #156 * Returned listener delete to cleanup * Stricter tolerance in AlyxPanel_test; bug fix for rounding negative numbers * Update alyx-matlab submodule * Minor changes to documentation * Update CHANGELOG.md * Added tests for tl.bindMpepServer and pnet spy * Fixture path fix * Moved audioDevices default to devices mock * Assertion added #204
1 parent 842cea0 commit 8f6ce9f

File tree

13 files changed

+403
-33
lines changed

13 files changed

+403
-33
lines changed

+hw/Timeline.m

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,13 @@
7171
DaqVendor = 'ni'
7272
% Device ID can be found with daq.getDevices()
7373
DaqIds = 'Dev1'
74-
% rate at which daq aquires data in Hz, see Rate
74+
% Rate at which daq aquires data in Hz, see Rate
7575
DaqSampleRate = 1000
76-
% determines the number of data samples to be processed each time,
76+
% Determines the number of data samples to be processed each time,
7777
% see Timeline.process(), constructor and
7878
% NotifyWhenDataAvailableExceeds
7979
DaqSamplesPerNotify
80-
% array of output classes, defining any signals you desire to be
80+
% Array of output classes, defining any signals you desire to be
8181
% sent from the daq. See Also HW.TLOUTPUT, HW.TLOUTPUTCLOCK
8282
Outputs = hw.TLOutputChrono
8383
% All configured inputs.
@@ -87,25 +87,25 @@
8787
'measurement', 'Voltage',...
8888
'terminalConfig', 'SingleEnded',...
8989
'axesScale', 1) % multiplicative vertical scaling for when live plotting the input
90-
% array of inputs to record while tl is running
90+
% Array of inputs to record while tl is running
9191
UseInputs = {'chrono'}
92-
% currently pauses for at least 2 secs as 'hack' before stopping
92+
% Currently pauses for at least 2 secs as 'hack' before stopping
9393
% main DAQ session to allow
9494
StopDelay = 2
95-
% expected experiment time so data structure is initialised to
95+
% Expected experiment time so data structure is initialised to
9696
% sensible size (in secs)
9797
MaxExpectedDuration = 2*60*60
98-
% default data type for the acquired data array (i.e.
98+
% Default data type for the acquired data array (i.e.
9999
% Data.rawDAQData)
100100
AquiredDataType = 'double'
101101
% If true, timeline is started by default (otherwise can be toggled
102102
% with the t key in expServer)
103103
UseTimeline matlab.lang.OnOffSwitchState = 'off'
104-
% if true the data are plotted as the data are aquired
104+
% If true the data are plotted as the data are aquired
105105
LivePlot matlab.lang.OnOffSwitchState = 'off'
106-
% figure position in normalized units, default is full screen
106+
% Figure position in normalized units, default is full screen
107107
FigureScale = [0 0 1 1]
108-
% if true the data buffer is written to disk as they're aquired NB:
108+
% If true the data buffer is written to disk as they're aquired NB:
109109
% in the future this will happen by default
110110
WriteBufferToDisk matlab.lang.OnOffSwitchState = 'off'
111111
end
@@ -120,17 +120,17 @@
120120
end
121121

122122
properties (Transient, Access = protected)
123-
% holds the listener for 'DataAvailable', see DataAvailable and
123+
% Holds the listener for 'DataAvailable', see DataAvailable and
124124
% Timeline.process()
125125
Listener
126-
% the last timestamp returned from the daq during the DataAvailable
126+
% The last timestamp returned from the daq during the DataAvailable
127127
% event. Used to check sampling continuity, see tl.process()
128128
LastTimestamp
129-
% the expRef string. See tl.start()
129+
% The expRef string. See tl.start()
130130
Ref
131-
% a struct contraining the Alyx token, user and url for ile
132-
% registration. See tl.start()
133-
AlyxInstance
131+
% An Alyx object instance used for file registration. See
132+
% tl.start()
133+
AlyxInstance Alyx
134134
% A structure containing timeline data
135135
Data
136136
% A figure handle for plotting the aquired data as it's processed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Starting after Rigbox 2.2.0, this file contains a curated, chronologically order
2424
- better organization of expServer `f32a0fe` 2019-10-02
2525
- bug fix for rounding negative numbers in AlyxPanel `31641f1` 2019-10-17
2626
- stricter and more accurate tolerance in AlyxPanel_test `31641f1` 2019-10-17
27+
- added tests for dat.mpepMessageParse and tl.bindMpepServer `bd15b95` 2019-10-21
2728
- HOTFIX to error when plotting supressed in Window calibrate `7d6b601` 2019-11-15
2829

2930
## [2.3.0](https://github.com/cortex-lab/Rigbox/releases/tag/v2.3.0)

cb-tools/burgbox/+io/WSCommunicator.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
properties (Transient)
2626
WebSocket
27-
EventMode = false
27+
EventMode = 'off'
2828
end
2929

3030
properties (Access = private, Transient)

cortexlab/+tl/bindMpepServer.m

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,10 @@ function processMpep(listener, msg)
8686
tls.AlyxInstance = ai;
8787
case 'expstart'
8888
% create a file path & experiment ref based on experiment info
89-
try
90-
% start Timeline
89+
try % start Timeline
90+
assert(~tlObj.IsRunning, ...
91+
'Rigbox:tl:bindMpepServer:timelineAlreadyRunning', ...
92+
'Timeline already started')
9193
tlObj.start(info.expRef, tls.AlyxInstance);
9294
% re-record the UDP event in Timeline since it wasn't started
9395
% when we tried earlier. Treat it as having arrived at time zero.
@@ -173,4 +175,3 @@ function log(varargin)
173175
end
174176

175177
end
176-
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
classdef (SharedTestFixtures={ % add 'fixtures' folder as test fixture
2+
matlab.unittest.fixtures.PathFixture(['..' filesep 'fixtures']),...
3+
matlab.unittest.fixtures.PathFixture(['..' filesep 'fixtures' filesep 'util'])})...
4+
bindMpepServer_test < matlab.unittest.TestCase & matlab.mock.TestCase
5+
6+
properties (SetAccess = protected)
7+
% Timeline mock object
8+
Timeline
9+
% Timeline behaviour object
10+
Behaviour
11+
% An experiment reference for the test
12+
Ref
13+
% Default ports used by bindMpepServer
14+
Ports = [9999, 1001]
15+
end
16+
17+
methods (TestClassSetup)
18+
function setTestFlag(testCase)
19+
% SETTESTFLAG Set test flag
20+
% Sets global INTEST flag to true and adds teardown. Also creates a
21+
% dummy expRef for tests.
22+
%
23+
% TODO Make into shared fixture
24+
25+
% Set INTEST flag
26+
assert(endsWith(which('dat.paths'),...
27+
fullfile('tests', 'fixtures', '+dat', 'paths.m')));
28+
setTestFlag(true)
29+
testCase.addTeardown(@setTestFlag, false)
30+
% Set test expRef
31+
testCase.Ref = dat.constructExpRef('test', now, 1);
32+
end
33+
end
34+
35+
methods (TestMethodSetup)
36+
function setMockRig(testCase)
37+
% SETMOCKRIG Inject mock rig with shadowed hw.devices
38+
% 1. Create mock timeline
39+
% 2. Set the mock rig object to be returned on calls to hw.devices
40+
% 3. Add teardowns
41+
%
42+
% See also mockRig, KbQueueCheck
43+
44+
% Create fresh Timeline mock
45+
[testCase.Timeline, testCase.Behaviour] = createMock(testCase, ...
46+
'AddedProperties', properties(hw.Timeline)', ...
47+
'AddedMethods', methods(hw.Timeline)');
48+
49+
% Inject our mock via calls to hw.devices
50+
rig.timeline = testCase.Timeline;
51+
hw.devices('testRig', false, rig);
52+
53+
% Clear mock histories just to be safe
54+
testCase.addTeardown(@testCase.clearMockHistory, testCase.Timeline);
55+
testCase.addTeardown(@clear, 'KbQueueCheck', 'pnet', 'devices')
56+
end
57+
end
58+
59+
methods (Test)
60+
function test_bindMpepListener(testCase)
61+
% Test binding of sockets and returning of tls object
62+
% NB Actually calls bindMpepServer
63+
port = randi(10000);
64+
[T, tls] = evalc(['tl.bindMpepServer(', num2str(port), ')']);
65+
% Check log
66+
testCase.verifyMatches(T, 'Bound UDP sockets', ...
67+
'failed to log socket bind')
68+
% Check returned fields
69+
expected = {'close'; 'process'; 'listen'; 'AlyxInstance'; 'tlObj'};
70+
testCase.verifyEqual(fieldnames(tls), expected, ...
71+
'Unexpected structure returned')
72+
% Check funciton handles
73+
actual = structfun(@(f)isa(f, 'function_handle'), tls);
74+
testCase.verifyEqual(actual, [true(3,1); false(2,1)])
75+
% Check Alyx instance and Timeline objects set
76+
testCase.verifyTrue(isa(tls.AlyxInstance, 'Alyx'), ...
77+
'Failed to create Alyx instance')
78+
testCase.verifyTrue(isequal(tls.tlObj, testCase.Timeline), ...
79+
'Failed to set Timeline')
80+
% Check socket opened on correct port
81+
history = pnet('gethistory');
82+
testCase.verifyEqual(history{1}, {'udpsocket', port}, ...
83+
'Failed to open socket on specified port')
84+
end
85+
86+
function test_close(testCase)
87+
% Test the close callback
88+
tls = tl.bindMpepServer; %#ok<NASGU> % Return tls object
89+
ports = testCase.Ports; % Default ports opened
90+
arrayfun(@(s) pnet('setoutput', s, 'close', []), ports); % Set output
91+
T = evalc('tls.close()'); % Callback
92+
% Check log
93+
testCase.verifyMatches(T, 'Unbinding', 'failed to log close')
94+
% Check close called on each socket
95+
history = pnet('gethistory'); % Get pnet call history
96+
correct = cellfun(@(a) strcmp(a{2}, 'close'), history(end-1:end));
97+
testCase.verifyTrue(all(correct), 'Failed to close sockets')
98+
end
99+
100+
function test_process(testCase)
101+
% Test process callback
102+
import matlab.unittest.constraints.IsOfClass
103+
import matlab.mock.constraints.Occurred
104+
[subject, series, seq] = dat.parseExpRef(testCase.Ref);
105+
106+
tls = tl.bindMpepServer; % Return tls object
107+
ports = testCase.Ports; % Default ports opened
108+
arrayfun(@(s) pnet('setoutput', s, 'readpacket', 1000), ports); % Set output
109+
pnet('setoutput', ports(2), 'gethost', {randi(99,1,4), 88}); % Set output
110+
111+
% Set messages
112+
% Stringify Alyx instance
113+
ai = Alyx.parseAlyxInstance(testCase.Ref, Alyx('user',''));
114+
% Function for constructing message strings
115+
str = @(cmd) sprintf('%s %s %s %d %s', cmd, subject, ...
116+
datestr(series, 'yyyymmdd'), seq, iff(strcmp(cmd,'alyx'),ai,''));
117+
% Set behaviour for IsRunning method to pass IsRunning assert
118+
testCase.assignOutputsWhen(get(testCase.Behaviour.IsRunning), false)
119+
% Commands
120+
cmd = {'alyx', 'expstart', 'expend', 'expinterupt'};
121+
% Set output for 'read'
122+
pnet('setoutput', ports(2), 'read', sequence(mapToCell(str, cmd)));
123+
% Trigger reads
124+
arrayfun(@(~) tls.process(), 1:length(cmd))
125+
126+
% Test Timeline interactions
127+
timeline = testCase.Behaviour;
128+
testCase.verifyThat([...
129+
get(timeline.IsRunning), ...
130+
timeline.start(testCase.Ref, IsOfClass(?Alyx)), ... % expstart
131+
withAnyInputs(timeline.record), ... % "
132+
withAnyInputs(timeline.stop), ... % expstop
133+
withAnyInputs(timeline.stop)], ... % expinterupt
134+
Occurred('RespectingOrder', false))
135+
136+
% Retrieve mock history for Timeline
137+
history = testCase.getMockHistory(testCase.Timeline);
138+
% Find inputs to start method
139+
f = @(method) @(a) strcmp(a.Name, method);
140+
actual = fun.filter(f('start'), history).Inputs{end};
141+
% Check AlyxInstance updated with the one we passed in above
142+
testCase.verifyEqual(actual.User, 'user', 'Failed to update AlyxInstance')
143+
144+
% Get pnet history
145+
history = pnet('gethistory');
146+
% Calls to write should equal the number of messages read
147+
writeCalls = cellfun(@(C) strcmp(C{2}, 'write'), history);
148+
testCase.verifyEqual(sum(writeCalls), length(cmd), 'Failed echo messages')
149+
150+
% Test process fails
151+
testCase.throwExceptionWhen(withAnyInputs(timeline.start), ...
152+
MException('Timeline:error', 'Error during experiment.'));
153+
% Clear pnet history
154+
pnet('clearhistory');
155+
% Set output for 'read'
156+
pnet('setoutput', ports(2), 'read', str('expstart'));
157+
% Trigger pnet read; use evalc to supress output
158+
evalc('tls.process()');
159+
% Set Timeline as already running and check for error
160+
testCase.assignOutputsWhen(get(testCase.Behaviour.IsRunning), true)
161+
evalc('tls.process()');
162+
163+
% Check message not echoed after error
164+
history = pnet('gethistory');
165+
% Calls to write should equal the number of messages read
166+
writeCalls = cellfun(@(C) strcmp(C{2}, 'write'), history);
167+
testCase.verifyFalse(any(writeCalls), 'Unexpected message echo')
168+
end
169+
170+
function test_listen(testCase)
171+
% TODO Add test for listen function of bindMpepServer
172+
end
173+
end
174+
175+
end
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
% mpepMessageParse test
2+
% preconditions
3+
subject = 'M20140123_CB';
4+
series = num2str(randi(10000));
5+
seq = randi(100);
6+
ref = dat.constructExpRef(subject, series, seq);
7+
8+
block = '5';
9+
stim = '1';
10+
duration = '36000';
11+
msg = @(cmd) sprintf('%s %s %s %d %s %s %s', ...
12+
cmd, subject, series, seq, block, stim, duration);
13+
14+
%% Test 1: infosave
15+
cmd = sprintf('infosave %s_%s_%d', subject, series, seq);
16+
info = dat.mpepMessageParse(cmd);
17+
18+
assert(strcmp(info.instruction, 'infosave'))
19+
assert(strcmp(info.subject, subject))
20+
assert(strcmp(info.series, series))
21+
assert(strcmp(info.exp, num2str(seq)))
22+
assert(strcmp(info.expRef, ref))
23+
24+
%% Test 2: hello
25+
info = dat.mpepMessageParse(msg('hello'));
26+
assert(strcmp(info.instruction, 'hello'))
27+
assert(isempty(info.expRef))
28+
29+
%% Test 3: full mpep instruction
30+
info = dat.mpepMessageParse(msg('expstart'));
31+
32+
assert(strcmp(info.instruction, 'expstart'))
33+
assert(strcmp(info.subject, subject))
34+
assert(strcmp(info.series, series))
35+
assert(strcmp(info.exp, num2str(seq)))
36+
assert(strcmp(info.expRef, ref))
37+
assert(strcmp(info.block, block))
38+
assert(strcmp(info.stim, stim))
39+
assert(strcmp(info.duration, duration))
40+
41+
%% Test 4: series to datestr
42+
series = datestr(now, 'yyyymmdd');
43+
cmd = sprintf('expinterrupt %s %s %d', subject, series, seq);
44+
info = dat.mpepMessageParse(cmd);
45+
46+
assert(strcmp(info.series, datestr(now, 'yyyy-mm-dd')))
47+
assert(strcmp(info.expRef, dat.constructExpRef(subject, now, seq)))
48+
49+
%% Test 5: empty sequence
50+
cmd = sprintf('expend %s %s', subject, series);
51+
ex.message = '';
52+
try
53+
dat.mpepMessageParse(cmd);
54+
catch ex
55+
end
56+
assert(contains(ex.message, 'not valid'))
57+
58+
%% Test 6: Alyx serialization
59+
% Test compatibility with parseAlyxInstance
60+
61+
token = char(randsample([48:57 65:89 97:122], 36, true)); % Generate token
62+
ai = Alyx.parseAlyxInstance(ref, Alyx('user', token)); % Stringify instance
63+
cmd = sprintf('alyx %s %s %d %s', subject, series, seq, ai); % Make message
64+
65+
info = dat.mpepMessageParse(cmd);
66+
67+
assert(strcmp(info.instruction, 'alyx'))
68+
assert(strcmp(info.subject, subject))
69+
assert(strcmp(info.series, series))
70+
assert(strcmp(info.exp, num2str(seq)))
71+

tests/expServer_test.m

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ function setupMock(testCase)
6666
% See also MOCKRIG, GIT.UPDATE
6767

6868
% Set INTEST flag to true
69-
testCase.setTestFlag(true)
70-
testCase.addTeardown(@testCase.setTestFlag, false)
69+
setTestFlag(true)
70+
testCase.addTeardown(@setTestFlag, false)
7171

7272
% Make sure git update not triggered
7373
root = getOr(dat.paths, 'rigbox'); % Rigbox root directory
@@ -923,12 +923,4 @@ function test_run_fail(testCase)
923923
end
924924
end
925925

926-
methods (Static)
927-
function setTestFlag(TF)
928-
% SETTESTFLAG Set global INTEST flag
929-
% Allows setting of test flag via callback function
930-
global INTEST
931-
INTEST = TF;
932-
end
933-
end
934926
end

tests/fixtures/+hw/devices.m

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@
2222
if isempty(mockRig)
2323
mockRig = struct(...
2424
'name', name, ...
25-
'clock', hw.ptb.Clock);
25+
'clock', hw.ptb.Clock, ...
26+
'audioDevices', struct(...
27+
'DeviceName', 'default',...
28+
'DeviceIndex', -1,...
29+
'DefaultSampleRate', 44100,...
30+
'NrOutputChannels', 2));
2631
end
2732

2833
% Set mock

0 commit comments

Comments
 (0)