diff --git a/.idea/TijdVoorDeTest.iml b/.idea/TijdVoorDeTest.iml
index f0ec1db..b6ef210 100644
--- a/.idea/TijdVoorDeTest.iml
+++ b/.idea/TijdVoorDeTest.iml
@@ -165,7 +165,6 @@
-
diff --git a/.idea/php.xml b/.idea/php.xml
index 3261d69..ced0bf0 100644
--- a/.idea/php.xml
+++ b/.idea/php.xml
@@ -36,170 +36,168 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
@@ -293,7 +291,7 @@
-
+
diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php
index 0b5a012..c673940 100644
--- a/.php-cs-fixer.dist.php
+++ b/.php-cs-fixer.dist.php
@@ -24,7 +24,6 @@ return new Config()
'no_unreachable_default_argument_value' => true,
'no_useless_else' => true,
'no_useless_return' => true,
- 'php_unit_strict' => true,
'phpdoc_line_span' => ['const' => 'single', 'method' => 'single', 'property' => 'single'],
'phpdoc_order' => true,
'single_line_empty_body' => true,
diff --git a/Justfile b/Justfile
index a22d394..750a6a6 100644
--- a/Justfile
+++ b/Justfile
@@ -43,5 +43,5 @@ clean:
reload-tests:
@docker compose exec php bin/console --env=test doctrine:database:drop --if-exists --force
@docker compose exec php bin/console --env=test doctrine:database:create
- @docker compose exec php bin/console --env=test doctrine:schema:create --quiet
- @docker compose exec php bin/console --env=test doctrine:fixtures:load --no-interaction --group=test
+ @docker compose exec php bin/console --env=test doctrine:migrations:migrate -n
+ @docker compose exec php bin/console --env=test doctrine:fixtures:load -n --group=test
diff --git a/composer.json b/composer.json
index 598566f..250061c 100644
--- a/composer.json
+++ b/composer.json
@@ -49,7 +49,6 @@
"twig/twig": "^3.21.1"
},
"require-dev": {
- "brianium/paratest": "^7.14",
"dama/doctrine-test-bundle": "^8.4",
"doctrine/doctrine-fixtures-bundle": "^4.3",
"friendsofphp/php-cs-fixer": "^3.89.0",
diff --git a/composer.lock b/composer.lock
index ae599f0..b22d2e4 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "944e218741f3c7a1f04072e075255c2c",
+ "content-hash": "752576f663ccc67d22525a025afc01df",
"packages": [
{
"name": "composer/pcre",
@@ -8750,99 +8750,6 @@
}
],
"packages-dev": [
- {
- "name": "brianium/paratest",
- "version": "v7.14.2",
- "source": {
- "type": "git",
- "url": "https://github.com/paratestphp/paratest.git",
- "reference": "de06de1ae1203b11976c6ca01d6a9081c8b33d45"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/paratestphp/paratest/zipball/de06de1ae1203b11976c6ca01d6a9081c8b33d45",
- "reference": "de06de1ae1203b11976c6ca01d6a9081c8b33d45",
- "shasum": ""
- },
- "require": {
- "ext-dom": "*",
- "ext-pcre": "*",
- "ext-reflection": "*",
- "ext-simplexml": "*",
- "fidry/cpu-core-counter": "^1.3.0",
- "jean85/pretty-package-versions": "^2.1.1",
- "php": "~8.3.0 || ~8.4.0 || ~8.5.0",
- "phpunit/php-code-coverage": "^12.4.0",
- "phpunit/php-file-iterator": "^6",
- "phpunit/php-timer": "^8",
- "phpunit/phpunit": "^12.4.1",
- "sebastian/environment": "^8.0.3",
- "symfony/console": "^6.4.20 || ^7.3.4",
- "symfony/process": "^6.4.20 || ^7.3.4"
- },
- "require-dev": {
- "doctrine/coding-standard": "^14.0.0",
- "ext-pcntl": "*",
- "ext-pcov": "*",
- "ext-posix": "*",
- "phpstan/phpstan": "^2.1.31",
- "phpstan/phpstan-deprecation-rules": "^2.0.3",
- "phpstan/phpstan-phpunit": "^2.0.7",
- "phpstan/phpstan-strict-rules": "^2.0.7",
- "symfony/filesystem": "^6.4.13 || ^7.3.2"
- },
- "bin": [
- "bin/paratest",
- "bin/paratest_for_phpstorm"
- ],
- "type": "library",
- "autoload": {
- "psr-4": {
- "ParaTest\\": [
- "src/"
- ]
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Brian Scaturro",
- "email": "scaturrob@gmail.com",
- "role": "Developer"
- },
- {
- "name": "Filippo Tessarotto",
- "email": "zoeslam@gmail.com",
- "role": "Developer"
- }
- ],
- "description": "Parallel testing for PHP",
- "homepage": "https://github.com/paratestphp/paratest",
- "keywords": [
- "concurrent",
- "parallel",
- "phpunit",
- "testing"
- ],
- "support": {
- "issues": "https://github.com/paratestphp/paratest/issues",
- "source": "https://github.com/paratestphp/paratest/tree/v7.14.2"
- },
- "funding": [
- {
- "url": "https://github.com/sponsors/Slamdunk",
- "type": "github"
- },
- {
- "url": "https://paypal.me/filippotessarotto",
- "type": "paypal"
- }
- ],
- "time": "2025-10-24T07:20:53+00:00"
- },
{
"name": "clue/ndjson-react",
"version": "v1.3.0",
diff --git a/src/Controller/QuizController.php b/src/Controller/QuizController.php
index 49db665..a14fab6 100644
--- a/src/Controller/QuizController.php
+++ b/src/Controller/QuizController.php
@@ -108,6 +108,7 @@ final class QuizController extends AbstractController
if (!$answer instanceof Answer) {
throw new BadRequestHttpException('Invalid Answer ID');
}
+
$givenAnswer = new GivenAnswer($candidate, $answer->question->quiz, $answer);
$this->entityManager->persist($givenAnswer);
$this->entityManager->flush();
diff --git a/src/Repository/QuizRepository.php b/src/Repository/QuizRepository.php
index 6b42a4c..807badf 100644
--- a/src/Repository/QuizRepository.php
+++ b/src/Repository/QuizRepository.php
@@ -49,11 +49,14 @@ class QuizRepository extends ServiceEntityRepository
DQL)
->setParameter('quiz', $quiz)
->execute();
- } catch (\Throwable $throwable) {
+ }
+ // @codeCoverageIgnoreStart
+ catch (\Throwable $throwable) {
$this->logger->error($throwable->getMessage());
$em->rollback();
throw new ErrorClearingQuizException(previous: $throwable);
}
+ // @codeCoverageIgnoreEnd
$em->commit();
}
@@ -76,17 +79,17 @@ class QuizRepository extends ServiceEntityRepository
c.id,
c.name,
sum(case when a.isRightAnswer = true then 1 else 0 end) as correct,
- qc.corrections,
+ qd.corrections,
max(ga.created) as end_time,
- qc.created as start_time,
- (sum(case when a.isRightAnswer = true then 1 else 0 end) + qc.corrections) as score
+ qd.created as start_time,
+ (sum(case when a.isRightAnswer = true then 1 else 0 end) + qd.corrections) as score
from Tvdt\Entity\Candidate c
join c.givenAnswers ga
join ga.answer a
- join c.quizData qc
- where qc.quiz = :quiz and ga.quiz = :quiz
- group by ga.quiz, c.id, qc.id
- order by score desc, max(ga.created) - qc.created asc
+ join c.quizData qd
+ where qd.quiz = :quiz and ga.quiz = :quiz
+ group by ga.quiz, c.id, qd.id
+ order by score desc, max(ga.created) - qd.created asc
DQL
)->setParameter('quiz', $quiz)->getResult();
@@ -95,7 +98,7 @@ class QuizRepository extends ServiceEntityRepository
name: $row['name'],
correct: (int) $row['correct'],
corrections: $row['corrections'],
- time: new DateTimeImmutable($row['end_time'])->diff($row['start_time']),
+ time: $row['start_time']->diff(new DateTimeImmutable($row['end_time'])),
score: $row['score'],
), $result);
}
diff --git a/tests/Repository/QuizRepositoryTest.php b/tests/Repository/QuizRepositoryTest.php
index 02bea19..79f0ad9 100644
--- a/tests/Repository/QuizRepositoryTest.php
+++ b/tests/Repository/QuizRepositoryTest.php
@@ -7,6 +7,9 @@ namespace Tvdt\Tests\Repository;
use PHPUnit\Framework\Attributes\CoversClass;
use Psr\Clock\ClockInterface;
use Symfony\Component\Clock\MockClock;
+use Tvdt\Entity\Answer;
+use Tvdt\Entity\GivenAnswer;
+use Tvdt\Entity\Question;
use Tvdt\Entity\Quiz;
use Tvdt\Entity\QuizCandidate;
use Tvdt\Repository\GivenAnswerRepository;
@@ -46,20 +49,166 @@ final class QuizRepositoryTest extends DatabaseTestCase
$this->assertCount(1, $krtekSeason->quizzes);
}
- public function testGetScores(): void
+ public function testTimeForCandidate(): void
{
$clock = new MockClock('2025-11-01 16:00:00');
self::getContainer()->set(ClockInterface::class, $clock);
$krtekSeason = $this->getSeasonByCode('krtek');
$candidate = $this->getCandidateBySeasonAndName($krtekSeason, 'Iris');
+ $quiz = $krtekSeason->activeQuiz;
+ $this->assertInstanceOf(Quiz::class, $quiz);
+
// Start Quiz
- $qc = new QuizCandidate($krtekSeason->activeQuiz, $candidate);
+ $qc = new QuizCandidate($quiz, $candidate);
$this->entityManager->persist($qc);
$this->entityManager->flush();
- dump($qc->created);
+ for ($i = 0; $i < 15; ++$i) {
+ $question = $this->questionRepository->findNextQuestionForCandidate($candidate);
+ $this->assertInstanceOf(Question::class, $question);
- $this->markTestIncomplete('TODO: Make fixtures first and write good test.');
+ $answer = $question->answers->first();
+ $this->assertInstanceOf(Answer::class, $answer);
+
+ $clock->sleep(10 + $i);
+ $qa = new GivenAnswer($candidate, $quiz, $answer);
+ $this->entityManager->persist($qa);
+ $this->entityManager->flush();
+ }
+
+ $result = $this->quizRepository->getScores($quiz);
+
+ $this->assertSame('Iris', $result[0]->name);
+ $this->assertSame(5, $result[0]->correct);
+ $this->assertEqualsWithDelta(5.0, $result[0]->score, \PHP_FLOAT_EPSILON);
+
+ $this->assertSame(4, $result[0]->time->i);
+ $this->assertSame(15, $result[0]->time->s);
+ }
+
+ public function testScoresAreCalculatedCorrectly(): void
+ {
+ $clock = new MockClock('2025-11-01 16:00:00');
+ self::getContainer()->set(ClockInterface::class, $clock);
+ $krtekSeason = $this->getSeasonByCode('krtek');
+ $candidate1 = $this->getCandidateBySeasonAndName($krtekSeason, 'Iris');
+ $candidate2 = $this->getCandidateBySeasonAndName($krtekSeason, 'Philine');
+
+ $quiz = $krtekSeason->activeQuiz;
+ $this->assertInstanceOf(Quiz::class, $quiz);
+
+ $qc1 = new QuizCandidate($quiz, $candidate1);
+ $qc2 = new QuizCandidate($quiz, $candidate2);
+ $this->entityManager->persist($qc1);
+ $this->entityManager->persist($qc2);
+ $this->entityManager->flush();
+
+ for ($i = 0; $i < 15; ++$i) {
+ $question = $this->questionRepository->findNextQuestionForCandidate($candidate1);
+ $this->assertInstanceOf(Question::class, $question);
+
+ $answer1 = $question->answers->first();
+ $answer2 = $question->answers[intdiv(\count($question->answers), 2)];
+ $this->assertInstanceOf(Answer::class, $answer1);
+ $this->assertInstanceOf(Answer::class, $answer2);
+
+ $clock->sleep(10);
+
+ $qa = new GivenAnswer($candidate1, $quiz, $answer1);
+ $this->entityManager->persist($qa);
+ $qa = new GivenAnswer($candidate2, $quiz, $answer2);
+ $this->entityManager->persist($qa);
+
+ $this->entityManager->flush();
+ }
+
+ $scores = $this->quizRepository->getScores($quiz);
+ $this->assertCount(2, $scores);
+ $this->assertSame('Iris', $scores[0]->name);
+ $this->assertSame('Philine', $scores[1]->name);
+ $this->assertEqualsWithDelta(5.0, $scores[0]->score, \PHP_FLOAT_EPSILON);
+ $this->assertEqualsWithDelta(4.0, $scores[1]->score, \PHP_FLOAT_EPSILON);
+ }
+
+ public function testCorrectionsCalculatedCorrectly(): void
+ {
+ $clock = new MockClock('2025-11-01 16:00:00');
+ self::getContainer()->set(ClockInterface::class, $clock);
+ $krtekSeason = $this->getSeasonByCode('krtek');
+ $candidate = $this->getCandidateBySeasonAndName($krtekSeason, 'Iris');
+
+ $quiz = $krtekSeason->activeQuiz;
+ $this->assertInstanceOf(Quiz::class, $quiz);
+
+ $qc = new QuizCandidate($quiz, $candidate);
+ $this->entityManager->persist($qc);
+ $this->entityManager->flush();
+
+ for ($i = 0; $i < 15; ++$i) {
+ $question = $this->questionRepository->findNextQuestionForCandidate($candidate);
+ $this->assertInstanceOf(Question::class, $question);
+
+ $answer = $question->answers->first();
+ $this->assertInstanceOf(Answer::class, $answer);
+
+ $clock->sleep(10);
+ $qa = new GivenAnswer($candidate, $quiz, $answer);
+ $this->entityManager->persist($qa);
+ $this->entityManager->flush();
+ }
+
+ $qc->corrections = 2;
+ $this->entityManager->flush();
+
+ $result = $this->quizRepository->getScores($quiz);
+
+ $this->assertEqualsWithDelta(7.0, $result[0]->score, \PHP_FLOAT_EPSILON);
+ }
+
+ public function testCandidatesWithSameScoreAreSortedCorrectlyByTime(): void
+ {
+ $clock = new MockClock('2025-11-01 16:00:00');
+ self::getContainer()->set(ClockInterface::class, $clock);
+ $krtekSeason = $this->getSeasonByCode('krtek');
+ $candidate1 = $this->getCandidateBySeasonAndName($krtekSeason, 'Iris');
+ $candidate2 = $this->getCandidateBySeasonAndName($krtekSeason, 'Philine');
+
+ $quiz = $krtekSeason->activeQuiz;
+ $this->assertInstanceOf(Quiz::class, $quiz);
+
+ $qc1 = new QuizCandidate($quiz, $candidate1);
+ $this->entityManager->persist($qc1);
+ $clock->sleep(10);
+ $qc2 = new QuizCandidate($quiz, $candidate2);
+ $this->entityManager->persist($qc2);
+ $this->entityManager->flush();
+
+ for ($i = 0; $i < 15; ++$i) {
+ $question = $this->questionRepository->findNextQuestionForCandidate($candidate1);
+ $this->assertInstanceOf(Question::class, $question);
+
+ $answer1 = $question->answers->first();
+ $answer2 = $question->answers->last();
+ $this->assertInstanceOf(Answer::class, $answer1);
+ $this->assertInstanceOf(Answer::class, $answer2);
+
+ $clock->sleep(10);
+
+ $qa = new GivenAnswer($candidate1, $quiz, $answer1);
+ $this->entityManager->persist($qa);
+
+ $qa = new GivenAnswer($candidate2, $quiz, $answer2);
+ $this->entityManager->persist($qa);
+
+ $this->entityManager->flush();
+ }
+
+ $result = $this->quizRepository->getScores($quiz);
+
+ $this->assertEqualsWithDelta(5.0, $result[0]->score, \PHP_FLOAT_EPSILON);
+ $this->assertEqualsWithDelta(5.0, $result[1]->score, \PHP_FLOAT_EPSILON);
+ $this->assertSame('Philine', $result[0]->name);
+ $this->assertSame('Iris', $result[1]->name);
}
}
diff --git a/tests/Security/Voter/SeasonVoterTest.php b/tests/Security/Voter/SeasonVoterTest.php
index 2941091..e882af7 100644
--- a/tests/Security/Voter/SeasonVoterTest.php
+++ b/tests/Security/Voter/SeasonVoterTest.php
@@ -90,6 +90,7 @@ final class SeasonVoterTest extends TestCase
{
$user = new User();
$user->roles = ['ROLE_ADMIN'];
+
$token = $this->createStub(TokenInterface::class);
$token->method('getUser')->willReturn($user);