Repository: psychopy/psychopy

Issue:
Routines with Mouse don't end after stop time, if end on press is set to never
## Description
If a routine with a mouse is created and the "End Routine on Press field is set", the mouse object is not stopped at the time configured under "stop", if there is at least one component with a variable length.

## Detail configuration

Code Component:
-> `TrialDuration = 5` under "Begin Routine"

Mouse Component:
-> Start: 1.0s
-> Stop: 3.0s
-> End Routine on Press: "Never"
-> Save Mouse State: "final" or "never

Text Component:
-> Start: 0.0s
-> Stop: TrialDuration

## Reproduction
Two files attached that reproduce the error. `Incorrect.psyexp` does not stop after the `TrialDuration` has expired, `Correct.psyexp` finishes after `TrialDuration` has expired.

[example.zip](https://github.com/psychopy/psychopy/files/7038119/example.zip)

## Analysis

It seem with some specific setting the code to initialize and stop the mouse is not generated:

Trial for `Correct.psyexp`:

    # -------Run Routine "trial"-------
    while continueRoutine:
        # get current time
        t = trialClock.getTime()
        tThisFlip = win.getFutureFlipTime(clock=trialClock)
        tThisFlipGlobal = win.getFutureFlipTime(clock=None)
        frameN = frameN + 1  # number of completed frames (so 0 is the first frame)
        # update/draw components on each frame
        # *mouse* updates
        if mouse.status == NOT_STARTED and tThisFlip >= 1.0-frameTolerance:
            # keep track of start time/frame for later
            mouse.frameNStart = frameN  # exact frame index
            mouse.tStart = t  # local t and not account for scr refresh
            mouse.tStartRefresh = tThisFlipGlobal  # on global time
            win.timeOnFlip(mouse, 'tStartRefresh')  # time at next scr refresh
            mouse.status = STARTED
            mouse.mouseClock.reset()
            prevButtonState = mouse.getPressed()  # if button is down already this ISN'T a new click
        if mouse.status == STARTED:
            # is it time to stop? (based on local clock)
            if tThisFlip > 3.0-frameTolerance:
                # keep track of stop time/frame for later
                mouse.tStop = t  # not accounting for scr refresh
                mouse.frameNStop = frameN  # exact frame index
                win.timeOnFlip(mouse, 'tStopRefresh')  # time at next scr refresh
                mouse.status = FINISHED
        if mouse.status == STARTED:  # only update if started and not finished!
            buttons = mouse.getPressed()
            if buttons != prevButtonState:  # button state changed?
                prevButtonState = buttons
                if sum(buttons) > 0:  # state changed to a new click
                    x, y = mouse.getPos()
                    mouse.x.append(x)
                    mouse.y.append(y)
                    buttons = mouse.getPressed()
                    mouse.leftButton.append(buttons[0])
                    mouse.midButton.append(buttons[1])
                    mouse.rightButton.append(buttons[2])
                    mouse.time.append(mouse.mouseClock.getTime())
        
        # *text* updates
        if text.status == NOT_STARTED and tThisFlip >= 0.0-frameTolerance:
            # keep track of start time/frame for later
            text.frameNStart = frameN  # exact frame index
            text.tStart = t  # local t and not account for scr refresh
            text.tStartRefresh = tThisFlipGlobal  # on global time
            win.timeOnFlip(text, 'tStartRefresh')  # time at next scr refresh
            text.setAutoDraw(True)
        if text.status == STARTED:
            # is it time to stop? (based on local clock)
            if tThisFlip > TrialDuration-frameTolerance:
                # keep track of stop time/frame for later
                text.tStop = t  # not accounting for scr refresh
                text.frameNStop = frameN  # exact frame index
                win.timeOnFlip(text, 'tStopRefresh')  # time at next scr refresh
                text.setAutoDraw(False)
        
        # check for quit (typically the Esc key)
        if endExpNow or defaultKeyboard.getKeys(keyList=["escape"]):
            core.quit()
        
        # check if all components have finished
        if not continueRoutine:  # a component has requested a forced-end of Routine
            break
        continueRoutine = False  # will revert to True if at least one component still running
        for thisComponent in trialComponents:
            if hasattr(thisComponent, "status") and thisComponent.status != FINISHED:
                continueRoutine = True
                break  # at least one component has not yet finished
        
        # refresh the screen
        if continueRoutine:  # don't flip if this routine is over or we'll get a blank screen
            win.flip()

-> Mouse is started at 1s and stopped at 3s.

    # -------Run Routine "trial"-------
    while continueRoutine:
        # get current time
        t = trialClock.getTime()
        tThisFlip = win.getFutureFlipTime(clock=trialClock)
        tThisFlipGlobal = win.getFutureFlipTime(clock=None)
        frameN = frameN + 1  # number of completed frames (so 0 is the first frame)
        # update/draw components on each frame
        
        # *text* updates
        if text.status == NOT_STARTED and tThisFlip >= 0.0-frameTolerance:
            # keep track of start time/frame for later
            text.frameNStart = frameN  # exact frame index
            text.tStart = t  # local t and not account for scr refresh
            text.tStartRefresh = tThisFlipGlobal  # on global time
            win.timeOnFlip(text, 'tStartRefresh')  # time at next scr refresh
            text.setAutoDraw(True)
        if text.status == STARTED:
            # is it time to stop? (based on local clock)
            if tThisFlip > TrialDuration-frameTolerance:
                # keep track of stop time/frame for later
                text.tStop = t  # not accounting for scr refresh
                text.frameNStop = frameN  # exact frame index
                win.timeOnFlip(text, 'tStopRefresh')  # time at next scr refresh
                text.setAutoDraw(False)
        
        # check for quit (typically the Esc key)
        if endExpNow or defaultKeyboard.getKeys(keyList=["escape"]):
            core.quit()
        
        # check if all components have finished
        if not continueRoutine:  # a component has requested a forced-end of Routine
            break
        continueRoutine = False  # will revert to True if at least one component still running
        for thisComponent in trialComponents:
            if hasattr(thisComponent, "status") and thisComponent.status != FINISHED:
                continueRoutine = True
                break  # at least one component has not yet finished
        
        # refresh the screen
        if continueRoutine:  # don't flip if this routine is over or we'll get a blank screen
            win.flip()

-> Mouse is neither started nor finished correctly.

## Workaround 

Set "Save Mouse State" to "On Click".

## Notes

* If the duration is not variable, the code may seem correct. However, in that case the Routine may be stopped by the RoutineTimer and the Mouse is still not correctly started/stopped (code still missing).
* One might argue that some specific combinations of parameter that cause this make no sense. However: a) in that case an error or warning about these combinations should be produced. b) There are some cases where this makes sense, e.g. we use a Mouse component in the builder that is checked in custom code. We don't want to define our own mouse start/stop code, to reduce the amount of code that has to be converted from python to JS.


Discussion:
I think the problem is that, if Mouse is set to not gather any data, it doesn't get *any* of its Each Frame code written, including the checks for whether it's started and stopped. These are used to end the routine, as it checks each component for `.status == FINISHED`, so even a Mouse with finite duration isn't marked as `FINISHED` if it isn't gathering data.

I think this should be as simple as moving the code to skip frame code writing to be after writing start/stop checks; when I change it, this experiment runs as expected, but I need to test what else this breaks. Will reference this issue in the pull request when I'm confident it'll work.

Resolving patch:
diff --git a/psychopy/experiment/components/mouse/__init__.py b/psychopy/experiment/components/mouse/__init__.py
--- a/psychopy/experiment/components/mouse/__init__.py
+++ b/psychopy/experiment/components/mouse/__init__.py
@@ -243,14 +243,6 @@ def writeFrameCode(self, buff):
         elif timeRelative in ['routine', 'mouse onset']:
             self.clockStr = '%s.mouseClock' % self.params['name'].val
 
-        # only write code for cases where we are storing data as we go (each
-        # frame or each click)
-
-        # might not be saving clicks, but want it to force end of trial
-        if (self.params['saveMouseState'].val not in
-                ['every frame', 'on click'] and forceEnd == 'never'):
-            return
-
         buff.writeIndented("# *%s* updates\n" % self.params['name'])
 
         # writes an if statement to determine whether to draw etc
@@ -280,6 +272,14 @@ def writeFrameCode(self, buff):
             # to get out of the if statement
             buff.setIndentLevel(-2, relative=True)
 
+        # only write code for cases where we are storing data as we go (each
+        # frame or each click)
+
+        # might not be saving clicks, but want it to force end of trial
+        if (self.params['saveMouseState'].val not in
+                ['every frame', 'on click'] and forceEnd == 'never'):
+            return
+
         # if STARTED and not FINISHED!
         code = ("if %(name)s.status == STARTED:  "
                 "# only update if started and not finished!\n") % self.params


Test patch: