feat: M3 import & indexing engine delivery layer with durable collision handling, space checking and console
This commit is contained in:
1 parent
93d991703c
commit
9d277d9e72
48 files changed
+5147
-4
No files matched your search
@@ -0,0 +1,225 @@
|
||||
<?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)];
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user