Compare commits
No commits in common. "8218de16f5cecc95b3940a682160d0e09c394d42" and "0b0733cbec11f6595cb4917ecb403713cb9b14a9" have entirely different histories.
8218de16f5
...
0b0733cbec
8 changed files with 21 additions and 598 deletions
|
|
@ -131,9 +131,6 @@ intentionally unclaimed; the built-in health endpoint is `/up`.
|
||||||
- Run the focused PHPUnit test while developing.
|
- Run the focused PHPUnit test while developing.
|
||||||
- Use `just backend-types-check` for Larastan and `just backend-test` for the
|
- Use `just backend-types-check` for Larastan and `just backend-test` for the
|
||||||
full PHPUnit suite during iteration.
|
full PHPUnit suite during iteration.
|
||||||
- The completion gate runs `backend-test-unit` and `backend-test-feature` as
|
|
||||||
concurrent processes. This is safe because each process owns its own
|
|
||||||
in-memory SQLite database.
|
|
||||||
- Fix failures caused by the change. Report unrelated baseline failures
|
- Fix failures caused by the change. Report unrelated baseline failures
|
||||||
precisely rather than expanding scope silently.
|
precisely rather than expanding scope silently.
|
||||||
- The shared `just test-all` command is the required completion gate. Focused
|
- The shared `just test-all` command is the required completion gate. Focused
|
||||||
|
|
|
||||||
|
|
@ -95,10 +95,6 @@ backend and does not proxy the frontend.
|
||||||
|
|
||||||
- Cypress is configured under `cypress/` and runs through `npm run test:e2e`
|
- Cypress is configured under `cypress/` and runs through `npm run test:e2e`
|
||||||
or `just frontend-cypress-run`.
|
or `just frontend-cypress-run`.
|
||||||
- Every Cypress spec must appear in exactly one `frontend_specs_*` group in
|
|
||||||
the root `justfile`. The completion gate runs those groups concurrently,
|
|
||||||
and its `cypress-spec-coverage` job rejects ungrouped, duplicate, or missing
|
|
||||||
specs. Keep `frontend-cypress-run` for whole-suite iteration.
|
|
||||||
- Prefer the cheapest test seam that proves the behavior. Cypress covers
|
- Prefer the cheapest test seam that proves the behavior. Cypress covers
|
||||||
routing, browser forms, authentication flows, request wiring, and responsive
|
routing, browser forms, authentication flows, request wiring, and responsive
|
||||||
behavior.
|
behavior.
|
||||||
|
|
|
||||||
10
ai/shared.md
10
ai/shared.md
|
|
@ -223,16 +223,12 @@ gate passes against that worktree:
|
||||||
3. Run the complete gate from the worktree root:
|
3. Run the complete gate from the worktree root:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
JUST_JOBS=4 direnv exec "$(git rev-parse --show-toplevel)" just test-all
|
direnv exec "$(git rev-parse --show-toplevel)" just test-all
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Do not hand-assemble a substitute from focused commands. `test-all` runs
|
4. Do not hand-assemble a substitute from focused commands. `test-all` runs
|
||||||
one pool of 13 jobs: frontend format, lint, type, build, and Cypress
|
frontend format and lint checks, frontend type checking, Larastan, the
|
||||||
groups; Larastan; the PHPUnit Unit and Feature suites; and the Cypress
|
production build, PHPUnit, and Cypress in fail-fast order.
|
||||||
spec coverage guard. Jobs report as they finish, failed output is buffered
|
|
||||||
and printed at the end, and every job runs even after another job fails.
|
|
||||||
`JUST_JOBS` limits concurrency and defaults to 4. Use 2 when resources are
|
|
||||||
constrained or 1 for a serial debugging run.
|
|
||||||
5. Everything must pass before completion. Report exact baseline or
|
5. Everything must pass before completion. Report exact baseline or
|
||||||
environmental failures rather than hiding them.
|
environmental failures rather than hiding them.
|
||||||
6. If the stack was started only for validation, stop it when finished:
|
6. If the stack was started only for validation, stop it when finished:
|
||||||
|
|
|
||||||
|
|
@ -1,249 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Database\Seeders;
|
|
||||||
|
|
||||||
use App\Element\CreateElementDto;
|
|
||||||
use App\Element\Element;
|
|
||||||
use App\Element\ElementRepository;
|
|
||||||
use App\Set\CreateSetDto;
|
|
||||||
use App\Set\CreateSetLevelDto;
|
|
||||||
use App\Set\Set;
|
|
||||||
use App\Set\SetLevel;
|
|
||||||
use App\Set\SetLevelRepository;
|
|
||||||
use App\Set\SetRepository;
|
|
||||||
use App\Shared\ValueObject\EmailAddress;
|
|
||||||
use App\User\UserRepository;
|
|
||||||
use Illuminate\Database\Seeder;
|
|
||||||
use RuntimeException;
|
|
||||||
|
|
||||||
class TanachSeeder extends Seeder
|
|
||||||
{
|
|
||||||
private const string SET_NAME = 'Tanach';
|
|
||||||
|
|
||||||
private const array LEVEL_KINDS = [
|
|
||||||
'chelek',
|
|
||||||
'sefer',
|
|
||||||
'perek',
|
|
||||||
];
|
|
||||||
|
|
||||||
private const array SEFARIM_BY_CHELEK = [
|
|
||||||
'Torah' => [
|
|
||||||
'Bereishis' => 50,
|
|
||||||
'Shemos' => 40,
|
|
||||||
'Vayikra' => 27,
|
|
||||||
'Bamidbar' => 36,
|
|
||||||
'Devarim' => 34,
|
|
||||||
],
|
|
||||||
"Nevi'im" => [
|
|
||||||
'Yehoshua' => 24,
|
|
||||||
'Shoftim' => 21,
|
|
||||||
'Shmuel Alef' => 31,
|
|
||||||
'Shmuel Beis' => 24,
|
|
||||||
'Melachim Alef' => 22,
|
|
||||||
'Melachim Beis' => 25,
|
|
||||||
'Yeshayahu' => 66,
|
|
||||||
'Yirmiyahu' => 52,
|
|
||||||
'Yechezkel' => 48,
|
|
||||||
'Hoshea' => 14,
|
|
||||||
'Yoel' => 4,
|
|
||||||
'Amos' => 9,
|
|
||||||
'Ovadiah' => 1,
|
|
||||||
'Yonah' => 4,
|
|
||||||
'Michah' => 7,
|
|
||||||
'Nachum' => 3,
|
|
||||||
'Chavakuk' => 3,
|
|
||||||
'Tzefaniah' => 3,
|
|
||||||
'Chaggai' => 2,
|
|
||||||
'Zechariah' => 14,
|
|
||||||
'Malachi' => 3,
|
|
||||||
],
|
|
||||||
'Kesuvim' => [
|
|
||||||
'Tehillim' => 150,
|
|
||||||
'Mishlei' => 31,
|
|
||||||
'Iyov' => 42,
|
|
||||||
'Shir HaShirim' => 8,
|
|
||||||
'Rus' => 4,
|
|
||||||
'Eichah' => 5,
|
|
||||||
'Koheles' => 12,
|
|
||||||
'Esther' => 10,
|
|
||||||
'Daniel' => 12,
|
|
||||||
'Ezra' => 10,
|
|
||||||
'Nechemiah' => 13,
|
|
||||||
'Divrei HaYamim Alef' => 29,
|
|
||||||
'Divrei HaYamim Beis' => 36,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
public function run(): void
|
|
||||||
{
|
|
||||||
$this->call(UserSeeder::class);
|
|
||||||
|
|
||||||
$user = app(UserRepository::class)->findByEmail(
|
|
||||||
new EmailAddress(UserSeeder::EMAIL),
|
|
||||||
);
|
|
||||||
if ($user === null) {
|
|
||||||
throw new RuntimeException('seeded user not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
$setRepository = app(SetRepository::class);
|
|
||||||
$tanach = $this->findTanach($setRepository);
|
|
||||||
if ($tanach === null) {
|
|
||||||
$tanach = $setRepository->create(new CreateSetDto(
|
|
||||||
name: self::SET_NAME,
|
|
||||||
creator: $user,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
$levels = $this->seedLevels(
|
|
||||||
repository: app(SetLevelRepository::class),
|
|
||||||
tanach: $tanach,
|
|
||||||
);
|
|
||||||
$this->seedElements(
|
|
||||||
repository: app(ElementRepository::class),
|
|
||||||
tanach: $tanach,
|
|
||||||
levels: $levels,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function findTanach(SetRepository $repository): ?Set
|
|
||||||
{
|
|
||||||
foreach ($repository->all() as $set) {
|
|
||||||
if ($set->getName() === self::SET_NAME) {
|
|
||||||
return $set;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array{
|
|
||||||
* chelek: SetLevel,
|
|
||||||
* sefer: SetLevel,
|
|
||||||
* perek: SetLevel
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
private function seedLevels(
|
|
||||||
SetLevelRepository $repository,
|
|
||||||
Set $tanach,
|
|
||||||
): array {
|
|
||||||
$levels = $repository->findBySet($tanach);
|
|
||||||
|
|
||||||
foreach ($levels as $depth => $level) {
|
|
||||||
$expectedKind = self::LEVEL_KINDS[$depth] ?? null;
|
|
||||||
if ($level->getKind() !== $expectedKind) {
|
|
||||||
throw new RuntimeException(
|
|
||||||
'Tanach levels do not match the expected hierarchy',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$missingKinds = array_slice(self::LEVEL_KINDS, count($levels));
|
|
||||||
foreach ($missingKinds as $kind) {
|
|
||||||
$levels[] = $repository->create(new CreateSetLevelDto(
|
|
||||||
set: $tanach,
|
|
||||||
kind: $kind,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'chelek' => $this->findLevel($levels, 'chelek'),
|
|
||||||
'sefer' => $this->findLevel($levels, 'sefer'),
|
|
||||||
'perek' => $this->findLevel($levels, 'perek'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param list<SetLevel> $levels
|
|
||||||
*/
|
|
||||||
private function findLevel(array $levels, string $kind): SetLevel
|
|
||||||
{
|
|
||||||
foreach ($levels as $level) {
|
|
||||||
if ($level->getKind() === $kind) {
|
|
||||||
return $level;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new RuntimeException("Tanach {$kind} level not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array{
|
|
||||||
* chelek: SetLevel,
|
|
||||||
* sefer: SetLevel,
|
|
||||||
* perek: SetLevel
|
|
||||||
* } $levels
|
|
||||||
*/
|
|
||||||
private function seedElements(
|
|
||||||
ElementRepository $repository,
|
|
||||||
Set $tanach,
|
|
||||||
array $levels,
|
|
||||||
): void {
|
|
||||||
$chelakim = $repository->findTopLevelBySet($tanach);
|
|
||||||
|
|
||||||
foreach (self::SEFARIM_BY_CHELEK as $chelekName => $sefarim) {
|
|
||||||
$chelek = $this->findOrCreateElement(
|
|
||||||
repository: $repository,
|
|
||||||
siblings: $chelakim,
|
|
||||||
name: $chelekName,
|
|
||||||
level: $levels['chelek'],
|
|
||||||
parentElement: null,
|
|
||||||
);
|
|
||||||
$seededSefarim = $repository->findByParentElement($chelek);
|
|
||||||
|
|
||||||
foreach ($sefarim as $seferName => $perekCount) {
|
|
||||||
$sefer = $this->findOrCreateElement(
|
|
||||||
repository: $repository,
|
|
||||||
siblings: $seededSefarim,
|
|
||||||
name: $seferName,
|
|
||||||
level: $levels['sefer'],
|
|
||||||
parentElement: $chelek,
|
|
||||||
);
|
|
||||||
$perakim = $repository->findByParentElement($sefer);
|
|
||||||
|
|
||||||
for (
|
|
||||||
$perekNumber = 1;
|
|
||||||
$perekNumber <= $perekCount;
|
|
||||||
$perekNumber++
|
|
||||||
) {
|
|
||||||
$this->findOrCreateElement(
|
|
||||||
repository: $repository,
|
|
||||||
siblings: $perakim,
|
|
||||||
name: "Perek {$perekNumber}",
|
|
||||||
level: $levels['perek'],
|
|
||||||
parentElement: $sefer,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param list<Element> $siblings
|
|
||||||
*/
|
|
||||||
private function findOrCreateElement(
|
|
||||||
ElementRepository $repository,
|
|
||||||
array &$siblings,
|
|
||||||
string $name,
|
|
||||||
SetLevel $level,
|
|
||||||
?Element $parentElement,
|
|
||||||
): Element {
|
|
||||||
foreach ($siblings as $sibling) {
|
|
||||||
if (
|
|
||||||
$sibling->getName() === $name
|
|
||||||
&& $sibling->getLevel()->getId() === $level->getId()
|
|
||||||
) {
|
|
||||||
return $sibling;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$element = $repository->create(new CreateElementDto(
|
|
||||||
name: $name,
|
|
||||||
level: $level,
|
|
||||||
parentElement: $parentElement,
|
|
||||||
));
|
|
||||||
$siblings[] = $element;
|
|
||||||
|
|
||||||
return $element;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,184 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature\Database;
|
|
||||||
|
|
||||||
use App\Element\Element;
|
|
||||||
use App\Element\ElementRepository;
|
|
||||||
use App\Set\Set;
|
|
||||||
use App\Set\SetLevelRepository;
|
|
||||||
use App\Set\SetRepository;
|
|
||||||
use App\Shared\ValueObject\EmailAddress;
|
|
||||||
use App\User\UserRepository;
|
|
||||||
use Database\Seeders\TanachSeeder;
|
|
||||||
use Database\Seeders\UserSeeder;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class TanachSeederTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
private const array SEFARIM_BY_CHELEK = [
|
|
||||||
'Torah' => [
|
|
||||||
'Bereishis' => 50,
|
|
||||||
'Shemos' => 40,
|
|
||||||
'Vayikra' => 27,
|
|
||||||
'Bamidbar' => 36,
|
|
||||||
'Devarim' => 34,
|
|
||||||
],
|
|
||||||
"Nevi'im" => [
|
|
||||||
'Yehoshua' => 24,
|
|
||||||
'Shoftim' => 21,
|
|
||||||
'Shmuel Alef' => 31,
|
|
||||||
'Shmuel Beis' => 24,
|
|
||||||
'Melachim Alef' => 22,
|
|
||||||
'Melachim Beis' => 25,
|
|
||||||
'Yeshayahu' => 66,
|
|
||||||
'Yirmiyahu' => 52,
|
|
||||||
'Yechezkel' => 48,
|
|
||||||
'Hoshea' => 14,
|
|
||||||
'Yoel' => 4,
|
|
||||||
'Amos' => 9,
|
|
||||||
'Ovadiah' => 1,
|
|
||||||
'Yonah' => 4,
|
|
||||||
'Michah' => 7,
|
|
||||||
'Nachum' => 3,
|
|
||||||
'Chavakuk' => 3,
|
|
||||||
'Tzefaniah' => 3,
|
|
||||||
'Chaggai' => 2,
|
|
||||||
'Zechariah' => 14,
|
|
||||||
'Malachi' => 3,
|
|
||||||
],
|
|
||||||
'Kesuvim' => [
|
|
||||||
'Tehillim' => 150,
|
|
||||||
'Mishlei' => 31,
|
|
||||||
'Iyov' => 42,
|
|
||||||
'Shir HaShirim' => 8,
|
|
||||||
'Rus' => 4,
|
|
||||||
'Eichah' => 5,
|
|
||||||
'Koheles' => 12,
|
|
||||||
'Esther' => 10,
|
|
||||||
'Daniel' => 12,
|
|
||||||
'Ezra' => 10,
|
|
||||||
'Nechemiah' => 13,
|
|
||||||
'Divrei HaYamim Alef' => 29,
|
|
||||||
'Divrei HaYamim Beis' => 36,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
public function test_it_seeds_the_complete_tanach_idempotently(): void
|
|
||||||
{
|
|
||||||
$this->seed(TanachSeeder::class);
|
|
||||||
$this->seed(TanachSeeder::class);
|
|
||||||
|
|
||||||
$this->assertDatabaseCount('users', 1);
|
|
||||||
$this->assertDatabaseCount('sets', 1);
|
|
||||||
$this->assertDatabaseCount('set_levels', 3);
|
|
||||||
$this->assertDatabaseCount('elements', 971);
|
|
||||||
|
|
||||||
$user = app(UserRepository::class)->findByEmail(
|
|
||||||
new EmailAddress(UserSeeder::EMAIL),
|
|
||||||
);
|
|
||||||
$this->assertNotNull($user);
|
|
||||||
|
|
||||||
$tanach = $this->findTanach();
|
|
||||||
$this->assertSame(
|
|
||||||
UserSeeder::EMAIL,
|
|
||||||
$tanach->getCreator()->getEmail()->value(),
|
|
||||||
);
|
|
||||||
|
|
||||||
$levels = app(SetLevelRepository::class)->findBySet($tanach);
|
|
||||||
$this->assertSame(
|
|
||||||
['chelek', 'sefer', 'perek'],
|
|
||||||
array_map(function ($level): string {
|
|
||||||
return $level->getKind();
|
|
||||||
}, $levels),
|
|
||||||
);
|
|
||||||
|
|
||||||
$elementRepository = app(ElementRepository::class);
|
|
||||||
$chelakim = $elementRepository->findTopLevelBySet($tanach);
|
|
||||||
$this->assertElements(
|
|
||||||
elements: $chelakim,
|
|
||||||
expectedNames: array_keys(self::SEFARIM_BY_CHELEK),
|
|
||||||
expectedKind: 'chelek',
|
|
||||||
);
|
|
||||||
|
|
||||||
$seferCount = 0;
|
|
||||||
$perekCount = 0;
|
|
||||||
|
|
||||||
foreach ($chelakim as $chelek) {
|
|
||||||
$expectedSefarim = self::SEFARIM_BY_CHELEK[
|
|
||||||
$chelek->getName()
|
|
||||||
] ?? null;
|
|
||||||
$this->assertNotNull($expectedSefarim);
|
|
||||||
|
|
||||||
$sefarim = $elementRepository->findByParentElement($chelek);
|
|
||||||
$this->assertElements(
|
|
||||||
elements: $sefarim,
|
|
||||||
expectedNames: array_keys($expectedSefarim),
|
|
||||||
expectedKind: 'sefer',
|
|
||||||
);
|
|
||||||
$seferCount += count($sefarim);
|
|
||||||
|
|
||||||
foreach ($sefarim as $sefer) {
|
|
||||||
$expectedPerekCount = $expectedSefarim[
|
|
||||||
$sefer->getName()
|
|
||||||
] ?? null;
|
|
||||||
$this->assertNotNull($expectedPerekCount);
|
|
||||||
|
|
||||||
$expectedPerekNames = [];
|
|
||||||
for (
|
|
||||||
$perekNumber = 1;
|
|
||||||
$perekNumber <= $expectedPerekCount;
|
|
||||||
$perekNumber++
|
|
||||||
) {
|
|
||||||
$expectedPerekNames[] = "Perek {$perekNumber}";
|
|
||||||
}
|
|
||||||
|
|
||||||
$perakim = $elementRepository->findByParentElement($sefer);
|
|
||||||
$this->assertElements(
|
|
||||||
elements: $perakim,
|
|
||||||
expectedNames: $expectedPerekNames,
|
|
||||||
expectedKind: 'perek',
|
|
||||||
);
|
|
||||||
$perekCount += count($perakim);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->assertSame(39, $seferCount);
|
|
||||||
$this->assertSame(929, $perekCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function findTanach(): Set
|
|
||||||
{
|
|
||||||
foreach (app(SetRepository::class)->all() as $set) {
|
|
||||||
if ($set->getName() === 'Tanach') {
|
|
||||||
return $set;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->fail('Tanach set was not seeded');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param list<Element> $elements
|
|
||||||
* @param list<string> $expectedNames
|
|
||||||
*/
|
|
||||||
private function assertElements(
|
|
||||||
array $elements,
|
|
||||||
array $expectedNames,
|
|
||||||
string $expectedKind,
|
|
||||||
): void {
|
|
||||||
$this->assertSame(
|
|
||||||
$expectedNames,
|
|
||||||
array_map(function (Element $element): string {
|
|
||||||
return $element->getName();
|
|
||||||
}, $elements),
|
|
||||||
);
|
|
||||||
|
|
||||||
foreach ($elements as $index => $element) {
|
|
||||||
$this->assertSame($expectedKind, $element->getKind());
|
|
||||||
$this->assertSame($index + 1, $element->getPosition());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -69,11 +69,7 @@ describe('set element layout', () => {
|
||||||
|
|
||||||
cy.location('pathname').should('equal', '/sets/41')
|
cy.location('pathname').should('equal', '/sets/41')
|
||||||
cy.get('h1').should('have.text', 'Bible')
|
cy.get('h1').should('have.text', 'Bible')
|
||||||
cy.contains('a', 'Back to dashboard').should(
|
cy.contains('a', 'Back to sets').should('have.attr', 'href', '/dashboard')
|
||||||
'have.attr',
|
|
||||||
'href',
|
|
||||||
'/dashboard',
|
|
||||||
)
|
|
||||||
cy.get('ol[aria-label="Bible element layout"] > li').then(($nodes) => {
|
cy.get('ol[aria-label="Bible element layout"] > li').then(($nodes) => {
|
||||||
expect([...$nodes].map((node) => node.dataset.elementId)).to.deep.equal([
|
expect([...$nodes].map((node) => node.dataset.elementId)).to.deep.equal([
|
||||||
'1',
|
'1',
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ function toggleElement(elementId: number): void {
|
||||||
<section class="set-layout">
|
<section class="set-layout">
|
||||||
<RouterLink class="back-link" :to="{ name: 'dashboard' }">
|
<RouterLink class="back-link" :to="{ name: 'dashboard' }">
|
||||||
<span aria-hidden="true">←</span>
|
<span aria-hidden="true">←</span>
|
||||||
Back to dashboard
|
Back to sets
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
|
||||||
<p v-if="loading" class="layout-state" role="status">Loading set layout...</p>
|
<p v-if="loading" class="layout-state" role="status">Loading set layout...</p>
|
||||||
|
|
|
||||||
159
justfile
159
justfile
|
|
@ -1,88 +1,30 @@
|
||||||
set shell := ["bash", "-c"]
|
set shell := ["bash", "-c"]
|
||||||
|
|
||||||
# How many jobs the parallel gate runs at once. Lower this when another
|
|
||||||
# worktree stack is active, or use 1 for a serial debugging run.
|
|
||||||
jobs := env('JUST_JOBS', '4')
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@just --list
|
@just --list
|
||||||
|
|
||||||
# Full completion gate. Every check runs in one bounded pool, with likely
|
# Full completion gate. Start the worktree stack before running it because
|
||||||
# long Cypress groups scheduled first and shorter jobs filling free lanes.
|
# Cypress exercises the frontend and its backend wiring.
|
||||||
# Start the worktree stack before running it because Cypress needs Vite.
|
|
||||||
test-all:
|
test-all:
|
||||||
just par "gate" {{ jobs }} \
|
@echo "==> frontend format + lint checks"
|
||||||
frontend-cypress-account frontend-cypress-session \
|
just frontend-format-check
|
||||||
frontend-cypress-sets backend-types-check \
|
just frontend-lint-check
|
||||||
frontend-cypress-scheduling frontend-build \
|
@echo "==> frontend type check"
|
||||||
backend-test-feature frontend-cypress-today \
|
just frontend-type-check
|
||||||
frontend-lint-check frontend-type-check backend-test-unit \
|
@echo "==> backend static analysis"
|
||||||
frontend-format-check cypress-spec-coverage
|
just backend-types-check
|
||||||
|
@echo "==> frontend production build"
|
||||||
# Run recipes concurrently, buffering each one's output to its own log.
|
just frontend-build
|
||||||
# Report jobs as they finish, then print every failed job's complete log.
|
@echo "==> backend tests"
|
||||||
[private]
|
just backend-test
|
||||||
par label max +targets:
|
@echo "==> frontend Cypress tests"
|
||||||
#!/usr/bin/env bash
|
just frontend-cypress-run
|
||||||
set -uo pipefail
|
|
||||||
|
|
||||||
targets=({{ targets }})
|
|
||||||
logs=$(mktemp -d)
|
|
||||||
declare -A target_of started_at
|
|
||||||
failed=()
|
|
||||||
next=0
|
|
||||||
running=0
|
|
||||||
|
|
||||||
echo "==> {{ label }} (${#targets[@]} jobs, up to {{ max }} at a time)"
|
|
||||||
|
|
||||||
while (( next < ${#targets[@]} || running > 0 )); do
|
|
||||||
while (( next < ${#targets[@]} && running < {{ max }} )); do
|
|
||||||
target="${targets[next]}"
|
|
||||||
just "$target" > "$logs/$target.log" 2>&1 &
|
|
||||||
target_of[$!]="$target"
|
|
||||||
started_at[$!]=$SECONDS
|
|
||||||
next=$(( next + 1 ))
|
|
||||||
running=$(( running + 1 ))
|
|
||||||
done
|
|
||||||
|
|
||||||
wait -n -p finished
|
|
||||||
status=$?
|
|
||||||
running=$(( running - 1 ))
|
|
||||||
target="${target_of[$finished]}"
|
|
||||||
elapsed=$(( SECONDS - started_at[$finished] ))
|
|
||||||
|
|
||||||
if (( status == 0 )); then
|
|
||||||
printf ' ok %-28s %4ds\n' "$target" "$elapsed"
|
|
||||||
else
|
|
||||||
printf ' FAIL %-28s %4ds\n' "$target" "$elapsed"
|
|
||||||
failed+=("$target")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if (( ${#failed[@]} == 0 )); then
|
|
||||||
rm -rf "$logs"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
for target in "${failed[@]}"; do
|
|
||||||
printf '\n--- %s ---\n' "$target"
|
|
||||||
cat "$logs/$target.log"
|
|
||||||
done
|
|
||||||
printf '\n{{ label }} failed: %s\n' "${failed[*]}"
|
|
||||||
printf 'logs kept in %s\n' "$logs"
|
|
||||||
exit 1
|
|
||||||
|
|
||||||
# Backend
|
# Backend
|
||||||
|
|
||||||
backend-test *args:
|
backend-test *args:
|
||||||
cd backend && php artisan test {{args}}
|
cd backend && php artisan test {{args}}
|
||||||
|
|
||||||
backend-test-unit *args:
|
|
||||||
cd backend && php artisan test --testsuite=Unit {{ args }}
|
|
||||||
|
|
||||||
backend-test-feature *args:
|
|
||||||
cd backend && php artisan test --testsuite=Feature {{ args }}
|
|
||||||
|
|
||||||
backend-types-check:
|
backend-types-check:
|
||||||
cd backend && composer types:check
|
cd backend && composer types:check
|
||||||
|
|
||||||
|
|
@ -111,74 +53,3 @@ frontend-build:
|
||||||
|
|
||||||
frontend-cypress-run:
|
frontend-cypress-run:
|
||||||
cd frontend/website && npm run test:e2e
|
cd frontend/website && npm run test:e2e
|
||||||
|
|
||||||
# Each Cypress spec must belong to exactly one feature group. The complete
|
|
||||||
# gate runs these groups concurrently; the whole-suite recipe above remains
|
|
||||||
# available for focused iteration.
|
|
||||||
frontend_specs_account := "confirm-email login signup"
|
|
||||||
frontend_specs_session := "guest-auth session-auth"
|
|
||||||
frontend_specs_sets := "set-layout sets-dashboard"
|
|
||||||
frontend_specs_scheduling := "set-scheduling"
|
|
||||||
frontend_specs_today := "today-assignments"
|
|
||||||
|
|
||||||
frontend-cypress-account:
|
|
||||||
just _cypress {{ frontend_specs_account }}
|
|
||||||
|
|
||||||
frontend-cypress-session:
|
|
||||||
just _cypress {{ frontend_specs_session }}
|
|
||||||
|
|
||||||
frontend-cypress-sets:
|
|
||||||
just _cypress {{ frontend_specs_sets }}
|
|
||||||
|
|
||||||
frontend-cypress-scheduling:
|
|
||||||
just _cypress {{ frontend_specs_scheduling }}
|
|
||||||
|
|
||||||
frontend-cypress-today:
|
|
||||||
just _cypress {{ frontend_specs_today }}
|
|
||||||
|
|
||||||
[private]
|
|
||||||
_cypress +names:
|
|
||||||
cd frontend/website && specs=$(for name in {{ names }}; do \
|
|
||||||
printf 'cypress/e2e/%s.cy.ts,' "$name"; done) && \
|
|
||||||
npm run test:e2e -- --spec "${specs%,}"
|
|
||||||
|
|
||||||
cypress-spec-coverage:
|
|
||||||
just _spec-coverage {{ frontend_specs_account }} \
|
|
||||||
{{ frontend_specs_session }} {{ frontend_specs_sets }} \
|
|
||||||
{{ frontend_specs_scheduling }} {{ frontend_specs_today }}
|
|
||||||
|
|
||||||
[private]
|
|
||||||
_spec-coverage +names:
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -uo pipefail
|
|
||||||
|
|
||||||
cd frontend/website
|
|
||||||
listed=$(printf '%s\n' {{ names }} | sort)
|
|
||||||
on_disk=$(ls cypress/e2e/*.cy.ts | xargs -n1 basename \
|
|
||||||
| sed 's/\.cy\.ts$//' | sort)
|
|
||||||
|
|
||||||
duplicated=$(echo "$listed" | uniq -d)
|
|
||||||
unique=$(echo "$listed" | uniq)
|
|
||||||
ungrouped=$(comm -13 <(echo "$unique") <(echo "$on_disk"))
|
|
||||||
missing=$(comm -23 <(echo "$unique") <(echo "$on_disk"))
|
|
||||||
status=0
|
|
||||||
|
|
||||||
if [ -n "$ungrouped" ]; then
|
|
||||||
echo "in no group, so never runs in the gate:"
|
|
||||||
printf ' %s\n' $ungrouped
|
|
||||||
status=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$missing" ]; then
|
|
||||||
echo "listed in a group but not on disk:"
|
|
||||||
printf ' %s\n' $missing
|
|
||||||
status=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$duplicated" ]; then
|
|
||||||
echo "in more than one group, so runs twice:"
|
|
||||||
printf ' %s\n' $duplicated
|
|
||||||
status=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit $status
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue