Skip to content

Commit 8e96782

Browse files
author
Martin Krulis
committed
Implementing CoW for exercise tests (and updating GC to clean abandoned tests).
1 parent 9973cd7 commit 8e96782

File tree

8 files changed

+231
-57
lines changed

8 files changed

+231
-57
lines changed

app/V1Module/presenters/ExercisesConfigPresenter.php

Lines changed: 65 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use App\Model\Entity\ExerciseEnvironmentConfig;
2222
use App\Model\Entity\ExerciseTest;
2323
use App\Model\Repository\Exercises;
24+
use App\Model\Repository\ExerciseTests;
2425
use App\Model\Repository\HardwareGroups;
2526
use App\Model\Repository\Pipelines;
2627
use App\Model\Repository\ReferenceSolutionSubmissions;
@@ -43,6 +44,12 @@ class ExercisesConfigPresenter extends BasePresenter {
4344
*/
4445
public $exercises;
4546

47+
/**
48+
* @var ExerciseTests
49+
* @inject
50+
*/
51+
public $exerciseTests;
52+
4653
/**
4754
* @var Pipelines
4855
* @inject
@@ -598,35 +605,52 @@ public function actionSetTests(string $id) {
598605
$req = $this->getRequest();
599606
$tests = $req->getPost("tests");
600607

608+
/*
609+
* We need to implement CoW on tests.
610+
* All modified tests has to be newly created (with new IDs) and these
611+
* new IDs has to be propagated into configuration and limits.
612+
* Therefore a replacement mapping of updated tests is kept.
613+
*/
614+
601615
$newTests = [];
616+
$namesToOldIds = []; // new test name => old test ID
617+
602618
foreach ($tests as $test) {
619+
// Perform checks on the test name...
603620
if (!array_key_exists("name", $test)) {
604621
throw new InvalidArgumentException("tests", "name item not found in particular test");
605622
}
606623

607-
$name = $test["name"];
624+
$name = trim($test["name"]);
625+
if (!preg_match('/^[-a-zA-Z0-9_()\[\].! ]+$/', $name)) {
626+
throw new InvalidArgumentException("tests", "test name contains illicit characters");
627+
}
628+
if (strlen($name) > 64) {
629+
throw new InvalidArgumentException("tests", "test name too long (exceeds 64 characters)");
630+
}
631+
if (array_key_exists($name, $newTests)) {
632+
throw new InvalidArgumentException("tests", "two tests with the same name '$name' were specified");
633+
}
634+
608635
$id = Arrays::get($test, "id", null);
609-
$description = Arrays::get($test, "description", "");
636+
$description = trim(Arrays::get($test, "description", ""));
610637

638+
// Prepare a test entity that is to be inserted into the new list of tests...
611639
$testEntity = $id ? $exercise->getExerciseTestById($id) : null;
612640
if ($testEntity === null) {
613641
// new exercise test was requested to be created
614642
if ($exercise->getExerciseTestByName($name)) {
615643
throw new InvalidArgumentException("tests", "given test name '$name' is already taken");
616644
}
617645

618-
$testEntity = new ExerciseTest(trim($name), $description, $this->getCurrentUser());
619-
} else {
620-
// update of existing exercise test with all appropriate fields
621-
$testEntity->setName(trim($name));
622-
$testEntity->setDescription($description);
623-
$testEntity->updatedNow();
646+
$testEntity = new ExerciseTest($name, $description, $this->getCurrentUser());
647+
} elseif ($testEntity->getName() !== $name || $testEntity->getDescription() !== $description) {
648+
// an update is needed => a copy is made and old ID mapping is kept
649+
$namesToOldIds[$name] = $id;
650+
$testEntity = new ExerciseTest($name, $description, $testEntity->getAuthor());
624651
}
652+
// otherwise, the $testEntity is unchanged
625653

626-
627-
if (array_key_exists($name, $newTests)) {
628-
throw new InvalidArgumentException("tests", "two tests with the same name '$name' were specified");
629-
}
630654
$newTests[$name] = $testEntity;
631655
}
632656

@@ -638,20 +662,39 @@ public function actionSetTests(string $id) {
638662
);
639663
}
640664

641-
// clear old tests and set new ones
642-
$exercise->getExerciseTests()->clear();
643-
$exercise->setExerciseTests(new ArrayCollection($newTests));
665+
$this->exercises->beginTransaction();
666+
try {
667+
// clear old tests and set new ones
668+
$exercise->getExerciseTests()->clear();
669+
$exercise->setExerciseTests(new ArrayCollection($newTests));
670+
$this->exercises->flush();
644671

645-
// update exercise configuration and test in here
646-
$this->exerciseConfigUpdater->testsUpdated($exercise, $this->getCurrentUser(), false);
672+
// now we need to get IDs of newly created tests
673+
$idMapping = []; // old ID => new ID
674+
foreach ($newTests as $test) {
675+
$this->exerciseTests->refresh($test);
676+
if (array_key_exists($test->getName(), $namesToOldIds)) {
677+
$oldId = $namesToOldIds[$test->getName()];
678+
$idMapping[$oldId] = $test->getId();
679+
}
680+
}
647681

648-
$exercise->updatedNow();
649-
$this->exercises->flush();
682+
// update exercise configuration and test in here
683+
$this->exerciseConfigUpdater->testsUpdated($exercise, $this->getCurrentUser(), $idMapping, false);
650684

651-
$this->configChecker->check($exercise);
652-
$this->exercises->flush();
685+
$exercise->updatedNow();
686+
$this->exercises->flush();
653687

654-
$this->sendSuccessResponse($exercise->getExerciseTests()->getValues());
688+
$this->configChecker->check($exercise);
689+
$this->exercises->flush();
690+
$this->exercises->commit();
691+
}
692+
catch (\Exception $e) {
693+
$this->exercises->rollBack();
694+
throw $e;
695+
}
696+
697+
$this->sendSuccessResponse(array_values($newTests));
655698
}
656699

657700
}

app/commands/cleanup/CleanupExerciseConfigs.php

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,16 +131,57 @@ private function cleanupLimits(DateTime $limit): int {
131131
return $deleteQuery->execute();
132132
}
133133

134+
/**
135+
* Delete tests and return number of deleted entities.
136+
* @param DateTime $limit
137+
* @return int
138+
*/
139+
private function cleanupTests(DateTime $limit): int {
140+
$usedTests = [];
141+
142+
/** @var Exercise $exercise */
143+
foreach ($this->exercises->findAllAndIReallyMeanAllOkay() as $exercise) {
144+
foreach ($exercise->getExerciseTestsIds() as $id) {
145+
$usedTests[$id] = true;
146+
}
147+
}
148+
149+
/** @var Assignment $assignment */
150+
foreach ($this->assignments->findAllAndIReallyMeanAllOkay() as $assignment) {
151+
foreach ($assignment->getExerciseTestsIds() as $id) {
152+
$usedTests[$id] = true;
153+
}
154+
}
155+
156+
$deleteQuery = $this->entityManager->createQuery('
157+
DELETE FROM App\Model\Entity\ExerciseTest t
158+
WHERE t.createdAt <= :date AND t.id NOT IN (:ids)
159+
');
160+
161+
$deleteQuery->setParameter(":date", $limit);
162+
$deleteQuery->setParameter("ids", array_keys($usedTests), Connection::PARAM_STR_ARRAY);
163+
return $deleteQuery->execute();
164+
}
165+
134166
protected function execute(InputInterface $input, OutputInterface $output) {
135167
$now = new DateTime();
136168
$limit = clone $now;
137169
$limit->modify("-14 days");
138170

139-
$deletedEnvsCount = $this->cleanupEnvironmentConfigs($limit);
140-
$deletedConfsCount = $this->cleanupExerciseConfigs($limit);
141-
$deletedLimsCount = $this->cleanupLimits($limit);
171+
$this->exercises->beginTransaction();
172+
try {
173+
$deletedEnvsCount = $this->cleanupEnvironmentConfigs($limit);
174+
$deletedConfsCount = $this->cleanupExerciseConfigs($limit);
175+
$deletedLimsCount = $this->cleanupLimits($limit);
176+
$deletedTestsCount = $this->cleanupTests($limit);
177+
$this->exercises->commit();
178+
}
179+
catch (\Exception $e) {
180+
$this->exercises->rollBack();
181+
throw $e;
182+
}
142183

143-
$output->writeln("Removed: {$deletedEnvsCount} environment configs; {$deletedConfsCount} exercise configs; {$deletedLimsCount} exercise limits");
184+
$output->writeln("Removed: {$deletedEnvsCount} environment configs; {$deletedConfsCount} exercise configs; {$deletedLimsCount} exercise limits; {$deletedTestsCount} exercise tests");
144185
return 0;
145186
}
146187
}

app/config/config.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ services:
343343
# models - repositories
344344
- App\Model\Repository\Comments
345345
- App\Model\Repository\Exercises
346+
- App\Model\Repository\ExerciseTests
346347
- App\Model\Repository\Assignments
347348
- App\Model\Repository\ExternalLogins
348349
- App\Model\Repository\Groups

app/helpers/ExerciseConfig/Config/ExerciseConfig.php

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,21 +31,21 @@ public function getEnvironments(): array {
3131

3232
/**
3333
* Add environment into this holder.
34-
* @param string $name
34+
* @param string $id
3535
* @return $this
3636
*/
37-
public function addEnvironment(string $name): ExerciseConfig {
38-
$this->environments[] = $name;
37+
public function addEnvironment(string $id): ExerciseConfig {
38+
$this->environments[] = $id;
3939
return $this;
4040
}
4141

4242
/**
4343
* Remove environment according to given name identification.
44-
* @param string $name
44+
* @param string $id
4545
* @return $this
4646
*/
47-
public function removeEnvironment(string $name): ExerciseConfig {
48-
if(($key = array_search($name, $this->environments)) !== false) {
47+
public function removeEnvironment(string $id): ExerciseConfig {
48+
if(($key = array_search($id, $this->environments)) !== false) {
4949
unset($this->environments[$key]);
5050
}
5151
return $this;
@@ -61,35 +61,51 @@ public function getTests(): array {
6161

6262
/**
6363
* Get test for the given test name.
64-
* @param string $name
64+
* @param string $id
6565
* @return Test|null
6666
*/
67-
public function getTest(string $name): ?Test {
68-
if (!array_key_exists($name, $this->tests)) {
67+
public function getTest(string $id): ?Test {
68+
if (!array_key_exists($id, $this->tests)) {
6969
return null;
7070
}
7171

72-
return $this->tests[$name];
72+
return $this->tests[$id];
7373
}
7474

7575
/**
7676
* Add test into this holder.
77-
* @param string $name
77+
* @param string $id
7878
* @param Test $test
7979
* @return $this
8080
*/
81-
public function addTest(string $name, Test $test): ExerciseConfig {
82-
$this->tests[$name] = $test;
81+
public function addTest(string $id, Test $test): ExerciseConfig {
82+
$this->tests[$id] = $test;
8383
return $this;
8484
}
8585

8686
/**
8787
* Remove test according to given test identification.
88-
* @param string $name
88+
* @param string $id
8989
* @return $this
9090
*/
91-
public function removeTest(string $name): ExerciseConfig {
92-
unset($this->tests[$name]);
91+
public function removeTest(string $id): ExerciseConfig {
92+
unset($this->tests[$id]);
93+
return $this;
94+
}
95+
96+
/**
97+
* Remove test according to given test identification.
98+
* @param string $id
99+
* @return $this
100+
*/
101+
public function changeTestId(string $oldId, string $newId): ExerciseConfig {
102+
$test = $this->getTest($oldId);
103+
if ($test) {
104+
if ($this->getTest($newId)) {
105+
throw new \Exception("Serious internal error. Newly created test ID is already present in exercise config!");
106+
}
107+
$this->removeTest($oldId)->addTest($newId, $test);
108+
}
93109
return $this;
94110
}
95111

app/helpers/ExerciseConfig/Limits/ExerciseLimits.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,23 @@ public function removeLimits(string $testId): ExerciseLimits {
5656
return $this;
5757
}
5858

59+
/**
60+
* Remove limits for given test identification.
61+
* @param string $testId
62+
* @return ExerciseLimits
63+
*/
64+
public function changeTestId(string $oldId, string $newId): ExerciseLimits {
65+
$limits = $this->getLimits($oldId);
66+
if ($limits) {
67+
if ($this->getLimits($newId)) {
68+
throw new \Exception("Serious internal error. Newly created test ID is already present in exercise limits!");
69+
}
70+
$this->removeLimits($oldId)->addLimits($newId, $limits);
71+
}
72+
return $this;
73+
}
74+
75+
5976
/**
6077
* Creates and returns properly structured array representing this object.
6178
* @return array

app/helpers/ExerciseConfig/Updater.php

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@ public function environmentsUpdated(Exercise $exercise, User $user, bool $flush
6565
* @param bool $flush
6666
* @throws ExerciseConfigException
6767
*/
68-
public function testsUpdated(Exercise $exercise, User $user, bool $flush = true) {
69-
$this->updateTestsInConfig($exercise, $user);
70-
$this->updateTestsInLimits($exercise, $user);
68+
public function testsUpdated(Exercise $exercise, User $user, array $idMappings = [], bool $flush = true) {
69+
$this->updateTestsInConfig($exercise, $user, $idMappings);
70+
$this->updateTestsInLimits($exercise, $user, $idMappings);
7171

7272
if ($flush) {
7373
$this->exercises->flush();
@@ -161,15 +161,20 @@ private function updateEnvironmentsInLimits(Exercise $exercise, User $user) {
161161
* @param User $user
162162
* @throws ExerciseConfigException
163163
*/
164-
private function updateTestsInConfig(Exercise $exercise, User $user) {
164+
private function updateTestsInConfig(Exercise $exercise, User $user, array $idMappings = []) {
165165
$exerciseConfig = $this->loader->loadExerciseConfig($exercise->getExerciseConfig()->getParsedConfig());
166-
$testNames = $exercise->getExerciseTestsIds();
166+
$testIds = $exercise->getExerciseTestsIds();
167+
168+
// rename test IDs according to provided mapping
169+
foreach ($idMappings as $oldId => $newId) {
170+
$exerciseConfig->changeTestId($oldId, $newId);
171+
}
167172

168173
// remove old tests
169-
foreach ($exerciseConfig->getTests() as $name => $test) {
170-
// test name not found in all newly created or updated tests, terminate it
171-
if (!in_array($name, $testNames)) {
172-
$exerciseConfig->removeTest($name);
174+
foreach ($exerciseConfig->getTests() as $id => $test) {
175+
// test ID not found in all newly created or updated tests, terminate it
176+
if (!in_array($id, $testIds)) {
177+
$exerciseConfig->removeTest($id);
173178
}
174179
}
175180

@@ -185,17 +190,22 @@ private function updateTestsInConfig(Exercise $exercise, User $user) {
185190
* @param User $user
186191
* @throws ExerciseConfigException
187192
*/
188-
private function updateTestsInLimits(Exercise $exercise, User $user) {
189-
$testNames = $exercise->getExerciseTestsIds();
193+
private function updateTestsInLimits(Exercise $exercise, User $user, array $idMappings = []) {
194+
$testIds = $exercise->getExerciseTestsIds();
190195

191196
foreach ($exercise->getExerciseLimits() as $exerciseLimits) {
192197
$limits = $this->loader->loadExerciseLimits($exerciseLimits->getParsedLimits());
193198

199+
// rename test IDs according to provided mapping
200+
foreach ($idMappings as $oldId => $newId) {
201+
$limits->changeTestId($oldId, $newId);
202+
}
203+
194204
// remove old tests
195-
foreach ($limits->getLimitsArray() as $name => $testLimits) {
196-
// test name not found in all newly created or updated tests, terminate it
197-
if (!in_array($name, $testNames)) {
198-
$limits->removeLimits($name);
205+
foreach ($limits->getLimitsArray() as $id => $testLimits) {
206+
// test ID not found in all newly created or updated tests, terminate it
207+
if (!in_array($id, $testIds)) {
208+
$limits->removeLimits($id);
199209
}
200210
}
201211

0 commit comments

Comments
 (0)