Files
wp-healthcheck/tests/lib/harness.php
Steve Hanlon 631385721f Next healthcheck due date, plus a test suite (hc-nkq)
Lets a technician record when a site should next be looked at, and surfaces
that in the dashboard so it can be used for planning.

Server:
- migration 0002 adds healthchecks.next_due, a VARCHAR(10) 'YYYY-MM-DD'
  calendar date rather than a timestamp — it's a diary date with no
  time-of-day, and a timestamp would render as the wrong day off-server.
- Kept per-session rather than on a sites table, so the history of what was
  scheduled when is preserved. A site's current next-due is the value on its
  most recent session: if the latest visit scheduled nothing, the site reads
  as unscheduled rather than showing the just-completed visit as overdue.
- Validate::optionalDate() round-trips through createFromFormat, which
  rejects both '2026-2-3' and '2026-02-30' (silently rolled to March 2nd).
- next_due follows the same missing-vs-null PUT contract as finished_at, and
  lives in its own Store::setNextDue() so finishing or reopening a session
  never disturbs the date and vice versa.
- Dashboard shows it on the site list, the per-site session table and the
  session detail, flagged overdue / today / soon (within a fortnight).

Plugin:
- Date control on both the active and finished panels — the moment you know
  when to return is often wrap-up, after the report is generated.
- Write-through like every other mutation. Bad input is rejected with a
  notice rather than silently clearing an existing date.
- Included in both the Markdown and HTML reports.

Tests (new — tests/, export-ignored from the plugin zip):
- Hand-rolled harness, no composer/PHPUnit, in keeping with the repo.
- 42 tests: plugin-side date parsing units, plus server integration tests
  driven over real HTTP against a temp `php -S` instance with a throwaway
  SQLite DB, so a real server/config.php is never touched.
- Mutation-checked: dropping the array_key_exists guard on PUT fails three
  tests, as intended.
2026-07-23 12:02:39 +01:00

110 lines
3.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
/**
* Minimal test harness — no composer, no PHPUnit, matching the rest of the repo.
*
* Usage:
* test('description', function () { assert_same($expected, $actual, 'why'); });
*
* Failures are recorded and reported at the end; the process exits non-zero if
* anything failed, so this is usable as a CI/pre-push gate.
*/
final class TestRun {
/** @var array<int, array{name:string, error:string}> */
public static array $failures = [];
public static int $passed = 0;
public static int $assertions = 0;
public static string $group = '';
}
final class AssertionFailed extends RuntimeException {}
function group(string $name): void {
TestRun::$group = $name;
echo "\n\033[1m" . $name . "\033[0m\n";
}
function test(string $name, callable $fn): void {
try {
$fn();
TestRun::$passed++;
echo " \033[32m✓\033[0m " . $name . "\n";
} catch (AssertionFailed $e) {
TestRun::$failures[] = ['name' => TestRun::$group . ' ' . $name, 'error' => $e->getMessage()];
echo " \033[31m✗ " . $name . "\033[0m\n " . str_replace("\n", "\n ", $e->getMessage()) . "\n";
} catch (Throwable $e) {
$where = basename($e->getFile()) . ':' . $e->getLine();
TestRun::$failures[] = ['name' => TestRun::$group . ' ' . $name, 'error' => get_class($e) . ': ' . $e->getMessage() . ' @ ' . $where];
echo " \033[31m✗ " . $name . "\033[0m\n " . get_class($e) . ': ' . $e->getMessage() . ' @ ' . $where . "\n";
}
}
function assert_same($expected, $actual, string $message = ''): void {
TestRun::$assertions++;
if ($expected !== $actual) {
throw new AssertionFailed(
($message !== '' ? $message . "\n" : '')
. 'expected: ' . compact_export($expected) . "\n"
. 'actual: ' . compact_export($actual)
);
}
}
function assert_true($actual, string $message = ''): void {
assert_same(true, $actual, $message !== '' ? $message : 'expected true');
}
function assert_null($actual, string $message = ''): void {
assert_same(null, $actual, $message !== '' ? $message : 'expected null');
}
function assert_contains(string $needle, string $haystack, string $message = ''): void {
TestRun::$assertions++;
if (strpos($haystack, $needle) === false) {
throw new AssertionFailed(
($message !== '' ? $message . "\n" : '')
. 'expected to find: ' . $needle . "\n"
. 'in: ' . truncate($haystack, 600)
);
}
}
function assert_not_contains(string $needle, string $haystack, string $message = ''): void {
TestRun::$assertions++;
if (strpos($haystack, $needle) !== false) {
throw new AssertionFailed(
($message !== '' ? $message . "\n" : '')
. 'did NOT expect to find: ' . $needle . "\n"
. 'in: ' . truncate($haystack, 600)
);
}
}
function compact_export($value): string {
if (is_string($value)) return "'" . $value . "'";
if ($value === null) return 'null';
if (is_bool($value)) return $value ? 'true' : 'false';
if (is_array($value)) return truncate(json_encode($value, JSON_UNESCAPED_SLASHES), 400);
return (string) $value;
}
function truncate(string $s, int $max): string {
return strlen($s) <= $max ? $s : substr($s, 0, $max) . '… (' . strlen($s) . ' bytes)';
}
function report_and_exit(): void {
$failed = count(TestRun::$failures);
echo "\n" . str_repeat('─', 60) . "\n";
if ($failed === 0) {
echo "\033[32mPASS\033[0m — " . TestRun::$passed . ' tests, ' . TestRun::$assertions . " assertions\n";
exit(0);
}
echo "\033[31mFAIL\033[0m — " . $failed . ' failed, ' . TestRun::$passed . " passed\n\n";
foreach (TestRun::$failures as $f) {
echo "" . $f['name'] . "\n " . str_replace("\n", "\n ", $f['error']) . "\n\n";
}
exit(1);
}