Task: #53687 Support instance-specific cfg write targets and add coverage V2 #4

Merged
norb merged 11 commits from 53687/task-support-instance-specific-cfg-write-target-v2 into master 2026-03-30 15:09:21 +00:00
2 changed files with 380 additions and 287 deletions
Showing only changes of commit 1c851632c3 - Show all commits

Task: #53687 Refactor CLI input validation into option validators, add validator coverage, and refactor cfg CLI tests

Alejandro Sosa 2026-03-30 13:57:29 +02:00

201
bin/cfg
View file

@ -22,40 +22,6 @@ foreach ($autoloadFiles as $autoloadFile) {
}
}
function readJsonInputFile(string $path): array
{
if (!is_readable($path))
{
throw new \RuntimeException("Input file is not readable: $path");
}
$content = file_get_contents($path);
if ($content === false)
{
throw new \RuntimeException("Can not read input file: $path");
}
try
{
$data = json_decode($content, true, 512, JSON_THROW_ON_ERROR);
}
catch (\JsonException $ex)
{
throw new \RuntimeException(sprintf(
'Input JSON is invalid (%s): %s',
(string)$ex->getCode(),
$ex->getMessage()
));
}
if (!is_array($data))
{
throw new \RuntimeException('Input JSON must decode to an object/array');
}
return $data;
}
function verboseLog(bool $enabled, string $message): void
alejandro.sosa marked this conversation as resolved Outdated

I don't really like this .. seems over complicating the code. It is just for beging able to have different order of arguments?

not that important to me .. and probably better put inside the input helper library or replace it altogether .. IIRC the one used is not maintained anymore

I don't really like this .. seems over complicating the code. It is just for beging able to have different order of arguments? not that important to me .. and probably better put inside the input helper library or replace it altogether .. IIRC the one used is not maintained anymore

@norb wrote in #4 (comment):

I don't really like this .. seems over complicating the code. It is just for beging able to have different order of arguments?

not that important to me .. and probably better put inside the input helper library or replace it altogether .. IIRC the one used is not maintained anymore

You're right. After reviewing it again with the alias cleanup in place, I realized we can keep this much simpler. The pre-parse block was added as a defensive workaround while multiple option variants were still in play, but with --siteDir now unified and a couple of small follow-up fixes, the standard parser flow is sufficient. I removed that extra layer to keep the code cleaner and easier to maintain.

@norb wrote in https://code.verua.online/rabe/Util-Settings/pulls/4#issuecomment-3732: > I don't really like this .. seems over complicating the code. It is just for beging able to have different order of arguments? > > not that important to me .. and probably better put inside the input helper library or replace it altogether .. IIRC the one used is not maintained anymore You're right. After reviewing it again with the alias cleanup in place, I realized we can keep this much simpler. The pre-parse block was added as a defensive workaround while multiple option variants were still in play, but with --siteDir now unified and a couple of small follow-up fixes, the standard parser flow is sufficient. I removed that extra layer to keep the code cleaner and easier to maintain.
alejandro.sosa marked this conversation as resolved Outdated

can you do this as validators? e.g.

->validator(new Input\Validator(

can you do this as validators? e.g. https://code.verua.online/rabe/helpers-cli-input/src/commit/c6fe64321ef06633c89bc055252f22d6ff676d52/example/example.php#L35
{
if ($enabled) fwrite(STDERR, "[verbose] $message".PHP_EOL);
@ -82,6 +48,46 @@ $collection = (new Input\InputCollection())
->add( Input\InputTypeFactory::build('LongOption')->name('in')->short('i') // {{{
->flags(AbstractInputType::FLAG_OPTIONAL | Input\AbstractInputType::FLAG_VALUE_REQUIRED)
->description('Path to a JSON data file to read (for write)')
->validator(new Input\Validator(
function (AbstractInputType $input, AbstractInputHandler $context)
{
$path = $context->find('in');
if ($path === null || $path === '') {
return null;
}
if (!is_readable($path))
{
throw new \Exception("Input file is not readable: $path");
}
$content = file_get_contents($path);
if ($content === false)
{
throw new \Exception("Can not read input file: $path");
}
try
{
$data = json_decode($content, true, 512, JSON_THROW_ON_ERROR);
}
catch (\JsonException $ex)
{
throw new \Exception(sprintf(
'Input JSON is invalid (%s): %s',
(string)$ex->getCode(),
$ex->getMessage()
));
}
if (!is_array($data))
{
throw new \Exception('Input JSON must decode to an object/array');
}
return $data;
}
))
) // }}}
->add( Input\InputTypeFactory::build('LongOption')->name('mode')->short('m') // {{{
@ -102,6 +108,55 @@ $collection = (new Input\InputCollection())
->add( Input\InputTypeFactory::build('LongOption')->name('siteDir') // {{{
->flags(AbstractInputType::FLAG_OPTIONAL | Input\AbstractInputType::FLAG_VALUE_REQUIRED)
->description('Site/instance directory below config/. Accepts owner_xyz or config/owner_xyz')
->validator(new Input\Validator(
function (AbstractInputType $input, AbstractInputHandler $context)
{
$siteInput = $context->find('siteDir');
if ($siteInput === null || $siteInput === false) {
return null;
}
alejandro.sosa marked this conversation as resolved Outdated

as agreed in chat, again to keep the code simple, use --siteDir without alias

same argument, if we think we will need arguments, implement in used lib or use different lib

as agreed in chat, again to keep the code simple, use --siteDir without alias same argument, if we think we will need arguments, implement in used lib or use different lib

@norb wrote in #4 (comment):

as agreed in chat, again to keep the code simple, use --siteDir without alias

same argument, if we think we will need arguments, implement in used lib or use different lib

👍🏼 Done, agreed. I removed aliases and kept only --siteDir to keep the CLI simple

@norb wrote in https://code.verua.online/rabe/Util-Settings/pulls/4#issuecomment-3733: > as agreed in chat, again to keep the code simple, use --siteDir without alias > > same argument, if we think we will need arguments, implement in used lib or use different lib 👍🏼 Done, agreed. I removed aliases and kept only --siteDir to keep the CLI simple
$siteInput = trim((string)$siteInput);
$appPath = $context->find('appPath');
if (!$appPath) {
$appPath = getcwd().'/';
}
$appPath = rtrim($appPath, '/').'/';
// Accept both "owner_xyz" and "config/owner_xyz" and normalize to site key.
if (str_starts_with($siteInput, $appPath.'config/'))
{
$siteInput = substr($siteInput, strlen($appPath.'config/'));
}
elseif (str_starts_with($siteInput, 'config/'))
{
$siteInput = substr($siteInput, strlen('config/'));
}
$siteInput = trim($siteInput, '/');
if ($siteInput === '')
{
throw new \Exception('Option --siteDir is empty.');
}
// Block directory traversal and hidden-dot segments.
if (str_contains($siteInput, '..') || preg_match('~(^|/)\.(?:/|$)~', $siteInput))
{
throw new \Exception("Invalid directory in --siteDir: '$siteInput'.");
}
// Allow only predictable path segments for instance directories.
foreach (explode('/', $siteInput) as $part)
{
if ($part === '' || !preg_match('/^[A-Za-z0-9._-]+$/', $part))
{
throw new \Exception("Invalid directory name segment '$part' in --siteDir.");
}
}
return $siteInput;
}
))
) // }}}
->add( Input\InputTypeFactory::build('Argument')->name('action') // {{{
@ -151,6 +206,14 @@ $collection = (new Input\InputCollection())
{
$setting = $context->find('setting');
$action = $context->find('action');
$inputPayload = $context->find('in');
if ($action === 'write')
{
if (($setting !== null && $setting !== '') && $inputPayload) {
throw new \Exception('Please use either SETTING or --in, not both.');
}
}
if ($setting === null || $setting === '') {
return ['key' => '', 'value' => null];
@ -274,44 +337,9 @@ if ($pkgPath = $argv->find('pkgPath')) $cfg->pkgPath(rtrim($pkgPath, '/').'/');
$site = null;
$siteFlag = 0x01;
$siteInput = $argv->find('siteDir');
if ($siteInput !== null && $siteInput !== false)
$site = $argv->find('siteDir');
if ($site !== null && $site !== false)
{
$siteInput = trim((string)$siteInput);
// Accept both "owner_xyz" and "config/owner_xyz" and normalize to site key.
if (str_starts_with($siteInput, $appPath.'config/'))
{
$siteInput = substr($siteInput, strlen($appPath.'config/'));
}
elseif (str_starts_with($siteInput, 'config/'))
{
$siteInput = substr($siteInput, strlen('config/'));
}
$siteInput = trim($siteInput, '/');
if ($siteInput === '')
{
fwrite(STDERR, 'Option --siteDir is empty.'.PHP_EOL);
exit(1);
}
// Block directory traversal and hidden-dot segments.
if (str_contains($siteInput, '..') || preg_match('~(^|/)\.(?:/|$)~', $siteInput))
{
fwrite(STDERR, "Invalid directory in --siteDir: '$siteInput'.".PHP_EOL);
exit(1);
}
// Allow only predictable path segments for instance directories.
foreach (explode('/', $siteInput) as $part)
{
if ($part === '' || !preg_match('/^[A-Za-z0-9._-]+$/', $part))
{
fwrite(STDERR, "Invalid directory name segment '$part' in --siteDir.".PHP_EOL);
exit(1);
}
}
$site = $siteInput;
// Reuse util-settings site resolution: config/<site>/<prefix>.conf.php
$cfg->site($site);
verboseLog($verbose, "siteDir resolved to '$site'");
@ -361,35 +389,18 @@ case 'show':
echo $out.PHP_EOL;
break;
case 'write':
$inputFile = $argv->find('in');
// Write needs exactly one payload source:
// either SETTING (key=value) or --in (JSON file).
// This avoids ambiguous input precedence and empty writes.
if ($inputFile && $settings['key'] !== '')
{
fwrite(STDERR, 'Please use either SETTING or --in, not both.'.PHP_EOL);
exit(1);
}
if (!$inputFile && $settings['key'] === '')
case 'write':
$inputPayload = $argv->find('in');
if (!$inputPayload && $settings['key'] === '')
{
fwrite(STDERR, 'Nothing to write: provide SETTING or --in.'.PHP_EOL);
exit(1);
}
$path = ($settings['key'] !== '') ? explode(':', $settings['key']) : [];
verboseLog($verbose, 'write source: '.($inputFile ? "--in ($inputFile)" : 'SETTING'));
if ($inputFile)
verboseLog($verbose, 'write source: '.($inputPayload ? '--in' : 'SETTING'));
if ($inputPayload)
{
try
{
$setting2write = readJsonInputFile($inputFile);
}
catch (\Throwable $e)
{
fwrite(STDERR, $e->getMessage().PHP_EOL);
exit(1);
}
$setting2write = $inputPayload;
}
else
{
@ -402,7 +413,7 @@ case 'write':
$setting2write = [array_pop($path) => $setting2write];
}
$writeType = ($site !== null) ? $siteFlag : null;
$writeType = ($site !== null && $site !== false) ? $siteFlag : null;
$file = $cfg->buildFileName($writeType);
verboseLog($verbose, "write target: $file");
if (is_readable($file))

View file

@ -8,6 +8,7 @@ class CfgTest extends TestCase
{
private string $tmpDir;
/* Lifecycle {{{ */
protected function setUp(): void
{
$this->tmpDir = rtrim(sys_get_temp_dir(), '/').'/util-settings-cfg-'.bin2hex(random_bytes(8));
@ -24,18 +25,16 @@ class CfgTest extends TestCase
{
$this->removeDir($this->tmpDir);
}
/* }}} */
/* Write Flow {{{ */
public function testWriteWithoutDirectoryNameUsesLocalConfigFile(): void
{
$result = $this->runCfg([
'-a',
$this->tmpDir,
'write',
'Extension',
$result = $this->runWrite([
'module:code="X100"',
]);
$this->assertSame(0, $result['code'], $result['output']);
$this->assertCfgSuccess($result);
$localFile = $this->tmpDir.'/config/Extension.conf.php';
$this->assertFileExists($localFile);
@ -51,24 +50,17 @@ class CfgTest extends TestCase
"<?php return [\n\t'module' => [\n\t\t'code' => '',\n\t],\n];\n"
);
$result = $this->runCfg([
'-a',
$this->tmpDir,
'write',
'Extension',
$result = $this->runWrite([
'module:code="X100"',
]);
$this->assertSame(0, $result['code'], $result['output']);
$this->assertCfgSuccess($result);
$this->assertFileExists($this->tmpDir.'/config/Extension.conf.php');
}
public function testWriteWithInputFileWritesMultipleSettings(): void
{
$inFile = $this->tmpDir.'/extension-in.json';
file_put_contents(
$inFile,
json_encode([
$inFile = $this->createJsonInputFile([
'module' => [
'code' => 'X100',
'label' => 'demo-module',
@ -76,21 +68,16 @@ class CfgTest extends TestCase
'feature' => [
'endpoint' => 'https://example.invalid/v1/resource',
],
], JSON_PRETTY_PRINT)
);
]);
$result = $this->runCfg([
'-a',
$this->tmpDir,
'write',
'Extension',
$result = $this->runWrite([
'--siteDir=owner_xyz',
'-i',
$inFile,
]);
$this->assertSame(0, $result['code'], $result['output']);
$cfg = require $this->tmpDir.'/config/owner_xyz/Extension.conf.php';
$this->assertCfgSuccess($result);
$cfg = $this->loadWrittenConfig('owner_xyz');
$this->assertSame('X100', $cfg['module']['code']);
$this->assertSame('demo-module', $cfg['module']['label']);
$this->assertSame('https://example.invalid/v1/resource', $cfg['feature']['endpoint']);
@ -98,68 +85,59 @@ class CfgTest extends TestCase
public function testWriteAcceptsJsonStringValueInSetting(): void
{
$result = $this->runCfg([
'-a',
$this->tmpDir,
'write',
'Extension',
$result = $this->runWrite([
'module={"code":"X100","label":"demo-module"}',
'--siteDir=owner_xyz',
]);
$this->assertSame(0, $result['code'], $result['output']);
$cfg = require $this->tmpDir.'/config/owner_xyz/Extension.conf.php';
$this->assertCfgSuccess($result);
$cfg = $this->loadWrittenConfig('owner_xyz');
$this->assertSame('X100', $cfg['module']['code']);
$this->assertSame('demo-module', $cfg['module']['label']);
}
public function testWriteSupportsNestedPathWithColonNotation(): void
{
$result = $this->runCfg([
'-a',
$this->tmpDir,
'write',
'Extension',
$result = $this->runWrite([
'module:flags:enabled=true',
'--siteDir=owner_xyz',
]);
$this->assertSame(0, $result['code'], $result['output']);
$cfg = require $this->tmpDir.'/config/owner_xyz/Extension.conf.php';
$this->assertCfgSuccess($result);
$cfg = $this->loadWrittenConfig('owner_xyz');
$this->assertTrue($cfg['module']['flags']['enabled']);
}
public function testWriteWithInputFileAndSettingReturnsError(): void
public function testWriteCoercesSimpleStringValueToJsonString(): void
{
$inFile = $this->tmpDir.'/extension-in.json';
file_put_contents($inFile, json_encode(['module' => ['code' => 'X100']]));
$result = $this->runCfg([
'-a',
$this->tmpDir,
'write',
'Extension',
'module:code="x"',
'-i',
$inFile,
$result = $this->runWrite([
'module:code=X100',
'--siteDir=owner_xyz',
]);
$this->assertSame(1, $result['code']);
$this->assertStringContainsString('Please use either SETTING or --in, not both.', $result['output']);
$this->assertCfgSuccess($result);
$cfg = $this->loadWrittenConfig('owner_xyz');
$this->assertSame('X100', $cfg['module']['code']);
}
public function testWriteRejectsInvalidJsonStringValue(): void
{
$result = $this->runWrite([
'module={"code":',
'--siteDir=owner_xyz',
]);
$this->assertCfgFailure($result, 'The value does not appear to be a valid JSON string.');
}
public function testWriteWithSiteDirCreatesAndWritesSiteConfig(): void
{
$result = $this->runCfg([
'-a',
$this->tmpDir,
'write',
'Extension',
$result = $this->runWrite([
'module:code="X100"',
'--siteDir=owner_xyz',
]);
$this->assertSame(0, $result['code'], $result['output']);
$this->assertCfgSuccess($result);
$siteFile = $this->tmpDir.'/config/owner_xyz/Extension.conf.php';
$this->assertFileExists($siteFile);
@ -171,28 +149,19 @@ class CfgTest extends TestCase
public function testWriteWithSiteDirMergesIntoExistingSiteConfig(): void
{
$firstWrite = $this->runCfg([
'-a',
$this->tmpDir,
'write',
'Extension',
$firstWrite = $this->runWrite([
'module:code="X100"',
'--siteDir=owner_xyz',
]);
$this->assertSame(0, $firstWrite['code'], $firstWrite['output']);
$this->assertCfgSuccess($firstWrite);
$secondWrite = $this->runCfg([
'-a',
$this->tmpDir,
'write',
'Extension',
$secondWrite = $this->runWrite([
'module:label="demo-module"',
'--siteDir=owner_xyz',
]);
$this->assertSame(0, $secondWrite['code'], $secondWrite['output']);
$this->assertCfgSuccess($secondWrite);
$siteFile = $this->tmpDir.'/config/owner_xyz/Extension.conf.php';
$cfg = require $siteFile;
$cfg = $this->loadWrittenConfig('owner_xyz');
$this->assertSame('X100', $cfg['module']['code']);
$this->assertSame('demo-module', $cfg['module']['label']);
@ -200,56 +169,62 @@ class CfgTest extends TestCase
public function testWriteWithSiteDirUsingConfigPrefixIsNormalized(): void
{
$result = $this->runCfg([
'-a',
$this->tmpDir,
'write',
'Extension',
$result = $this->runWrite([
'module:code="X100"',
'--siteDir=config/owner_xyz',
]);
$this->assertSame(0, $result['code'], $result['output']);
$this->assertCfgSuccess($result);
$this->assertFileExists($this->tmpDir.'/config/owner_xyz/Extension.conf.php');
$this->assertFileDoesNotExist($this->tmpDir.'/config/config/owner_xyz/Extension.conf.php');
}
public function testWriteWithSiteDirUsingAbsoluteConfigPrefixIsNormalized(): void
{
$result = $this->runWrite([
'module:code="X100"',
'--siteDir='.$this->tmpDir.'/config/owner_xyz',
]);
$this->assertCfgSuccess($result);
$this->assertFileExists($this->tmpDir.'/config/owner_xyz/Extension.conf.php');
}
public function testWriteWithNestedSiteDirWritesToNestedSiteConfig(): void
{
$result = $this->runWrite([
'module:code="X100"',
'--siteDir=owner_xyz/sub_a',
]);
$this->assertCfgSuccess($result);
$this->assertFileExists($this->tmpDir.'/config/owner_xyz/sub_a/Extension.conf.php');
}
public function testWriteWithSiteDirWritesToSiteConfig(): void
{
$result = $this->runCfg([
'-a',
$this->tmpDir,
'write',
'Extension',
$result = $this->runWrite([
'module:code="X100"',
'--siteDir=owner_xyz',
]);
$this->assertSame(0, $result['code'], $result['output']);
$this->assertCfgSuccess($result);
$this->assertFileExists($this->tmpDir.'/config/owner_xyz/Extension.conf.php');
}
public function testWriteToExistingSiteConfigCreatesBackup(): void
{
$firstWrite = $this->runCfg([
'-a',
$this->tmpDir,
'write',
'Extension',
$firstWrite = $this->runWrite([
'module:code="first"',
'--siteDir=owner_xyz',
]);
$this->assertSame(0, $firstWrite['code'], $firstWrite['output']);
$this->assertCfgSuccess($firstWrite);
$secondWrite = $this->runCfg([
'-a',
$this->tmpDir,
'write',
'Extension',
$secondWrite = $this->runWrite([
'module:code="second"',
'--siteDir=owner_xyz',
]);
$this->assertSame(0, $secondWrite['code'], $secondWrite['output']);
$this->assertCfgSuccess($secondWrite);
$siteFile = $this->tmpDir.'/config/owner_xyz/Extension.conf.php';
$backupFile = $siteFile.'.bak';
@ -260,74 +235,180 @@ class CfgTest extends TestCase
$this->assertSame('second', $current['module']['code']);
$this->assertSame('first', $backup['module']['code']);
}
/* }}} */
/* Show Flow {{{ */
public function testShowWithSiteReturnsSiteSpecificValue(): void
{
$this->runCfg([
'-a',
$this->tmpDir,
'write',
'Extension',
$this->runWrite([
'module:code="X100"',
'--siteDir=owner_xyz',
]);
$show = $this->runCfg([
'-a',
$this->tmpDir,
'show',
'Extension',
$show = $this->runShow([
'module:code',
'--siteDir=owner_xyz',
]);
$this->assertSame(0, $show['code'], $show['output']);
$this->assertCfgSuccess($show);
$this->assertStringContainsString('X100', $show['output']);
}
/* }}} */
public function testSiteDirRejectsTraversal(): void
/* Validation {{{ */
public function testInvalidActionReturnsError(): void
{
$result = $this->runCfg([
'-a',
$this->tmpDir,
'write',
'delete',
'Extension',
'module:code="X100"',
'--siteDir=../owner_xyz',
]);
$this->assertSame(1, $result['code']);
$this->assertStringContainsString('Invalid directory in --siteDir', $result['output']);
}
public function testSiteDirRejectsInvalidSegment(): void
{
$result = $this->runCfg([
'-a',
$this->tmpDir,
'write',
'Extension',
'module:code="X100"',
'--siteDir=owner xyz',
]);
$this->assertSame(1, $result['code']);
$this->assertStringContainsString("Invalid directory name segment 'owner xyz' in --siteDir.", $result['output']);
$this->assertCfgFailure($result, "'delete' not found");
}
public function testSiteDirRejectsEmptyValue(): void
{
$result = $this->runCfg([
'-a',
$this->tmpDir,
'write',
'Extension',
$result = $this->runWrite([
'module:code="X100"',
'--siteDir=',
]);
$this->assertSame(1, $result['code']);
$this->assertStringContainsString('a value is required for --siteDir', $result['output']);
$this->assertCfgFailure($result, 'a value is required for --siteDir');
}
/**
* @dataProvider siteDirValidationDataProvider
*/
public function testSiteDirValidation(string $siteDir, string $expectedMessage): void
{
$result = $this->runWrite([
'module:code="X100"',
'--siteDir='.$siteDir,
]);
$this->assertCfgFailure($result, $expectedMessage);
}
/**
* @dataProvider inputFileValidationDataProvider
*/
public function testInputFileValidation(?string $fileContents, string $pathSuffix, string $expectedMessage): void
{
$inFile = $this->tmpDir.'/'.$pathSuffix;
if ($fileContents !== null) {
file_put_contents($inFile, $fileContents);
}
$result = $this->runWrite([
'-i',
$inFile,
]);
$this->assertCfgFailure($result, $expectedMessage);
}
/**
* @dataProvider writePayloadSourceValidationDataProvider
*/
public function testWritePayloadSourceValidation(array $extraArgs, string $expectedMessage): void
{
$inFile = $this->createJsonInputFile(['module' => ['code' => 'X100']]);
$args = array_merge(
[
'-a',
$this->tmpDir,
'write',
'Extension',
],
array_map(
fn (string $arg): string => $arg === '__INPUT_FILE__' ? $inFile : $arg,
$extraArgs
)
);
$result = $this->runCfg($args);
$this->assertCfgFailure($result, $expectedMessage);
}
public function siteDirValidationDataProvider(): array
{
return [
'traversal' => ['../owner_xyz', 'Invalid directory in --siteDir'],
'invalid segment' => ['owner xyz', "Invalid directory name segment 'owner xyz' in --siteDir."],
'hidden dot segment' => ['owner_xyz/./secret', "Invalid directory in --siteDir: 'owner_xyz/./secret'."],
'config root only' => ['config/', 'Option --siteDir is empty.'],
];
}
public function inputFileValidationDataProvider(): array
{
return [
'missing file' => [null, 'missing.json', 'Input file is not readable'],
'invalid json' => ['{"module":', 'invalid.json', 'Input JSON is invalid'],
'scalar json' => ['true', 'scalar.json', 'Input JSON must decode to an object/array'],
];
}
public function writePayloadSourceValidationDataProvider(): array
{
return [
'missing payload source' => [[], 'Nothing to write: provide SETTING or --in.'],
'both payload sources' => [['module:code="x"', '-i', '__INPUT_FILE__'], 'Please use either SETTING or --in, not both.'],
];
}
/* }}} */
/* Helpers {{{ */
private function runWrite(array $args): array
{
return $this->runCfg(array_merge([
'-a',
$this->tmpDir,
'write',
'Extension',
], $args));
}
private function runShow(array $args): array
{
return $this->runCfg(array_merge([
'-a',
$this->tmpDir,
'show',
'Extension',
], $args));
}
private function createJsonInputFile(array $contents, string $name = 'extension-in.json'): string
{
$path = $this->tmpDir.'/'.$name;
file_put_contents($path, json_encode($contents, JSON_PRETTY_PRINT));
return $path;
}
private function loadWrittenConfig(?string $siteDir = null): array
{
$path = $siteDir === null
? $this->tmpDir.'/config/Extension.conf.php'
: $this->tmpDir.'/config/'.$siteDir.'/Extension.conf.php';
return require $path;
}
private function assertCfgSuccess(array $result): void
{
$this->assertSame(0, $result['code'], $result['output']);
}
private function assertCfgFailure(array $result, string $message): void
{
$this->assertSame(1, $result['code'], $result['output']);
$this->assertStringContainsString($message, $result['output']);
}
private function runCfg(array $args): array
@ -372,4 +453,5 @@ class CfgTest extends TestCase
rmdir($dir);
}
/* }}} */
}