Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
4.49% covered (danger)
4.49%
12 / 267
16.28% covered (danger)
16.28%
7 / 43
CRAP
0.00% covered (danger)
0.00%
0 / 1
taoQtiTest_helpers_TestSession
4.49% covered (danger)
4.49%
12 / 267
16.28% covered (danger)
16.28%
7 / 43
6528.93
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 setResultServer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getResultServer
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setTest
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setLock
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setReadOnly
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isReadOnly
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isLocked
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getTest
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 submitItemResults
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
20
 endTestSession
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
12
 rewind
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 submitTestResults
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 getItemRefUri
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getTestDefinitionUri
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 buildLtiOutcomeProcessing
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 suspend
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 resume
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 beginTestSession
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 jumpTo
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 moveNext
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 moveBack
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 skip
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 moveNextTestPart
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 moveNextAssessmentSection
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 moveNextAssessmentItem
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 closeTestPart
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 closeAssessmentSection
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
30
 closeAssessmentItem
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 checkTimeLimits
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getTimeoutCode
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 closeTimer
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
132
 setState
37.50% covered (danger)
37.50%
3 / 8
0.00% covered (danger)
0.00%
0 / 1
7.91
 triggerResultItemTransmissionEvent
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 triggerResultTestVariablesTransmissionEvent
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 isManualScored
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
30
 triggerEventChange
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 triggerEventPaused
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 triggerEventResumed
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 triggerStateChanged
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getEventManager
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getServiceLocator
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSessionMemento
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; under version 2
7 * of the License (non-upgradable).
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 *
18 * Copyright (c) 2013-2014 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
19 *
20 */
21
22use oat\oatbox\event\EventManager;
23use oat\oatbox\service\ServiceManager;
24use oat\taoQtiTest\helpers\TestSessionMemento;
25use oat\taoQtiTest\models\classes\event\ResultTestVariablesTransmissionEvent;
26use oat\taoQtiTest\models\event\QtiTestChangeEvent;
27use oat\taoQtiTest\models\event\QtiTestStateChangeEvent;
28use oat\taoQtiTest\models\event\ResultItemVariablesTransmissionEvent;
29use oat\taoResultServer\models\classes\ResultStorageWrapper;
30use oat\taoTests\models\event\TestExecutionPausedEvent;
31use oat\taoTests\models\event\TestExecutionResumedEvent;
32use qtism\common\datatypes\QtiDuration;
33use qtism\common\datatypes\QtiFloat;
34use qtism\common\enums\BaseType;
35use qtism\common\enums\Cardinality;
36use qtism\data\AssessmentItemRef;
37use qtism\data\AssessmentTest;
38use qtism\data\expressions\ExpressionCollection;
39use qtism\data\expressions\NumberCorrect;
40use qtism\data\expressions\NumberPresented;
41use qtism\data\expressions\operators\Divide;
42use qtism\data\expressions\Variable;
43use qtism\data\ExtendedAssessmentItemRef;
44use qtism\data\processing\OutcomeProcessing;
45use qtism\data\rules\OutcomeRuleCollection;
46use qtism\data\rules\SetOutcomeValue;
47use qtism\data\state\OutcomeDeclaration;
48use qtism\runtime\common\OutcomeVariable;
49use qtism\runtime\common\ProcessingException;
50use qtism\runtime\processing\OutcomeProcessingEngine;
51use qtism\runtime\tests\AbstractSessionManager;
52use qtism\runtime\tests\AssessmentItemSession;
53use qtism\runtime\tests\AssessmentItemSessionException;
54use qtism\runtime\tests\AssessmentTestPlace;
55use qtism\runtime\tests\AssessmentTestSession;
56use qtism\runtime\tests\AssessmentTestSessionException;
57use qtism\runtime\tests\AssessmentTestSessionState;
58use qtism\runtime\tests\Route;
59use Symfony\Component\Lock\LockInterface;
60use Zend\ServiceManager\ServiceLocatorAwareInterface;
61
62/**
63 * A TAO Specific extension of QtiSm's AssessmentTestSession class.
64 *
65 * @author Jérôme Bogaerts <jerome@taotesting.com>
66 *
67 */
68class taoQtiTest_helpers_TestSession extends AssessmentTestSession
69{
70    /**
71     * The ResultServer to be used to transmit Item and Test results.
72     *
73     * @var ResultStorageWrapper
74     */
75    private $resultServer;
76
77    /**
78     * The TAO Resource describing the test.
79     *
80     * @var core_kernel_classes_Resource
81     */
82    private $test;
83
84    /**
85     * @var int
86     */
87    private $timeoutCode = null;
88
89    /**
90     * Nr of times setState has been called
91     *
92     * @var integer
93     */
94    private $setStateCount = 0;
95
96    /**
97     * @var Lock|null
98     */
99    private $lock = null;
100
101    /**
102     * Mode specifies the type of access you require to the test session.
103     * In readonly mode exception will be thrown on attempt to persist it in the test session storage
104     * @var boolean
105     */
106    private $readOnly = false;
107
108    /**
109     * Create a new TAO QTI Test Session.
110     *
111     * @param AssessmentTest $assessmentTest The AssessmentTest object representing the QTI test definition.
112     * @param AbstractSessionManager $manager The manager to be used to create new AssessmentItemSession objects.
113     * @param Route $route The Route (sequence of items) to be taken by the candidate for this test session.
114     * @param ResultStorageWrapper $resultServer The Result Server where Item and Test Results must be sent to.
115     * @param core_kernel_classes_Resource $test The TAO Resource describing the test.
116     */
117    public function __construct(
118        AssessmentTest $assessmentTest,
119        AbstractSessionManager $manager,
120        Route $route,
121        ResultStorageWrapper $resultServer,
122        core_kernel_classes_Resource $test
123    ) {
124        parent::__construct($assessmentTest, $manager, $route);
125        $this->setResultServer($resultServer);
126        $this->setTest($test);
127    }
128
129    /**
130     * Set the ResultServer to be used to transmit Item and Test results.
131     *
132     * @param ResultStorageWrapper $resultServer
133     */
134    protected function setResultServer(ResultStorageWrapper $resultServer)
135    {
136        $this->resultServer = $resultServer;
137    }
138
139    /**
140     * Get the ResultServer in use to transmit Item and Test results.
141     *
142     * @return ResultStorageWrapper
143     */
144    protected function getResultServer()
145    {
146        return $this->resultServer;
147    }
148
149    /**
150     * Set the TAO Resource describing the test in database.
151     *
152     * @param core_kernel_classes_Resource $test A Resource from the database describing a TAO test.
153     */
154    protected function setTest(core_kernel_classes_Resource $test)
155    {
156        $this->test = $test;
157    }
158
159    /**
160     * @param LockInterface $lock
161     */
162    public function setLock(LockInterface $lock)
163    {
164        $this->lock = $lock;
165    }
166
167    /**
168     * This mode specifies the type of access you require to the test session.
169     * In readonly mode exception will be thrown on attempt to persist it in the test session storage
170     * @param bool $readOnly
171     */
172    public function setReadOnly(bool $readOnly)
173    {
174        $this->readOnly = $readOnly;
175    }
176
177    /**
178     * Get access mode
179     * @return bool
180     */
181    public function isReadOnly()
182    {
183        return $this->readOnly;
184    }
185
186    /**
187     * @return bool
188     */
189    public function isLocked()
190    {
191        return ($this->lock instanceof LockInterface) && $this->lock->isAcquired();
192    }
193
194    /**
195     * Get the TAO Resource describing the test in database.
196     *
197     * @return core_kernel_classes_Resource A Resource from the database describing a TAO test.
198     */
199    public function getTest()
200    {
201        return $this->test;
202    }
203
204    /**
205     * @param AssessmentItemSession $itemSession
206     * @param int $occurence
207     * @throws AssessmentTestSessionException
208     * @throws taoQtiTest_helpers_TestSessionException
209     */
210    protected function submitItemResults(AssessmentItemSession $itemSession, $occurence = 0)
211    {
212        parent::submitItemResults($itemSession, $occurence);
213
214        $item = $itemSession->getAssessmentItem();
215
216        common_Logger::t("submitting results for item '" . $item->getIdentifier() . "." . $occurence .  "'.");
217
218        try {
219            $itemVariableSet = [];
220
221            // Get the item session we just responsed and send to the
222            // result server.
223            $itemSession = $this->getItemSession($item, $occurence);
224
225            foreach ($itemSession->getKeys() as $identifier) {
226                common_Logger::t("Examination of variable '${identifier}'");
227                $itemVariableSet[] = $itemSession->getVariable($identifier);
228            }
229
230            $this->triggerResultItemTransmissionEvent($itemVariableSet, $occurence, $item);
231        } catch (AssessmentTestSessionException $e) {
232            // Error whith parent::endAttempt().
233            $msg = "An error occured while ending the attempt item '" . $item->getIdentifier() . "." . $occurence
234                .  "'.";
235            throw new taoQtiTest_helpers_TestSessionException(
236                $msg,
237                taoQtiTest_helpers_TestSessionException::RESULT_SUBMISSION_ERROR,
238                $e
239            );
240        } catch (taoQtiCommon_helpers_ResultTransmissionException $e) {
241            // Error with Result Server.
242            $msg = "An error occured while transmitting item results for item '" . $item->getIdentifier() . "."
243                . $occurence .  "'.";
244            throw new taoQtiTest_helpers_TestSessionException(
245                $msg,
246                taoQtiTest_helpers_TestSessionException::RESULT_SUBMISSION_ERROR,
247                $e
248            );
249        }
250    }
251
252    /**
253     * QTISM endTestSession method overriding.
254     *
255     * It consists of including an additional processing when the test ends,
256     * in order to send the LtiOutcome
257     *
258     * @see http://www.imsglobal.org/lis/ Outcome Management Service
259     * @throws AssessmentTestSessionException
260     * @throws taoQtiTest_helpers_TestSessionException If the session is already ended or if an error occurs whil
261     *                                                 transmitting/processing the result.
262     */
263    public function endTestSession()
264    {
265        $sessionMemento = $this->getSessionMemento();
266        parent::endTestSession();
267
268        common_Logger::i('Ending test session: ' . $this->getSessionId());
269        try {
270            // Compute the LtiOutcome variable for LTI support.
271            $this->setVariable(
272                new OutcomeVariable(
273                    'LtiOutcome',
274                    Cardinality::SINGLE,
275                    BaseType::FLOAT,
276                    new QtiFloat(0.0)
277                )
278            );
279            $outcomeProcessingEngine = new OutcomeProcessingEngine($this->buildLtiOutcomeProcessing(), $this);
280            $outcomeProcessingEngine->process();
281
282            // if numberPresented returned 0, division by 0 -> null.
283            $testUri = $this->getTest()->getUri();
284            $var = $this->getVariable('LtiOutcome');
285            $varIdentifier = $var->getIdentifier();
286
287            common_Logger::t("Submitting test result '${varIdentifier}' related to test '${testUri}'.");
288            $this->triggerResultTestVariablesTransmissionEvent(
289                [$var],
290                $testUri
291            );
292        } catch (ProcessingException $e) {
293            $msg = "An error occured while processing the 'LtiOutcome' outcome variable.";
294            throw new taoQtiTest_helpers_TestSessionException(
295                $msg,
296                taoQtiTest_helpers_TestSessionException::RESULT_SUBMISSION_ERROR,
297                $e
298            );
299        } catch (taoQtiCommon_helpers_ResultTransmissionException $e) {
300            $msg = "An error occured during test-level outcome results transmission.";
301            throw new taoQtiTest_helpers_TestSessionException(
302                $msg,
303                taoQtiTest_helpers_TestSessionException::RESULT_SUBMISSION_ERROR,
304                $e
305            );
306        } finally {
307            $this->unsetVariable('LtiOutcome');
308        }
309
310        $this->triggerEventChange($sessionMemento);
311    }
312
313    /**
314     * Rewind the test to its first position
315     * @param boolean $allowTimeout Whether or not it is allowed to jump if the timeLimits in force of the jump target
316     *                              are not respected.
317     * @throws UnexpectedValueException
318     * @throws AssessmentTestSessionException If $position is out of the Route bounds or the jump is not allowed because
319     *                                        of time constraints.
320     * @throws AssessmentItemSessionException
321     */
322    public function rewind($allowTimeout = false)
323    {
324        $position = 0;
325        $this->suspendItemSession();
326        $route = $this->getRoute();
327        $oldPosition = $route->getPosition();
328
329        try {
330            $route->setPosition($position);
331            $this->selectEligibleItems();
332
333            // Check the time limits after the jump is trully performed.
334            if ($allowTimeout === false) {
335                $this->checkTimeLimits(false, true);
336            }
337
338            // No exception thrown, interact!
339            $this->interactWithItemSession();
340        } catch (AssessmentTestSessionException $e) {
341            // Rollback to previous position.
342            $route->setPosition($oldPosition);
343            throw $e;
344        } catch (OutOfBoundsException $e) {
345            $msg = "Position '${position}' is out of the Route bounds.";
346            throw new AssessmentTestSessionException($msg, AssessmentTestSessionException::FORBIDDEN_JUMP, $e);
347        }
348    }
349
350    /**
351     * AssessmentTestSession implementations must override this method in order to submit test results
352     * from the current AssessmentTestSession to the appropriate data source.
353     *
354     * This method is triggered once at the end of the AssessmentTestSession.
355     *
356     * * @throws AssessmentTestSessionException With error code RESULT_SUBMISSION_ERROR if an error occurs while
357     *                                          transmitting results.
358     */
359    protected function submitTestResults()
360    {
361        $testUri = $this->getTest()->getUri();
362
363        common_Logger::t("Submitting test result related to test '" . $testUri . "'.");
364        $this->triggerResultTestVariablesTransmissionEvent(
365            $this->getAllVariables()->getArrayCopy(),
366            $testUri
367        );
368    }
369
370    /**
371     * Get the TAO URI of an item from an ExtendedAssessmentItemRef object.
372     *
373     * @param ExtendedAssessmentItemRef $itemRef
374     * @return string A URI.
375     */
376    protected static function getItemRefUri(AssessmentItemRef $itemRef)
377    {
378        $parts = explode('|', $itemRef->getHref());
379        return $parts[0];
380    }
381
382    /**
383     * Get the TAO Uri of the Test Definition from an ExtendedAssessmentItemRef object.
384     *
385     * @param ExtendedAssessmentItemRef $itemRef
386     * @return string A URI.
387     */
388    protected static function getTestDefinitionUri(ExtendedAssessmentItemRef $itemRef)
389    {
390        $parts = explode('|', $itemRef->getHref());
391        return $parts[2];
392    }
393
394    /**
395     * Build the OutcomeProcessing object representing the set of QTI instructions
396     * to be performed to compute the LtiOutcome variable value.
397     *
398     * @return OutcomeProcessing A QTI Data Model OutcomeProcessing object.
399     */
400    protected function buildLtiOutcomeProcessing()
401    {
402
403        // ltiOutcome is calculated based on the SCORE_RATIO_WEIGHTED outcome for weighted items or SCORE_RATIO for not
404        // rated items
405        $ratioVariable = $this->getVariable('SCORE_RATIO_WEIGHTED');
406        if (is_null($ratioVariable)) {
407            $ratioVariable = $this->getVariable('SCORE_RATIO');
408        }
409
410        if (is_null($ratioVariable)) {
411            //if no SCORE_RATIO outcome has been found, we keep support legacy ltiOutcome calculation algorithm
412            //it is based on number of presented item for backwards compatibility
413            $numberCorrect = new NumberCorrect();
414            $numberPresented = new NumberPresented();
415            $divide = new Divide(new ExpressionCollection([$numberCorrect, $numberPresented]));
416            $outcomeRule = new SetOutcomeValue('LtiOutcome', $divide);
417        } else {
418            $outcomeRule = new SetOutcomeValue('LtiOutcome', new Variable($ratioVariable->getIdentifier()));
419        }
420
421        return new OutcomeProcessing(new OutcomeRuleCollection([$outcomeRule]));
422    }
423
424    /**
425     * Suspend the current test session if it is running.
426     */
427    public function suspend()
428    {
429        $sessionMemento = $this->getSessionMemento();
430        $running = $this->isRunning();
431        parent::suspend();
432        if ($running) {
433            $this->triggerEventChange($sessionMemento);
434            $this->triggerEventPaused();
435            common_Logger::i("QTI Test with session ID '" . $this->getSessionId() . "' suspended.");
436        }
437    }
438
439    /**
440     * Resume the current test session if it is suspended.
441     */
442    public function resume()
443    {
444        $sessionMemento = $this->getSessionMemento();
445        $suspended = $this->getState() === AssessmentTestSessionState::SUSPENDED;
446        parent::resume();
447        if ($suspended) {
448            $this->triggerEventChange($sessionMemento);
449            $this->triggerEventResumed();
450            common_Logger::i("QTI Test with session ID '" . $this->getSessionId() . "' resumed.");
451        }
452    }
453
454    /**
455     * Begins the test session. Calling this method will make the state
456     * change into AssessmentTestSessionState::INTERACTING.
457     *
458     * @qtism-test-interaction
459     * @qtism-test-duration-update
460     */
461    public function beginTestSession()
462    {
463        // fake increase of state count to ensure setState triggers event
464        $this->setStateCount++;
465        $sessionMemento = $this->getSessionMemento();
466        parent::beginTestSession();
467        $this->triggerStateChanged($sessionMemento);
468        $this->triggerEventChange($sessionMemento);
469    }
470
471    /**
472     * Perform a 'jump' to a given position in the Route sequence. The current navigation
473     * mode must be LINEAR to be able to jump.
474     *
475     * @param integer $position The position in the route the jump has to be made.
476     * @param boolean $allowTimeout Whether or not it is allowed to jump if the timeLimits in force of the jump target
477     *                              are not respected.
478     * @throws AssessmentTestSessionException If $position is out of the Route bounds or the jump is not allowed because
479     *                                        of time constraints.
480     * @qtism-test-interaction
481     * @qtism-test-duration-update
482     */
483    public function jumpTo($position, $allowTimeout = false)
484    {
485        $sessionMemento = $this->getSessionMemento();
486        parent::jumpTo($position, $allowTimeout);
487        $this->triggerEventChange($sessionMemento);
488    }
489
490    /**
491     * Ask the test session to move to next RouteItem in the Route sequence.
492     *
493     * If $allowTimeout is set to true, the very next RouteItem in the Route sequence will bet set
494     * as the current RouteItem, whether or not it is timed out or not.
495     *
496     * On the other hand, if $allowTimeout is set to false, the next RouteItem in the Route sequence
497     * which is not timed out will be set as the current RouteItem. If there is no more following RouteItems
498     * that are not timed out in the Route sequence, the test session ends gracefully.
499     *
500     * @param boolean $allowTimeout If set to true, the next RouteItem in the Route sequence does not have to respect
501     *                              the timeLimits in force. Default value is false.
502     * @throws AssessmentTestSessionException If the test session is not running or an issue occurs during the
503     *                                        transition (e.g. branching, preConditions, ...).
504     * @qtism-test-interaction
505     * @qtism-test-duration-update
506     */
507    public function moveNext($allowTimeout = false)
508    {
509        $sessionMemento = $this->getSessionMemento();
510        parent::moveNext($allowTimeout);
511        $this->triggerEventChange($sessionMemento);
512    }
513
514    /**
515     * Ask the test session to move to the previous RouteItem in the Route sequence.
516     *
517     * If $allowTimeout is set to true, the previous RouteItem in the Route sequence will bet set
518     * as the current RouteItem, whether or not it is timed out.
519     *
520     * On the other hand, if $allowTimeout is set to false, the previous RouteItem in the Route sequence
521     * which is not timed out will be set as the current RouteItem. If there is no more previous RouteItems
522     * that are not timed out in the Route sequence, the current RouteItem remains the same and an
523     * AssessmentTestSessionException with the appropriate timing error code is thrown.
524     *
525     * @param boolean $allowTimeout If set to true, the next RouteItem in the sequence does not have to respect
526     *                              timeLimits in force. Default value is false.
527     * @throws AssessmentTestSessionException If the test session is not running or an issue occurs during the
528     *                                        transition (e.g. branching, preConditions, ...) or
529     *                                        if $allowTimeout = false and there absolutely no possibility to move
530     *                                        backward (even the first RouteItem is timed out).
531     * @qtism-test-interaction
532     * @qtism-test-duration-update
533     */
534    public function moveBack($allowTimeout = false)
535    {
536        $sessionMemento = $this->getSessionMemento();
537        parent::moveBack($allowTimeout);
538        $this->triggerEventChange($sessionMemento);
539    }
540
541    /**
542     * Skip the current item.
543     *
544     * @throws AssessmentTestSessionException If the test session is not running or it is the last route item of the
545     *                                        testPart but the SIMULTANEOUS submission mode is in force and not all
546     *                                        responses were provided.
547     * @qtism-test-interaction
548     * @qtism-test-duration-update
549     */
550    public function skip()
551    {
552        $sessionMemento = $this->getSessionMemento();
553        parent::skip();
554        $this->triggerEventChange($sessionMemento);
555    }
556
557    /**
558     * Set the position in the Route at the very next TestPart in the Route sequence or, if the current
559     * testPart is the last one of the test session, the test session ends gracefully. If the submission mode
560     * is simultaneous, the pending responses are processed.
561     *
562     * @throws AssessmentTestSessionException If the test is currently not running.
563     */
564    public function moveNextTestPart()
565    {
566        $sessionMemento = $this->getSessionMemento();
567        parent::moveNextTestPart();
568        $this->triggerEventChange($sessionMemento);
569    }
570
571    /**
572     * Set the position in the Route at the very next assessmentSection in the route sequence.
573     *
574     * * If there is no assessmentSection left in the flow, the test session ends gracefully.
575     * * If there are still pending responses, they are processed.
576     *
577     * @throws AssessmentTestSessionException If the test is not running.
578     */
579    public function moveNextAssessmentSection()
580    {
581        $sessionMemento = $this->getSessionMemento();
582        parent::moveNextAssessmentSection();
583        $this->triggerEventChange($sessionMemento);
584    }
585
586    /**
587     * Set the position in the Route at the very next assessmentItem in the route sequence.
588     *
589     * * If there is no item left in the flow, the test session ends gracefully.
590     * * If there are still pending responses, they are processed.
591     *
592     * @throws AssessmentTestSessionException If the test is not running.
593     */
594    public function moveNextAssessmentItem()
595    {
596        $sessionMemento = $this->getSessionMemento();
597        parent::moveNextAssessmentItem();
598        $this->triggerEventChange($sessionMemento);
599    }
600
601    /**
602     * Set the position in the Route at the very next TestPart in the Route sequence or, if the current
603     * testPart is the last one of the test session, the test session ends gracefully. If the submission mode
604     * is simultaneous, the pending responses are processed.
605     *
606     * @throws AssessmentTestSessionException If the test is currently not running.
607     */
608    public function closeTestPart()
609    {
610        $sessionMemento = $this->getSessionMemento();
611        if ($this->isRunning() === false) {
612            $msg = "Cannot move to the next testPart while the state of the test session is INITIAL or CLOSED.";
613            throw new AssessmentTestSessionException($msg, AssessmentTestSessionException::STATE_VIOLATION);
614        }
615
616        $route = $this->getRoute();
617        $from = $route->current();
618
619        while ($route->valid() === true && $route->current()->getTestPart() === $from->getTestPart()) {
620            $itemSession = $this->getCurrentAssessmentItemSession();
621            $itemSession->endItemSession();
622            $this->nextRouteItem();
623        }
624
625        if ($this->isRunning() === true) {
626            $this->interactWithItemSession();
627        }
628
629        $this->triggerEventChange($sessionMemento);
630    }
631
632    /**
633     * Set the position in the Route at the very next assessmentSection in the route sequence.
634     *
635     * * If there is no assessmentSection left in the flow, the test session ends gracefully.
636     * * If there are still pending responses, they are processed.
637     *
638     * @throws AssessmentTestSessionException If the test is not running.
639     */
640    public function closeAssessmentSection()
641    {
642        $sessionMemento = $this->getSessionMemento();
643        if ($this->isRunning() === false) {
644            $msg = "Cannot move to the next assessmentSection while the state of the test session is INITIAL or "
645                . "CLOSED.";
646            throw new AssessmentTestSessionException($msg, AssessmentTestSessionException::STATE_VIOLATION);
647        }
648
649        $route = $this->getRoute();
650        $from = $route->current();
651
652        while (
653            $route->valid() === true
654            && $route->current()->getAssessmentSection() === $from->getAssessmentSection()
655        ) {
656            $itemSession = $this->getCurrentAssessmentItemSession();
657            $itemSession->endItemSession();
658            $this->nextRouteItem();
659        }
660
661        if ($this->isRunning() === true) {
662            $this->interactWithItemSession();
663        }
664
665        $this->triggerEventChange($sessionMemento);
666    }
667
668    /**
669     * Set the position in the Route at the very next assessmentItem in the route sequence.
670     *
671     * * If there is no item left in the flow, the test session ends gracefully.
672     * * If there are still pending responses, they are processed.
673     *
674     * @throws AssessmentTestSessionException If the test is not running.
675     */
676    public function closeAssessmentItem()
677    {
678        $sessionMemento = $this->getSessionMemento();
679        if ($this->isRunning() === false) {
680            $msg = "Cannot move to the next testPart while the state of the test session is INITIAL or CLOSED.";
681            throw new AssessmentTestSessionException($msg, AssessmentTestSessionException::STATE_VIOLATION);
682        }
683
684        $itemSession = $this->getCurrentAssessmentItemSession();
685        $itemSession->endItemSession();
686        $this->nextRouteItem();
687
688        if ($this->isRunning() === true) {
689            $this->interactWithItemSession();
690        }
691
692        $this->triggerEventChange($sessionMemento);
693    }
694
695    /**
696     * @param bool $includeMinTime
697     * @param bool $includeAssessmentItem
698     * @param bool $acceptableLatency
699     * @throws AssessmentTestSessionException
700     */
701    public function checkTimeLimits($includeMinTime = false, $includeAssessmentItem = false, $acceptableLatency = true)
702    {
703        try {
704            parent::checkTimeLimits($includeMinTime, $includeAssessmentItem, $acceptableLatency);
705        } catch (AssessmentTestSessionException $e) {
706            $this->timeoutCode = $e->getCode();
707            throw $e;
708        }
709    }
710
711    /**
712     * @return null|int
713     */
714    public function getTimeoutCode()
715    {
716        return $this->timeoutCode;
717    }
718
719    /**
720     * Closes a timer
721     * @param string $identifier
722     * @param string [$type]
723     */
724    public function closeTimer($identifier, $type = null)
725    {
726        switch ($type) {
727            case 'assessmentTest':
728                $places = AssessmentTestPlace::ASSESSMENT_TEST;
729                break;
730
731            case 'testPart':
732                $places = AssessmentTestPlace::TEST_PART;
733                break;
734
735            case 'assessmentSection':
736                $places = AssessmentTestPlace::ASSESSMENT_SECTION;
737                break;
738
739            case 'assessmentItemRef':
740                $places = AssessmentTestPlace::ASSESSMENT_ITEM;
741                break;
742
743            default:
744                $places = AssessmentTestPlace::ASSESSMENT_TEST
745                    | AssessmentTestPlace::TEST_PART
746                    | AssessmentTestPlace::ASSESSMENT_SECTION
747                    | AssessmentTestPlace::ASSESSMENT_ITEM;
748        }
749
750        $constraints = $this->getTimeConstraints($places);
751        foreach ($constraints as $constraint) {
752            $source = $constraint->getSource();
753            $placeId = $source->getIdentifier();
754            if ($placeId === $identifier) {
755                if (($maxTime = $constraint->getAdjustedMaxTime()) !== null) {
756                    $constraintDuration = $constraint->getDuration();
757                    if ($constraintDuration instanceof QtiDuration) {
758                        $constraintDuration->sub($constraintDuration);
759                        $constraintDuration->add($maxTime);
760                        if ($constraint->getApplyExtraTime()) {
761                            $extraTime = $constraint->getTimer()->getExtraTime();
762                            $constraintDuration->add(new QtiDuration('PT' . $extraTime . 'S'));
763                        }
764                    }
765                }
766            }
767        }
768    }
769
770    /**
771     * Override setState to trigger events on state change
772     * Only trigger on thir call or higher:
773     *
774     * Call Nr 1: called in constructor
775     * Call Nr 2: called in initialiser
776     * Call Nr 3+: real state change
777     *
778     * Except during creation of session in beginTestSession
779     * triggerStateChanged is triggered manually
780     *
781     * @inheritdoc
782     * @param int $state
783     */
784    public function setState($state)
785    {
786        $this->setStateCount++;
787        if ($this->setStateCount <= 2) {
788            return parent::setState($state);
789        } else {
790            $previousState = $this->getState();
791            $sessionMemento = $this->getSessionMemento();
792            parent::setState($state);
793            if ($previousState !== null && $previousState !== $state) {
794                $this->triggerStateChanged($sessionMemento);
795            }
796        }
797    }
798
799    private function triggerResultItemTransmissionEvent(
800        array $variables,
801        $occurrence,
802        ExtendedAssessmentItemRef $item
803    ): void {
804        $transmissionId = sprintf('%s.%s.%s', $this->getSessionId(), $item, $occurrence);
805        $this->getEventManager()->trigger(new ResultItemVariablesTransmissionEvent(
806            $this->getSessionId(),
807            $variables,
808            $transmissionId,
809            self::getItemRefUri($item),
810            self::getTestDefinitionUri($item)
811        ));
812    }
813
814    private function triggerResultTestVariablesTransmissionEvent(
815        array $variables,
816        string $testUri = ''
817    ): void {
818        $this->getEventManager()->trigger(new ResultTestVariablesTransmissionEvent(
819            $this->getSessionId(),
820            $variables,
821            $this->getSessionId(),
822            $testUri
823        ));
824    }
825
826    public function isManualScored(): bool
827    {
828        /** @var AssessmentItemRef $itemRef */
829        foreach ($this->getRoute()->getAssessmentItemRefs() as $itemRef) {
830            foreach ($itemRef->getComponents() as $component) {
831                if (
832                    $component instanceof OutcomeDeclaration
833                    && $component->isExternallyScored()
834                ) {
835                    return true;
836                }
837            }
838        }
839
840        return false;
841    }
842
843    /**
844     * @param TestSessionMemento $sessionMemento
845     */
846    protected function triggerEventChange(TestSessionMemento $sessionMemento)
847    {
848        $event = new QtiTestChangeEvent($this, $sessionMemento);
849        if ($event instanceof ServiceLocatorAwareInterface) {
850            $event->setServiceLocator($this->getServiceLocator());
851        }
852        $this->getEventManager()->trigger($event);
853    }
854
855    protected function triggerEventPaused()
856    {
857        $event = new TestExecutionPausedEvent(
858            $this->getSessionId()
859        );
860        $this->getEventManager()->trigger($event);
861    }
862
863    protected function triggerEventResumed()
864    {
865        $event = new TestExecutionResumedEvent(
866            $this->getSessionId()
867        );
868        $this->getEventManager()->trigger($event);
869    }
870
871    /**
872     * @param TestSessionMemento $sessionMemento
873     */
874    protected function triggerStateChanged(TestSessionMemento $sessionMemento)
875    {
876        $event = new QtiTestStateChangeEvent($this, $sessionMemento);
877        if ($event instanceof ServiceLocatorAwareInterface) {
878            $event->setServiceLocator($this->getServiceLocator());
879        }
880        $this->getEventManager()->trigger($event);
881    }
882
883    /**
884     * @return EventManager
885     */
886    protected function getEventManager()
887    {
888        return $this->getServiceLocator()->get(EventManager::SERVICE_ID);
889    }
890
891    protected function getServiceLocator()
892    {
893        return ServiceManager::getServiceManager();
894    }
895
896    /**
897     * @return TestSessionMemento
898     */
899    protected function getSessionMemento()
900    {
901        return new TestSessionMemento($this);
902    }
903}