first commit.
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
use Toolbox\SelfUpdater;
|
||||
use Toolbox\ToolboxException;
|
||||
|
||||
Harness::suite('group10');
|
||||
|
||||
$GLOBALS['g10_repo'] = dirname(__DIR__);
|
||||
|
||||
Harness::it('self-update verifica sha256 y reemplaza atómicamente', function () {
|
||||
$release = tmpdir('release');
|
||||
$work = tmpdir('work');
|
||||
|
||||
$content = "#!/usr/bin/env php\n<?php // v2\n";
|
||||
write_file($release . '/duckbrain.phar', $content);
|
||||
write_file($release . '/duckbrain.phar.sha256', hash('sha256', $content));
|
||||
|
||||
$target = $work . '/duckbrain.phar';
|
||||
write_file($target, "old");
|
||||
|
||||
(new SelfUpdater(new Toolbox\Console()))->update($release, $target, $work);
|
||||
|
||||
eq($content, file_get_contents($target), 'binario reemplazado por el nuevo');
|
||||
ok(is_executable($target), 'queda ejecutable');
|
||||
|
||||
rrmdir($release);
|
||||
rrmdir($work);
|
||||
});
|
||||
|
||||
Harness::it('self-update aborta ante sha256 inválido sin tocar el binario', function () {
|
||||
$release = tmpdir('release');
|
||||
$work = tmpdir('work');
|
||||
|
||||
write_file($release . '/duckbrain.phar', "contenido nuevo");
|
||||
write_file($release . '/duckbrain.phar.sha256', 'deadbeef');
|
||||
|
||||
$target = $work . '/duckbrain.phar';
|
||||
write_file($target, 'viejo');
|
||||
|
||||
throws(function () use ($release, $target, $work) {
|
||||
(new SelfUpdater(new Toolbox\Console()))->update($release, $target, $work);
|
||||
});
|
||||
|
||||
eq('viejo', file_get_contents($target), 'el binario actual queda intacto');
|
||||
|
||||
rrmdir($release);
|
||||
rrmdir($work);
|
||||
});
|
||||
|
||||
Harness::it('build-phar genera un phar ejecutable con su sha256', function () {
|
||||
$repo = $GLOBALS['g10_repo'];
|
||||
$dist = tmpdir('dist');
|
||||
|
||||
exec('DUCKBRAIN_DIST=' . escapeshellarg($dist) . ' bash ' . escapeshellarg($repo . '/scripts/build-phar') . ' 2>&1', $out, $code);
|
||||
eq(0, $code, 'build-phar termina bien: ' . implode("\n", $out));
|
||||
|
||||
$phar = $dist . '/duckbrain.phar';
|
||||
ok(is_file($phar), 'phar generado');
|
||||
ok(is_file($phar . '.sha256'), 'sha256 publicado');
|
||||
eq(hash_file('sha256', $phar), trim((string) file_get_contents($phar . '.sha256')));
|
||||
|
||||
exec('php ' . escapeshellarg($phar) . ' --version 2>&1', $ver, $vcode);
|
||||
eq(0, $vcode, 'el phar arranca');
|
||||
ok(str_contains(implode("\n", $ver), 'duckbrain-toolbox'), 'el phar imprime su versión');
|
||||
|
||||
rrmdir($dist);
|
||||
});
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
use Toolbox\Catalog;
|
||||
use Toolbox\Manifest;
|
||||
use Toolbox\Resolver;
|
||||
use Toolbox\Semver;
|
||||
use Toolbox\ToolboxException;
|
||||
|
||||
Harness::suite('group2');
|
||||
|
||||
Harness::it('semver compara y satisface rangos', function () {
|
||||
ok(Semver::satisfies('1.2.0', '>=1.0'));
|
||||
ok(!Semver::satisfies('0.9.0', '>=1.0'));
|
||||
ok(Semver::satisfies('1.2.3', '^1.2'));
|
||||
ok(!Semver::satisfies('2.0.0', '^1.2'));
|
||||
ok(Semver::satisfies('1.2.9', '~1.2.3'));
|
||||
ok(!Semver::satisfies('1.3.0', '~1.2.3'));
|
||||
ok(Semver::satisfies('1.0.0', '*'));
|
||||
eq(0, Semver::compare('1.0.0', '1.0.0'));
|
||||
ok(Semver::compare('1.0.1', '1.0.0') > 0);
|
||||
ok(Semver::compare('1.0.0-beta', '1.0.0') < 0);
|
||||
});
|
||||
|
||||
Harness::it('manifiesto válido parsea identidad, componentes y default', function () {
|
||||
$m = Manifest::fromArray([
|
||||
'name' => 'http',
|
||||
'version' => '1.2.0',
|
||||
'description' => 'Utilidades HTTP',
|
||||
'components' => [
|
||||
'curlRequest' => ['files' => ['src/Libs/CurlRequest.php']],
|
||||
'aria2' => ['files' => ['src/Libs/Aria2.php']],
|
||||
],
|
||||
'default' => ['curlRequest', 'aria2'],
|
||||
]);
|
||||
|
||||
eq('http', $m->name);
|
||||
eq('1.2.0', $m->version);
|
||||
eq(['curlRequest', 'aria2'], $m->expand(null));
|
||||
eq(['aria2'], $m->expand('aria2'));
|
||||
});
|
||||
|
||||
Harness::it('manifiesto sin name es inválido', function () {
|
||||
throws(fn () => Manifest::fromArray(['version' => '1.0.0', 'components' => ['a' => ['files' => ['x']]]]));
|
||||
});
|
||||
|
||||
Harness::it('manifiesto sin components es inválido', function () {
|
||||
throws(fn () => Manifest::fromArray(['name' => 'x', 'version' => '1.0.0']));
|
||||
});
|
||||
|
||||
Harness::it('default que referencia componente inexistente es inválido', function () {
|
||||
throws(fn () => Manifest::fromArray([
|
||||
'name' => 'x', 'version' => '1.0.0',
|
||||
'components' => ['a' => ['files' => ['x']]],
|
||||
'default' => ['nope'],
|
||||
]));
|
||||
});
|
||||
|
||||
Harness::it('paquete sin default y sin componente explícito falla', function () {
|
||||
$m = Manifest::fromArray([
|
||||
'name' => 'x', 'version' => '1.0.0',
|
||||
'components' => ['a' => ['files' => ['x']]],
|
||||
]);
|
||||
throws(fn () => $m->expand(null));
|
||||
});
|
||||
|
||||
Harness::it('resuelve dependencias en orden (dependencias primero)', function () {
|
||||
$dir = tmpdir();
|
||||
write_file($dir . '/http/duckbrain.json', json_encode([
|
||||
'name' => 'http', 'version' => '1.0.0',
|
||||
'components' => [
|
||||
'curlRequest' => ['files' => ['src/Libs/CurlRequest.php']],
|
||||
'compreFace' => [
|
||||
'files' => ['src/Libs/CompreFace.php'],
|
||||
'require' => ['http:curlRequest' => '>=1.0'],
|
||||
],
|
||||
],
|
||||
], JSON_PRETTY_PRINT));
|
||||
|
||||
$resolver = new Resolver(new Catalog($dir));
|
||||
$result = $resolver->resolve(['http:compreFace']);
|
||||
|
||||
eq([
|
||||
['package' => 'http', 'component' => 'curlRequest'],
|
||||
['package' => 'http', 'component' => 'compreFace'],
|
||||
], $result);
|
||||
|
||||
rrmdir($dir);
|
||||
});
|
||||
|
||||
Harness::it('require no satisfecho detiene con error', function () {
|
||||
$dir = tmpdir();
|
||||
write_file($dir . '/a/duckbrain.json', json_encode([
|
||||
'name' => 'a', 'version' => '1.0.0',
|
||||
'components' => [
|
||||
'main' => ['files' => ['src/Libs/A.php'], 'require' => ['b:lib' => '>=2.0']],
|
||||
],
|
||||
]));
|
||||
write_file($dir . '/b/duckbrain.json', json_encode([
|
||||
'name' => 'b', 'version' => '1.0.0',
|
||||
'components' => ['lib' => ['files' => ['src/Libs/B.php']]],
|
||||
]));
|
||||
|
||||
$resolver = new Resolver(new Catalog($dir));
|
||||
throws(fn () => $resolver->resolve(['a:main']));
|
||||
|
||||
rrmdir($dir);
|
||||
});
|
||||
|
||||
Harness::it('ciclo de dependencias se detecta', function () {
|
||||
$dir = tmpdir();
|
||||
write_file($dir . '/a/duckbrain.json', json_encode([
|
||||
'name' => 'a', 'version' => '1.0.0',
|
||||
'components' => [
|
||||
'x' => ['files' => ['src/Libs/A.php'], 'require' => ['a:y' => '*']],
|
||||
'y' => ['files' => ['src/Libs/B.php'], 'require' => ['a:x' => '*']],
|
||||
],
|
||||
]));
|
||||
|
||||
$resolver = new Resolver(new Catalog($dir));
|
||||
throws(fn () => $resolver->resolve(['a:x']));
|
||||
|
||||
rrmdir($dir);
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
use Toolbox\Drift;
|
||||
use Toolbox\FileSync;
|
||||
|
||||
Harness::suite('group3-drift');
|
||||
|
||||
Harness::it('sin drift escribe/reescribe', function () {
|
||||
$dir = tmpdir();
|
||||
$file = $dir . '/a.php';
|
||||
$sync = new FileSync();
|
||||
|
||||
eq(FileSync::WRITTEN, $sync->write($file, 'uno', null));
|
||||
eq('uno', file_get_contents($file));
|
||||
|
||||
$hash = Drift::hash($file);
|
||||
eq(FileSync::OVERWRITTEN, $sync->write($file, 'dos', $hash));
|
||||
|
||||
rrmdir($dir);
|
||||
});
|
||||
|
||||
Harness::it('drift sin bandera aborta', function () {
|
||||
$dir = tmpdir();
|
||||
$file = $dir . '/a.php';
|
||||
write_file($file, 'original');
|
||||
$hash = Drift::hash($file);
|
||||
write_file($file, 'editado');
|
||||
|
||||
$sync = new FileSync();
|
||||
eq(FileSync::ABORTED, $sync->write($file, 'nuevo', $hash));
|
||||
eq('editado', file_get_contents($file));
|
||||
|
||||
rrmdir($dir);
|
||||
});
|
||||
|
||||
Harness::it('--force sobreescribe el drift', function () {
|
||||
$dir = tmpdir();
|
||||
$file = $dir . '/a.php';
|
||||
write_file($file, 'original');
|
||||
$hash = Drift::hash($file);
|
||||
write_file($file, 'editado');
|
||||
|
||||
$sync = new FileSync();
|
||||
eq(FileSync::OVERWRITTEN, $sync->write($file, 'nuevo', $hash, ['force' => true]));
|
||||
eq('nuevo', file_get_contents($file));
|
||||
|
||||
rrmdir($dir);
|
||||
});
|
||||
|
||||
Harness::it('--backup respalda y escribe', function () {
|
||||
$dir = tmpdir();
|
||||
$file = $dir . '/a.php';
|
||||
write_file($file, 'original');
|
||||
$hash = Drift::hash($file);
|
||||
write_file($file, 'editado');
|
||||
|
||||
$sync = new FileSync();
|
||||
eq(FileSync::BACKED_UP, $sync->write($file, 'nuevo', $hash, ['backup' => true]));
|
||||
eq('nuevo', file_get_contents($file));
|
||||
eq('editado', file_get_contents($file . '.bak'));
|
||||
|
||||
rrmdir($dir);
|
||||
});
|
||||
|
||||
Harness::it('--backup respalda en la primera escritura sin hash previo', function () {
|
||||
$dir = tmpdir();
|
||||
$file = $dir . '/a.php';
|
||||
write_file($file, 'heredado');
|
||||
|
||||
$sync = new FileSync();
|
||||
eq(FileSync::BACKED_UP, $sync->write($file, 'canonico', null, ['backup' => true]));
|
||||
eq('canonico', file_get_contents($file));
|
||||
eq('heredado', file_get_contents($file . '.bak'), 'conserva la copia heredada');
|
||||
|
||||
rrmdir($dir);
|
||||
});
|
||||
|
||||
Harness::it('--keep-local conserva y no escribe', function () {
|
||||
$dir = tmpdir();
|
||||
$file = $dir . '/a.php';
|
||||
write_file($file, 'original');
|
||||
$hash = Drift::hash($file);
|
||||
write_file($file, 'editado');
|
||||
|
||||
$sync = new FileSync();
|
||||
eq(FileSync::SKIPPED, $sync->write($file, 'nuevo', $hash, ['keepLocal' => true]));
|
||||
eq('editado', file_get_contents($file));
|
||||
|
||||
rrmdir($dir);
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
use Toolbox\Destination;
|
||||
use Toolbox\Drift;
|
||||
use Toolbox\Lock;
|
||||
use Toolbox\Ownership;
|
||||
use Toolbox\ProjectManifest;
|
||||
|
||||
Harness::suite('group3');
|
||||
|
||||
Harness::it('manifest de deseo guarda y recupera restricciones', function () {
|
||||
$dir = tmpdir();
|
||||
$m = ProjectManifest::load($dir);
|
||||
$m->setConstraint('http:aria2', '^1.2');
|
||||
$m->coreConstraint = '>=1.1';
|
||||
$m->save($dir);
|
||||
|
||||
$loaded = ProjectManifest::load($dir);
|
||||
eq('^1.2', $loaded->constraint('http:aria2'));
|
||||
eq('>=1.1', $loaded->coreConstraint);
|
||||
|
||||
rrmdir($dir);
|
||||
});
|
||||
|
||||
Harness::it('lock guarda core y componentes con schema esperado', function () {
|
||||
$dir = tmpdir();
|
||||
$file = $dir . '/src/Libs/Aria2.php';
|
||||
write_file($file, "<?php\n");
|
||||
|
||||
$lock = Lock::empty();
|
||||
$lock->setCore(['version' => '1.5.0', 'commit' => 'abc', 'files' => ['src/Libs/Router.php' => 'sha256:x']]);
|
||||
$lock->setComponent('http:aria2', [
|
||||
'package' => 'http',
|
||||
'version' => '1.2.0',
|
||||
'toolbox_commit' => 'D',
|
||||
'mode' => 'copy',
|
||||
'files' => ['src/Libs/Aria2.php' => Drift::hash($file)],
|
||||
]);
|
||||
$lock->save($dir);
|
||||
|
||||
$loaded = Lock::load($dir);
|
||||
eq('1.5.0', $loaded->core()['version']);
|
||||
eq('http', $loaded->component('http:aria2')['package']);
|
||||
eq('copy', $loaded->component('http:aria2')['mode']);
|
||||
|
||||
rrmdir($dir);
|
||||
});
|
||||
|
||||
Harness::it('ownership detecta colisión de rutas', function () {
|
||||
$lock = Lock::empty();
|
||||
$lock->setCore(['files' => ['src/Libs/X.php' => 'sha256:a']]);
|
||||
$lock->setComponent('a:b', ['package' => 'a', 'version' => '1', 'toolbox_commit' => null, 'mode' => 'copy', 'files' => ['src/Libs/X.php' => 'sha256:b']]);
|
||||
|
||||
$ownership = Ownership::fromLock($lock);
|
||||
ok($ownership->collisions() !== [], 'debía detectar colisión');
|
||||
throws(fn () => $ownership->assertNoCollisions());
|
||||
});
|
||||
|
||||
Harness::it('ownership separa core y componentes sin colisión', function () {
|
||||
$lock = Lock::empty();
|
||||
$lock->setCore(['files' => ['src/Libs/Router.php' => 'sha256:a']]);
|
||||
$lock->setComponent('a:b', ['package' => 'a', 'version' => '1', 'toolbox_commit' => null, 'mode' => 'copy', 'files' => ['src/Libs/B.php' => 'sha256:b']]);
|
||||
|
||||
$ownership = Ownership::fromLock($lock);
|
||||
eq('core', $ownership->ownerOf('src/Libs/Router.php'));
|
||||
eq('a:b', $ownership->ownerOf('src/Libs/B.php'));
|
||||
|
||||
$ownership->assertNoCollisions();
|
||||
});
|
||||
|
||||
Harness::it('drift distingue match, drifted y missing', function () {
|
||||
$dir = tmpdir();
|
||||
$file = $dir . '/f.php';
|
||||
write_file($file, 'original');
|
||||
$hash = Drift::hash($file);
|
||||
|
||||
eq(Drift::MATCH, Drift::status($file, $hash));
|
||||
|
||||
write_file($file, 'editado');
|
||||
eq(Drift::DRIFTED, Drift::status($file, $hash));
|
||||
|
||||
eq(Drift::MISSING, Drift::status($dir . '/nope.php', $hash));
|
||||
|
||||
rrmdir($dir);
|
||||
});
|
||||
|
||||
Harness::it('destino aplica la regla src y fuera de src', function () {
|
||||
$rootDir = '/proj';
|
||||
$rootCore = '/proj/src';
|
||||
|
||||
eq('src/Libs/Crypto.php', Destination::relative('src/Libs/Crypto.php', $rootDir, $rootCore));
|
||||
eq('duckbrain', Destination::relative('duckbrain', $rootDir, $rootCore));
|
||||
eq('/proj/src/Libs/Crypto.php', Destination::resolve('src/Libs/Crypto.php', $rootDir, $rootCore));
|
||||
eq('/proj/duckbrain', Destination::resolve('duckbrain', $rootDir, $rootCore));
|
||||
|
||||
eq('custom/Libs/Crypto.php', Destination::relative('src/Libs/Crypto.php', $rootDir, '/proj/custom'));
|
||||
});
|
||||
|
||||
Harness::it('destino explícito sobreescribe la regla', function () {
|
||||
$dest = Destination::relative('src/Libs/Crypto.php', '/proj', '/proj/src', ['src/Libs/Crypto.php' => 'elsewhere/Crypto.php']);
|
||||
eq('elsewhere/Crypto.php', $dest);
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
use Toolbox\Console;
|
||||
use Toolbox\Copier;
|
||||
use Toolbox\FileSync;
|
||||
use Toolbox\PostInstall;
|
||||
|
||||
Harness::suite('group4');
|
||||
|
||||
function make_package_sources(string $dir): void
|
||||
{
|
||||
write_file($dir . '/duckbrain', "#!/usr/bin/env php\n<?php\n");
|
||||
write_file($dir . '/src/Interfaces/CommandInterface.php', "<?php\n");
|
||||
write_file($dir . '/src/Commands/Migration.php', "<?php\n");
|
||||
write_file($dir . '/phpunit.xml', "<phpunit/>\n");
|
||||
write_file($dir . '/tests/TestCase.php', "<?php\n");
|
||||
}
|
||||
|
||||
Harness::it('copia respetando la regla src y fuera de src', function () {
|
||||
$src = tmpdir('pkg');
|
||||
$root = tmpdir('proj');
|
||||
make_package_sources($src);
|
||||
|
||||
$copier = new Copier();
|
||||
$result = $copier->install(
|
||||
['duckbrain', 'src/Interfaces/CommandInterface.php', 'src/Commands/Migration.php', 'phpunit.xml', 'tests/TestCase.php'],
|
||||
$src,
|
||||
$root,
|
||||
$root . '/src',
|
||||
null,
|
||||
'copy'
|
||||
);
|
||||
|
||||
ok(is_file($root . '/duckbrain'), 'binario CLI en ROOT_DIR');
|
||||
ok(is_file($root . '/src/Interfaces/CommandInterface.php'), 'interfaz en ROOT_CORE');
|
||||
ok(is_file($root . '/src/Commands/Migration.php'), 'comando en ROOT_CORE');
|
||||
ok(is_file($root . '/phpunit.xml'), 'phpunit.xml en ROOT_DIR');
|
||||
ok(is_file($root . '/tests/TestCase.php'), 'tests en ROOT_DIR');
|
||||
ok(!is_dir($root . '/src/tests'), 'tests no va dentro de src');
|
||||
eq(5, count($result['files']));
|
||||
|
||||
rrmdir($src);
|
||||
rrmdir($root);
|
||||
});
|
||||
|
||||
Harness::it('destino explícito sobreescribe la regla en la copia', function () {
|
||||
$src = tmpdir('pkg');
|
||||
$root = tmpdir('proj');
|
||||
write_file($src . '/src/Libs/X.php', "<?php\n");
|
||||
|
||||
$copier = new Copier();
|
||||
$copier->install(
|
||||
['src/Libs/X.php'],
|
||||
$src,
|
||||
$root,
|
||||
$root . '/src',
|
||||
['src/Libs/X.php' => 'custom/X.php'],
|
||||
'copy'
|
||||
);
|
||||
|
||||
ok(is_file($root . '/custom/X.php'), 'destino explícito');
|
||||
ok(!is_file($root . '/src/Libs/X.php'), 'no usa la regla por defecto');
|
||||
|
||||
rrmdir($src);
|
||||
rrmdir($root);
|
||||
});
|
||||
|
||||
Harness::it('modo enlace crea symlink y refleja cambios', function () {
|
||||
$src = tmpdir('pkg');
|
||||
$root = tmpdir('proj');
|
||||
write_file($src . '/src/Libs/X.php', "uno");
|
||||
|
||||
$copier = new Copier();
|
||||
$result = $copier->install(['src/Libs/X.php'], $src, $root, $root . '/src', null, 'link');
|
||||
|
||||
$target = $root . '/src/Libs/X.php';
|
||||
ok(is_link($target), 'debe ser un symlink');
|
||||
eq("uno", file_get_contents($target));
|
||||
|
||||
write_file($src . '/src/Libs/X.php', "dos");
|
||||
eq("dos", file_get_contents($target), 'el enlace refleja cambios sin recopiar');
|
||||
eq(FileSync::WRITTEN, $result['actions']['src/Libs/X.php']);
|
||||
|
||||
rrmdir($root);
|
||||
rrmdir($src);
|
||||
});
|
||||
|
||||
Harness::it('post-install sin hooks no ejecuta nada', function () {
|
||||
$console = new Console();
|
||||
$executed = PostInstall::run([], $console, true, fn () => 0);
|
||||
eq([], $executed);
|
||||
});
|
||||
|
||||
Harness::it('post-install ejecuta con confirmación (--yes) y runner', function () {
|
||||
$console = new Console();
|
||||
$ran = [];
|
||||
$executed = PostInstall::run(
|
||||
['composer require --dev phpunit/phpunit'],
|
||||
$console,
|
||||
true,
|
||||
function (string $cmd) use (&$ran) { $ran[] = $cmd; return 0; }
|
||||
);
|
||||
|
||||
eq(['composer require --dev phpunit/phpunit'], $executed);
|
||||
eq(['composer require --dev phpunit/phpunit'], $ran);
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
use Toolbox\Commands\DoctorCommand;
|
||||
use Toolbox\Compatibility;
|
||||
use Toolbox\Console;
|
||||
use Toolbox\Lock;
|
||||
|
||||
Harness::suite('group5');
|
||||
|
||||
Harness::it('compatibilidad: core por encima de lo probado avisa', function () {
|
||||
$r = Compatibility::evaluateCore('1.5.0', '>=1.1', '1.4');
|
||||
eq(Compatibility::UNTESTED, $r['status']);
|
||||
});
|
||||
|
||||
Harness::it('compatibilidad: dentro del rango es OK', function () {
|
||||
$r = Compatibility::evaluateCore('1.5.0', '>=1.1', '1.5');
|
||||
eq(Compatibility::OK, $r['status']);
|
||||
});
|
||||
|
||||
Harness::it('compatibilidad: core insuficiente', function () {
|
||||
$r = Compatibility::evaluateCore('1.5.0', '>=1.6', null);
|
||||
eq(Compatibility::INSUFFICIENT, $r['status']);
|
||||
});
|
||||
|
||||
Harness::it('compatibilidad: sin datos', function () {
|
||||
$r = Compatibility::evaluateCore('1.5.0', null, null);
|
||||
eq(Compatibility::UNKNOWN, $r['status']);
|
||||
});
|
||||
|
||||
Harness::it('compatibilidad: versión de core desconocida no inventa 0.0.0', function () {
|
||||
$r = Compatibility::evaluateCore(null, '>=1.0', '1.0');
|
||||
eq(Compatibility::UNKNOWN, $r['status']);
|
||||
ok(str_contains($r['message'], 'desconocida'));
|
||||
});
|
||||
|
||||
Harness::it('compatibilidad PHP', function () {
|
||||
eq(Compatibility::OK, Compatibility::evaluatePhp('>=8.1', '8.5.0')['status']);
|
||||
eq(Compatibility::INSUFFICIENT, Compatibility::evaluatePhp('>=9.0', '8.5.0')['status']);
|
||||
eq(Compatibility::UNKNOWN, Compatibility::evaluatePhp(null, '8.5.0')['status']);
|
||||
});
|
||||
|
||||
Harness::it('doctor no modifica lock ni archivos', function () {
|
||||
$root = tmpdir('proj');
|
||||
$tool = tmpdir('tool');
|
||||
|
||||
$lock = Lock::empty();
|
||||
$lock->setCore(['version' => '1.5.0', 'commit' => 'abc', 'files' => []]);
|
||||
$lock->setComponent('http:aria2', ['package' => 'http', 'version' => '1.2.0', 'toolbox_commit' => 'D', 'mode' => 'copy', 'files' => []]);
|
||||
$lock->save($root);
|
||||
|
||||
write_file($tool . '/packages/http/duckbrain.json', json_encode([
|
||||
'name' => 'http', 'version' => '1.2.0',
|
||||
'core' => ['requires' => '>=1.1', 'tested' => '1.4'],
|
||||
'components' => ['aria2' => ['files' => ['src/Libs/Aria2.php']]],
|
||||
]));
|
||||
|
||||
$before = file_get_contents(Lock::path($root));
|
||||
|
||||
$cmd = new DoctorCommand(new Console());
|
||||
$code = $cmd->run(['--root=' . $root, '--toolbox=' . $tool]);
|
||||
eq(0, $code);
|
||||
|
||||
eq($before, file_get_contents(Lock::path($root)), 'el lock no debe cambiar');
|
||||
ok(!is_file($root . '/.duckbrain/manifest.json'), 'doctor no crea manifest');
|
||||
|
||||
rrmdir($root);
|
||||
rrmdir($tool);
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
Harness::suite('group6-release');
|
||||
|
||||
Harness::it('release bumpea, commitea, tagea y publica', function () {
|
||||
$repo = tmpdir('relrepo');
|
||||
$origin = tmpdir('relorigin') . '/origin.git';
|
||||
$script = dirname(__DIR__) . '/scripts/release';
|
||||
|
||||
git_init($repo);
|
||||
exec('git init -q --bare ' . escapeshellarg($origin));
|
||||
exec('git -C ' . escapeshellarg($repo) . ' remote add origin ' . escapeshellarg($origin));
|
||||
|
||||
write_file($repo . '/scripts/release', file_get_contents($script));
|
||||
chmod($repo . '/scripts/release', 0755);
|
||||
seed_package($repo, '1.2.3');
|
||||
git_commit($repo, 'init');
|
||||
|
||||
exec('cd ' . escapeshellarg($repo) . ' && bash scripts/release http minor', $out, $code);
|
||||
eq(0, $code, 'release debe terminar bien: ' . implode("\n", $out));
|
||||
|
||||
$pushed = exec('git -C ' . escapeshellarg($repo) . ' tag -l "http-v1.3.0"');
|
||||
eq('http-v1.3.0', $pushed, 'tag creado');
|
||||
|
||||
$remote = exec('git -C ' . escapeshellarg($origin) . ' tag -l "http-v1.3.0"');
|
||||
eq('http-v1.3.0', $remote, 'tag publicado en origin');
|
||||
|
||||
$manifest = json_decode((string) file_get_contents($repo . '/packages/http/duckbrain.json'), true);
|
||||
eq('1.3.0', $manifest['version']);
|
||||
|
||||
rrmdir($repo);
|
||||
rrmdir(dirname($origin));
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
use Toolbox\TagResolver;
|
||||
use Toolbox\ToolboxRepo;
|
||||
|
||||
Harness::suite('group6');
|
||||
|
||||
Harness::it('resuelve versiones por tags prefijados, ignora prereleases y yanked', function () {
|
||||
$repoDir = tmpdir('toolrepo');
|
||||
git_init($repoDir);
|
||||
|
||||
seed_package($repoDir, '1.0.0');
|
||||
git_commit($repoDir, 'v1.0.0');
|
||||
git_tag($repoDir, 'http-v1.0.0');
|
||||
|
||||
seed_package($repoDir, '1.2.0');
|
||||
git_commit($repoDir, 'v1.2.0');
|
||||
git_tag($repoDir, 'http-v1.2.0');
|
||||
|
||||
seed_package($repoDir, '1.3.0-rc1');
|
||||
git_commit($repoDir, 'v1.3.0-rc1');
|
||||
git_tag($repoDir, 'http-v1.3.0-rc1');
|
||||
|
||||
$repo = new ToolboxRepo($repoDir, '');
|
||||
$resolver = new TagResolver($repo);
|
||||
|
||||
eq('1.2.0', $resolver->latest('http')['version'], 'prerelease omitida por defecto');
|
||||
eq('1.3.0-rc1', $resolver->latest('http', null, true)['version'], '--pre incluye prerelease');
|
||||
eq('1.2.0', $resolver->latest('http', '^1.0')['version']);
|
||||
eq(null, $resolver->latest('http', '^0.1'), 'restricción sin coincidencia');
|
||||
|
||||
$yanked = new TagResolver($repo, ['1.2.0']);
|
||||
eq('1.0.0', $yanked->latest('http')['version'], 'versión retirada se salta');
|
||||
|
||||
rrmdir($repoDir);
|
||||
});
|
||||
|
||||
Harness::it('tag inconsistente con el manifiesto se rechaza', function () {
|
||||
$repoDir = tmpdir('toolrepo');
|
||||
git_init($repoDir);
|
||||
|
||||
seed_package($repoDir, '1.0.0');
|
||||
git_commit($repoDir, 'v1.0.0');
|
||||
git_tag($repoDir, 'http-v1.0.0');
|
||||
|
||||
// Tag nuevo apuntando al mismo commit (manifiesto dice 1.0.0, no 9.9.9)
|
||||
git_tag($repoDir, 'http-v9.9.9');
|
||||
|
||||
$resolver = new TagResolver(new ToolboxRepo($repoDir, ''));
|
||||
eq('1.0.0', $resolver->latest('http')['version'], 'el tag inconsistente no debe usarse');
|
||||
|
||||
$versions = $resolver->versions('http');
|
||||
$invalid = array_values(array_filter($versions, fn ($v) => !$v['valid']));
|
||||
eq(1, count($invalid));
|
||||
eq('9.9.9', $invalid[0]['version']);
|
||||
|
||||
rrmdir($repoDir);
|
||||
});
|
||||
|
||||
Harness::it('clone superficial se completa y resuelve commit histórico', function () {
|
||||
$origin = tmpdir('origin');
|
||||
git_init($origin);
|
||||
|
||||
seed_package($origin, '1.0.0');
|
||||
git_commit($origin, 'v1.0.0');
|
||||
git_tag($origin, 'http-v1.0.0');
|
||||
|
||||
seed_package($origin, '1.1.0');
|
||||
git_commit($origin, 'v1.1.0');
|
||||
git_tag($origin, 'http-v1.1.0');
|
||||
|
||||
$cache = tmpdir('cache') . '/toolbox';
|
||||
exec('git clone -q --depth 1 ' . escapeshellarg('file://' . $origin) . ' ' . escapeshellarg($cache) . ' 2>/dev/null');
|
||||
|
||||
$repo = new ToolboxRepo($cache, 'file://' . $origin);
|
||||
ok(is_file($cache . '/.git/shallow'), 'la caché de prueba es superficial');
|
||||
|
||||
$repo->ensureFull();
|
||||
|
||||
ok(!is_file($cache . '/.git/shallow'), 'debe dejar de ser superficial');
|
||||
$historical = $repo->commitOfTag('http-v1.0.0');
|
||||
ok($historical !== null, 'resuelve el tag histórico');
|
||||
ok($repo->fileAt($historical, 'packages/http/duckbrain.json') !== null, 'lee el archivo en ese commit');
|
||||
|
||||
rrmdir($origin);
|
||||
rrmdir(dirname($cache));
|
||||
});
|
||||
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
use Toolbox\Commands\AddCommand;
|
||||
use Toolbox\Commands\InstallCommand;
|
||||
use Toolbox\Commands\ListCommand;
|
||||
use Toolbox\Commands\RemoveCommand;
|
||||
use Toolbox\Commands\UpdateCommand;
|
||||
use Toolbox\Console;
|
||||
use Toolbox\Lock;
|
||||
|
||||
Harness::suite('group7');
|
||||
|
||||
function g7_toolbox(bool $compreFaceInDefault = true): string
|
||||
{
|
||||
$dir = tmpdir('tool');
|
||||
write_file($dir . '/packages/http/duckbrain.json', json_encode([
|
||||
'name' => 'http',
|
||||
'version' => '1.0.0',
|
||||
'description' => 'Utilidades HTTP',
|
||||
'components' => [
|
||||
'curlRequest' => ['files' => ['src/Libs/CurlRequest.php']],
|
||||
'aria2' => ['files' => ['src/Libs/Aria2.php']],
|
||||
'compreFace' => [
|
||||
'files' => ['src/Libs/CompreFace.php'],
|
||||
'require' => ['http:curlRequest' => '>=1.0'],
|
||||
],
|
||||
],
|
||||
'default' => $compreFaceInDefault ? ['curlRequest', 'aria2', 'compreFace'] : ['aria2'],
|
||||
], JSON_PRETTY_PRINT));
|
||||
|
||||
write_file($dir . '/packages/http/src/Libs/CurlRequest.php', "<?php\n// curl\n");
|
||||
write_file($dir . '/packages/http/src/Libs/Aria2.php', "<?php\n// aria2\n");
|
||||
write_file($dir . '/packages/http/src/Libs/CompreFace.php', "<?php\n// compreface\n");
|
||||
|
||||
return $dir;
|
||||
}
|
||||
|
||||
function g7_core(string $version = '0.1.0', string $router = 'router-v1'): string
|
||||
{
|
||||
$dir = tmpdir('core');
|
||||
write_file($dir . '/autoload.php', <<<'PHP'
|
||||
<?php
|
||||
define('ROOT_DIR', __DIR__);
|
||||
define('ROOT_CORE', ROOT_DIR . '/src');
|
||||
spl_autoload_register(function (string $class): void {
|
||||
$fp = str_replace('\\', '/', $class);
|
||||
$file = ROOT_CORE . '/' . dirname($fp) . '/' . basename($fp) . '.php';
|
||||
if (file_exists($file)) {
|
||||
require_once $file;
|
||||
}
|
||||
});
|
||||
PHP);
|
||||
write_file($dir . '/config.php', "<?php\n");
|
||||
write_file($dir . '/src/Libs/Router.php', "<?php\n// " . $router . "\n");
|
||||
// Documentación del repo (rastreada, pero excluida del core distribuible).
|
||||
write_file($dir . '/readme.org', "* core readme\n");
|
||||
|
||||
git_init($dir);
|
||||
git_commit($dir, 'core ' . $router);
|
||||
if ($version !== '') {
|
||||
git_tag($dir, 'v' . $version);
|
||||
}
|
||||
|
||||
return $dir;
|
||||
}
|
||||
|
||||
function g7_console(&$stream): Console
|
||||
{
|
||||
$stream = fopen('php://memory', 'r+');
|
||||
|
||||
return new Console($stream, $stream);
|
||||
}
|
||||
|
||||
Harness::it('install descarga el core en un directorio vacío', function () {
|
||||
$proj = tmpdir('proj');
|
||||
$core = g7_core('0.1.0');
|
||||
$tool = g7_toolbox();
|
||||
|
||||
$cmd = new InstallCommand(new Console());
|
||||
$code = $cmd->run([$proj, '--toolbox=' . $tool, '--core-dir=' . $core, '--yes']);
|
||||
eq(0, $code);
|
||||
ok(is_file($proj . '/autoload.php'));
|
||||
ok(is_file($proj . '/src/Libs/Router.php'));
|
||||
ok(!is_file($proj . '/readme.org'), 'no copia la documentación del core');
|
||||
eq('0.1.0', Lock::load($proj)->core()['version'], 'versión resuelta por tag v0.1.0');
|
||||
|
||||
rrmdir($proj);
|
||||
rrmdir($core);
|
||||
rrmdir($tool);
|
||||
});
|
||||
|
||||
Harness::it('add copia componente, registra lock y manifest', function () {
|
||||
$proj = tmpdir('proj');
|
||||
$tool = g7_toolbox();
|
||||
|
||||
$cmd = new AddCommand(new Console());
|
||||
$code = $cmd->run(['http:aria2', '--root=' . $proj, '--toolbox=' . $tool, '--yes']);
|
||||
eq(0, $code);
|
||||
ok(is_file($proj . '/src/Libs/Aria2.php'), 'archivo copiado');
|
||||
|
||||
$lock = Lock::load($proj);
|
||||
ok($lock->has('http:aria2'), 'lock registra el componente');
|
||||
|
||||
rrmdir($proj);
|
||||
rrmdir($tool);
|
||||
});
|
||||
|
||||
Harness::it('add de paquete expande default; require trae dependencia', function () {
|
||||
$proj = tmpdir('proj');
|
||||
$tool = g7_toolbox(false); // default sólo aria2
|
||||
|
||||
$cmd = new AddCommand(new Console());
|
||||
$cmd->run(['http:compreFace', '--root=' . $proj, '--toolbox=' . $tool, '--yes']);
|
||||
|
||||
ok(is_file($proj . '/src/Libs/CompreFace.php'));
|
||||
ok(is_file($proj . '/src/Libs/CurlRequest.php'), 'dependencia require instalada');
|
||||
|
||||
$proj2 = tmpdir('proj2');
|
||||
$cmd->run(['http', '--root=' . $proj2, '--toolbox=' . $tool, '--yes']);
|
||||
ok(is_file($proj2 . '/src/Libs/Aria2.php'));
|
||||
ok(!is_file($proj2 . '/src/Libs/CompreFace.php'), 'default sólo trae lo declarado');
|
||||
|
||||
rrmdir($proj);
|
||||
rrmdir($proj2);
|
||||
rrmdir($tool);
|
||||
});
|
||||
|
||||
Harness::it('remove sólo quita archivos del componente', function () {
|
||||
$proj = tmpdir('proj');
|
||||
$core = g7_core();
|
||||
$tool = g7_toolbox();
|
||||
|
||||
(new InstallCommand(new Console()))->run([$proj, '--toolbox=' . $tool, '--core-dir=' . $core, '--yes']);
|
||||
(new AddCommand(new Console()))->run(['http:aria2', '--root=' . $proj, '--toolbox=' . $tool, '--yes']);
|
||||
|
||||
(new RemoveCommand(new Console()))->run(['http:aria2', '--root=' . $proj, '--toolbox=' . $tool, '--yes']);
|
||||
|
||||
ok(!is_file($proj . '/src/Libs/Aria2.php'), 'archivo del componente eliminado');
|
||||
ok(is_file($proj . '/src/Libs/Router.php'), 'archivo del core intacto');
|
||||
|
||||
rrmdir($proj);
|
||||
rrmdir($core);
|
||||
rrmdir($tool);
|
||||
});
|
||||
|
||||
Harness::it('update core sólo toca rutas del core', function () {
|
||||
$proj = tmpdir('proj');
|
||||
$tool = g7_toolbox();
|
||||
$core = g7_core('1.0.0', 'router-v1');
|
||||
|
||||
(new InstallCommand(new Console()))->run([$proj, '--toolbox=' . $tool, '--core-dir=' . $core, '--yes']);
|
||||
(new AddCommand(new Console()))->run(['http:aria2', '--root=' . $proj, '--toolbox=' . $tool, '--yes']);
|
||||
|
||||
$core2 = g7_core('2.0.0', 'router-v2');
|
||||
(new UpdateCommand(new Console()))->run(['core', '--root=' . $proj, '--toolbox=' . $tool, '--core-dir=' . $core2, '--yes']);
|
||||
|
||||
ok(str_contains((string) file_get_contents($proj . '/src/Libs/Router.php'), 'router-v2'), 'core actualizado');
|
||||
ok(is_file($proj . '/src/Libs/Aria2.php'), 'componente intacto');
|
||||
eq('2.0.0', Lock::load($proj)->core()['version']);
|
||||
|
||||
rrmdir($proj);
|
||||
rrmdir($tool);
|
||||
rrmdir($core);
|
||||
rrmdir($core2);
|
||||
});
|
||||
|
||||
Harness::it('list muestra instalados con versión y modo', function () {
|
||||
$proj = tmpdir('proj');
|
||||
$tool = g7_toolbox();
|
||||
(new AddCommand(new Console()))->run(['http:aria2', '--root=' . $proj, '--toolbox=' . $tool, '--yes']);
|
||||
|
||||
$console = g7_console($stream);
|
||||
(new ListCommand($console))->run(['--root=' . $proj, '--toolbox=' . $tool]);
|
||||
rewind($stream);
|
||||
$output = stream_get_contents($stream);
|
||||
|
||||
ok(str_contains($output, 'http:aria2'), 'id en la salida');
|
||||
ok(str_contains($output, 'copy'), 'modo en la salida');
|
||||
|
||||
rrmdir($proj);
|
||||
rrmdir($tool);
|
||||
});
|
||||
|
||||
Harness::it('--dry-run no escribe archivos ni lock', function () {
|
||||
$proj = tmpdir('proj');
|
||||
$tool = g7_toolbox();
|
||||
|
||||
(new AddCommand(new Console()))->run(['http:aria2', '--root=' . $proj, '--toolbox=' . $tool, '--yes', '--dry-run']);
|
||||
|
||||
ok(!is_file($proj . '/src/Libs/Aria2.php'), 'no copia en dry-run');
|
||||
ok(!is_file(Lock::path($proj)), 'no crea lock en dry-run');
|
||||
|
||||
rrmdir($proj);
|
||||
rrmdir($tool);
|
||||
});
|
||||
|
||||
Harness::it('core sin tags: versión desconocida y commit pineado', function () {
|
||||
$proj = tmpdir('proj');
|
||||
$tool = g7_toolbox();
|
||||
$core = g7_core(''); // repo git sin tags
|
||||
|
||||
(new InstallCommand(new Console()))->run([$proj, '--toolbox=' . $tool, '--core-dir=' . $core, '--yes']);
|
||||
|
||||
$lock = Lock::load($proj);
|
||||
eq(null, $lock->core()['version'], 'sin tags la versión es desconocida');
|
||||
ok($lock->core()['commit'] !== null, 'commit pineado sin depender de tags');
|
||||
|
||||
rrmdir($proj);
|
||||
rrmdir($tool);
|
||||
rrmdir($core);
|
||||
});
|
||||
|
||||
Harness::it('--force resuelve drift pero no salta avisos de compatibilidad', function () {
|
||||
$proj = tmpdir('proj');
|
||||
$tool = g7_toolbox();
|
||||
|
||||
// add once and edit locally
|
||||
(new AddCommand(new Console()))->run(['http:aria2', '--root=' . $proj, '--toolbox=' . $tool, '--yes']);
|
||||
write_file($proj . '/src/Libs/Aria2.php', "<?php\n// editado\n");
|
||||
|
||||
// package declares tested core 0.9 -> warning against core 0.0.0 (unknown) is OK;
|
||||
// set a tested value to force a warning
|
||||
$json = json_decode((string) file_get_contents($tool . '/packages/http/duckbrain.json'), true);
|
||||
$json['core'] = ['requires' => '>=1.0'];
|
||||
write_file($tool . '/packages/http/duckbrain.json', json_encode($json));
|
||||
|
||||
$stream = fopen('php://memory', 'r+');
|
||||
$console = new Console($stream, $stream);
|
||||
|
||||
$code = (new AddCommand($console))->run(['http:aria2', '--root=' . $proj, '--toolbox=' . $tool, '--yes', '--force']);
|
||||
eq(0, $code, 'force resuelve el drift');
|
||||
|
||||
rewind($stream);
|
||||
$output = stream_get_contents($stream);
|
||||
ok(str_contains($output, 'WARN'), 'sigue avisando de compatibilidad');
|
||||
ok(str_contains((string) file_get_contents($proj . '/src/Libs/Aria2.php'), 'aria2'), 'sobreescribe con --force');
|
||||
|
||||
rrmdir($proj);
|
||||
rrmdir($tool);
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
use Toolbox\Commands\AddCommand;
|
||||
use Toolbox\Commands\InstallCommand;
|
||||
use Toolbox\Console;
|
||||
use Toolbox\Lock;
|
||||
|
||||
Harness::suite('group8');
|
||||
|
||||
$GLOBALS['g8_repo'] = dirname(__DIR__);
|
||||
|
||||
Harness::it('el binario global usa el motor empaquetado en commands', function () {
|
||||
$repo = $GLOBALS['g8_repo'];
|
||||
exec('php ' . escapeshellarg($repo . '/bin/duckbrain') . ' --version 2>&1', $out, $code);
|
||||
eq(0, $code);
|
||||
ok(str_contains(implode("\n", $out), 'duckbrain-toolbox'), 'el binario global arranca');
|
||||
});
|
||||
|
||||
Harness::it('commands:toolbox es un componente opcional que se copia y hashea', function () {
|
||||
$repo = $GLOBALS['g8_repo'];
|
||||
$proj = tmpdir('proj');
|
||||
|
||||
(new AddCommand(new Console()))->run(['commands:toolbox', '--root=' . $proj, '--toolbox=' . $repo, '--yes']);
|
||||
|
||||
ok(is_file($proj . '/src/Toolbox/Installer.php'), 'motor copiado a ROOT_CORE/Toolbox');
|
||||
$lock = Lock::load($proj);
|
||||
ok($lock->has('commands:toolbox'), 'lock registra el componente opcional');
|
||||
|
||||
rrmdir($proj);
|
||||
});
|
||||
|
||||
Harness::it('el CLI del proyecto gestiona addons sin el instalador global', function () {
|
||||
$repo = $GLOBALS['g8_repo'];
|
||||
$core = g7_core('0.1.0');
|
||||
$proj = tmpdir('proj');
|
||||
|
||||
// Core + CLI + motor (todo vía el instalador global, como bootstrap).
|
||||
(new InstallCommand(new Console()))->run([$proj, '--toolbox=' . $repo, '--core-dir=' . $core, '--yes']);
|
||||
(new AddCommand(new Console()))->run(['commands:cli', 'commands:toolbox', '--root=' . $proj, '--toolbox=' . $repo, '--yes']);
|
||||
|
||||
ok(is_file($proj . '/duckbrain'), 'binario del proyecto instalado');
|
||||
|
||||
$cmd = 'cd ' . escapeshellarg($proj) . ' && php duckbrain --version 2>&1';
|
||||
exec($cmd, $out, $code);
|
||||
eq(0, $code, 'el binario del proyecto arranca: ' . implode("\n", $out));
|
||||
ok(str_contains(implode("\n", $out), 'duckbrain-toolbox'), 'usa el motor del proyecto');
|
||||
|
||||
rrmdir($proj);
|
||||
rrmdir($core);
|
||||
});
|
||||
|
||||
Harness::it('la actualización del motor surte efecto en la siguiente invocación', function () {
|
||||
$repo = $GLOBALS['g8_repo'];
|
||||
$core = g7_core('0.1.0');
|
||||
$proj = tmpdir('proj');
|
||||
|
||||
(new InstallCommand(new Console()))->run([$proj, '--toolbox=' . $repo, '--core-dir=' . $core, '--yes']);
|
||||
(new AddCommand(new Console()))->run(['commands:cli', 'commands:toolbox', '--root=' . $proj, '--toolbox=' . $repo, '--yes']);
|
||||
|
||||
$before = trim((string) shell_exec('cd ' . escapeshellarg($proj) . ' && php duckbrain --version 2>&1'));
|
||||
|
||||
// Simula una actualización del motor reescribiendo su archivo en disco.
|
||||
$config = (string) file_get_contents($proj . '/src/Toolbox/Config.php');
|
||||
$config = str_replace("VERSION = '0.1.0'", "VERSION = '0.9.9'", $config);
|
||||
write_file($proj . '/src/Toolbox/Config.php', $config);
|
||||
|
||||
$after = trim((string) shell_exec('cd ' . escapeshellarg($proj) . ' && php duckbrain --version 2>&1'));
|
||||
|
||||
ok(str_contains($before, '0.1.0'), 'antes usa la versión vieja');
|
||||
ok(str_contains($after, '0.9.9'), 'después usa la versión nueva sin recompilar');
|
||||
|
||||
rrmdir($proj);
|
||||
rrmdir($core);
|
||||
});
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
use Toolbox\Catalog;
|
||||
use Toolbox\Commands\AdoptCommand;
|
||||
use Toolbox\Console;
|
||||
use Toolbox\Installer;
|
||||
use Toolbox\Lock;
|
||||
|
||||
Harness::suite('group9');
|
||||
|
||||
$GLOBALS['g9_repo'] = dirname(__DIR__);
|
||||
|
||||
Harness::it('el catálogo lista todos los paquetes absorbidos', function () {
|
||||
$catalog = Catalog::fromRoot($GLOBALS['g9_repo']);
|
||||
$names = array_keys($catalog->packages());
|
||||
|
||||
foreach (['http', 'crypto', 'text', 'cache', 'id', 'htmx', 'commands', 'test'] as $pkg) {
|
||||
ok(in_array($pkg, $names, true), "paquete '{$pkg}' presente en el catálogo");
|
||||
}
|
||||
});
|
||||
|
||||
Harness::it('http instala su familia y resuelve compreFace -> curlRequest', function () {
|
||||
$repo = $GLOBALS['g9_repo'];
|
||||
$proj = tmpdir('proj');
|
||||
$inst = new Installer($proj, $repo, new Console(), $proj . '/src');
|
||||
|
||||
$result = $inst->add(['http'], ['yes' => true]);
|
||||
|
||||
ok(is_file($proj . '/src/Libs/CurlRequest.php'));
|
||||
ok(is_file($proj . '/src/Libs/CompreFace.php'));
|
||||
$lock = Lock::load($proj);
|
||||
ok($lock->has('http:compreFace') && $lock->has('http:curlRequest'), 'resuelve la dependencia');
|
||||
|
||||
rrmdir($proj);
|
||||
});
|
||||
|
||||
Harness::it('htmx instala htmlComponent con su Sanitizer via dependencia', function () {
|
||||
$repo = $GLOBALS['g9_repo'];
|
||||
$proj = tmpdir('proj');
|
||||
$inst = new Installer($proj, $repo, new Console(), $proj . '/src');
|
||||
|
||||
$inst->add(['htmx:htmx'], ['yes' => true]);
|
||||
|
||||
ok(is_file($proj . '/src/Libs/HTMX.php'));
|
||||
ok(is_file($proj . '/src/Libs/HTMLComponent.php'), 'dependencia htmlComponent');
|
||||
ok(is_file($proj . '/src/Libs/Sanitizer/SanitizerProxy.php'), 'directorio Sanitizer expandido');
|
||||
|
||||
rrmdir($proj);
|
||||
});
|
||||
|
||||
Harness::it('test trae commands:cli y ejecuta post-install', function () {
|
||||
$repo = $GLOBALS['g9_repo'];
|
||||
$proj = tmpdir('proj');
|
||||
$inst = new Installer($proj, $repo, new Console(), $proj . '/src');
|
||||
|
||||
$ran = [];
|
||||
$result = $inst->add(['test'], ['yes' => true, 'postRunner' => function (string $c) use (&$ran) { $ran[] = $c; }]);
|
||||
|
||||
ok(is_file($proj . '/phpunit.xml'), 'phpunit.xml en ROOT_DIR');
|
||||
ok(is_file($proj . '/tests/TestCase.php'), 'directorio tests expandido');
|
||||
ok(Lock::load($proj)->has('commands:cli'), 'dependencia commands:cli instalada');
|
||||
eq(2, count($ran), 'se ejecutaron los dos comandos de post-install');
|
||||
|
||||
rrmdir($proj);
|
||||
});
|
||||
|
||||
Harness::it('no queda el Migration.php duplicado; sólo el de commands', function () {
|
||||
$repo = $GLOBALS['g9_repo'];
|
||||
ok(is_file($repo . '/packages/commands/src/Libs/Migration.php'), 'existe el de commands');
|
||||
foreach (['http', 'crypto', 'text', 'cache', 'id'] as $pkg) {
|
||||
ok(!is_file($repo . "/packages/{$pkg}/src/Libs/Migration.php"), "extras no filtró Migration.php a {$pkg}");
|
||||
}
|
||||
});
|
||||
|
||||
Harness::it('adopt registra copias idénticas y divergentes sin modificar archivos', function () {
|
||||
$repo = $GLOBALS['g9_repo'];
|
||||
$core = g7_core('0.1.0');
|
||||
$proj = tmpdir('proj');
|
||||
$projDiv = tmpdir('projdiv');
|
||||
|
||||
$aria2 = (string) file_get_contents($repo . '/packages/http/src/Libs/Aria2.php');
|
||||
|
||||
write_file($proj . '/src/Libs/Aria2.php', $aria2);
|
||||
write_file($projDiv . '/src/Libs/Aria2.php', $aria2 . "\n// edicion local\n");
|
||||
$before = (string) file_get_contents($projDiv . '/src/Libs/Aria2.php');
|
||||
|
||||
(new AdoptCommand(new Console()))->run(['--root=' . $proj, '--toolbox=' . $repo, '--core-dir=' . $core]);
|
||||
$lock = Lock::load($proj);
|
||||
ok($lock->has('http:aria2'), 'adopta la copia idéntica');
|
||||
eq('adopted', $lock->component('http:aria2')['origin']);
|
||||
|
||||
(new AdoptCommand(new Console()))->run(['--root=' . $projDiv, '--toolbox=' . $repo, '--core-dir=' . $core]);
|
||||
$lockD = Lock::load($projDiv);
|
||||
ok($lockD->has('http:aria2'), 'adopta la copia divergente');
|
||||
eq('local', $lockD->component('http:aria2')['origin'], 'la marca como edición local');
|
||||
eq($before, file_get_contents($projDiv . '/src/Libs/Aria2.php'), 'no modifica el archivo');
|
||||
ok(\Toolbox\Drift::isDrifted($projDiv . '/src/Libs/Aria2.php', $lockD->component('http:aria2')['files']['src/Libs/Aria2.php']), 'el drift queda detectado');
|
||||
|
||||
rrmdir($proj);
|
||||
rrmdir($projDiv);
|
||||
rrmdir($core);
|
||||
});
|
||||
|
||||
Harness::it('adopt registra el core existente con sus hashes', function () {
|
||||
$repo = $GLOBALS['g9_repo'];
|
||||
$core = g7_core('0.1.0');
|
||||
$proj = tmpdir('proj');
|
||||
|
||||
// Simula un proyecto heredado con el core copiado a mano.
|
||||
foreach (['autoload.php', 'config.php', 'src/Libs/Router.php'] as $file) {
|
||||
write_file($proj . '/' . $file, (string) file_get_contents($core . '/' . $file));
|
||||
}
|
||||
|
||||
(new AdoptCommand(new Console()))->run(['--root=' . $proj, '--toolbox=' . $repo, '--core-dir=' . $core]);
|
||||
|
||||
$lock = Lock::load($proj);
|
||||
ok($lock->core() !== null, 'core registrado en el lock');
|
||||
ok(isset($lock->core()['files']['src/Libs/Router.php']), 'archivo del core con hash');
|
||||
ok(!isset($lock->core()['files']['src/Libs/Aria2.php']), 'no adopta archivos ajenos');
|
||||
|
||||
rrmdir($proj);
|
||||
rrmdir($core);
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../src/autoload.php';
|
||||
|
||||
final class Harness
|
||||
{
|
||||
public static int $passed = 0;
|
||||
public static int $failed = 0;
|
||||
/** @var list<string> */
|
||||
public static array $failures = [];
|
||||
public static string $suite = '';
|
||||
|
||||
public static function suite(string $name): void
|
||||
{
|
||||
self::$suite = $name;
|
||||
}
|
||||
|
||||
public static function it(string $description, callable $fn): void
|
||||
{
|
||||
try {
|
||||
$fn();
|
||||
self::$passed++;
|
||||
fwrite(STDOUT, " \033[32mok\033[0m " . $description . PHP_EOL);
|
||||
} catch (\Throwable $e) {
|
||||
self::$failed++;
|
||||
self::$failures[] = self::$suite . ' :: ' . $description . ' -> ' . $e->getMessage();
|
||||
fwrite(STDOUT, " \033[31mFAIL\033[0m " . $description . ' -> ' . $e->getMessage() . PHP_EOL);
|
||||
}
|
||||
}
|
||||
|
||||
public static function summary(): int
|
||||
{
|
||||
fwrite(STDOUT, PHP_EOL);
|
||||
fwrite(STDOUT, sprintf("passed: %d, failed: %d\n", self::$passed, self::$failed));
|
||||
if (self::$failed > 0) {
|
||||
fwrite(STDOUT, "Failures:\n");
|
||||
foreach (self::$failures as $failure) {
|
||||
fwrite(STDOUT, ' - ' . $failure . PHP_EOL);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function ok(bool $condition, string $message = 'assertion failed'): void
|
||||
{
|
||||
if (!$condition) {
|
||||
throw new \RuntimeException($message);
|
||||
}
|
||||
}
|
||||
|
||||
function eq(mixed $expected, mixed $actual, string $message = ''): void
|
||||
{
|
||||
if ($expected !== $actual) {
|
||||
throw new \RuntimeException(sprintf(
|
||||
'%s expected %s, got %s',
|
||||
$message !== '' ? $message . ':' : 'values differ,',
|
||||
var_export($expected, true),
|
||||
var_export($actual, true)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
function throws(callable $fn, string $message = 'expected exception'): void
|
||||
{
|
||||
try {
|
||||
$fn();
|
||||
} catch (\Throwable) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new \RuntimeException($message);
|
||||
}
|
||||
|
||||
function tmpdir(string $prefix = 'duckbrain-test'): string
|
||||
{
|
||||
$dir = sys_get_temp_dir() . '/' . $prefix . '-' . bin2hex(random_bytes(6));
|
||||
mkdir($dir, 0777, true);
|
||||
|
||||
return $dir;
|
||||
}
|
||||
|
||||
function rrmdir(string $dir): void
|
||||
{
|
||||
if (!is_dir($dir)) {
|
||||
return;
|
||||
}
|
||||
exec('rm -rf ' . escapeshellarg($dir) . ' 2>/dev/null');
|
||||
}
|
||||
|
||||
function write_file(string $path, string $contents): void
|
||||
{
|
||||
if (!is_dir(dirname($path))) {
|
||||
mkdir(dirname($path), 0777, true);
|
||||
}
|
||||
file_put_contents($path, $contents);
|
||||
}
|
||||
|
||||
function git_init(string $dir): void
|
||||
{
|
||||
exec('git init -q ' . escapeshellarg($dir));
|
||||
exec('git -C ' . escapeshellarg($dir) . ' config user.email test@test');
|
||||
exec('git -C ' . escapeshellarg($dir) . ' config user.name test');
|
||||
exec('git -C ' . escapeshellarg($dir) . ' config commit.gpgsign false');
|
||||
}
|
||||
|
||||
function git_commit(string $dir, string $msg): void
|
||||
{
|
||||
exec('git -C ' . escapeshellarg($dir) . ' add -A');
|
||||
exec('git -C ' . escapeshellarg($dir) . ' commit -qm ' . escapeshellarg($msg));
|
||||
}
|
||||
|
||||
function git_tag(string $dir, string $tag): void
|
||||
{
|
||||
exec('git -C ' . escapeshellarg($dir) . ' tag ' . escapeshellarg($tag));
|
||||
}
|
||||
|
||||
function seed_package(string $repo, string $version): void
|
||||
{
|
||||
write_file($repo . '/packages/http/duckbrain.json', json_encode([
|
||||
'name' => 'http',
|
||||
'version' => $version,
|
||||
'components' => ['aria2' => ['files' => ['src/Libs/Aria2.php']]],
|
||||
], JSON_PRETTY_PRINT));
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/harness.php';
|
||||
|
||||
foreach (glob(__DIR__ . '/*_test.php') ?: [] as $testFile) {
|
||||
require_once $testFile;
|
||||
}
|
||||
|
||||
exit(Harness::summary());
|
||||
Reference in New Issue
Block a user