diff --git a/+hw/+ptb/Window.m b/+hw/+ptb/Window.m index 9e3b1de6..24753d7c 100644 --- a/+hw/+ptb/Window.m +++ b/+hw/+ptb/Window.m @@ -547,14 +547,17 @@ function fillRect(obj, colour, rect) Screen('PreloadTextures', obj.PtbHandle, tex); end - function [nx, ny] = drawText(obj, text, x, y, colour, vSpacing, wrapAt) + function [nx, ny] = drawText(obj, text, varargin) % DRAWTEXT Draw some text to the screen + % [NX, NY] = DRAWTEXT(OBJ, TEXT, X, Y, COLOUR, TEXTSIZE, VSPACING, WRAPAT) % The outputs may be used as the new start positions to draw further % text to the screen. % % Inputs: % text (char) - The text to be written to screen. May contain % newline characters '\n'. + % + % Inputs (Optional): % x (numerical|char) - The top-left x coordinate of the text in % px. If empty the left-most area part of the screen is used. % May also be one of the following string options: 'center', @@ -565,9 +568,12 @@ function fillRect(obj, colour, rect) % color - The CLUT index for the text (scalar, RGB or RGBA vector) % If color is left out, the current text color from previous % text drawing commands is used. - % vSpacing - The spacing between the lines in px. Defaults to 1. - % wrapAt (char) - automatically break text longer than this string - % into newline separated strings of roughly the same length + % textSize (numerical) - The size of the text in px. Defaults to + % PTB current setting (usually the system default). + % vSpacing (numerical) - The spacing between the lines in px. + % Defaults to 1. + % wrapAt (numerical) - Automatically break text longer than this + % number of chars. % % Outputs: % nx - The approximate x-coordinate of the 'cursor position' in px @@ -580,23 +586,32 @@ function fillRect(obj, colour, rect) % obj.flip() % % See also DRAWFORMATTEDTEXT, DRAWTEXTURE, WRAPSTRING - if nargin < 7 - wrapAt = []; - end - if nargin < 6 - vSpacing = []; - end - if nargin < 5 - colour = []; - end - if nargin < 4 - y = []; - end - if nargin < 3 - x = []; - end - [nx, ny] = DrawFormattedText(obj.PtbHandle, text, x, y, colour, wrapAt, [], [],... - vSpacing); + + p = inputParser; + p.addRequired('text', @ischar) + p.addOptional('x', [], @(x) ischar(x) || isnumeric(x)) + p.addOptional('y', [], @(x) ischar(x) || isnumeric(x)) + p.addOptional('colour', [], @isnumeric) + p.addOptional('textSize', ... + Screen('TextSize', obj.PtbHandle), ... + @(x) isnumeric(x) || isscalar(x)) + p.addOptional('vSpacing', [], @(x) isnumeric(x) || isscalar(x)) + p.addOptional('wrapAt', [], @isnumeric) + p.parse(text, varargin{:}) + p = p.Results; + + % Set text size or restore to default on exit + oldTextSize = Screen('TextSize', obj.PtbHandle, p.textSize); + mess = onCleanup(@() Screen('TextSize', obj.PtbHandle, oldTextSize)); + + % Draw the text to screen + [nx, ny] = DrawFormattedText(... + obj.PtbHandle, ... + p.text, ... + p.x, p.y, ... + p.colour, ... + p.wrapAt, [], [],... + p.vSpacing); % Screen('DrawText', obj.PtbHandle, text, x, y, colour, [], real(yPosIsBaseline)); end diff --git a/+hw/debugWindow.m b/+hw/debugWindow.m index a6908249..e32d7bb1 100644 --- a/+hw/debugWindow.m +++ b/+hw/debugWindow.m @@ -3,38 +3,54 @@ % Uses Psychtoolbox to open and control an on-screen window that is % useful for debugging. Also returns a dummy viewing model. % +% Input (Optional): +% open (logical): Immediately open the PTB window after instantiating. +% Default true. +% +% Outputs: +% window (hw.ptb.Window): A Window object configured for a 800x600 +% stimulus screen, with PTB warning and sync tests supressed. +% viewingModel (hw.BasicScreenViewingModel): A ViewingModel object +% configured for a subject positioned squarely in front of the +% window, 7cm away. +% +% Example: +% % Open a window for testing and set to middle grey +% win = hw.debugWindow; +% win.BackgroundColour = 255/2; +% win.flip(); +% +% Example: +% % Save the debug window and model into a test rig's hardware file +% [stimWindow, stimViewingModel] = hw.debugWindow(false); +% hwPath = getOr(dat.paths('testRig', 'rigConfig')); +% save(fullfile(hwPath, 'hardware.mat'), 'stim*', '-append') +% +% See also hw.ptb.Window, PsychDebugWindowConfiguration +% % Part of Rigbox % 2012-10 CB created -if nargin < 1 - open = true; -end +% Default is to immediately open the window +if nargin < 1, open = true; end +% Set some reasonable parameters for the window (800x600 resolution) pixelWidth = 800; pixelHeight= 600; -viewWidth = 0.2; +viewWidth = 0.2; % Assume window is 200mm wide on the screen viewHeight = viewWidth*pixelHeight/pixelWidth; -% oldSyncTests = Screen('Preference', 'SkipSyncTests', 2); -% oldVerbosity = Screen('Preference', 'Verbosity', 0); -% cleanup1 = onCleanup(@() Screen('Preference', 'SkipSyncTests', oldSyncTests)); -% cleanup2 = onCleanup(@() Screen('Preference', 'Verbosity', oldVerbosity)); - +% Create the window object window = hw.ptb.Window; -window.PtbVerbosity = 0; -window.PtbSyncTests = 2; +window.PtbVerbosity = 0; % Supress warnings +window.PtbSyncTests = 2; % Supress sync tests (always fail when windowed) window.OpenBounds = SetRect(50, 50, pixelWidth+50, pixelHeight+50); +if open, window.open(); end % Open now if open == true -if open - window.open(); -end - - +% Create the viewing model viewingModel = hw.BasicScreenViewingModel; viewingModel.ScreenWidthPixels = pixelWidth; viewingModel.ScreenWidthMetres = viewWidth; viewingModel.SubjectPos = [.5*viewWidth .5*viewHeight .07]; -end - diff --git a/+srv/expServer.m b/+srv/expServer.m index 0af48923..030a203b 100644 --- a/+srv/expServer.m +++ b/+srv/expServer.m @@ -26,6 +26,8 @@ function expServer(varargin) % database. This parameter is only used with the 'expRef' parameter. % % Key bindings: +% q - Quit expServer. +% h - View list of key bindings. % t - Toggle Timeline on and off. The default state is defined in the % hardware file but may be overridden as the first input argument. % w - Toggle reward on and off. This switches the output of the first @@ -34,9 +36,12 @@ function expServer(varargin) % file. % space - Deliver default reward, specified by the DefaultCommand % property in the hardware file. -% m - Perform water calibration. +% m - Perform reward calibration for the current reward controller. +% v - View the current water calibration for current reward controller. % b - Toggle the background colour between the default and white. % g - Perform gamma correction +% 1 - Select first reward controller. +% 2 - Select second reward controller. % % Example 1: Run in listener mode with Timeline disabled % srv.expServer('UseTimelineOverride', false) @@ -53,13 +58,20 @@ function expServer(varargin) %% Fixed Parameters global AGL GL GLU %#ok -quitKey = KbName('q'); -rewardToggleKey = KbName('w'); -rewardPulseKey = KbName('space'); -rewardCalibrationKey = KbName('m'); -gammaCalibrationKey = KbName('g'); -timelineToggleKey = KbName('t'); -toggleBackground = KbName('b'); +key = struct(... + 'quit', KbName('q'), ... + 'help', KbName('h'), ... + 'rewardToggle', KbName('w'), ... + 'rewardPulse', KbName('space'), ... + 'rewardCalibration', KbName('m'), ... + 'viewCalibration', KbName('v'), ... + 'gammaCalibration', KbName('g'), ... + 'timelineToggle', KbName('t'), ... + 'toggleBackground', KbName('b'), ... + 'selectFirst', KbName('1'), ... + 'selectSecond', KbName('2')); + +rewardId = 1; % Function for constructing a full ID for warnings and errors fullID = @(id) strjoin([{'Rigbox:srv:expServer'}, ensureCell(id)],':'); @@ -151,11 +163,8 @@ function expServer(varargin) rig.stimWindow.BackgroundColour = bgColour; rig.stimWindow.open(); -fprintf('\n quit, toggle reward, toggle timeline\n'); -fprintf(['<%s> reward pulse, <%s> perform reward calibration\n' ... - '<%s> perform gamma calibration\n'], KbName(rewardPulseKey), ... - KbName(rewardCalibrationKey), KbName(gammaCalibrationKey)); -log('Started presentation server on port %i', communicator.DefaultListenPort); +fprintf('\n<%s> quit, <%s> help\n', key.quit, key.help); +log('Started presentation server on port %i', communicator.DefaultListenPort) if nargin < 1 || isempty(p.useTimelineOverride) % toggle use of timeline according to rig default setting @@ -170,6 +179,7 @@ function expServer(varargin) 'Experiment ref ''%s'' does not exist', p.expRef) runExp(p.expRef, p.preDelay, p.postDelay, p.alyx); end +calibrationDisplayed = false; %% Main loop for service running = ~singleShot; @@ -183,19 +193,24 @@ function expServer(varargin) [~, firstPress] = KbQueueCheck; % check if the quit key was pressed - if firstPress(quitKey) > 0 - log('Quitting (quit key pressed)'); + if firstPress(key.quit) > 0 + log('Quitting (quit key pressed)') running = false; end + % check if help key was pressed + if firstPress(key.help) > 0 + showHelp(); + end + % check if the quit key was pressed - if firstPress(timelineToggleKey) > 0 + if firstPress(key.timelineToggle) > 0 toggleTimeline(); end % check for reward toggle - if firstPress(rewardToggleKey) > 0 - log('Toggling reward valve'); + if firstPress(key.rewardToggle) > 0 + log('Toggling reward valve') curr = rig.daqController.Value(rewardId); sig = rig.daqController.SignalGenerators(rewardId); if curr == sig.OpenValue @@ -206,36 +221,42 @@ function expServer(varargin) end % check for reward pulse - if firstPress(rewardPulseKey) > 0 + if firstPress(key.rewardPulse) > 0 log('Delivering default reward'); def = [rig.daqController.SignalGenerators(rewardId).DefaultCommand]; rig.daqController.command(def); end % check for reward calibration - if firstPress(rewardCalibrationKey) > 0 - log('Performing a reward delivery calibration'); + if firstPress(key.rewardCalibration) > 0 + log('Performing a reward delivery calibration') calibrateWaterDelivery(); end - % check for gamma calibration - if firstPress(gammaCalibrationKey) > 0 - log('Performing a gamma calibration'); - calibrateGamma(); + % check for view calibration + if firstPress(key.viewCalibration) > 0 + calibrationDisplayed = ~calibrationDisplayed; + iff(calibrationDisplayed, @() viewWaterCalibration, @() rig.stimWindow.flip) end - if firstPress(toggleBackground) > 0 - log('Changing background to white'); - whiteScreen(); + % check for gamma calibration + if firstPress(key.gammaCalibration) > 0 + log('Performing a gamma calibration') + calibrateGamma(); end - if firstPress(KbName('1')) > 0 - rewardId = 1; - end - if firstPress(KbName('2')) > 0 - rewardId = 2; + if firstPress(key.toggleBackground) > 0 + log('Changing background to white') + % WHITESCREEN Changes screen background to white + rig.stimWindow.BackgroundColour = rig.stimWindow.White; + rig.stimWindow.flip(); + rig.stimWindow.BackgroundColour = bgColour; end + % Toggle reward ID + if firstPress(key.selectFirst) > 0, rewardId = 1; end + if firstPress(key.selectSecond) > 0, rewardId = 2; end + % pause a little while to allow other OS processing pause(5e-3); end @@ -362,7 +383,7 @@ function handleMessage(id, data, host) fid = fopen(hwInfo, 'w'); fprintf(fid, '%s', obj2json(rig)); fclose(fid); - if ~strcmp(dat.parseExpRef(expRef), 'default') && ~isempty(getOr(dat.paths, 'databaseURL')) + if ~strcmp(dat.parseExpRef(expRef), 'default') && ~isempty(getOr(rig.paths, 'databaseURL')) try alyx.registerFile(hwInfo); catch ex @@ -381,27 +402,107 @@ function handleMessage(id, data, host) end function calibrateWaterDelivery() + % CALIBRATEWATERDELIVERY Performs measured reward deliveries for calibration + % Runs a water calibration for the currently selected reward ID, + % saves to the hardware file and displays a plot of the calibration. + % + % See also viewWaterCalibration, hw.calibrate + assert(isfield(rig, 'scale'), fullID('noScaleObject'), ... + 'reward calibrations require a scale object in the hardware file') daqController = rig.daqController; chan = daqController.ChannelNames(rewardId); - %perform measured deliveries + + % Perform measured deliveries rig.scale.init(); calibration = hw.calibrate(chan, daqController, rig.scale, 20e-3, 150e-3); rig.scale.cleanup(); - %different delivery durations appear in each column, repeats in each row - %from the data, make a measuredDelivery structure - ul = [calibration.volumeMicroLitres]; - log('Delivered volumes ranged from %.1ful to %.1ful', min(ul), max(ul)); - rigHwFile = fullfile(pick(dat.paths, 'rigConfig'), 'hardware.mat'); + % Different delivery durations appear in each column, repeats in each + % row from the data, make a measuredDelivery structure + ul = [calibration.volumeMicroLitres]; + log('Delivered volumes ranged from %.1ful to %.1ful', min(ul), max(ul)) + % Save the calibration into the rig hardware file + rigHwFile = fullfile(rig.paths.rigConfig, 'hardware.mat'); save(rigHwFile, 'daqController', '-append'); + + % Show a plot of the new calibration + viewWaterCalibration() + calibrationDisplayed = true; end - function whiteScreen() - % WHITESCREEN Changes screen background to white - rig.stimWindow.BackgroundColour = rig.stimWindow.White; + function viewWaterCalibration() + % VIEWWATERCALIBRATION Displays a plot of the most recent reward calibration + % Prints a plot of valve open time vs reward volume from the last + % saved calibration of the currently selected reward controller. + % + % See also calibrateWaterDelivery + + % Check if calibration is available for current reward Signal Generator + sigGens = rig.daqController.SignalGenerators; + noCalibration = ... + rewardId > length(sigGens) || ... + ~isprop(sigGens(rewardId), 'Calibrations') || ... + isempty(sigGens(rewardId).Calibrations); + + if noCalibration + % If empty, log missing calibration and return. + log('No reward calibration found for reward controller') + return + end + + % Fetch the most recent calibration + log('Displaying calibration') + [newestDate, I] = max([sigGens(rewardId).Calibrations.dateTime]); + c = sigGens(rewardId).Calibrations(I); + + % Create a figure and plot the data + fig = figure('Color', 'w', 'Visible', 'off'); + plot([c.measuredDeliveries.durationSecs], ... + [c.measuredDeliveries.volumeMicroLitres], 'x-'); + + % Set some labels, etc. + xlabel('Duration (sec)'); + ylabel('Volume (\muL)'); + set(gca, 'FontSize', 16, 'YLim', [0 5]) + title(datestr(newestDate)) + + % Draw the plot to the screen + cdata = getOr(getframe(fig), 'cdata'); % Get the cdata from the plot + imageDisplay = rig.stimWindow.makeTexture(cdata); % Load texture + rig.stimWindow.BackgroundColour = rig.stimWindow.White; % Match background + rig.stimWindow.drawTexture(imageDisplay); % Draw plot texture + rig.stimWindow.flip; % Show on screen + rig.stimWindow.BackgroundColour = bgColour; % Restore background colour + close(fig) % Close our invisible figure + end + + function showHelp() + % VIEWHELP Print the hotkeys for expServer + % Displays the list of keys and their functions to the log and to the + % Stimulus Window. + + % Convert key codes to key names + keyNames = mapToCell(@KbName, struct2cell(key)); + msg = sprintf(['Displaying help\n\r',... + '<%s> quit \n', ... + '<%s> help \n', ... + '<%s> toggle reward \n', ... + '<%s> reward pulse \n', ... + '<%s> perform reward calibration \n', ... + '<%s> view reward calibration \n', ... + '<%s> perform gamma calibration \n', ... + '<%s> toggle timeline \n', ... + '<%s> toggle white screen \n', ... + '<%s> select 1st reward controller \n', ... + '<%s> select 2nd reward controller'], keyNames{:}); + + % Draw white text to centre of screen at 40 chars per line, 1px + % spacing, 20px text size + w = rig.stimWindow.White; % Set a white background + rig.stimWindow.drawText(msg, 'centerblock', 'center', w, 20, 1, 40); rig.stimWindow.flip(); - rig.stimWindow.BackgroundColour = bgColour; + log(msg) % Display in command window too end function calibrateGamma() @@ -445,7 +546,7 @@ function saveGamma(cal) % SAVEGAMMA Save calibration struct to saved stimWindow object % Loads saved stimWindow object from this rig's hardware file, updates % the Calibration property with input, then saves. - rigHwFile = fullfile(pick(dat.paths, 'rigConfig'), 'hardware.mat'); + rigHwFile = fullfile(rig.paths.rigConfig, 'hardware.mat'); stimWindow = load(rigHwFile,'stimWindow'); stimWindow = stimWindow.stimWindow; stimWindow.Calibration = cal; diff --git a/tests/checkCoverage.m b/tests/checkCoverage.m index a14eeab9..73dc5650 100644 --- a/tests/checkCoverage.m +++ b/tests/checkCoverage.m @@ -21,6 +21,8 @@ % Try to divine test function location funLoc = fileparts(which(erase(testFile,'_test'))); end +assert(~isempty(funLoc), ... + 'Failed to find file/package under test, please provide the location') import matlab.unittest.TestRunner import matlab.unittest.plugins.CodeCoveragePlugin runner = TestRunner.withTextOutput; diff --git a/tests/expServer_test.m b/tests/expServer_test.m index 506b3223..ed56171b 100644 --- a/tests/expServer_test.m +++ b/tests/expServer_test.m @@ -37,11 +37,6 @@ function setupFolder(testCase) assert(mkdir(fullfile(mainRepo, 'test')), ... 'Failed to create subject folder') - % Save a custom path disabling Alyx - paths.databaseURL = []; - configDir = getOr(dat.paths, 'rigConfig'); - save(fullfile(configDir, 'paths.mat'), 'paths') - % Alyx queue location qDir = getOr(dat.paths, 'localAlyxQueue'); assert(mkdir(qDir), 'Failed to create alyx queue') @@ -75,6 +70,7 @@ function setMockRig(testCase) 'AddedMethods', methods(exp.Experiment)'); % Inject our mocks via calls to hw.devices + testCase.Rig.paths = rmfield(dat.paths, 'databaseURL'); % Add paths to object hw.devices('testRig', false, testCase.Rig); % Set some default behaviours for some of the objects @@ -95,7 +91,8 @@ function setMockRig(testCase) % Clear mock histories just to be safe clearHistory = @(mock) testCase.clearMockHistory(mock); - structfun(@(mock) testCase.addTeardown(clearHistory, mock), testCase.Rig); + structfun(@(mock) testCase.addTeardown(clearHistory, mock), ... + rmfield(testCase.Rig, 'paths')); % All Rig fields mocks except 'paths' testCase.addTeardown(@clear, ... 'KbQueueCheck', 'configureDummyExperiment', 'devices') end @@ -308,7 +305,7 @@ function test_reward_switch(testCase) 'ClosedValue', rand); generators = [s() s()]; - % Assign output for 'ClosedValue' property + % Assign output for 'SignalGenerators' property testCase.assignOutputsWhen(... get(testCase.RigBehaviours.daqController.SignalGenerators), generators) @@ -456,7 +453,7 @@ function test_alyx(testCase) import matlab.mock.actions.Invoke %%% Test warning free %%% - testCase.assertEmpty(getOr(dat.paths, 'databaseURL'), ... + testCase.assertEmpty(getOr(testCase.Rig.paths, 'databaseURL'), ... 'Expected databaseURL field to be unset for this test') testCase.verifyWarningFree(@srv.expServer) @@ -471,15 +468,11 @@ function test_alyx(testCase) % - 'Alyx:registerFile:UnableToValidate' % - 'Alyx:flushQueue:NotConnected' - % Set custom paths. First add teardown to restore behaviour, then - % delete paths file - customPath = fullfile(getOr(dat.paths, 'rigConfig'), 'paths.mat'); - paths.databaseURL = []; - testCase.addTeardown(@superSave, customPath, struct('paths', paths)) - % Remove custom paths - delete(customPath) + % Add dataseURL back to paths testCase.assertNotEmpty(getOr(dat.paths, 'databaseURL'), ... 'Expected databaseURL field to be unset for this test') + testCase.Rig.paths = dat.paths; % Add paths to object + hw.devices('testRig', false, testCase.Rig); % Inject our our mock experiment via function call in srv.prepareExp exp.configureDummyExperiment([], [], testCase.Experiment); @@ -946,6 +939,95 @@ function test_single_shot(testCase) experiment.run(inputs.expRef)], ... Occurred('RespectingOrder', true)) end + + function test_viewHelp(testCase) + % Test behaviour when help key pressed. Help text should be printed + % to the command window and to the screen. + import matlab.mock.constraints.Occurred + % Simulate pressing help key then quit + KbQueueCheck(-1, sequence({'h', 'q'})); + + T = evalc('srv.expServer'); + testCase.verifyMatches(T, 'Displaying help', ... + 'Failed to print help to the command window') + % Retrieve mock history for the Window + behaviour = testCase.RigBehaviours.stimWindow; + testCase.verifyThat([... + get(behaviour.White), ... + withAnyInputs(behaviour.drawText),... + withAnyInputs(behaviour.flip)], ... + Occurred('RespectingOrder', true)) + end + + function test_viewCalibration(testCase) + % Test behaviour when view calibration key pressed. The plot should + % be printed to the screen if a calibration is available for the + % currently selected controller. NB: Doesn't check whether the most + % recent calibration is used. + import matlab.mock.constraints.Occurred + + % Set some calibration data + n = 5; % Number of simulated deliveries + durationSecs = linspace(20e-3, 150e-3, n); + volumeMicroLitres = linspace(0.5, 3.5, n); + % Create some generators to test function with. Unlike those returned by + % mockRig, these will be subclasses of the hw.RewardValveControl class and + % therefore we can keep them in a heterogeneous array. We must use objects + % here as expServer tests for particular properties. + generators = hw.RewardValveControl.empty(0,2); + calibrations = struct('dateTime', [], 'measuredDeliveries', []); + for i = 1:2 + generators(i) = createMock(testCase, ?hw.RewardValveControl); + for j = 1:2 + % Two calibrations each; one yesterday, one the day before + calibrations(i).dateTime = now-i; + % Create a 1xn struct of measured deliveries + mkStruct = @(a,b)struct('durationSecs',a,'volumeMicroLitres',b); + calibrations(i).measuredDeliveries = arrayfun(mkStruct, ... + durationSecs, volumeMicroLitres + rand); % Change volumes a bit + end + generators(i).Calibrations = calibrations; % Assign our calibrations + end + % Assign generators to daqController mock + testCase.assignOutputsWhen(... + get(testCase.RigBehaviours.daqController.SignalGenerators), generators) + + % Simulate pressing view toggle, then switching controller, then quit + KbQueueCheck(-1, sequence({'v', 'v', '2', 'v', 'q'})); + srv.expServer; + + % Retrieve mock history for the Window + behaviour = testCase.RigBehaviours.stimWindow; + testCase.verifyThat([... + withAnyInputs(behaviour.makeTexture),... + withAnyInputs(behaviour.drawTexture),... + withAnyInputs(behaviour.flip)], ... + Occurred('RespectingOrder', false), 'Failed to print to Window') + + % Check that both signal generators were check exactly once. Assumes + % that if it check the Calibrations property, it probably used it. + for i = 1:2 + history = testCase.getMockHistory(generators(1)); + expected = ... + numel(history) == 1 && ... + isa(history, 'matlab.mock.history.SuccessfulMethodCall') && ... + strcmp(history.Name,"findprop") && ... + strcmp(history.Inputs{2}, 'Calibrations'); + testCase.verifyTrue(expected, 'Failed to query reward controller') + end + + % Finally check how it deals with missing reward controllers + testCase.clearMockHistory(testCase.Rig.stimWindow) + generators(1).Calibrations = []; % No calibrations for controller + % Assign only one generator to daqController mock + testCase.assignOutputsWhen(... + get(testCase.RigBehaviours.daqController.SignalGenerators), generators(1)) + % Simulate selection of 2nd reward controller + KbQueueCheck(-1, sequence({'v', '2', 'v', 'q'})); + T = evalc('srv.expServer'); + testCase.verifyMatches(T, 'No reward calibration') + testCase.verifyNotCalled(withAnyInputs(behaviour.drawTexture)) + end end end diff --git a/tests/fixtures/+hw/devices.m b/tests/fixtures/+hw/devices.m index d5107a99..0b8b4b02 100644 --- a/tests/fixtures/+hw/devices.m +++ b/tests/fixtures/+hw/devices.m @@ -22,6 +22,7 @@ if isempty(mockRig) mockRig = struct(... 'name', name, ... + 'paths', dat.paths, ... 'clock', hw.ptb.Clock, ... 'audioDevices', struct(... 'DeviceName', 'default',...