mirror of
https://github.com/MarijnDoeve/TijdVoorDeTest.git
synced 2026-07-05 15:10:16 +02:00
Compare commits
10 Commits
v0.1.1
..
466f0aeb54
| Author | SHA1 | Date | |
|---|---|---|---|
|
466f0aeb54
|
|||
|
d55cced5f7
|
|||
|
9c22345d2d
|
|||
|
0f18e4afe5
|
|||
|
20c97d9cb5
|
|||
|
e5198507ae
|
|||
|
d94eeced8c
|
|||
|
7e09fcdafb
|
|||
|
d8b671046b
|
|||
|
cd63ef339f
|
+15
-60
@@ -17,11 +17,12 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
quality:
|
||||
name: Code Quality
|
||||
tests:
|
||||
name: Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
checks: write
|
||||
pull-requests: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -39,68 +40,26 @@ jobs:
|
||||
compose.yaml
|
||||
compose.override.yaml
|
||||
set: |
|
||||
*.cache-from=type=gha,scope=${{github.ref}}-quality
|
||||
*.cache-from=type=gha,scope=${{github.ref}}
|
||||
*.cache-from=type=gha,scope=refs/heads/main
|
||||
*.cache-to=type=gha,scope=${{github.ref}}-quality,mode=${{ github.event_name == 'pull_request' && 'min' || 'max' }}
|
||||
*.cache-to=type=gha,scope=${{github.ref}},mode=max
|
||||
- name: Start services
|
||||
run: docker compose up php database --wait --no-build
|
||||
- name: Warm up dev cache
|
||||
run: docker compose exec -T php bin/console cache:warmup --env=dev
|
||||
- name: Lint Twig templates
|
||||
id: twig_lint
|
||||
continue-on-error: true
|
||||
run: docker compose exec -T php bin/console lint:twig --format=github templates
|
||||
- name: Coding Style
|
||||
id: cs
|
||||
continue-on-error: true
|
||||
run: docker compose exec -T php vendor/bin/php-cs-fixer check --diff --show-progress=none
|
||||
- name: Twig Coding Style
|
||||
id: twig_cs
|
||||
continue-on-error: true
|
||||
run: docker compose exec -T php vendor/bin/twig-cs-fixer check
|
||||
- name: Static Analysis (PHPStan)
|
||||
id: phpstan
|
||||
continue-on-error: true
|
||||
run: docker compose exec -T php vendor/bin/phpstan analyse --no-progress --no-ansi --error-format=github
|
||||
- name: Rector
|
||||
id: rector
|
||||
continue-on-error: true
|
||||
run: docker compose exec -T php vendor/bin/rector process --dry-run --no-progress-bar --output-format=github
|
||||
- name: Check HTTP reachability
|
||||
run: curl -v --fail-with-body http://localhost
|
||||
- name: Assert all checks passed
|
||||
if: always()
|
||||
run: |
|
||||
outcomes="${{ steps.twig_lint.outcome }} ${{ steps.cs.outcome }} ${{ steps.twig_cs.outcome }} ${{ steps.phpstan.outcome }} ${{ steps.rector.outcome }}"
|
||||
if echo "$outcomes" | grep -q "failure"; then exit 1; fi
|
||||
|
||||
tests:
|
||||
name: Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
checks: write
|
||||
pull-requests: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Build Docker images
|
||||
uses: docker/bake-action@v5
|
||||
with:
|
||||
pull: true
|
||||
load: true
|
||||
files: |
|
||||
compose.yaml
|
||||
compose.override.yaml
|
||||
set: |
|
||||
*.cache-from=type=gha,scope=${{github.ref}}-tests
|
||||
*.cache-from=type=gha,scope=refs/heads/main
|
||||
*.cache-to=type=gha,scope=${{github.ref}}-tests,mode=${{ github.event_name == 'pull_request' && 'min' || 'max' }}
|
||||
- name: Start services
|
||||
run: docker compose up php database --wait --no-build
|
||||
- name: Check Mercure reachability
|
||||
if: false
|
||||
run: curl -vkI --fail-with-body https://localhost/.well-known/mercure?topic=test
|
||||
- name: Create test database
|
||||
run: docker compose exec -T php bin/console -e test doctrine:database:create
|
||||
- name: Run migrations
|
||||
@@ -119,16 +78,15 @@ jobs:
|
||||
run: docker compose exec -T php bin/console -e test doctrine:schema:validate
|
||||
|
||||
build-deploy:
|
||||
name: Build and Deploy
|
||||
name: Build and deploy to ${{ startsWith(github.ref, 'refs/tags/') && 'production' || (github.ref == 'refs/heads/main' && 'acceptance' || '') }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
environment:
|
||||
name: ${{ startsWith(github.ref, 'refs/tags/') && 'production' || 'acceptance' }}
|
||||
name: ${{ startsWith(github.ref, 'refs/tags/') && 'production' || (github.ref == 'refs/heads/main' && 'acceptance' || '') }}
|
||||
url: ${{ vars.URL }}
|
||||
needs: [quality, tests]
|
||||
needs: tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
if: (github.ref == 'refs/heads/main' && false) || startsWith(github.ref, 'refs/tags/')
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -148,17 +106,14 @@ jobs:
|
||||
REPO_LOWER=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]')
|
||||
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
|
||||
TAG="${GITHUB_REF#refs/tags/}"
|
||||
SENTRY_VERSION="${TAG#v}"
|
||||
{
|
||||
echo "tag=$TAG"
|
||||
echo "sentry_version=$SENTRY_VERSION"
|
||||
echo "full_name=ghcr.io/${REPO_LOWER}:$TAG"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
SHORT_SHA=$(git rev-parse --short HEAD)
|
||||
{
|
||||
echo "tag=$SHORT_SHA"
|
||||
echo "sentry_version=$SHORT_SHA"
|
||||
echo "full_name=ghcr.io/${REPO_LOWER}:$SHORT_SHA"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
@@ -184,12 +139,12 @@ jobs:
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
with:
|
||||
release: ${{steps.meta.outputs.sentry_version}}
|
||||
environment: ${{ startsWith(github.ref, 'refs/tags/') && 'production' || 'acceptance' }}
|
||||
release: ${{steps.meta.outputs.tag}}
|
||||
environment: ${{ startsWith(github.ref, 'refs/tags/') && 'production' || (github.ref == 'refs/heads/main' && 'acceptance' || '') }}
|
||||
|
||||
- name: Trigger Portainer Deployment
|
||||
shell: bash
|
||||
env:
|
||||
PORTAINER_WEBHOOK: ${{secrets.PORTAINER_WEBHOOK}}
|
||||
run: |
|
||||
curl -v -X POST "${PORTAINER_WEBHOOK}?IMAGE_TAG=${{steps.meta.outputs.tag}}" --fail-with-body
|
||||
curl -v -X POST "$PORTAINER_WEBHOOK"?IMAGE_TAG=${{steps.meta.outputs.tag}} --fail-with-body
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
failOnNotice="true"
|
||||
failOnWarning="true"
|
||||
bootstrap="tests/bootstrap.php"
|
||||
cacheDirectory="/tmp/phpunit.cache"
|
||||
cacheDirectory=".phpunit.cache"
|
||||
>
|
||||
<php>
|
||||
<ini name="display_errors" value="1" />
|
||||
|
||||
@@ -18,40 +18,39 @@ class QuizSpreadsheetService
|
||||
{
|
||||
public function generateTemplate(bool $fillExample = true): \Closure
|
||||
{
|
||||
$quiz = new Quiz();
|
||||
$spreadsheet = new Spreadsheet();
|
||||
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
$sheet->getStyle('1:1')->getFont()->setBold(true);
|
||||
|
||||
$sheet->setCellValue('A1', 'Question');
|
||||
$sheet->getColumnDimension('A')->setWidth(30);
|
||||
$sheet->getStyle('A:A')->getAlignment()->setWrapText(true);
|
||||
|
||||
$counter = 1;
|
||||
foreach (range('B', 'L', 2) as $column) {
|
||||
$sheet->setCellValue($column.'1', 'Answer '.$counter++);
|
||||
$sheet->getColumnDimension($column)->setWidth(30);
|
||||
$sheet->getStyle($column.':'.$column)->getAlignment()->setWrapText(true);
|
||||
}
|
||||
|
||||
foreach (range('C', 'M', 2) as $column) {
|
||||
$sheet->setCellValue($column.'1', 'Correct');
|
||||
$sheet->getColumnDimension($column)->setAutoSize(true);
|
||||
}
|
||||
|
||||
if ($fillExample) {
|
||||
$geslacht = new Question();
|
||||
$geslacht->question = 'Is de mol een man of een vrouw?';
|
||||
$geslacht->ordering = 1;
|
||||
$geslacht->addAnswer(new Answer('Man', true));
|
||||
$geslacht->addAnswer(new Answer('Vrouw'));
|
||||
$quiz->addQuestion($geslacht);
|
||||
$sheet->setCellValue('B2', 'Man');
|
||||
$sheet->setCellValue('C2', true);
|
||||
|
||||
$identiteit = new Question();
|
||||
$identiteit->question = 'Wie is de mol?';
|
||||
$identiteit->ordering = 2;
|
||||
foreach ([
|
||||
['Emma', false],
|
||||
['Jan', false],
|
||||
['Sara', false],
|
||||
['Piet', false],
|
||||
['Lisa', true],
|
||||
['Kees', false],
|
||||
['Anna', false],
|
||||
['Henk', false],
|
||||
['Nina', false],
|
||||
['Joost', false],
|
||||
] as $i => [$name, $correct]) {
|
||||
$answer = new Answer($name, $correct);
|
||||
$answer->ordering = $i + 1;
|
||||
$identiteit->addAnswer($answer);
|
||||
$sheet->setCellValue('D2', 'Vrouw');
|
||||
$sheet->setCellValue('E2', false);
|
||||
|
||||
$sheet->setCellValue('A2', 'Is de mol een man of een vrouw?');
|
||||
}
|
||||
|
||||
$quiz->addQuestion($identiteit);
|
||||
}
|
||||
|
||||
return $this->quizToXlsx($quiz);
|
||||
return $this->toXlsx($spreadsheet);
|
||||
}
|
||||
|
||||
/** @throws SpreadsheetDataException */
|
||||
@@ -103,7 +102,7 @@ class QuizSpreadsheetService
|
||||
}
|
||||
|
||||
if (1 === $answerCounter) {
|
||||
$errors[] = \sprintf('Question %d has no answers', $question->ordering);
|
||||
$errors[] = \sprintf('Question %d has no answers', $answerCounter);
|
||||
}
|
||||
|
||||
$quiz->addQuestion($question);
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
{% if is_granted('IS_AUTHENTICATED') or app.current_route() == 'tvdt_quiz_select_season' %}
|
||||
<div class="quiz-topbar">
|
||||
{% if is_granted('IS_AUTHENTICATED') %}
|
||||
<a href="{{ path('tvdt_backoffice_index') }}" class="btn btn-outline-secondary btn-sm">
|
||||
{{ 'Backoffice'|trans }}
|
||||
</a>
|
||||
<a href="{{ path('tvdt_login_logout') }}" class="btn btn-outline-secondary btn-sm">
|
||||
{{ 'Logout'|trans }}
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ path('tvdt_backoffice_index') }}" class="btn btn-outline-secondary btn-sm">
|
||||
{{ 'Manage Quiz'|trans }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -5,8 +5,6 @@ declare(strict_types=1);
|
||||
namespace Tvdt\Tests\Service;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Reader;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Writer;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@@ -71,17 +69,12 @@ final class QuizSpreadsheetServiceTest extends TestCase
|
||||
$quiz = new Quiz();
|
||||
$this->subject->xlsxToQuiz($quiz, new File($path));
|
||||
|
||||
$this->assertCount(2, $quiz->questions);
|
||||
$this->assertCount(1, $quiz->questions);
|
||||
|
||||
/** @var Question $first */
|
||||
$first = $quiz->questions->first();
|
||||
$this->assertSame('Is de mol een man of een vrouw?', $first->question);
|
||||
$this->assertCount(2, $first->answers);
|
||||
|
||||
/** @var Question $second */
|
||||
$second = $quiz->questions->last();
|
||||
$this->assertSame('Wie is de mol?', $second->question);
|
||||
$this->assertCount(10, $second->answers);
|
||||
/** @var Question $question */
|
||||
$question = $quiz->questions->first();
|
||||
$this->assertSame('Is de mol een man of een vrouw?', $question->question);
|
||||
$this->assertCount(2, $question->answers);
|
||||
}
|
||||
|
||||
public function testQuizToXlsxEmptyQuizImportsWithNoQuestions(): void
|
||||
@@ -149,44 +142,17 @@ final class QuizSpreadsheetServiceTest extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
public function testXlsxToQuizStopsAtBlankRow(): void
|
||||
{
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$sheet->setCellValue('A1', 'Question');
|
||||
$sheet->setCellValue('B1', 'Answer 1');
|
||||
$sheet->setCellValue('C1', 'Correct');
|
||||
$sheet->setCellValue('A2', 'First question');
|
||||
$sheet->setCellValue('B2', 'Yes');
|
||||
$sheet->setCellValue('C2', true);
|
||||
// Row 3 intentionally blank — should halt parsing
|
||||
$sheet->setCellValue('A4', 'Second question');
|
||||
$sheet->setCellValue('B4', 'No');
|
||||
$sheet->setCellValue('C4', false);
|
||||
|
||||
$path = $this->createTempPath('.xlsx');
|
||||
ob_start();
|
||||
new Writer\Xlsx($spreadsheet)->save('php://output');
|
||||
file_put_contents($path, ob_get_clean());
|
||||
|
||||
$quiz = new Quiz();
|
||||
$this->subject->xlsxToQuiz($quiz, new File($path));
|
||||
|
||||
$this->assertCount(1, $quiz->questions);
|
||||
/** @var Question $first */
|
||||
$first = $quiz->questions->first();
|
||||
$this->assertSame('First question', $first->question);
|
||||
}
|
||||
|
||||
/** @return \Iterator<string, array{int, string, int, string, int, int}> */
|
||||
public static function answerCountHeaderProvider(): \Iterator
|
||||
/** @return array<string, array{int, string, int, string, int, int}> */
|
||||
public static function answerCountHeaderProvider(): array
|
||||
{
|
||||
// Columns (0-based): Question=0, Answer1=1, Correct=2, Answer2=3, Correct=4, …
|
||||
// Answer N is at index 1+2*(N-1) = 2N-1, Correct N at 2+2*(N-1) = 2N.
|
||||
yield '2 answers → 2 header pairs' => [2, 'Answer 2', 3, 'Correct', 4, 5];
|
||||
yield '6 answers → 6 header pairs' => [6, 'Answer 6', 11, 'Correct', 12, 13];
|
||||
yield '7 answers → 7 header pairs' => [7, 'Answer 7', 13, 'Correct', 14, 15];
|
||||
yield '10 answers → 10 header pairs' => [10, 'Answer 10', 19, 'Correct', 20, 21];
|
||||
return [
|
||||
'2 answers → 2 header pairs' => [2, 'Answer 2', 3, 'Correct', 4, 5],
|
||||
'6 answers → 6 header pairs' => [6, 'Answer 6', 11, 'Correct', 12, 13],
|
||||
'7 answers → 7 header pairs' => [7, 'Answer 7', 13, 'Correct', 14, 15],
|
||||
'10 answers → 10 header pairs' => [10, 'Answer 10', 19, 'Correct', 20, 21],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('answerCountHeaderProvider')]
|
||||
@@ -262,7 +228,7 @@ final class QuizSpreadsheetServiceTest extends TestCase
|
||||
{
|
||||
$quiz = new Quiz();
|
||||
foreach ($counts as $i => $count) {
|
||||
$quiz->addQuestion($this->makeQuestion('Question '.$i, $count));
|
||||
$quiz->addQuestion($this->makeQuestion("Question $i", $count));
|
||||
}
|
||||
|
||||
return $quiz;
|
||||
@@ -274,7 +240,7 @@ final class QuizSpreadsheetServiceTest extends TestCase
|
||||
$question->question = $text;
|
||||
$question->ordering = 1;
|
||||
for ($i = 1; $i <= $answerCount; ++$i) {
|
||||
$question->addAnswer(new Answer('Answer '.$i, isRightAnswer: false));
|
||||
$question->addAnswer(new Answer("Answer $i", isRightAnswer: false));
|
||||
}
|
||||
|
||||
return $question;
|
||||
@@ -283,7 +249,7 @@ final class QuizSpreadsheetServiceTest extends TestCase
|
||||
/** @return array<int, string|null> */
|
||||
private function readFirstRow(string $path): array
|
||||
{
|
||||
$rows = new Reader\Xlsx()->setReadDataOnly(true)->load($path)->getActiveSheet()->toArray(formatData: false);
|
||||
$rows = (new Reader\Xlsx())->setReadDataOnly(true)->load($path)->getActiveSheet()->toArray(formatData: false);
|
||||
|
||||
return $rows[0] ?? [];
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tvdt\Tests\Twig;
|
||||
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
use function Safe\file_get_contents;
|
||||
use function Safe\preg_match_all;
|
||||
|
||||
final class TemplateReferencesTest extends TestCase
|
||||
{
|
||||
private static string $templatesDir;
|
||||
|
||||
public static function setUpBeforeClass(): void
|
||||
{
|
||||
self::$templatesDir = \dirname(__DIR__, 2).'/templates';
|
||||
}
|
||||
|
||||
/** @return iterable<string, array{string, string}> */
|
||||
public static function templateReferenceProvider(): iterable
|
||||
{
|
||||
$templatesDir = \dirname(__DIR__, 2).'/templates';
|
||||
$iterator = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($templatesDir, \RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
);
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
Assert::assertInstanceOf(\SplFileInfo::class, $file);
|
||||
if ('twig' !== $file->getExtension()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$content = file_get_contents($file->getPathname());
|
||||
$sourceFile = str_replace($templatesDir.'/', '', $file->getPathname());
|
||||
|
||||
// Match extends, include(), and embed tags — capture the quoted template name
|
||||
preg_match_all(
|
||||
'/(?:extends|include|embed)\s*\(?[\'"]([^\'"]+)[\'"]\)?/',
|
||||
$content,
|
||||
$matches,
|
||||
);
|
||||
|
||||
foreach ($matches[1] as $referencedTemplate) {
|
||||
yield \sprintf('%s → %s', $sourceFile, $referencedTemplate) => [$sourceFile, $referencedTemplate];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[DataProvider('templateReferenceProvider')]
|
||||
public function testReferencedTemplateExists(string $sourceFile, string $referencedTemplate): void
|
||||
{
|
||||
$absolutePath = self::$templatesDir.'/'.$referencedTemplate;
|
||||
|
||||
$this->assertFileExists(
|
||||
$absolutePath,
|
||||
\sprintf("Template '%s' references '%s' which does not exist at '%s'.", $sourceFile, $referencedTemplate, $absolutePath),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user