Files
MeshVault/tests/Unit/Console/ConsoleTest.php

226 lines
7.6 KiB
PHP

<?php
declare(strict_types=1);
namespace MeshVault\Tests\Unit\Console;
use MeshVault\Console\Console;
use MeshVault\Console\ConsoleServices;
use MeshVault\Indexer\ImportException;
use PHPUnit\Framework\TestCase;
use RuntimeException;
final class ConsoleTest extends TestCase
{
public function testHelpIsSuccessfulAndListsEverySubcommand(): void
{
[$code, $out, $err] = $this->execute(['help']);
self::assertSame(0, $code);
self::assertSame('', $err);
foreach (['import', 'worker', 'worker-once', '--ext'] as $fragment) {
self::assertStringContainsString($fragment, $out);
}
}
public function testMissingCommandIsAUsageError(): void
{
[$code, $out, $err] = $this->execute([]);
self::assertSame(1, $code);
self::assertSame('', $out);
self::assertStringContainsString('Usage: meshvault', $err);
}
public function testUnknownCommandIsAUsageError(): void
{
[$code, , $err] = $this->execute(['nope']);
self::assertSame(1, $code);
self::assertStringContainsString('Unknown command', $err);
}
public function testImportWithoutSourceIsAUsageError(): void
{
[$code, , $err] = $this->execute(['import']);
self::assertSame(1, $code);
self::assertStringContainsString('import <source-path>', $err);
}
public function testImportRejectsExtraOperandsAndUnknownOptions(): void
{
[$code, , $err] = $this->execute(['import', 'a', 'b']);
self::assertSame(1, $code);
self::assertStringContainsString('one source path', $err);
[$code, , $err] = $this->execute(['import', 'a', '--deep']);
self::assertSame(1, $code);
self::assertStringContainsString('Unknown option', $err);
self::assertStringContainsString('--deep', $err);
}
public function testImportRejectsEmptyExtensionList(): void
{
foreach (['--ext=', '--ext= , '] as $option) {
[$code, , $err] = $this->execute(['import', 'a', $option]);
self::assertSame(1, $code, $option);
self::assertStringContainsString('--ext', $err);
}
}
public function testImportQueuesAJobWithDefaultExtensions(): void
{
$services = new FakeConsoleServices(importResult: 42);
[$code, $out] = $this->execute(['import', '/srv/incoming'], $services);
self::assertSame(0, $code);
self::assertSame(
[['/srv/incoming', ['stl', 'jpg', 'jpeg', 'png', 'gif', 'pdf', 'txt', 'md']]],
$services->imports
);
self::assertStringContainsString('42', $out);
}
public function testImportNormalizesTheExtensionOption(): void
{
$services = new FakeConsoleServices(importResult: 7);
[$code] = $this->execute(['import', '/srv/in', '--ext= STL , .3mf ,stl'], $services);
self::assertSame(0, $code);
self::assertSame([['/srv/in', ['stl', '3mf']]], $services->imports);
}
public function testImportFailureReportsTheReasonCodeAndExitsTwo(): void
{
$services = new FakeConsoleServices(importError: new ImportException('SCAN_EMPTY', 'Nothing found.'));
[$code, $out, $err] = $this->execute(['import', '/srv/in'], $services);
self::assertSame(2, $code);
self::assertSame('', $out);
self::assertStringContainsString('SCAN_EMPTY', $err);
self::assertStringContainsString('Nothing found.', $err);
}
public function testUnexpectedFailureIsReportedWithoutLeakingDetails(): void
{
$services = new FakeConsoleServices(
importError: new RuntimeException('SQLSTATE[08006] password=hunter2')
);
[$code, , $err] = $this->execute(['import', '/srv/in'], $services);
self::assertSame(2, $code);
self::assertStringNotContainsString('hunter2', $err);
self::assertStringNotContainsString('#0 ', $err);
self::assertStringContainsString('RuntimeException', $err);
}
public function testWorkerOnceReportsTheProcessedJob(): void
{
$services = new FakeConsoleServices(jobs: [11]);
[$code, $out] = $this->execute(['worker-once'], $services);
self::assertSame(0, $code);
self::assertSame(1, $services->runs);
self::assertStringContainsString('11', $out);
}
public function testWorkerOnceReportsAnIdleQueue(): void
{
$services = new FakeConsoleServices(jobs: [null]);
[$code, $out] = $this->execute(['worker-once'], $services);
self::assertSame(0, $code);
self::assertStringContainsString('No pending import jobs', $out);
}
public function testWorkerOnceRejectsOperands(): void
{
[$code, , $err] = $this->execute(['worker-once', 'x']);
self::assertSame(1, $code);
self::assertStringContainsString('takes no arguments', $err);
}
public function testWorkerLoopsUntilTheIterationBoundAndSleepsWhenIdle(): void
{
$services = new FakeConsoleServices(jobs: [5, null, null]);
$slept = [];
[$code, $out] = $this->execute(
['worker', '--max-iterations=3', '--sleep=2'],
$services,
static function (int $seconds) use (&$slept): void {
$slept[] = $seconds;
}
);
self::assertSame(0, $code);
self::assertSame(3, $services->runs);
// Only idle passes sleep; a processed job is followed immediately by the next claim.
self::assertSame([2, 2], $slept);
self::assertStringContainsString('stopped', $out);
}
public function testWorkerStopsWhenAStopIsRequested(): void
{
$services = new FakeConsoleServices(jobs: [1, 2, 3]);
$calls = 0;
[$code, $out] = $this->execute(
['worker', '--max-iterations=99'],
$services,
null,
static function () use (&$calls): bool {
$calls++;
return $calls > 2;
}
);
self::assertSame(0, $code);
self::assertSame(2, $services->runs);
self::assertStringContainsString('stopped', $out);
}
public function testWorkerRejectsInvalidOptions(): void
{
foreach (['--max-iterations=0', '--max-iterations=x', '--sleep=-1', '--bogus', 'operand'] as $argument) {
[$code, , $err] = $this->execute(['worker', $argument]);
self::assertSame(1, $code, $argument);
self::assertNotSame('', $err);
}
}
public function testWorkerFailureExitsTwo(): void
{
$services = new FakeConsoleServices(
workerError: new ImportException('LOCK_UNAVAILABLE', 'Another worker holds the library lock.')
);
[$code, , $err] = $this->execute(['worker', '--max-iterations=1'], $services);
self::assertSame(2, $code);
self::assertStringContainsString('LOCK_UNAVAILABLE', $err);
}
/** @return array{0: int, 1: string, 2: string} */
private function execute(
array $argv,
?ConsoleServices $services = null,
?callable $sleep = null,
?callable $stopRequested = null
): array {
$out = fopen('php://memory', 'w+');
$err = fopen('php://memory', 'w+');
$console = new Console(
$services ?? new FakeConsoleServices(),
$out,
$err,
$sleep ?? static function (int $seconds): void {
},
$stopRequested ?? static fn (): bool => false
);
$code = $console->run($argv);
rewind($out);
rewind($err);
return [$code, (string) stream_get_contents($out), (string) stream_get_contents($err)];
}
}