Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ jobs:
run: composer coverage:check

- name: Sanitize artifact name
if: always()
id: artifact
run: |
name="coverage-${{ matrix.php-version }}-${{ matrix.symfony-version }}"
Expand Down
17 changes: 17 additions & 0 deletions tests/CoverageCiGateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,23 @@ public function testCoverageReportUploadRunsEvenWhenGateFails(): void
);
}

public function testArtifactNameIsAvailableWhenTestsOrCoverageGateFail(): void
{
$workflow = self::readWorkflow();

self::assertMatchesRegularExpression(
'/- name: Sanitize artifact name(?:(?!- name:).)*if: always\(\)(?:(?!- name:).)*id: artifact(?:(?!- name:).)*GITHUB_OUTPUT/s',
$workflow,
'The artifact name step must run even after a failure and publish its output',
);
self::assertStringContainsString('name="coverage-${{ matrix.php-version }}-${{ matrix.symfony-version }}"', $workflow);
self::assertStringContainsString('name="${name//\\*/x}"', $workflow);
self::assertMatchesRegularExpression(
'/- name: Upload coverage report(?:(?!- name:).)*name: \${{ steps\.artifact\.outputs\.name }}/s',
$workflow,
);
}

public function testThresholdIsDefinedOnlyInComposerScript(): void
{
$workflow = self::readWorkflow();
Expand Down
67 changes: 46 additions & 21 deletions tests/Fixtures/polling_watcher_mid_sweep_runner.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* (the prefix of the tree that was visited in earlier ticks) is detected
* on the next sweep.
*
* Usage: php polling_watcher_mid_sweep_runner.php <temp_dir> <autoload_path>
* Usage: php polling_watcher_mid_sweep_runner.php <temp_dir> <autoload_path> [tail]
*
* Exit codes:
* 0 — success (detection occurred on the next sweep)
Expand All @@ -19,6 +19,7 @@

$tempDir = $argv[1] ?? '';
$autoloadPath = $argv[2] ?? '';
$tail = ($argv[3] ?? '') === 'tail';

if ($tempDir === '' || $autoloadPath === '' || !is_dir($tempDir)) {
fprintf(STDERR, "Usage: php polling_watcher_mid_sweep_runner.php <temp_dir> <autoload_path>\n");
Expand Down Expand Up @@ -76,39 +77,63 @@

$checkMethod = $watcherClass->getMethod('checkFileSystemChanges');

// Tick 1: processes first 500 files.
$checkMethod->invoke($watcher);
// Select from the actual traversal order, not from file names or creation order.
$entries = iterator_to_array(new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($tempDir, FilesystemIterator::SKIP_DOTS | FilesystemIterator::UNIX_PATHS),
RecursiveIteratorIterator::LEAVES_ONLY,
), false);
$target = $entries[$tail ? 599 : 0]->getPathname();
$iteratorsProp = $watcherClass->getProperty('iterators');

// Tick 2: completes the sweep (remaining 100 files).
// Tick 1 visits the prefix and leaves the sweep in progress.
$checkMethod->invoke($watcher);

// Verify the sweep completed (no resume state).
$iteratorsProp = $watcherClass->getProperty('iterators');
$iterators = $iteratorsProp->getValue($watcher);
if (!empty($iterators)) {
fprintf(STDERR, "FAIL: Sweep did not complete after 2 ticks (iterators not empty)\n");
if (empty($iterators)) {
fprintf(STDERR, "FAIL: Sweep completed before the second tick\n");
exit(3);
}

// Now modify file0.php (in the already-scanned prefix).
// Ensure the mtime is strictly greater than lastMTime.
sleep(1);
file_put_contents($tempDir . '/file0.php', '<?php // changed');
clearstatcache(true, $tempDir . '/file0.php');
if ($tail) {
// Exercise detection at the end of the next sweep as well.
$checkMethod->invoke($watcher);
}

$modifiedMTime = filemtime($tempDir . '/file0.php');
// In the prefix case this modification happens between ticks, after the
// watcher visited the file. The mtime must exceed its whole-second watermark.
sleep(1);
file_put_contents($target, '<?php // changed');
clearstatcache(true, $target);
$modifiedMTime = filemtime($target);
if (!is_int($modifiedMTime) || $modifiedMTime <= $initialMTime) {
fprintf(STDERR, "FAIL: Modified file mtime (%s) must exceed initial lastMTime (%d)\n", var_export($modifiedMTime, true), $initialMTime);
exit(3);
}

// Tick 3: starts a new sweep, should detect the change on file0.php.
$checkMethod->invoke($watcher);

$afterMTime = $lastMTimeProp->getValue($watcher);
if (!$tail) {
// The remaining part of the old sweep must not revisit the prefix.
$checkMethod->invoke($watcher);
if ($lastMTimeProp->getValue($watcher) !== $initialMTime) {
fprintf(STDERR, "FAIL: Modified prefix was detected before the next sweep\n");
exit(2);
}
}
$iterators = $iteratorsProp->getValue($watcher);
if (!empty($iterators)) {
fprintf(STDERR, "FAIL: Sweep did not complete after 2 ticks\n");
exit(3);
}

if ($afterMTime <= $initialMTime) {
fprintf(STDERR, "FAIL: lastMTime not updated after modifying a file in the already-scanned region (was %d, now %d)\n", $initialMTime, $afterMTime);
// A whole new sweep takes two ticks; the changed file can be in either one.
$checkMethod->invoke($watcher);
if ($tail && $lastMTimeProp->getValue($watcher) !== $initialMTime) {
fprintf(STDERR, "FAIL: Tail file was detected in the first tick\n");
exit(2);
}
if ($lastMTimeProp->getValue($watcher) === $initialMTime) {
$checkMethod->invoke($watcher);
}
if ($lastMTimeProp->getValue($watcher) !== $modifiedMTime) {
fprintf(STDERR, "FAIL: Modified file was not detected during the next sweep\n");
exit(2);
}

Expand Down
13 changes: 13 additions & 0 deletions tests/PollingMonitorWatcherTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,19 @@ public function testFileModifiedInAlreadyScannedRegionTriggersReloadOnNextSweep(
);
}

public function testFileAtEndOfNextSweepIsDetectedOnSecondTick(): void
{
$autoloadPath = \realpath(__DIR__ . '/../vendor/autoload.php');
if ($autoloadPath === false) {
self::markTestSkipped('vendor/autoload.php not found.');
}

$scriptFile = __DIR__ . '/Fixtures/polling_watcher_mid_sweep_runner.php';
$exitCode = $this->runPhpScript($scriptFile, [$this->tempDir, $autoloadPath, 'tail']);

self::assertSame(0, $exitCode, 'A file at the end of a sweep should be detected on its second tick.');
}

/**
* Adding or removing a directory between ticks must not throw, and
* the sweep must still converge. APFS readdir order is hash-based (not
Expand Down
Loading