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
91 changes: 91 additions & 0 deletions src/Console/bin/bones
Original file line number Diff line number Diff line change
Expand Up @@ -1675,6 +1675,46 @@ namespace Bones {
return $other ? $stubs : ['/' . self::PLUGIN_STUBS];
}

/**
* The JavaScript tests that sit beside the code they test, found the way the jest.config.js
* written by `migrate:to-v2` finds them: every `__tests__` folder, and every `*.test.*` or
* `*.spec.*` script, with the `__snapshots__` and `__mocks__` folders Jest keeps beside them.
* They live in resources/assets, which `--wp` ships. The skip list matches exact paths, so
* each one is listed where it is.
*
* vendor/ and node_modules/ are not walked: they are not the plugin's own code. Neither is a
* folder that cannot be read, which the copy could not read either.
*
* @since 3.2.0
* @param string $relative The folder to walk, relative to the plugin root.
* @return string[] Entries of the deploy skip list.
*/
protected function javascriptTestsToSkip(string $relative = ''): array
{
$entries = [];

foreach (scandir(__DIR__ . $relative) ?: [] as $name) {
// The deploy never copies a dot entry, so there is nothing to list in one.
if (strpos($name, '.') === 0 || in_array($name, ['vendor', 'node_modules'], true)) {
continue;
}

$path = "{$relative}/{$name}";

if (is_dir(__DIR__ . $path)) {
if (in_array($name, ['__tests__', '__snapshots__', '__mocks__'], true)) {
$entries[] = $path;
} elseif (!is_link(__DIR__ . $path) && is_readable(__DIR__ . $path)) {
$entries = array_merge($entries, $this->javascriptTestsToSkip($path));
}
} elseif (preg_match('/\.(test|spec)\.(ts|tsx|js|jsx)$/', $name)) {
$entries[] = $path;
}
}

return $entries;
}

/**
* Return true if the command is available in the system.
*
Expand Down Expand Up @@ -2719,6 +2759,14 @@ namespace Bones {
'/phpcs.xml.dist',
'/mix-manifest.json',
'/release.sh',
// The plugin's test suites: PHPUnit's, where PHPUnit looks for it, and Jest's.
'/tests',
'/phpunit.xml',
'/phpunit.xml.dist',
'/phpunit.dist.xml',
'/jest.config.js',
'/jest.config.json',
...$this->javascriptTestsToSkip(),
...$this->pluginStubsToSkip(),
]);

Expand Down Expand Up @@ -2759,6 +2807,8 @@ namespace Bones {
$this->pruneDevPackages($path);
}

$this->assertAutoloadedFilesShipped($path, $is_create_zip);

/**
* Fires when the console deploy is completed.
*
Expand Down Expand Up @@ -3110,6 +3160,47 @@ namespace Bones {
}
}

/**
* Stop when the package's Composer autoloader requires a file the package does not hold.
*
* Composer requires its `files` entries on every request, so one that points at a path the
* deploy left out is a fatal error the moment the plugin loads. It happens when autoload-dev
* names a file in `tests/` and the dev autoloader stays: with --keep-dev, or when no dev
* package is installed and so none is pruned and the autoloader is not rebuilt.
*
* @since 3.2.0
* @param string $path The deployed copy.
* @param bool $isTemporary True when the copy is the folder a zip is made from.
*/
protected function assertAutoloadedFilesShipped(string $path, bool $isTemporary): void
{
$map = "{$path}/vendor/composer/autoload_files.php";

if (!is_file($map)) {
return;
}

// The map works out its paths from where it sits, so in the copy they point into the copy.
$files = (static fn() => require $map)();
$missing = array_filter(is_array($files) ? $files : [], fn($file) => !is_file($file));

if (empty($missing)) {
return;
}

$root = (realpath($path) ?: $path) . '/';
$list = implode(', ', array_map(fn($file) => str_replace($root, '', $file), $missing));

if ($isTemporary) {
$this->deleteDirectory($path);
}

$this->error(
"The package's Composer autoloader requires files the deploy left out ({$list}), so the plugin would stop with a fatal error as it loads. Move them out of composer.json's autoload-dev, or keep them in the package with the wpbones_console_deploy_default_skip_files_folders filter.",
);
exit(1);
}

/**
* The command that runs Composer: a composer.phar in the plugin root, else `composer` on PATH.
*/
Expand Down
165 changes: 165 additions & 0 deletions tests/Console/DeployTestSuitesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
<?php

declare(strict_types=1);

namespace WPKirk\WPBones\Tests\Console;

use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
use WPKirk\WPBones\Tests\Support\BonesProcess;

/**
* A plugin's own test suites shipped with every deploy unless its deploy.php listed them: the
* default skip list named no `tests/`, no PHPUnit or Jest configuration, and no Jest test, and
* since `--wp` keeps `resources/assets`, every `__tests__` folder beside the scripts it tests
* went to WordPress.org with them. Scotty 2.2.0 shipped four, removed by hand before the SVN
* commit.
*
* Since 3.2.0 the deploy leaves out the PHP suite where PHPUnit looks for it, and the JavaScript
* tests where the jest.config.js that `migrate:to-v2` writes looks for them.
*/
#[Group('console')]
final class DeployTestSuitesTest extends TestCase
{
private BonesProcess $bones;

private string $target = '';

protected function setUp(): void
{
parent::setUp();

$this->bones = new BonesProcess();
$this->target = $this->bones->root . '/out';

foreach (
[
'plugin/Http/Controllers/TestController.php' => "<?php\n// runtime code\n",
'tests/bootstrap.php' => "<?php\n",
'tests/Unit/ExampleTest.php' => "<?php\n",
'phpunit.xml' => "<phpunit/>\n",
'phpunit.xml.dist' => "<phpunit/>\n",
'phpunit.dist.xml' => "<phpunit/>\n",
'jest.config.js' => "module.exports = {};\n",
'jest.config.json' => "{}\n",
'resources/assets/apps/components/Header.tsx' => "export {};\n",
'resources/assets/apps/components/__tests__/Header.test.tsx' => "test('x', () => {});\n",
'resources/assets/apps/components/__tests__/fixtures/data.json' => "{}\n",
'resources/assets/apps/utils/format.ts' => "export {};\n",
'resources/assets/apps/utils/format.test.ts' => "test('x', () => {});\n",
'resources/assets/apps/utils/format.spec.js' => "test('x', () => {});\n",
'resources/assets/apps/utils/latest.ts' => "export {};\n",
'resources/assets/apps/utils/__snapshots__/format.test.ts.snap' => "// snapshot\n",
'resources/assets/apps/utils/__mocks__/api.ts' => "export {};\n",
'__mocks__/fs.js' => "module.exports = {};\n",
'vendor/acme/lib/__tests__/lib.test.js' => "test('x', () => {});\n",
'vendor/acme/lib/functions.php' => "<?php\n",
// Composer's map of the files it requires on every request; $vendorDir and $baseDir are
// where the copy sits, as in the real one.
'vendor/composer/autoload_files.php' => "<?php\n\$vendorDir = dirname(__DIR__);\n\$baseDir = dirname(\$vendorDir);\n\nreturn array(\n 'a1' => \$vendorDir . '/acme/lib/functions.php',\n);\n",
]
as $file => $content
) {
$path = $this->bones->plugin . '/' . $file;

if (!is_dir(dirname($path))) {
mkdir(dirname($path), 0777, true);
}

file_put_contents($path, $content);
}
}

protected function tearDown(): void
{
$this->bones->remove();

parent::tearDown();
}

/** @return array{status:int, output:string} */
private function deploy(string ...$options): array
{
return $this->bones->run(['deploy', $this->target, '--no-build', ...$options]);
}

public function test_the_php_suite_does_not_reach_the_package(): void
{
$result = $this->deploy();

$this->assertSame(0, $result['status'], $result['output']);
$this->assertFileExists($this->target . '/wp-kirk.php', 'the deploy did not run');
$this->assertDirectoryDoesNotExist($this->target . '/tests');
$this->assertFileDoesNotExist($this->target . '/phpunit.xml');
$this->assertFileDoesNotExist($this->target . '/phpunit.xml.dist');
$this->assertFileDoesNotExist($this->target . '/phpunit.dist.xml');
}

public function test_the_javascript_tests_do_not_reach_a_wordpress_org_package(): void
{
$result = $this->deploy('--wp');
$assets = $this->target . '/resources/assets/apps';

$this->assertSame(0, $result['status'], $result['output']);
$this->assertFileExists($assets . '/components/Header.tsx', '--wp did not keep resources/assets');
$this->assertFileDoesNotExist($this->target . '/jest.config.js');
$this->assertFileDoesNotExist($this->target . '/jest.config.json');
$this->assertDirectoryDoesNotExist($assets . '/components/__tests__');
$this->assertFileDoesNotExist($assets . '/utils/format.test.ts');
$this->assertFileDoesNotExist($assets . '/utils/format.spec.js');
$this->assertDirectoryDoesNotExist($assets . '/utils/__snapshots__');
$this->assertDirectoryDoesNotExist($assets . '/utils/__mocks__');
$this->assertDirectoryDoesNotExist($this->target . '/__mocks__');
}

public function test_an_autoloaded_file_the_deploy_left_out_stops_it(): void
{
// autoload-dev "files" pointing into tests/: the dev autoloader stays when nothing is pruned
// (no dev package installed, as here, or --keep-dev), and it would require a missing file
// on every request.
file_put_contents($this->bones->plugin . '/tests/helpers.php', "<?php\n");
file_put_contents(
$this->bones->plugin . '/vendor/composer/autoload_files.php',
"<?php\n\$vendorDir = dirname(__DIR__);\n\$baseDir = dirname(\$vendorDir);\n\nreturn array(\n 'a1' => \$vendorDir . '/acme/lib/functions.php',\n 'b2' => \$baseDir . '/tests/helpers.php',\n);\n"
);

$result = $this->deploy();

$this->assertNotSame(0, $result['status'], 'a package that cannot load was reported as deployed');
$this->assertStringContainsString('tests/helpers.php', $result['output']);
$this->assertStringNotContainsString('acme/lib/functions.php', $result['output']);
}

public function test_an_unreadable_folder_is_not_walked_noisily(): void
{
mkdir($this->bones->plugin . '/assets/private', 0777, true);
chmod($this->bones->plugin . '/assets/private', 0);

try {
$result = $this->deploy();
} finally {
chmod($this->bones->plugin . '/assets/private', 0755);
}

$this->assertSame(0, $result['status'], $result['output']);
$this->assertStringNotContainsString('Warning', $result['output']);
}

public function test_the_code_beside_the_tests_still_ships(): void
{
$result = $this->deploy('--wp');

$this->assertSame(0, $result['status'], $result['output']);
$this->assertFileExists($this->target . '/plugin/Http/Controllers/TestController.php');
$this->assertFileExists($this->target . '/resources/assets/apps/utils/format.ts');
$this->assertFileExists($this->target . '/resources/assets/apps/utils/latest.ts');
}

public function test_vendor_is_left_as_composer_installed_it(): void
{
$result = $this->deploy();

$this->assertSame(0, $result['status'], $result['output']);
$this->assertFileExists($this->target . '/vendor/acme/lib/__tests__/lib.test.js');
}
}
Loading