Skip to content

feat: Add init command#182

Merged
roxblnfk merged 17 commits into
php-testo:1.xfrom
burn1ngbear:1.x
May 20, 2026
Merged

feat: Add init command#182
roxblnfk merged 17 commits into
php-testo:1.xfrom
burn1ngbear:1.x

Conversation

@burn1ngbear
Copy link
Copy Markdown
Contributor

@burn1ngbear burn1ngbear commented May 15, 2026

What was changed

See issue 174

Checklist

  • Closes #
  • Tested
    • Tested manually
    • Unit tests added
  • Documentation

@burn1ngbear burn1ngbear requested a review from a team as a code owner May 15, 2026 07:44
@burn1ngbear
Copy link
Copy Markdown
Contributor Author

#174

@burn1ngbear
Copy link
Copy Markdown
Contributor Author

@roxblnfk review pls

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an init command to the Symfony Console bridge so projects can bootstrap Testo configuration, test directories, and a Composer script.

Changes:

  • Added a new Init console command.
  • Added a testo.php configuration stub.
  • Registered the init command in the CLI entrypoint.

Reviewed changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated 2 comments.

File Description
bridge/symfony-console/src/Command/Init.php Implements the new project initialization flow.
bridge/symfony-console/resources/stubs/testo.php Provides the generated configuration template.
bridge/symfony-console/bin/testo Registers the new init command with the console application.
Comments suppressed due to low confidence (2)

bridge/symfony-console/src/Command/Init.php:136

  • When testo.php does not already exist, this block is skipped entirely, so the init command never writes the new configuration file on a fresh project. The stub generation should also run for the missing-file case; otherwise the final message points to a configuration file that was not created.
        if (\is_file(self::DESTINATION)) {
            if (!$input->isInteractive()) {
                $io->warning('testo.php already exists. Skipping (non-interactive mode).');
                return Command::SUCCESS;
            }

            if ($io->confirm('testo.php already exists. Overwrite?', false)) {

bridge/symfony-console/src/Command/Init.php:140

  • The raw path values are interpolated into PHP single-quoted strings in the stub. A user-supplied path containing an apostrophe (or other PHP string-sensitive characters) will generate an invalid or incorrect testo.php; the generated values should be escaped/serialized before being written into PHP code.
                $stubContent = \str_replace(
                    ['__SRC_PATH__', '__TESTS_UNIT_PATH__'],
                    [(string) $srcPath, (string) $testsUnitPath],
                    \file_get_contents(self::STUB),

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread bridge/symfony-console/src/Command/Init.php Outdated
Comment thread bridge/symfony-console/resources/stubs/testo.php Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Comment thread bridge/symfony-console/resources/stubs/testo.php
@roxblnfk roxblnfk changed the title Add init command feat: Add init command May 15, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 3 changed files in this pull request and generated 6 comments.

Comments suppressed due to low confidence (2)

bridge/symfony-console/src/Command/Init.php:153

  • file_get_contents(self::STUB) isn’t checked for failure. If the stub path is missing/unreadable, this will produce an invalid/empty testo.php. Consider validating the stub read and failing the command with a clear error message instead of proceeding.
                ['__SRC_PATH__', '__SUITES__'],
                [(string) $srcPath, $suitesCode],
                \file_get_contents(self::STUB),
            );
            \file_put_contents(self::DESTINATION, $stubContent);

bridge/symfony-console/src/Command/Init.php:155

  • file_put_contents(self::DESTINATION, ...) isn’t checked. If writing testo.php fails (permissions, existing file locked), the command will still report success. Consider checking for false and returning Command::FAILURE with an actionable error message.
                \file_get_contents(self::STUB),
            );
            \file_put_contents(self::DESTINATION, $stubContent);
            $io->success('Created testo.php');
        }

Comment thread bridge/symfony-console/src/Command/Init.php Outdated
Comment thread bridge/symfony-console/src/Command/Init.php Outdated
Comment thread bridge/symfony-console/src/Command/Init.php Outdated
Comment thread bridge/symfony-console/src/Command/Init.php
Comment thread bridge/symfony-console/src/Command/Init.php Outdated
Comment thread bridge/symfony-console/src/Command/Init.php Outdated
burn1ngbear and others added 3 commits May 20, 2026 13:25
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 8 changed files in this pull request and generated 12 comments.

Comment on lines +89 to +93
if ($path->isDir()) {
return;
}

\mkdir((string) $path, 0755, true);
Comment on lines +57 to +67
$basePath = Path::create((string) $input->getOption('path'));
self::ensureDirectory($basePath, $io);

$srcPath = self::discoverSourceDirectory($input, $io);

$testsPath = $basePath->join('tests');
self::ensureDirectory($testsPath, $io);

$suites = self::discoverSuites($testsPath, $io);
$composerKeys = self::updateComposerScripts($suites, $basePath, $io);

}

if (!$input->isInteractive()) {
throw new \RuntimeException('src/ directory not found. Skipping (non-interactive mode).');
Comment on lines +162 to +181
/** @var array{scripts?: array<string, string>}&array<string, mixed> $composer */
$composer = \json_decode(
\file_get_contents((string) $composerJsonPath),
associative: true,
flags: \JSON_THROW_ON_ERROR,
);

if (!isset($composer['scripts'][self::SCRIPT_ALL_KEY])) {
$composer['scripts'][self::SCRIPT_ALL_KEY] = self::SCRIPT_ALL_COMMAND;
$io->success(\sprintf('Added "%s" script to composer.json', self::SCRIPT_ALL_KEY));
}

foreach ($suites as $suite) {
$key = \sprintf(self::SCRIPT_SUITE_KEY_TEMPLATE, \strtolower($suite));
$keys[] = $key;

if (!isset($composer['scripts'][$key])) {
$composer['scripts'][$key] = \sprintf(self::SCRIPT_SUITE_COMMAND_TEMPLATE, $suite);
$io->success(\sprintf('Added "%s" script to composer.json', $key));
}
Comment on lines +184 to +188
\file_put_contents(
(string) $composerJsonPath,
\json_encode($composer, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE) . "\n",
);

\file_get_contents(self::STUB),
);

\file_put_contents((string) $configPath, $stub);
Comment on lines +155 to +161
$keys = [self::SCRIPT_ALL_KEY];

$composerJsonPath = $basePath->join('composer.json');
if (!$composerJsonPath->isFile()) {
return $keys;
}

Comment on lines +208 to +250
public function customPathPlacesTestsAndConfigUnderSubdirectory(): void
{
$this->sandbox->makeDir('src');
$this->sandbox->writeFile(
'app/composer.json',
\json_encode(['name' => 'acme/sub-app'], \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES) . "\n",
);
$rootComposer = \json_encode(['name' => 'acme/root'], \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES) . "\n";
$this->sandbox->writeFile('composer.json', $rootComposer);

$tester = $this->run(['--path' => 'app']);

Assert::same(
$tester->getStatusCode(),
Command::SUCCESS,
'init must succeed when --path points at a subdirectory; output: ' . $tester->getDisplay(),
);

Assert::true(\is_dir($this->sandbox->path('app/tests/Unit')), 'tests/Unit/ must be created under --path');
Assert::true(\is_file($this->sandbox->path('app/testo.php')), 'testo.php must be created under --path');

Assert::false(
\is_file($this->sandbox->path('testo.php')),
'no testo.php must be created at the project root when --path is set',
);
Assert::false(
\is_dir($this->sandbox->path('tests')),
'no tests/ must be created at the project root when --path is set',
);

$subScripts = $this->readComposerScripts('app/composer.json');
Assert::same(
$subScripts['test:unit'] ?? null,
'vendor/bin/testo --suite=Unit',
'composer scripts must be written to the composer.json colocated with --path',
);

Assert::same(
\file_get_contents($this->sandbox->path('composer.json')),
$rootComposer,
'the project-root composer.json must be left untouched when --path is set',
);
}
{
$target = $this->path . \DIRECTORY_SEPARATOR . \ltrim($relative, '/\\');
$dir = \dirname($target);
\is_dir($dir) or \mkdir($dir, 0o755, true);

public function destroy(): void
{
\chdir($this->originalCwd);
@roxblnfk roxblnfk merged commit cd940d6 into php-testo:1.x May 20, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants