From fe05368d5e4eec15f66e03a8687504872e3b9f2a Mon Sep 17 00:00:00 2001 From: Gabriel Felipe Soares Date: Fri, 22 Sep 2023 12:05:19 +0200 Subject: [PATCH 01/23] feat: make the first item consider testPart pre-conditions --- src/qtism/runtime/tests/AbstractSessionManager.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/qtism/runtime/tests/AbstractSessionManager.php b/src/qtism/runtime/tests/AbstractSessionManager.php index 9fa7d5aa3..7228b2836 100644 --- a/src/qtism/runtime/tests/AbstractSessionManager.php +++ b/src/qtism/runtime/tests/AbstractSessionManager.php @@ -200,6 +200,7 @@ protected function createRoute(AssessmentTest $test): Route // Do the same as for branch rules for pre conditions, except that they must be // attached on the first item of the route. $route->getFirstRouteItem()->addPreConditions($current->getPreConditions()); + $route->getFirstRouteItem()->addPreConditions($testPart->getPreConditions()); } array_push($routeStack, $route); From 94379038428115b8404eb09b258414a77876ae60 Mon Sep 17 00:00:00 2001 From: Andrei Shapiro Date: Fri, 22 Sep 2023 15:53:51 +0300 Subject: [PATCH 02/23] chore: ignore branch rule if item/section from one test part pointing to another test part --- src/qtism/runtime/tests/Route.php | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/qtism/runtime/tests/Route.php b/src/qtism/runtime/tests/Route.php index 6263cb33b..87eb425d4 100644 --- a/src/qtism/runtime/tests/Route.php +++ b/src/qtism/runtime/tests/Route.php @@ -1117,10 +1117,11 @@ public function branch($identifier): void if ($targetRouteItems[$occurence]->getTestPart() !== $this->current()->getTestPart()) { // From IMS QTI: - // In case of an item or section, the target must refer to an item or section - // in the same testPart [...] - $msg = 'Branchings to items outside of the current testPart is forbidden by the QTI 2.1 specification.'; - throw new OutOfBoundsException($msg); + // In the case of an item or section, the target must refer to an item or section in the same test-part + // that has not yet been presented. + $this->next(); + + return; } $this->setPosition($this->getRouteItemPosition($targetRouteItems[$occurence])); @@ -1133,10 +1134,11 @@ public function branch($identifier): void if (isset($assessmentSectionIdentifierMap[$id])) { if ($assessmentSectionIdentifierMap[$id][0]->getTestPart() !== $this->current()->getTestPart()) { // From IMS QTI: - // In case of an item or section, the target must refer to an item or section - // in the same testPart [...] - $msg = 'Branchings to assessmentSections outside of the current testPart is forbidden by the QTI 2.1 specification.'; - throw new OutOfBoundsException($msg); + // In the case of an item or section, the target must refer to an item or section in the same test-part + // that has not yet been presented. + $this->next(); + + return; } // We branch to the first RouteItem belonging to the section. From b472bb040a3ed9a7cfc183599460ca0b3122a594 Mon Sep 17 00:00:00 2001 From: Andrei Shapiro Date: Fri, 22 Sep 2023 16:39:21 +0300 Subject: [PATCH 03/23] test: update unit tests --- test/qtismtest/runtime/tests/RouteTest.php | 23 ++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/test/qtismtest/runtime/tests/RouteTest.php b/test/qtismtest/runtime/tests/RouteTest.php index a5b66b178..d52abcdd2 100644 --- a/test/qtismtest/runtime/tests/RouteTest.php +++ b/test/qtismtest/runtime/tests/RouteTest.php @@ -601,10 +601,10 @@ public function testBranchUnknownTarget(): void public function testBranchToAssessmentItemRefOutsideOfTestPart(): void { - $route = self::buildSimpleRoute(Route::class, 2, 1); - $this->expectException(OutOfBoundsException::class); - $this->expectExceptionMessage('Branchings to items outside of the current testPart is forbidden by the QTI 2.1 specification.'); - $route->branch('Q2'); + $route = self::buildSimpleRoute(Route::class, 2, 2); + $route->branch('Q3'); + + $this->assertEquals('Q2', $route->current()->getAssessmentItemRef()->getIdentifier()); } public function testBranchToSameTestPart(): void @@ -655,24 +655,27 @@ public function testBranchToSectionOutsideOfTestPart(): void $assessmentItemRef1 = new AssessmentItemRef('Q1', 'Q1.xml'); $assessmentItemRef2 = new AssessmentItemRef('Q2', 'Q2.xml'); + $assessmentItemRef3 = new AssessmentItemRef('Q3', 'Q3.xml'); $assessmentSection1 = new AssessmentSection('S1', 'Section 1', true); - $assessmentSection1->setSectionParts(new SectionPartCollection([$assessmentItemRef1])); + $assessmentSection1->setSectionParts(new SectionPartCollection([$assessmentItemRef1, $assessmentItemRef2])); $assessmentSection2 = new AssessmentSection('S2', 'Section 2', true); - $assessmentSection2->setSectionParts(new SectionPartCollection([$assessmentItemRef2])); + $assessmentSection2->setSectionParts(new SectionPartCollection([$assessmentItemRef3])); $testPart1 = new TestPart('T1', new AssessmentSectionCollection([$assessmentSection1])); $testPart2 = new TestPart('T2', new AssessmentSectionCollection([$assessmentSection2])); $assessmentTest = new AssessmentTest('Test1', 'Test 1', new TestPartCollection([$testPart1, $testPart2])); $route->addRouteItem($assessmentItemRef1, $assessmentSection1, $testPart1, $assessmentTest); - $route->addRouteItem($assessmentItemRef2, $assessmentSection2, $testPart2, $assessmentTest); - - $this->expectException(OutOfBoundsException::class); - $this->expectExceptionMessage('Branchings to assessmentSections outside of the current testPart is forbidden by the QTI 2.1 specification.'); + $route->addRouteItem($assessmentItemRef2, $assessmentSection1, $testPart1, $assessmentTest); + $route->addRouteItem($assessmentItemRef3, $assessmentSection2, $testPart2, $assessmentTest); $route->branch('S2'); + $currentRoute = $route->current(); + + $this->assertEquals('Q2', $currentRoute->getAssessmentItemRef()->getIdentifier()); + $this->assertEquals('S1', $currentRoute->getAssessmentSection()->getIdentifier()); } public function testGetRouteItemPositionUnknownRouteItem(): void From 7430ae7e35a47cb461a414255876092095a2a53f Mon Sep 17 00:00:00 2001 From: bugalot Date: Fri, 22 Sep 2023 15:31:35 +0200 Subject: [PATCH 04/23] feat: implement branchRules at testPart level. --- .../runtime/tests/AbstractSessionManager.php | 10 +++++ .../AssessmentTestSessionBranchingsTest.php | 18 +++++++++ .../branchings/branchings_testparts.xml | 37 +++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 test/samples/custom/runtime/branchings/branchings_testparts.xml diff --git a/src/qtism/runtime/tests/AbstractSessionManager.php b/src/qtism/runtime/tests/AbstractSessionManager.php index 7228b2836..e1d60d95a 100644 --- a/src/qtism/runtime/tests/AbstractSessionManager.php +++ b/src/qtism/runtime/tests/AbstractSessionManager.php @@ -31,6 +31,7 @@ use qtism\data\IAssessmentItem; use qtism\data\NavigationMode; use qtism\data\SubmissionMode; +use qtism\data\TestPart; /** * The AbstractSessionManager class is a bed for instantiating @@ -148,6 +149,7 @@ protected function createRoute(AssessmentTest $test): Route { $routeStack = []; + /** @var TestPart $testPart */ foreach ($test->getTestParts() as $testPart) { $assessmentSectionStack = []; @@ -213,10 +215,18 @@ protected function createRoute(AssessmentTest $test): Route } } } + + // $route contains the currently processed testPart. + // Now, decorate last RouteItem of SelectableRoute with BranchRule objects if any. + if (!empty($route) && $route->count() > 0) { + $route->getLastRouteItem()->addBranchRules($testPart->getBranchRules()); + } } $finalRoutes = $routeStack; $route = new SelectableRoute(); + + /** @var SelectableRoute $finalRoute */ foreach ($finalRoutes as $finalRoute) { $route->appendRoute($finalRoute); } diff --git a/test/qtismtest/runtime/tests/AssessmentTestSessionBranchingsTest.php b/test/qtismtest/runtime/tests/AssessmentTestSessionBranchingsTest.php index 7265502f1..a1fc8f8c0 100644 --- a/test/qtismtest/runtime/tests/AssessmentTestSessionBranchingsTest.php +++ b/test/qtismtest/runtime/tests/AssessmentTestSessionBranchingsTest.php @@ -248,4 +248,22 @@ public function testBranchingOnPreconditon(): void $this::assertNull($session['Q03.SCORE']); $this::assertSame(1.0, $session['Q04.SCORE']->getValue()); } + + public function testBranchingOnTestPartsSimple1(): void + { + $session = self::instantiate(self::samplesDir() . 'custom/runtime/branchings/branchings_testparts.xml'); + $session->beginTestSession(); + + // We are starting at item Q01, in testPart P01. + $this->assertEquals('Q01', $session->getCurrentAssessmentItemRef()->getIdentifier()); + + // Let's jump to testPart P03. + $session->beginAttempt(); + $session->endAttempt(new State([new ResponseVariable('RESPONSE', Cardinality::SINGLE, BaseType::IDENTIFIER, new QtiIdentifier('GotoP03'))])); + $session->moveNext(); + + // We expect to land in testPart P03, item Q03. + $this->assertEquals('Q03', $session->getCurrentAssessmentItemRef()->getIdentifier()); + $this->assertEquals('P03', $session->getCurrentTestPart()->getIdentifier()); + } } diff --git a/test/samples/custom/runtime/branchings/branchings_testparts.xml b/test/samples/custom/runtime/branchings/branchings_testparts.xml new file mode 100644 index 000000000..881cf54d1 --- /dev/null +++ b/test/samples/custom/runtime/branchings/branchings_testparts.xml @@ -0,0 +1,37 @@ + + + + + + + + GotoP02 + + + + + + + GotoP03 + + + + + + + + + + + + + + + + + + + From a01223ba5a29e0a67a61c8441d7d5d628cd463a0 Mon Sep 17 00:00:00 2001 From: Gabriel Felipe Soares Date: Mon, 25 Sep 2023 10:45:50 +0200 Subject: [PATCH 05/23] chore: add test part preconditions to item just one time --- src/qtism/runtime/tests/AbstractSessionManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qtism/runtime/tests/AbstractSessionManager.php b/src/qtism/runtime/tests/AbstractSessionManager.php index e1d60d95a..e65f139f4 100644 --- a/src/qtism/runtime/tests/AbstractSessionManager.php +++ b/src/qtism/runtime/tests/AbstractSessionManager.php @@ -202,7 +202,6 @@ protected function createRoute(AssessmentTest $test): Route // Do the same as for branch rules for pre conditions, except that they must be // attached on the first item of the route. $route->getFirstRouteItem()->addPreConditions($current->getPreConditions()); - $route->getFirstRouteItem()->addPreConditions($testPart->getPreConditions()); } array_push($routeStack, $route); @@ -220,6 +219,7 @@ protected function createRoute(AssessmentTest $test): Route // Now, decorate last RouteItem of SelectableRoute with BranchRule objects if any. if (!empty($route) && $route->count() > 0) { $route->getLastRouteItem()->addBranchRules($testPart->getBranchRules()); + $route->getFirstRouteItem()->addPreConditions($testPart->getPreConditions()); } } From e5c592ec5a1d86b38a1f6b994711ccacd82e6fb2 Mon Sep 17 00:00:00 2001 From: Andrei Shapiro Date: Mon, 25 Sep 2023 17:04:33 +0300 Subject: [PATCH 06/23] chore: rely on original branch rules instead of setting them to the last item --- .../runtime/tests/AbstractSessionManager.php | 3 --- .../runtime/tests/AssessmentTestSession.php | 27 ++++++++++++++++--- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/qtism/runtime/tests/AbstractSessionManager.php b/src/qtism/runtime/tests/AbstractSessionManager.php index e65f139f4..05171309a 100644 --- a/src/qtism/runtime/tests/AbstractSessionManager.php +++ b/src/qtism/runtime/tests/AbstractSessionManager.php @@ -197,8 +197,6 @@ protected function createRoute(AssessmentTest $test): Route // Add to the last item of the selection the branch rules of the AssessmentSection/testPart // on which the selection is applied... Only if the route contains something (empty assessmentSection edge case). if ($route->count() > 0) { - $route->getLastRouteItem()->addBranchRules($current->getBranchRules()); - // Do the same as for branch rules for pre conditions, except that they must be // attached on the first item of the route. $route->getFirstRouteItem()->addPreConditions($current->getPreConditions()); @@ -218,7 +216,6 @@ protected function createRoute(AssessmentTest $test): Route // $route contains the currently processed testPart. // Now, decorate last RouteItem of SelectableRoute with BranchRule objects if any. if (!empty($route) && $route->count() > 0) { - $route->getLastRouteItem()->addBranchRules($testPart->getBranchRules()); $route->getFirstRouteItem()->addPreConditions($testPart->getPreConditions()); } } diff --git a/src/qtism/runtime/tests/AssessmentTestSession.php b/src/qtism/runtime/tests/AssessmentTestSession.php index bb0b71a10..2572839f2 100644 --- a/src/qtism/runtime/tests/AssessmentTestSession.php +++ b/src/qtism/runtime/tests/AssessmentTestSession.php @@ -2406,10 +2406,31 @@ protected function nextRouteItem($ignoreBranchings = false, $ignorePreConditions $stop = false; while ($route->valid() === true && $stop === false) { + $currentRouteItem = $route->current(); + $branchRules = $currentRouteItem->getBranchRules(); + + if ($branchRules->count() === 0) { + $currentSection = $currentRouteItem->getAssessmentSection(); + $sectionItems = $route->getRouteItemsByAssessmentSection($currentSection)->getArrayCopy(); + + if (end($sectionItems) === $currentRouteItem) { + $branchRules = $currentSection->getBranchRules(); + } + + if ($branchRules->count() === 0) { + $testPartItems = $route->getCurrentTestPartRouteItems()->getArrayCopy(); + + if (end($testPartItems) === $currentRouteItem) { + $branchRules = $currentRouteItem->getTestPart()->getBranchRules(); + } + } + } + + $numberOfBranchRules = $branchRules->count(); + // Branchings? - if ($ignoreBranchings === false && count($route->current()->getBranchRules()) > 0 && $this->mustApplyBranchRules() === true) { - $branchRules = $route->current()->getBranchRules(); - for ($i = 0; $i < count($branchRules); $i++) { + if ($ignoreBranchings === false && $numberOfBranchRules > 0 && $this->mustApplyBranchRules() === true) { + for ($i = 0; $i < $numberOfBranchRules; $i++) { $engine = new ExpressionEngine($branchRules[$i]->getExpression(), $this); $condition = $engine->process(); if ($condition !== null && $condition->getValue() === true) { From c623ab4dbbb3df9acfefcbe23967c5c444c8b7f0 Mon Sep 17 00:00:00 2001 From: Andrei Shapiro Date: Tue, 26 Sep 2023 10:16:07 +0300 Subject: [PATCH 07/23] refactor: move logic to get branch rules to Route class --- .../runtime/tests/AssessmentTestSession.php | 21 +------------ src/qtism/runtime/tests/Route.php | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/src/qtism/runtime/tests/AssessmentTestSession.php b/src/qtism/runtime/tests/AssessmentTestSession.php index 2572839f2..268d4ad2a 100644 --- a/src/qtism/runtime/tests/AssessmentTestSession.php +++ b/src/qtism/runtime/tests/AssessmentTestSession.php @@ -2406,26 +2406,7 @@ protected function nextRouteItem($ignoreBranchings = false, $ignorePreConditions $stop = false; while ($route->valid() === true && $stop === false) { - $currentRouteItem = $route->current(); - $branchRules = $currentRouteItem->getBranchRules(); - - if ($branchRules->count() === 0) { - $currentSection = $currentRouteItem->getAssessmentSection(); - $sectionItems = $route->getRouteItemsByAssessmentSection($currentSection)->getArrayCopy(); - - if (end($sectionItems) === $currentRouteItem) { - $branchRules = $currentSection->getBranchRules(); - } - - if ($branchRules->count() === 0) { - $testPartItems = $route->getCurrentTestPartRouteItems()->getArrayCopy(); - - if (end($testPartItems) === $currentRouteItem) { - $branchRules = $currentRouteItem->getTestPart()->getBranchRules(); - } - } - } - + $branchRules = $route->getEffectiveBranchRules(); $numberOfBranchRules = $branchRules->count(); // Branchings? diff --git a/src/qtism/runtime/tests/Route.php b/src/qtism/runtime/tests/Route.php index 87eb425d4..de895ecdc 100644 --- a/src/qtism/runtime/tests/Route.php +++ b/src/qtism/runtime/tests/Route.php @@ -34,6 +34,7 @@ use qtism\data\AssessmentSectionCollection; use qtism\data\AssessmentTest; use qtism\data\NavigationMode; +use qtism\data\rules\BranchRuleCollection; use qtism\data\SubmissionMode; use qtism\data\TestPart; use qtism\runtime\common\VariableIdentifier; @@ -1185,4 +1186,33 @@ public function getRouteItemPosition(RouteItem $routeItem): int throw new OutOfBoundsException($msg); } } + + public function getEffectiveBranchRules(): BranchRuleCollection + { + $currentRouteItem = $this->current(); + $branchRules = $currentRouteItem->getBranchRules(); + + if ($branchRules->count() > 0) { + return $branchRules; + } + + $currentSection = $currentRouteItem->getAssessmentSection(); + $sectionItems = $this->getRouteItemsByAssessmentSection($currentSection)->getArrayCopy(); + + if (end($sectionItems) === $currentRouteItem) { + $branchRules = $currentSection->getBranchRules(); + } + + if ($branchRules->count() > 0) { + return $branchRules; + } + + $testPartItems = $this->getCurrentTestPartRouteItems()->getArrayCopy(); + + if (end($testPartItems) === $currentRouteItem) { + $branchRules = $currentRouteItem->getTestPart()->getBranchRules(); + } + + return $branchRules; + } } From 613f9855cd4a63680553233ecef1c0e80aabc1cc Mon Sep 17 00:00:00 2001 From: Gabriel Felipe Soares Date: Tue, 26 Sep 2023 11:01:29 +0200 Subject: [PATCH 08/23] feat: create method to return effective preconditions --- .../runtime/tests/AssessmentTestSession.php | 6 +++-- src/qtism/runtime/tests/RouteItem.php | 24 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/qtism/runtime/tests/AssessmentTestSession.php b/src/qtism/runtime/tests/AssessmentTestSession.php index bb0b71a10..532d23ade 100644 --- a/src/qtism/runtime/tests/AssessmentTestSession.php +++ b/src/qtism/runtime/tests/AssessmentTestSession.php @@ -15,7 +15,7 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * - * Copyright (c) 2013-2020 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT); + * Copyright (c) 2013-2023 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT); * * @author Jérôme Bogaerts * @license GPLv2 @@ -2437,8 +2437,10 @@ protected function nextRouteItem($ignoreBranchings = false, $ignorePreConditions $route->next(); } + $preConditions = $route->current()->getEffectivePreConditions(); + // Preconditions on target? - if ($ignorePreConditions === false && $route->valid() === true && ($preConditions = $route->current()->getPreConditions()) && count($preConditions) > 0 && $this->mustApplyPreConditions() === true) { + if ($ignorePreConditions === false && $route->valid() === true && $preConditions->count() > 0 && $this->mustApplyPreConditions() === true) { for ($i = 0; $i < count($preConditions); $i++) { $engine = new ExpressionEngine($preConditions[$i]->getExpression(), $this); $condition = $engine->process(); diff --git a/src/qtism/runtime/tests/RouteItem.php b/src/qtism/runtime/tests/RouteItem.php index 444d19cbd..4280186cd 100644 --- a/src/qtism/runtime/tests/RouteItem.php +++ b/src/qtism/runtime/tests/RouteItem.php @@ -268,6 +268,30 @@ public function getPreConditions(): PreConditionCollection return $this->preConditions; } + /** + * Get the PreConditions that actually need to be applied considering all the parent elements: TestPart and Section + * + * @return PreConditionCollection A collection of PreCondition objects. + */ + public function getEffectivePreConditions(): PreConditionCollection + { + $routeItemPreConditions = new PreConditionCollection([]); + + foreach ($this->getTestPart()->getPreConditions() as $preCondition) { + $routeItemPreConditions->attach($preCondition); + } + + foreach ($this->getAssessmentSection()->getPreConditions() as $preCondition) { + $routeItemPreConditions->attach($preCondition); + } + + foreach ($this->getPreConditions() as $preCondition) { + $routeItemPreConditions->attach($preCondition); + } + + return $routeItemPreConditions; + } + /** * Set the PreCondition objects to be applied prior to the RouteItem. * From fd854bf774f2da3e818bc4308cc0c16c6fc9516b Mon Sep 17 00:00:00 2001 From: Gabriel Felipe Soares Date: Tue, 26 Sep 2023 14:05:38 +0200 Subject: [PATCH 09/23] chore: make the test part and section pre-conditions to be applied in item level --- src/qtism/runtime/tests/AbstractSessionManager.php | 1 - src/qtism/runtime/tests/AssessmentTestSession.php | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/qtism/runtime/tests/AbstractSessionManager.php b/src/qtism/runtime/tests/AbstractSessionManager.php index e65f139f4..68bf751ea 100644 --- a/src/qtism/runtime/tests/AbstractSessionManager.php +++ b/src/qtism/runtime/tests/AbstractSessionManager.php @@ -219,7 +219,6 @@ protected function createRoute(AssessmentTest $test): Route // Now, decorate last RouteItem of SelectableRoute with BranchRule objects if any. if (!empty($route) && $route->count() > 0) { $route->getLastRouteItem()->addBranchRules($testPart->getBranchRules()); - $route->getFirstRouteItem()->addPreConditions($testPart->getPreConditions()); } } diff --git a/src/qtism/runtime/tests/AssessmentTestSession.php b/src/qtism/runtime/tests/AssessmentTestSession.php index 532d23ade..085f2df33 100644 --- a/src/qtism/runtime/tests/AssessmentTestSession.php +++ b/src/qtism/runtime/tests/AssessmentTestSession.php @@ -2437,10 +2437,8 @@ protected function nextRouteItem($ignoreBranchings = false, $ignorePreConditions $route->next(); } - $preConditions = $route->current()->getEffectivePreConditions(); - // Preconditions on target? - if ($ignorePreConditions === false && $route->valid() === true && $preConditions->count() > 0 && $this->mustApplyPreConditions() === true) { + if ($ignorePreConditions === false && $route->valid() === true && ($preConditions = $route->current()->getEffectivePreConditions()) && count($preConditions) > 0 && $this->mustApplyPreConditions() === true) { for ($i = 0; $i < count($preConditions); $i++) { $engine = new ExpressionEngine($preConditions[$i]->getExpression(), $this); $condition = $engine->process(); From 831e5ca4bb2968075e1ab9876af55c9d7e1ea0b1 Mon Sep 17 00:00:00 2001 From: Andrei Shapiro Date: Tue, 26 Sep 2023 15:17:16 +0300 Subject: [PATCH 10/23] refactor: move branch selection to the RouteItem --- .../runtime/tests/AssessmentTestSession.php | 2 +- src/qtism/runtime/tests/Route.php | 30 --------- src/qtism/runtime/tests/RouteItem.php | 64 +++++++++++++++++++ 3 files changed, 65 insertions(+), 31 deletions(-) diff --git a/src/qtism/runtime/tests/AssessmentTestSession.php b/src/qtism/runtime/tests/AssessmentTestSession.php index 268d4ad2a..e118120b5 100644 --- a/src/qtism/runtime/tests/AssessmentTestSession.php +++ b/src/qtism/runtime/tests/AssessmentTestSession.php @@ -2406,7 +2406,7 @@ protected function nextRouteItem($ignoreBranchings = false, $ignorePreConditions $stop = false; while ($route->valid() === true && $stop === false) { - $branchRules = $route->getEffectiveBranchRules(); + $branchRules = $route->current()->getEffectiveBranchRules(); $numberOfBranchRules = $branchRules->count(); // Branchings? diff --git a/src/qtism/runtime/tests/Route.php b/src/qtism/runtime/tests/Route.php index de895ecdc..87eb425d4 100644 --- a/src/qtism/runtime/tests/Route.php +++ b/src/qtism/runtime/tests/Route.php @@ -34,7 +34,6 @@ use qtism\data\AssessmentSectionCollection; use qtism\data\AssessmentTest; use qtism\data\NavigationMode; -use qtism\data\rules\BranchRuleCollection; use qtism\data\SubmissionMode; use qtism\data\TestPart; use qtism\runtime\common\VariableIdentifier; @@ -1186,33 +1185,4 @@ public function getRouteItemPosition(RouteItem $routeItem): int throw new OutOfBoundsException($msg); } } - - public function getEffectiveBranchRules(): BranchRuleCollection - { - $currentRouteItem = $this->current(); - $branchRules = $currentRouteItem->getBranchRules(); - - if ($branchRules->count() > 0) { - return $branchRules; - } - - $currentSection = $currentRouteItem->getAssessmentSection(); - $sectionItems = $this->getRouteItemsByAssessmentSection($currentSection)->getArrayCopy(); - - if (end($sectionItems) === $currentRouteItem) { - $branchRules = $currentSection->getBranchRules(); - } - - if ($branchRules->count() > 0) { - return $branchRules; - } - - $testPartItems = $this->getCurrentTestPartRouteItems()->getArrayCopy(); - - if (end($testPartItems) === $currentRouteItem) { - $branchRules = $currentRouteItem->getTestPart()->getBranchRules(); - } - - return $branchRules; - } } diff --git a/src/qtism/runtime/tests/RouteItem.php b/src/qtism/runtime/tests/RouteItem.php index 444d19cbd..12af46064 100644 --- a/src/qtism/runtime/tests/RouteItem.php +++ b/src/qtism/runtime/tests/RouteItem.php @@ -426,4 +426,68 @@ public function getTimeLimits($excludeItem = false): RouteTimeLimitsCollection return $timeLimits; } + + public function getEffectiveBranchRules(): BranchRuleCollection + { + if ($this->getBranchRules()->count() > 0) { + return $this->getBranchRules(); + } + + $sectionBranchRules = $this->getEffectiveSectionBranchRules(); + + if ($sectionBranchRules === null || $sectionBranchRules->count() > 0) { + return $sectionBranchRules ?? new BranchRuleCollection(); + } + + $testPartSections = $this->getTestPart()->getAssessmentSections()->getArrayCopy(); + + if ( + end($testPartSections) === $this->getAssessmentSection() + && $this->getTestPart()->getBranchRules()->count() > 0 + ) { + return $this->getTestPart()->getBranchRules(); + } + + return new BranchRuleCollection(); + } + + /** + * Selects branching rules from the section/subsection. + * Branching rules will be selected only if the item or subsection is the last one in the parent section. + * + * @return ?BranchRuleCollection Returns the branching rules for the last section or null if the element/subsection + * is not the last. + */ + public function getEffectiveSectionBranchRules(): ?BranchRuleCollection + { + $sections = $this->getAssessmentSections()->getArrayCopy(); + $currentSection = array_pop($sections); + $currentSectionItems = $currentSection->getSectionParts()->getArrayCopy(); + + if (end($currentSectionItems) !== $this->getAssessmentItemRef()) { + return null; + } + + if ($currentSection->getBranchRules()->count() > 0) { + return $currentSection->getBranchRules(); + } + + $lastSection = $currentSection; + + foreach (array_reverse($sections) as $section) { + $sectionParts = $section->getSectionParts()->getArrayCopy(); + + if (end($sectionParts) !== $lastSection) { + return null; + } + + if ($section->getBranchRules()->count() > 0) { + return $section->getBranchRules(); + } + + $lastSection = $section; + } + + return new BranchRuleCollection(); + } } From d104236d6ce5a0e8e927a870cc84dee12ccc2615 Mon Sep 17 00:00:00 2001 From: Andrei Shapiro Date: Tue, 26 Sep 2023 16:10:47 +0300 Subject: [PATCH 11/23] chore: use last parent item section instead of currunt item subsection --- src/qtism/runtime/tests/RouteItem.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/qtism/runtime/tests/RouteItem.php b/src/qtism/runtime/tests/RouteItem.php index ac7f2ce78..696b2903e 100644 --- a/src/qtism/runtime/tests/RouteItem.php +++ b/src/qtism/runtime/tests/RouteItem.php @@ -464,9 +464,10 @@ public function getEffectiveBranchRules(): BranchRuleCollection } $testPartSections = $this->getTestPart()->getAssessmentSections()->getArrayCopy(); + $currentItemSections = $this->getAssessmentSections()->getArrayCopy(); if ( - end($testPartSections) === $this->getAssessmentSection() + end($testPartSections) === $currentItemSections[0] && $this->getTestPart()->getBranchRules()->count() > 0 ) { return $this->getTestPart()->getBranchRules(); @@ -482,9 +483,12 @@ public function getEffectiveBranchRules(): BranchRuleCollection * @return ?BranchRuleCollection Returns the branching rules for the last section or null if the element/subsection * is not the last. */ - public function getEffectiveSectionBranchRules(): ?BranchRuleCollection + private function getEffectiveSectionBranchRules(): ?BranchRuleCollection { + /** @var AssessmentSection[] $sections */ $sections = $this->getAssessmentSections()->getArrayCopy(); + + // Remove the current section from the section list, as this section contains a list of items, not sections. $currentSection = array_pop($sections); $currentSectionItems = $currentSection->getSectionParts()->getArrayCopy(); @@ -498,6 +502,9 @@ public function getEffectiveSectionBranchRules(): ?BranchRuleCollection $lastSection = $currentSection; + // Iterate through parent sections. + // Note: $sections should not contain the current section, as `$section->getSectionParts()` would then return a + // list of items instead of sections. foreach (array_reverse($sections) as $section) { $sectionParts = $section->getSectionParts()->getArrayCopy(); From c2738e621e00ecc78823dad63b05cabff3e32ee2 Mon Sep 17 00:00:00 2001 From: Andrei Shapiro Date: Tue, 26 Sep 2023 18:19:19 +0300 Subject: [PATCH 12/23] refactor: add methods to check if the item, subsection or section is the last in list --- src/qtism/data/AssessmentSection.php | 7 +++++++ src/qtism/data/TestPart.php | 7 +++++++ src/qtism/runtime/tests/RouteItem.php | 22 ++++++++-------------- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/qtism/data/AssessmentSection.php b/src/qtism/data/AssessmentSection.php index 58f0f59da..69a36a21f 100644 --- a/src/qtism/data/AssessmentSection.php +++ b/src/qtism/data/AssessmentSection.php @@ -320,4 +320,11 @@ public function getComponents(): QtiComponentCollection return new QtiComponentCollection($comp); } + + public function isLastSectionPart(SectionPart $sectionPart): bool + { + $sectionParts = $this->getSectionParts()->getArrayCopy(); + + return end($sectionParts) === $sectionPart; + } } diff --git a/src/qtism/data/TestPart.php b/src/qtism/data/TestPart.php index b9e7a01bf..a3fcb9618 100644 --- a/src/qtism/data/TestPart.php +++ b/src/qtism/data/TestPart.php @@ -419,4 +419,11 @@ public function __clone() { $this->setObservers(new SplObjectStorage()); } + + public function isLastSection(AssessmentSection $assessmentSection): bool + { + $sections = $this->getAssessmentSections()->getArrayCopy(); + + return end($sections) === $assessmentSection; + } } diff --git a/src/qtism/runtime/tests/RouteItem.php b/src/qtism/runtime/tests/RouteItem.php index 696b2903e..c7cf57535 100644 --- a/src/qtism/runtime/tests/RouteItem.php +++ b/src/qtism/runtime/tests/RouteItem.php @@ -463,25 +463,22 @@ public function getEffectiveBranchRules(): BranchRuleCollection return $sectionBranchRules ?? new BranchRuleCollection(); } - $testPartSections = $this->getTestPart()->getAssessmentSections()->getArrayCopy(); + $testPart = $this->getTestPart(); $currentItemSections = $this->getAssessmentSections()->getArrayCopy(); - if ( - end($testPartSections) === $currentItemSections[0] - && $this->getTestPart()->getBranchRules()->count() > 0 - ) { - return $this->getTestPart()->getBranchRules(); + if (!$testPart->isLastSection($currentItemSections[0])) { + return new BranchRuleCollection(); } - return new BranchRuleCollection(); + return $testPart->getBranchRules(); } /** * Selects branching rules from the section/subsection. * Branching rules will be selected only if the item or subsection is the last one in the parent section. * - * @return ?BranchRuleCollection Returns the branching rules for the last section or null if the element/subsection - * is not the last. + * @return BranchRuleCollection|null Returns the branching rules for the last section or null if the + * element/subsection is not the last. */ private function getEffectiveSectionBranchRules(): ?BranchRuleCollection { @@ -490,9 +487,8 @@ private function getEffectiveSectionBranchRules(): ?BranchRuleCollection // Remove the current section from the section list, as this section contains a list of items, not sections. $currentSection = array_pop($sections); - $currentSectionItems = $currentSection->getSectionParts()->getArrayCopy(); - if (end($currentSectionItems) !== $this->getAssessmentItemRef()) { + if ($currentSection === null || !$currentSection->isLastSectionPart($this->getAssessmentItemRef())) { return null; } @@ -506,9 +502,7 @@ private function getEffectiveSectionBranchRules(): ?BranchRuleCollection // Note: $sections should not contain the current section, as `$section->getSectionParts()` would then return a // list of items instead of sections. foreach (array_reverse($sections) as $section) { - $sectionParts = $section->getSectionParts()->getArrayCopy(); - - if (end($sectionParts) !== $lastSection) { + if (!$section->isLastSectionPart($lastSection)) { return null; } From 1bd8c24e532609646af06a92dbb37467ace50a0c Mon Sep 17 00:00:00 2001 From: Gabriel Felipe Soares Date: Tue, 26 Sep 2023 18:10:30 +0200 Subject: [PATCH 13/23] chore: remove necessity to add pre-conditions while factoring route --- src/qtism/runtime/tests/AbstractSessionManager.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/qtism/runtime/tests/AbstractSessionManager.php b/src/qtism/runtime/tests/AbstractSessionManager.php index 5f6b1650d..7bb0387cc 100644 --- a/src/qtism/runtime/tests/AbstractSessionManager.php +++ b/src/qtism/runtime/tests/AbstractSessionManager.php @@ -194,14 +194,6 @@ protected function createRoute(AssessmentTest $test): Route $route->appendRoute($r); } - // Add to the last item of the selection the branch rules of the AssessmentSection/testPart - // on which the selection is applied... Only if the route contains something (empty assessmentSection edge case). - if ($route->count() > 0) { - // Do the same as for branch rules for pre conditions, except that they must be - // attached on the first item of the route. - $route->getFirstRouteItem()->addPreConditions($current->getPreConditions()); - } - array_push($routeStack, $route); array_pop($assessmentSectionStack); } elseif ($current instanceof AssessmentItemRef) { From 24c9b3581e19589e8210a626023cc8a2f8e15e73 Mon Sep 17 00:00:00 2001 From: Gabriel Felipe Soares Date: Tue, 26 Sep 2023 18:53:02 +0200 Subject: [PATCH 14/23] chore: add unit test to cover pre-condition on test part and section --- ...AssessmentTestSessionPreConditionsTest.php | 24 +++++++++++++++++++ ...econditions_on_section_prevails_linear.xml | 20 ++++++++++++++++ ...onditions_on_test_part_prevails_linear.xml | 24 +++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 test/samples/custom/runtime/preconditions/preconditions_on_section_prevails_linear.xml create mode 100644 test/samples/custom/runtime/preconditions/preconditions_on_test_part_prevails_linear.xml diff --git a/test/qtismtest/runtime/tests/AssessmentTestSessionPreConditionsTest.php b/test/qtismtest/runtime/tests/AssessmentTestSessionPreConditionsTest.php index 9030ba643..15ea4cb00 100644 --- a/test/qtismtest/runtime/tests/AssessmentTestSessionPreConditionsTest.php +++ b/test/qtismtest/runtime/tests/AssessmentTestSessionPreConditionsTest.php @@ -163,4 +163,28 @@ public function testKillerTestEpicWin(): void $this::assertFalse($testSession->isRunning()); } + + public function testSectionPreConditionOnLinearTestWorks(): void + { + $testSession = self::instantiate(self::samplesDir() . 'custom/runtime/preconditions/preconditions_on_section_prevails_linear.xml'); + $testSession->beginTestSession(); + $testSession->beginAttempt(); + $testSession->moveNext(); + + // Q02 is skipped due precondition + $this::assertSame($testSession->getRoute()->current()->getAssessmentItemRef()->getIdentifier(), 'Q03'); + } + + public function testTestPartPreConditionOnLinearTestWorks(): void + { + $testSession = self::instantiate(self::samplesDir() . 'custom/runtime/preconditions/preconditions_on_test_part_prevails_linear.xml'); + $testSession->beginTestSession(); + $testSession->beginAttempt(); + $testSession->moveNext(); + + // P02, S02, Q02 is skipped due precondition + $this::assertSame($testSession->getRoute()->current()->getTestPart()->getIdentifier(), 'P03'); + $this::assertSame($testSession->getRoute()->current()->getAssessmentSection()->getIdentifier(), 'S03'); + $this::assertSame($testSession->getRoute()->current()->getAssessmentItemRef()->getIdentifier(), 'Q03'); + } } diff --git a/test/samples/custom/runtime/preconditions/preconditions_on_section_prevails_linear.xml b/test/samples/custom/runtime/preconditions/preconditions_on_section_prevails_linear.xml new file mode 100644 index 000000000..eb5a39667 --- /dev/null +++ b/test/samples/custom/runtime/preconditions/preconditions_on_section_prevails_linear.xml @@ -0,0 +1,20 @@ + + + + + + + + + false + + + + + + + + diff --git a/test/samples/custom/runtime/preconditions/preconditions_on_test_part_prevails_linear.xml b/test/samples/custom/runtime/preconditions/preconditions_on_test_part_prevails_linear.xml new file mode 100644 index 000000000..9902444d1 --- /dev/null +++ b/test/samples/custom/runtime/preconditions/preconditions_on_test_part_prevails_linear.xml @@ -0,0 +1,24 @@ + + + + + + + + + + false + + + + + + + + + + + From 85f9d100ef6e630175ec57053d2bfba81b59a21f Mon Sep 17 00:00:00 2001 From: Gabriel Felipe Soares Date: Wed, 27 Sep 2023 11:06:27 +0200 Subject: [PATCH 15/23] chore: simplify test and cover all cases on a single xml --- .../runtime/tests/AssessmentTestSession.php | 10 ++- ...AssessmentTestSessionPreConditionsTest.php | 30 +++---- ...econditions_on_section_prevails_linear.xml | 20 ----- ...onditions_on_test_part_prevails_linear.xml | 24 ------ ...test_part_section_item_combined_linear.xml | 79 +++++++++++++++++++ 5 files changed, 102 insertions(+), 61 deletions(-) delete mode 100644 test/samples/custom/runtime/preconditions/preconditions_on_section_prevails_linear.xml delete mode 100644 test/samples/custom/runtime/preconditions/preconditions_on_test_part_prevails_linear.xml create mode 100644 test/samples/custom/runtime/preconditions/preconditions_on_test_part_section_item_combined_linear.xml diff --git a/src/qtism/runtime/tests/AssessmentTestSession.php b/src/qtism/runtime/tests/AssessmentTestSession.php index 6e26df62a..f5eee8c4f 100644 --- a/src/qtism/runtime/tests/AssessmentTestSession.php +++ b/src/qtism/runtime/tests/AssessmentTestSession.php @@ -2441,16 +2441,20 @@ protected function nextRouteItem($ignoreBranchings = false, $ignorePreConditions // Preconditions on target? if ($ignorePreConditions === false && $route->valid() === true && ($preConditions = $route->current()->getEffectivePreConditions()) && count($preConditions) > 0 && $this->mustApplyPreConditions() === true) { + $preConditionFailed = false; + for ($i = 0; $i < count($preConditions); $i++) { $engine = new ExpressionEngine($preConditions[$i]->getExpression(), $this); $condition = $engine->process(); - if ($condition !== null && $condition->getValue() === true) { - // The item must be presented. - $stop = true; + if ($condition === null || $condition->getValue() === false) { + // The item must NOT be presented. + $preConditionFailed = true; break; } } + + $stop = !$preConditionFailed; } else { $stop = true; } diff --git a/test/qtismtest/runtime/tests/AssessmentTestSessionPreConditionsTest.php b/test/qtismtest/runtime/tests/AssessmentTestSessionPreConditionsTest.php index 15ea4cb00..f3ac97d50 100644 --- a/test/qtismtest/runtime/tests/AssessmentTestSessionPreConditionsTest.php +++ b/test/qtismtest/runtime/tests/AssessmentTestSessionPreConditionsTest.php @@ -164,27 +164,29 @@ public function testKillerTestEpicWin(): void $this::assertFalse($testSession->isRunning()); } - public function testSectionPreConditionOnLinearTestWorks(): void + public function testTestParAndSectionAndItemPreConditionOnLinearTestWorks(): void { - $testSession = self::instantiate(self::samplesDir() . 'custom/runtime/preconditions/preconditions_on_section_prevails_linear.xml'); + $testSession = self::instantiate(self::samplesDir() . 'custom/runtime/preconditions/preconditions_on_test_part_section_item_combined_linear.xml'); $testSession->beginTestSession(); $testSession->beginAttempt(); $testSession->moveNext(); - // Q02 is skipped due precondition - $this::assertSame($testSession->getRoute()->current()->getAssessmentItemRef()->getIdentifier(), 'Q03'); - } + // P02, S03, Q04 are skipped due precondition, but Q04.1 is passed + $this::assertSame($testSession->getRoute()->current()->getAssessmentItemRef()->getIdentifier(), 'Q04.1'); + + $testSession->moveNext(); + + // P05 precondition passed + $this::assertSame($testSession->getRoute()->current()->getAssessmentItemRef()->getIdentifier(), 'Q05'); + + $testSession->moveNext(); + + // S06 precondition passed + $this::assertSame($testSession->getRoute()->current()->getAssessmentItemRef()->getIdentifier(), 'Q06'); - public function testTestPartPreConditionOnLinearTestWorks(): void - { - $testSession = self::instantiate(self::samplesDir() . 'custom/runtime/preconditions/preconditions_on_test_part_prevails_linear.xml'); - $testSession->beginTestSession(); - $testSession->beginAttempt(); $testSession->moveNext(); - // P02, S02, Q02 is skipped due precondition - $this::assertSame($testSession->getRoute()->current()->getTestPart()->getIdentifier(), 'P03'); - $this::assertSame($testSession->getRoute()->current()->getAssessmentSection()->getIdentifier(), 'S03'); - $this::assertSame($testSession->getRoute()->current()->getAssessmentItemRef()->getIdentifier(), 'Q03'); + // S07 precondition passed + $this::assertSame($testSession->getRoute()->current()->getAssessmentItemRef()->getIdentifier(), 'Q07'); } } diff --git a/test/samples/custom/runtime/preconditions/preconditions_on_section_prevails_linear.xml b/test/samples/custom/runtime/preconditions/preconditions_on_section_prevails_linear.xml deleted file mode 100644 index eb5a39667..000000000 --- a/test/samples/custom/runtime/preconditions/preconditions_on_section_prevails_linear.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - false - - - - - - - - diff --git a/test/samples/custom/runtime/preconditions/preconditions_on_test_part_prevails_linear.xml b/test/samples/custom/runtime/preconditions/preconditions_on_test_part_prevails_linear.xml deleted file mode 100644 index 9902444d1..000000000 --- a/test/samples/custom/runtime/preconditions/preconditions_on_test_part_prevails_linear.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - false - - - - - - - - - - - diff --git a/test/samples/custom/runtime/preconditions/preconditions_on_test_part_section_item_combined_linear.xml b/test/samples/custom/runtime/preconditions/preconditions_on_test_part_section_item_combined_linear.xml new file mode 100644 index 000000000..5e3932bb9 --- /dev/null +++ b/test/samples/custom/runtime/preconditions/preconditions_on_test_part_section_item_combined_linear.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + false + + + + + + + + + + false + + + + + + + + + true + + + + true + + + + false + + + + + true + + + + + + + + true + + + + + + + + + + true + + + + + + + + + + true + + + + + From 0657936271466f04f68011ecf4b09f8d9c83a0f7 Mon Sep 17 00:00:00 2001 From: Andrei Shapiro Date: Thu, 28 Sep 2023 02:46:24 +0300 Subject: [PATCH 16/23] test: add test to check branching rules on each test element: test part, section, subsection, item --- .../AssessmentTestSessionBranchingsTest.php | 40 +++++++ .../runtime/branchings/branching_rules.xml | 103 ++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 test/samples/custom/runtime/branchings/branching_rules.xml diff --git a/test/qtismtest/runtime/tests/AssessmentTestSessionBranchingsTest.php b/test/qtismtest/runtime/tests/AssessmentTestSessionBranchingsTest.php index a1fc8f8c0..9523d1fcb 100644 --- a/test/qtismtest/runtime/tests/AssessmentTestSessionBranchingsTest.php +++ b/test/qtismtest/runtime/tests/AssessmentTestSessionBranchingsTest.php @@ -266,4 +266,44 @@ public function testBranchingOnTestPartsSimple1(): void $this->assertEquals('Q03', $session->getCurrentAssessmentItemRef()->getIdentifier()); $this->assertEquals('P03', $session->getCurrentTestPart()->getIdentifier()); } + + public function testBranchingRules(): void + { + $session = self::instantiate(self::samplesDir() . 'custom/runtime/branchings/branching_rules.xml'); + $session->beginTestSession(); + + $this->assertEquals('testPart-1', $session->getCurrentTestPart()->getIdentifier()); + $this->assertEquals('assessmentSection-1', $session->getCurrentAssessmentSection()->getIdentifier()); + $this->assertEquals('item-1', $session->getCurrentAssessmentItemRef()->getIdentifier()); + + $session->moveNext(); + + $this->assertEquals('testPart-1', $session->getCurrentTestPart()->getIdentifier()); + $this->assertEquals('assessmentSection-1', $session->getCurrentAssessmentSection()->getIdentifier()); + $this->assertEquals('item-3', $session->getCurrentAssessmentItemRef()->getIdentifier()); + + $session->moveNext(); + + $this->assertEquals('testPart-1', $session->getCurrentTestPart()->getIdentifier()); + $this->assertEquals('assessmentSection-3', $session->getCurrentAssessmentSection()->getIdentifier()); + $this->assertEquals('item-5', $session->getCurrentAssessmentItemRef()->getIdentifier()); + + $session->moveNext(); + + $this->assertEquals('testPart-3', $session->getCurrentTestPart()->getIdentifier()); + $this->assertEquals('assessmentSection-5', $session->getCurrentAssessmentSection()->getIdentifier()); + $this->assertEquals('item-7', $session->getCurrentAssessmentItemRef()->getIdentifier()); + + $session->moveNext(); + + $this->assertEquals('testPart-4', $session->getCurrentTestPart()->getIdentifier()); + $this->assertEquals('subsection-1', $session->getCurrentAssessmentSection()->getIdentifier()); + $this->assertEquals('item-9', $session->getCurrentAssessmentItemRef()->getIdentifier()); + + $session->moveNext(); + + $this->assertEquals('testPart-5', $session->getCurrentTestPart()->getIdentifier()); + $this->assertEquals('assessmentSection-8', $session->getCurrentAssessmentSection()->getIdentifier()); + $this->assertEquals('item-11', $session->getCurrentAssessmentItemRef()->getIdentifier()); + } } diff --git a/test/samples/custom/runtime/branchings/branching_rules.xml b/test/samples/custom/runtime/branchings/branching_rules.xml new file mode 100644 index 000000000..cf9e3557e --- /dev/null +++ b/test/samples/custom/runtime/branchings/branching_rules.xml @@ -0,0 +1,103 @@ + + + + + true + + + + + true + + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + + + + + + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From c4b6a1b91acbdf0c3823e62114d156abae058b35 Mon Sep 17 00:00:00 2001 From: Andrei Shapiro Date: Thu, 28 Sep 2023 13:11:08 +0300 Subject: [PATCH 17/23] chore: add and class props to SectionPart, rely on them --- .../common/collections/AbstractCollection.php | 5 ++ src/qtism/data/AssessmentSection.php | 9 +++ src/qtism/data/SectionPart.php | 24 ++++++++ src/qtism/data/TestPart.php | 29 ++++++---- src/qtism/runtime/tests/RouteItem.php | 58 ++++--------------- 5 files changed, 66 insertions(+), 59 deletions(-) diff --git a/src/qtism/common/collections/AbstractCollection.php b/src/qtism/common/collections/AbstractCollection.php index 2c3c57ccd..d17354245 100644 --- a/src/qtism/common/collections/AbstractCollection.php +++ b/src/qtism/common/collections/AbstractCollection.php @@ -96,6 +96,11 @@ public function count(): int return count($this->dataPlaceHolder); } + public function isEmpty(): bool + { + return empty($this->dataPlaceHolder); + } + /** * Return the current element of the collection while iterating. * diff --git a/src/qtism/data/AssessmentSection.php b/src/qtism/data/AssessmentSection.php index 69a36a21f..698be5585 100644 --- a/src/qtism/data/AssessmentSection.php +++ b/src/qtism/data/AssessmentSection.php @@ -288,6 +288,15 @@ public function getSectionParts(): SectionPartCollection */ public function setSectionParts(SectionPartCollection $sectionParts): void { + if (!$sectionParts->isEmpty()) { + /** @var SectionPart $sectionPart */ + foreach ($sectionParts as $sectionPart) { + $sectionPart->setParent($this); + } + + $sectionPart->setIsLast(true); + } + $this->sectionParts = $sectionParts; } diff --git a/src/qtism/data/SectionPart.php b/src/qtism/data/SectionPart.php index c146c58d0..298cfecd0 100644 --- a/src/qtism/data/SectionPart.php +++ b/src/qtism/data/SectionPart.php @@ -117,6 +117,10 @@ class SectionPart extends QtiComponent implements QtiIdentifiable, Shufflable */ private $timeLimits = null; + private ?SectionPart $parent = null; + + private bool $isLast = false; + /** * Create a new instance of SectionPart. * @@ -363,4 +367,24 @@ public function __clone() // Reset observers. $this->setObservers(new SplObjectStorage()); } + + public function getParent(): ?SectionPart + { + return $this->parent; + } + + public function setParent(SectionPart $parent): void + { + $this->parent = $parent; + } + + public function isLast(): bool + { + return $this->isLast; + } + + public function setIsLast(bool $isLast): void + { + $this->isLast = $isLast; + } } diff --git a/src/qtism/data/TestPart.php b/src/qtism/data/TestPart.php index a3fcb9618..7c8f97147 100644 --- a/src/qtism/data/TestPart.php +++ b/src/qtism/data/TestPart.php @@ -348,20 +348,25 @@ public function getAssessmentSections(): SectionPartCollection */ public function setAssessmentSections(SectionPartCollection $assessmentSections): void { - if (count($assessmentSections) > 0) { - // Check that we have only AssessmentSection and/ord AssessmentSectionRef objects. - foreach ($assessmentSections as $assessmentSection) { - if (!$assessmentSection instanceof AssessmentSection && !$assessmentSection instanceof AssessmentSectionRef) { - $msg = 'A TestPart contain only contain AssessmentSection or AssessmentSectionRef objects.'; - throw new InvalidArgumentException($msg); - } - } + if ($assessmentSections->isEmpty()) { + throw new InvalidArgumentException('A TestPart must contain at least one AssessmentSection.'); + } - $this->assessmentSections = $assessmentSections; - } else { - $msg = 'A TestPart must contain at least one AssessmentSection.'; - throw new InvalidArgumentException($msg); + // Check that we have only AssessmentSection and/ord AssessmentSectionRef objects. + foreach ($assessmentSections as $assessmentSection) { + if ( + !$assessmentSection instanceof AssessmentSection + && !$assessmentSection instanceof AssessmentSectionRef + ) { + throw new InvalidArgumentException( + 'A TestPart contain only contain AssessmentSection or AssessmentSectionRef objects.' + ); + } } + + $assessmentSection->setIsLast(true); + + $this->assessmentSections = $assessmentSections; } /** diff --git a/src/qtism/runtime/tests/RouteItem.php b/src/qtism/runtime/tests/RouteItem.php index c7cf57535..4245520b9 100644 --- a/src/qtism/runtime/tests/RouteItem.php +++ b/src/qtism/runtime/tests/RouteItem.php @@ -453,66 +453,30 @@ public function getTimeLimits($excludeItem = false): RouteTimeLimitsCollection public function getEffectiveBranchRules(): BranchRuleCollection { - if ($this->getBranchRules()->count() > 0) { + if (!$this->getBranchRules()->isEmpty()) { return $this->getBranchRules(); } - $sectionBranchRules = $this->getEffectiveSectionBranchRules(); - - if ($sectionBranchRules === null || $sectionBranchRules->count() > 0) { - return $sectionBranchRules ?? new BranchRuleCollection(); - } - - $testPart = $this->getTestPart(); - $currentItemSections = $this->getAssessmentSections()->getArrayCopy(); - - if (!$testPart->isLastSection($currentItemSections[0])) { - return new BranchRuleCollection(); - } - - return $testPart->getBranchRules(); - } - - /** - * Selects branching rules from the section/subsection. - * Branching rules will be selected only if the item or subsection is the last one in the parent section. - * - * @return BranchRuleCollection|null Returns the branching rules for the last section or null if the - * element/subsection is not the last. - */ - private function getEffectiveSectionBranchRules(): ?BranchRuleCollection - { /** @var AssessmentSection[] $sections */ $sections = $this->getAssessmentSections()->getArrayCopy(); + $currentSectionPart = $this->getAssessmentItemRef(); - // Remove the current section from the section list, as this section contains a list of items, not sections. - $currentSection = array_pop($sections); - - if ($currentSection === null || !$currentSection->isLastSectionPart($this->getAssessmentItemRef())) { - return null; - } - - if ($currentSection->getBranchRules()->count() > 0) { - return $currentSection->getBranchRules(); - } - - $lastSection = $currentSection; - - // Iterate through parent sections. - // Note: $sections should not contain the current section, as `$section->getSectionParts()` would then return a - // list of items instead of sections. foreach (array_reverse($sections) as $section) { - if (!$section->isLastSectionPart($lastSection)) { - return null; + if (!$currentSectionPart->isLast()) { + return new BranchRuleCollection(); } - if ($section->getBranchRules()->count() > 0) { + if (!$section->getBranchRules()->isEmpty()) { return $section->getBranchRules(); } - $lastSection = $section; + $currentSectionPart = $section; + } + + if (!$sections[0]->isLast()) { + return new BranchRuleCollection(); } - return new BranchRuleCollection(); + return $this->getTestPart()->getBranchRules(); } } From 045e56cea97d32d51d3cf8bca41936c4bfbee66c Mon Sep 17 00:00:00 2001 From: Andrei Shapiro Date: Thu, 28 Sep 2023 13:43:41 +0300 Subject: [PATCH 18/23] chore: use while instead of foreach, rely on getParent method --- src/qtism/runtime/tests/RouteItem.php | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/qtism/runtime/tests/RouteItem.php b/src/qtism/runtime/tests/RouteItem.php index 4245520b9..bea931fea 100644 --- a/src/qtism/runtime/tests/RouteItem.php +++ b/src/qtism/runtime/tests/RouteItem.php @@ -453,27 +453,33 @@ public function getTimeLimits($excludeItem = false): RouteTimeLimitsCollection public function getEffectiveBranchRules(): BranchRuleCollection { + // Checking branching rules at the Item level if (!$this->getBranchRules()->isEmpty()) { return $this->getBranchRules(); } - /** @var AssessmentSection[] $sections */ - $sections = $this->getAssessmentSections()->getArrayCopy(); $currentSectionPart = $this->getAssessmentItemRef(); - foreach (array_reverse($sections) as $section) { + // Checking branching rules at the Section/Subsection level + // To get branching rules for the current section, you need to make sure that the current part of the + // section (item or subsection) is the last part of the current section. + while ($parent = $currentSectionPart->getParent()) { if (!$currentSectionPart->isLast()) { return new BranchRuleCollection(); } - if (!$section->getBranchRules()->isEmpty()) { - return $section->getBranchRules(); + if (!$parent->getBranchRules()->isEmpty()) { + return $parent->getBranchRules(); } - $currentSectionPart = $section; + // If there are no branching rules for the current section, we proceed to check its parent section + // (if it exists) + $currentSectionPart = $parent; } - if (!$sections[0]->isLast()) { + // Checking branching rules at the Test Part level + // In this case, $currentSectionPart is the top-level section of the current item + if (!$currentSectionPart->isLast()) { return new BranchRuleCollection(); } From 68bd42b8f627b03eb2e7ad674c99e82ef16d6a30 Mon Sep 17 00:00:00 2001 From: Andrei Shapiro Date: Thu, 28 Sep 2023 14:07:21 +0300 Subject: [PATCH 19/23] chore: remove comment --- src/qtism/data/TestPart.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/qtism/data/TestPart.php b/src/qtism/data/TestPart.php index 7c8f97147..5453db2d9 100644 --- a/src/qtism/data/TestPart.php +++ b/src/qtism/data/TestPart.php @@ -352,7 +352,6 @@ public function setAssessmentSections(SectionPartCollection $assessmentSections) throw new InvalidArgumentException('A TestPart must contain at least one AssessmentSection.'); } - // Check that we have only AssessmentSection and/ord AssessmentSectionRef objects. foreach ($assessmentSections as $assessmentSection) { if ( !$assessmentSection instanceof AssessmentSection From 41360e652901981aee49e7f1250e2d64526c5aeb Mon Sep 17 00:00:00 2001 From: Andrei Shapiro Date: Thu, 28 Sep 2023 14:41:17 +0300 Subject: [PATCH 20/23] chore: use do-while --- src/qtism/runtime/tests/RouteItem.php | 31 +++++++++++---------------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/src/qtism/runtime/tests/RouteItem.php b/src/qtism/runtime/tests/RouteItem.php index bea931fea..c29e9a75b 100644 --- a/src/qtism/runtime/tests/RouteItem.php +++ b/src/qtism/runtime/tests/RouteItem.php @@ -458,31 +458,26 @@ public function getEffectiveBranchRules(): BranchRuleCollection return $this->getBranchRules(); } - $currentSectionPart = $this->getAssessmentItemRef(); + if (!$this->getAssessmentItemRef()->isLast()) { + return new BranchRuleCollection(); + } + + $parentSection = $this->getAssessmentSection(); // Checking branching rules at the Section/Subsection level // To get branching rules for the current section, you need to make sure that the current part of the - // section (item or subsection) is the last part of the current section. - while ($parent = $currentSectionPart->getParent()) { - if (!$currentSectionPart->isLast()) { - return new BranchRuleCollection(); + // section is the last part of the current section. + do { + if (!$parentSection->getBranchRules()->isEmpty()) { + return $parentSection->getBranchRules(); } - if (!$parent->getBranchRules()->isEmpty()) { - return $parent->getBranchRules(); + if (!$parentSection->isLast()) { + return new BranchRuleCollection(); } + } while ($parentSection = $parentSection->getParent()); - // If there are no branching rules for the current section, we proceed to check its parent section - // (if it exists) - $currentSectionPart = $parent; - } - - // Checking branching rules at the Test Part level - // In this case, $currentSectionPart is the top-level section of the current item - if (!$currentSectionPart->isLast()) { - return new BranchRuleCollection(); - } - + // Return branching rules from the Test Part level return $this->getTestPart()->getBranchRules(); } } From 5f1a8b26d2f5404694e959eeef0fdc95f575b864 Mon Sep 17 00:00:00 2001 From: Gabriel Felipe Soares Date: Thu, 5 Oct 2023 15:23:31 +0200 Subject: [PATCH 21/23] feat: add specific method to test preconditions on session that can be used externally --- .../runtime/tests/AssessmentTestSession.php | 54 +++++++++++++------ 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/src/qtism/runtime/tests/AssessmentTestSession.php b/src/qtism/runtime/tests/AssessmentTestSession.php index f5eee8c4f..d3cdef245 100644 --- a/src/qtism/runtime/tests/AssessmentTestSession.php +++ b/src/qtism/runtime/tests/AssessmentTestSession.php @@ -2440,21 +2440,8 @@ protected function nextRouteItem($ignoreBranchings = false, $ignorePreConditions } // Preconditions on target? - if ($ignorePreConditions === false && $route->valid() === true && ($preConditions = $route->current()->getEffectivePreConditions()) && count($preConditions) > 0 && $this->mustApplyPreConditions() === true) { - $preConditionFailed = false; - - for ($i = 0; $i < count($preConditions); $i++) { - $engine = new ExpressionEngine($preConditions[$i]->getExpression(), $this); - $condition = $engine->process(); - - if ($condition === null || $condition->getValue() === false) { - // The item must NOT be presented. - $preConditionFailed = true; - break; - } - } - - $stop = !$preConditionFailed; + if ($ignorePreConditions === false && $route->valid() && $this->mustApplyPreConditions()) { + $stop = $this->routeItemMatchesPreconditions($route->current()); } else { $stop = true; } @@ -2471,6 +2458,41 @@ protected function nextRouteItem($ignoreBranchings = false, $ignorePreConditions } } + public function routeMatchesPreconditions(): bool + { + $route = $this->getRoute(); + + if (!$route->valid()) { + return true; + } + + if (!$this->mustApplyPreConditions()) { + return true; + } + + return $this->routeItemMatchesPreconditions($route->current()); + } + + private function routeItemMatchesPreconditions(RouteItem $routeItem): bool + { + $preConditions = $routeItem->getEffectivePreConditions(); + + if ($preConditions->count() === 0) { + return true; + } + + for ($i = 0; $i < $preConditions->count(); $i++) { + $engine = new ExpressionEngine($preConditions[$i]->getExpression(), $this); + $condition = $engine->process(); + + if ($condition === null || $condition->getValue() === false) { + return false; + } + } + + return true; + } + /** * Set the position in the Route at the very next TestPart in the Route sequence or, if the current * testPart is the last one of the test session, the test session ends gracefully. If the submission mode @@ -3130,6 +3152,6 @@ protected function mustApplyBranchRules(): bool protected function mustApplyPreConditions($nextRouteItem = false): bool { $routeItem = ($nextRouteItem === false) ? $this->getCurrentRouteItem() : $this->getRoute()->getNext(); - return ($routeItem->getTestPart()->getNavigationMode() === NavigationMode::LINEAR || $this->mustForcePreconditions() === true); + return ($routeItem && $routeItem->getTestPart()->getNavigationMode() === NavigationMode::LINEAR) || $this->mustForcePreconditions() === true; } } From 97cfaf2fed028d7ab77a4411da437617f3a9ef9f Mon Sep 17 00:00:00 2001 From: Gabriel Felipe Soares Date: Mon, 9 Oct 2023 17:18:04 +0200 Subject: [PATCH 22/23] feat: handle test part preconditions when non-linear --- .../runtime/tests/AssessmentTestSession.php | 52 +++++++++++++------ ...AssessmentTestSessionPreConditionsTest.php | 5 ++ ...test_part_section_item_combined_linear.xml | 19 +++++++ 3 files changed, 60 insertions(+), 16 deletions(-) diff --git a/src/qtism/runtime/tests/AssessmentTestSession.php b/src/qtism/runtime/tests/AssessmentTestSession.php index d3cdef245..c6fc4a431 100644 --- a/src/qtism/runtime/tests/AssessmentTestSession.php +++ b/src/qtism/runtime/tests/AssessmentTestSession.php @@ -43,6 +43,7 @@ use qtism\data\IAssessmentItem; use qtism\data\NavigationMode; use qtism\data\processing\ResponseProcessing; +use qtism\data\rules\PreConditionCollection; use qtism\data\ShowHide; use qtism\data\state\Weight; use qtism\data\storage\php\PhpStorageException; @@ -2440,11 +2441,7 @@ protected function nextRouteItem($ignoreBranchings = false, $ignorePreConditions } // Preconditions on target? - if ($ignorePreConditions === false && $route->valid() && $this->mustApplyPreConditions()) { - $stop = $this->routeItemMatchesPreconditions($route->current()); - } else { - $stop = true; - } + $stop = !($ignorePreConditions === false) || $this->routeMatchesPreconditions($route); // After a first iteration, we will not performed branching again, as they are executed // as soon as we leave an item. Chains of branch rules are not expected. @@ -2458,25 +2455,31 @@ protected function nextRouteItem($ignoreBranchings = false, $ignorePreConditions } } - public function routeMatchesPreconditions(): bool + public function routeMatchesPreconditions(Route $route = null): bool { - $route = $this->getRoute(); + $route = $route ?? $this->getRoute(); if (!$route->valid()) { return true; } - if (!$this->mustApplyPreConditions()) { - return true; + $routeItem = $route->current(); + $testPart = $routeItem->getTestPart(); + $navigationMode = $testPart->getNavigationMode(); + + if ($navigationMode === NavigationMode::LINEAR || $this->mustForcePreconditions()) { + return $this->preConditionsMatch($routeItem->getEffectivePreConditions()); } - return $this->routeItemMatchesPreconditions($route->current()); + if ($navigationMode === NavigationMode::NONLINEAR) { + return $this->preConditionsMatch($testPart->getPreConditions()); + } + + return true; } - private function routeItemMatchesPreconditions(RouteItem $routeItem): bool + private function preConditionsMatch(PreConditionCollection $preConditions): bool { - $preConditions = $routeItem->getEffectivePreConditions(); - if ($preConditions->count() === 0) { return true; } @@ -3081,7 +3084,7 @@ public function isNextRouteItemPredictible(): bool } // Case 4. The next item has preconditions. - if ($this->mustApplyPreConditions(true) && count($this->getRoute()->getNext()->getPreConditions()) > 0) { + if ($this->mustApplyPreConditions(true)) { return false; } @@ -3151,7 +3154,24 @@ protected function mustApplyBranchRules(): bool */ protected function mustApplyPreConditions($nextRouteItem = false): bool { - $routeItem = ($nextRouteItem === false) ? $this->getCurrentRouteItem() : $this->getRoute()->getNext(); - return ($routeItem && $routeItem->getTestPart()->getNavigationMode() === NavigationMode::LINEAR) || $this->mustForcePreconditions() === true; + if ($this->mustForcePreconditions()) { + return true; + } + + $routeItem = $nextRouteItem === false ? $this->getCurrentRouteItem() : $this->getRoute()->getNext(); + + if (!$routeItem instanceof RouteItem) { + return false; + } + + $testPart = $routeItem->getTestPart(); + $navigationMode = $testPart->getNavigationMode(); + + if ($navigationMode === NavigationMode::LINEAR) { + return $routeItem->getEffectivePreConditions()->count() > 0; + } + + // Now NonLinear Test part pre-conditions must be considered + return $navigationMode === NavigationMode::NONLINEAR && $testPart->getPreConditions()->count() > 0; } } diff --git a/test/qtismtest/runtime/tests/AssessmentTestSessionPreConditionsTest.php b/test/qtismtest/runtime/tests/AssessmentTestSessionPreConditionsTest.php index f3ac97d50..31a9af711 100644 --- a/test/qtismtest/runtime/tests/AssessmentTestSessionPreConditionsTest.php +++ b/test/qtismtest/runtime/tests/AssessmentTestSessionPreConditionsTest.php @@ -188,5 +188,10 @@ public function testTestParAndSectionAndItemPreConditionOnLinearTestWorks(): voi // S07 precondition passed $this::assertSame($testSession->getRoute()->current()->getAssessmentItemRef()->getIdentifier(), 'Q07'); + + $testSession->moveNext(); + + // P08 is nonlinear, but it will be skipped, cause pre-conditions apply to non-linear test parts + $this::assertSame($testSession->getRoute()->current()->getAssessmentItemRef()->getIdentifier(), 'Q09'); } } diff --git a/test/samples/custom/runtime/preconditions/preconditions_on_test_part_section_item_combined_linear.xml b/test/samples/custom/runtime/preconditions/preconditions_on_test_part_section_item_combined_linear.xml index 5e3932bb9..d18d736b8 100644 --- a/test/samples/custom/runtime/preconditions/preconditions_on_test_part_section_item_combined_linear.xml +++ b/test/samples/custom/runtime/preconditions/preconditions_on_test_part_section_item_combined_linear.xml @@ -76,4 +76,23 @@ + + + + false + + + + + + + + + + + true + + + + From a5b719ad2eb130a9636103e7c6ab4ef264fa17f3 Mon Sep 17 00:00:00 2001 From: Andrei Shapiro Date: Wed, 11 Oct 2023 11:16:32 +0300 Subject: [PATCH 23/23] feat: allow to branch to the same test part, do not ignore test part branch rules --- src/qtism/runtime/tests/AssessmentTestSession.php | 14 ++++++++++++++ src/qtism/runtime/tests/Route.php | 8 -------- test/qtismtest/runtime/tests/RouteTest.php | 4 ++-- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/qtism/runtime/tests/AssessmentTestSession.php b/src/qtism/runtime/tests/AssessmentTestSession.php index c6fc4a431..304d82e5f 100644 --- a/src/qtism/runtime/tests/AssessmentTestSession.php +++ b/src/qtism/runtime/tests/AssessmentTestSession.php @@ -43,6 +43,7 @@ use qtism\data\IAssessmentItem; use qtism\data\NavigationMode; use qtism\data\processing\ResponseProcessing; +use qtism\data\rules\BranchRule; use qtism\data\rules\PreConditionCollection; use qtism\data\ShowHide; use qtism\data\state\Weight; @@ -2514,8 +2515,21 @@ public function moveNextTestPart(): void $route = $this->getRoute(); $from = $route->current(); + $branchRules = $from->getTestPart()->getBranchRules(); while ($route->valid() === true && $route->current()->getTestPart() === $from->getTestPart()) { + /** @var BranchRule $branchRule */ + foreach ($branchRules as $branchRule) { + $engine = new ExpressionEngine($branchRule->getExpression(), $this); + $condition = $engine->process(); + + if ($condition !== null && $condition->getValue() === true) { + $route->branch($branchRule->getTarget()); + + break 2; + } + } + $route->next(); } diff --git a/src/qtism/runtime/tests/Route.php b/src/qtism/runtime/tests/Route.php index 87eb425d4..e2f36f71b 100644 --- a/src/qtism/runtime/tests/Route.php +++ b/src/qtism/runtime/tests/Route.php @@ -1150,14 +1150,6 @@ public function branch($identifier): void // Check for a testPart. $testPartIdentifierMap = $this->getTestPartIdentifierMap(); if (isset($testPartIdentifierMap[$id])) { - // We branch to the first RouteItem belonging to the testPart. - if ($testPartIdentifierMap[$id][0]->getTestPart() === $this->current()->getTestPart()) { - // From IMS QTI: - // For testParts, the target must refer to another testPart. - $msg = 'Cannot branch to the same testPart.'; - throw new OutOfBoundsException($msg); - } - // We branch to the first RouteItem belonging to the testPart. $this->setPosition($this->getRouteItemPosition($testPartIdentifierMap[$id][0])); diff --git a/test/qtismtest/runtime/tests/RouteTest.php b/test/qtismtest/runtime/tests/RouteTest.php index d52abcdd2..298f1b779 100644 --- a/test/qtismtest/runtime/tests/RouteTest.php +++ b/test/qtismtest/runtime/tests/RouteTest.php @@ -610,9 +610,9 @@ public function testBranchToAssessmentItemRefOutsideOfTestPart(): void public function testBranchToSameTestPart(): void { $route = self::buildSimpleRoute(Route::class, 2, 1); - $this->expectException(OutOfBoundsException::class); - $this->expectExceptionMessage('Cannot branch to the same testPart.'); $route->branch('T1'); + + $this->assertEquals('T1', $route->current()->getTestPart()->getIdentifier()); } public function testBranchToAnotherTestPart(): void