Skip to content

Commit 6582f9b

Browse files
test: improving code coverage for SatisCommandBuilder and LockProcessor
* test: improving code coverage for SatisCommandBuilder and LockProcessor * 📝 Add docstrings to `test-improving-code-coverage` Docstrings generation was requested by @Trusted97. * #2 (comment) The following files were modified: * `config/reference.php` * `src/Service/SatisManager.php` * chore: simplify logic for ProcessFactory * chore: adding test for ProcessFactory --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 3a14a09 commit 6582f9b

File tree

10 files changed

+1708
-38
lines changed

10 files changed

+1708
-38
lines changed

config/reference.php

Lines changed: 1355 additions & 0 deletions
Large diffs are not rendered by default.

config/services.yaml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,15 @@ services:
5151
tags:
5252
- { name: serializer.normalizer, priority: 1000 }
5353

54+
App\Process\EnvironmentProvider:
55+
arguments:
56+
$composerHome: "%composer.home%"
57+
58+
5459
App\Process\ProcessFactory:
5560
arguments:
5661
$rootPath: "%kernel.project_dir%"
57-
$composerHome: "%composer.home%"
62+
$envProvider: '@App\Process\EnvironmentProvider'
5863

5964
App\Service\SatisManager:
6065
public: true

src/Event/BuildEvent.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use App\DTO\RepositoryInterface;
66
use Symfony\Contracts\EventDispatcher\Event;
77

8-
final class BuildEvent extends Event
8+
class BuildEvent extends Event
99
{
1010
public const string NAME = 'satis_build';
1111

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace App\Process;
4+
5+
class EnvironmentProvider
6+
{
7+
public function __construct(
8+
private readonly string $composerHome,
9+
) {
10+
}
11+
12+
public function getEnv(): array
13+
{
14+
$env = [];
15+
16+
foreach ($_SERVER as $key => $value) {
17+
if (\is_string($value) && false !== $envValue = \getenv($key)) {
18+
$env[$key] = $envValue;
19+
}
20+
}
21+
22+
foreach ($_ENV as $key => $value) {
23+
if (\is_string($value)) {
24+
$env[$key] = $value;
25+
}
26+
}
27+
28+
$env['COMPOSER_HOME'] ??= $this->composerHome;
29+
$env['COMPOSER_NO_INTERACTION'] = '1';
30+
31+
return $env;
32+
}
33+
}

src/Process/ProcessFactory.php

Lines changed: 15 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,47 +4,28 @@
44

55
use Symfony\Component\Process\Process;
66

7-
class ProcessFactory
7+
final readonly class ProcessFactory
88
{
9-
protected string $rootPath;
10-
11-
protected string $composerHome;
12-
13-
public function __construct(string $rootPath, string $composerHome)
14-
{
15-
$this->rootPath = $rootPath;
16-
$this->composerHome = $composerHome;
9+
public function __construct(
10+
private string $rootPath,
11+
private EnvironmentProvider $envProvider,
12+
) {
1713
}
1814

1915
public function create(array $command, ?int $timeout = null): Process
2016
{
21-
$exec = \reset($command);
22-
$command[\key($command)] = $this->rootPath . '/' . $exec;
23-
24-
return new Process($command, $this->rootPath, $this->getEnv(), null, $timeout);
25-
}
26-
27-
protected function getEnv(): array
28-
{
29-
$env = [];
30-
31-
foreach ($_SERVER as $k => $v) {
32-
if (\is_string($v) && false !== $v = \getenv($k)) {
33-
$env[$k] = $v;
34-
}
17+
if (empty($command)) {
18+
throw new \InvalidArgumentException('Command array cannot be empty.');
3519
}
3620

37-
foreach ($_ENV as $k => $v) {
38-
if (\is_string($v)) {
39-
$env[$k] = $v;
40-
}
41-
}
42-
43-
if (empty($env['COMPOSER_HOME'])) {
44-
$env['COMPOSER_HOME'] = $this->composerHome;
45-
}
46-
$env['COMPOSER_NO_INTERACTION'] = 1;
21+
$command[0] = $this->rootPath . '/' . $command[0];
4722

48-
return $env;
23+
return new Process(
24+
command: $command,
25+
cwd: $this->rootPath,
26+
env: $this->envProvider->getEnv(),
27+
input: null,
28+
timeout: $timeout
29+
);
4930
}
5031
}

src/Service/SatisManager.php

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
use Symfony\Component\Process\Exception\RuntimeException;
1212

1313
#[AsEventListener(event: BuildEvent::class, method: 'onBuild', priority: 100)]
14-
final class SatisManager
14+
class SatisManager
1515
{
1616
protected string $satisFilename;
1717

@@ -62,6 +62,19 @@ public function run(): \Generator
6262
}
6363
}
6464

65+
/**
66+
* Handle a BuildEvent by executing the Satis build for the event's repository.
67+
*
68+
* Acquires the build lock, runs the generated Satis process, releases the lock,
69+
* and updates the event's status with the process exit code (`1` if an internal
70+
* RuntimeException occurs).
71+
*
72+
* @param BuildEvent $event the build event; its repository (if any) is used to
73+
* determine the build target and its status will be
74+
* updated with the process exit code
75+
*
76+
* @throws \JsonException
77+
*/
6578
public function onBuild(BuildEvent $event): void
6679
{
6780
$repository = $event->getRepository();
@@ -80,6 +93,20 @@ public function onBuild(BuildEvent $event): void
8093
$event->setStatus($status);
8194
}
8295

96+
/**
97+
* Build the command argument array for executing a Satis build.
98+
*
99+
* Constructs a command array based on the configured satis file and output directory,
100+
* optionally scoping the build to a single repository and adding extra options or arguments.
101+
*
102+
* @param string|null $repositoryName name of a single repository to target, or `null` to include all repositories
103+
* @param array $options associative or list-style Satis options to include (added via the builder's options API)
104+
* @param array $extraArgs additional positional arguments to append to the command
105+
*
106+
* @throws \JsonException if encoding the built command to JSON for logging fails
107+
*
108+
* @return array the constructed command as an array of command and arguments suitable for Process execution
109+
*/
83110
protected function getCommandLine(?string $repositoryName = null, array $options = [], array $extraArgs = []): array
84111
{
85112
$configuration = $this->manager->getConfig();
@@ -99,7 +126,7 @@ protected function getCommandLine(?string $repositoryName = null, array $options
99126
$satisCommandBuilder->withRepository($repositoryName);
100127
}
101128

102-
$this->logger->info(\json_encode($satisCommandBuilder->build()));
129+
$this->logger->info(\json_encode($satisCommandBuilder->build(), \JSON_THROW_ON_ERROR));
103130

104131
return $satisCommandBuilder->build();
105132
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
namespace App\Tests\Process;
4+
5+
use App\Process\EnvironmentProvider;
6+
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
7+
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
8+
9+
class EnvironmentProviderTest extends KernelTestCase
10+
{
11+
protected function tearDown(): void
12+
{
13+
\putenv('TEST_KEY');
14+
unset($_SERVER['TEST_KEY'], $_ENV['TEST_ENV_KEY']);
15+
}
16+
17+
public function testGetEnvSetsComposerDefaults(): void
18+
{
19+
$parameterBag = static::getContainer()->get(ParameterBagInterface::class);
20+
$provider = new EnvironmentProvider($parameterBag->get('composer.home'));
21+
22+
$env = $provider->getEnv();
23+
24+
$this->assertSame('/app/var/composer', $env['COMPOSER_HOME']);
25+
$this->assertSame('1', $env['COMPOSER_NO_INTERACTION']);
26+
}
27+
28+
public function testGetEnvIncludesServerVariables(): void
29+
{
30+
$_SERVER['TEST_KEY'] = 'value';
31+
\putenv('TEST_KEY=value');
32+
33+
$parameterBag = static::getContainer()->get(ParameterBagInterface::class);
34+
$provider = new EnvironmentProvider($parameterBag->get('composer.home'));
35+
36+
$env = $provider->getEnv();
37+
38+
$this->assertArrayHasKey('TEST_KEY', $env);
39+
$this->assertSame('value', $env['TEST_KEY']);
40+
}
41+
42+
public function testGetEnvIncludesEnvVariables(): void
43+
{
44+
$_ENV['TEST_ENV_KEY'] = 'env_value';
45+
46+
$parameterBag = static::getContainer()->get(ParameterBagInterface::class);
47+
$provider = new EnvironmentProvider($parameterBag->get('composer.home'));
48+
49+
$env = $provider->getEnv();
50+
51+
$this->assertArrayHasKey('TEST_ENV_KEY', $env);
52+
$this->assertSame('env_value', $env['TEST_ENV_KEY']);
53+
}
54+
55+
public function testComposerHomeNotOverwrittenIfProvidedInEnv(): void
56+
{
57+
\putenv('COMPOSER_HOME=/app/var/composer');
58+
$_SERVER['COMPOSER_HOME'] = '/app/var/composer';
59+
60+
$parameterBag = static::getContainer()->get(ParameterBagInterface::class);
61+
$provider = new EnvironmentProvider($parameterBag->get('composer.home'));
62+
63+
$env = $provider->getEnv();
64+
65+
$this->assertSame('/app/var/composer', $env['COMPOSER_HOME']);
66+
}
67+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
namespace App\Tests\Process;
4+
5+
use App\Process\EnvironmentProvider;
6+
use App\Process\ProcessFactory;
7+
use PHPUnit\Framework\TestCase;
8+
use Symfony\Component\Process\Process;
9+
10+
class ProcessFactoryTest extends TestCase
11+
{
12+
public function testCreateReturnsConfiguredProcess(): void
13+
{
14+
$env = ['A' => 'B'];
15+
16+
$provider = $this->createMock(EnvironmentProvider::class);
17+
$provider->method('getEnv')->willReturn($env);
18+
19+
$factory = new ProcessFactory('/root', $provider);
20+
21+
$process = $factory->create(['bin/tool', 'arg1', 'arg2'], 123);
22+
23+
$this->assertInstanceOf(Process::class, $process);
24+
$this->assertSame('\'/root/bin/tool\' \'arg1\' \'arg2\'', $process->getCommandLine());
25+
$this->assertSame('/root', $process->getWorkingDirectory());
26+
$this->assertSame($env, $process->getEnv());
27+
$this->assertSame(123.0, $process->getTimeout());
28+
}
29+
30+
public function testCreateThrowsOnEmptyCommand(): void
31+
{
32+
$provider = $this->createMock(EnvironmentProvider::class);
33+
$factory = new ProcessFactory('/root', $provider);
34+
35+
$this->expectException(\InvalidArgumentException::class);
36+
37+
$factory->create([]);
38+
}
39+
40+
public function testCommandIsPrefixedWithRootPath(): void
41+
{
42+
$provider = $this->createMock(EnvironmentProvider::class);
43+
$provider->method('getEnv')->willReturn([]);
44+
45+
$factory = new ProcessFactory('/abc', $provider);
46+
47+
$process = $factory->create(['vendor/bin/satis']);
48+
49+
$this->assertSame('\'/abc/vendor/bin/satis\'', $process->getCommandLine());
50+
}
51+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
namespace App\Tests\Service;
4+
5+
use App\DTO\Repository;
6+
use App\Service\LockProcessor;
7+
use App\Service\RepositoryManager;
8+
use PHPUnit\Framework\TestCase;
9+
10+
final class LockProcessorTest extends TestCase
11+
{
12+
public function testProcessFileAddsRepositories(): void
13+
{
14+
$json = <<<JSON
15+
{
16+
"packages": [
17+
{
18+
"name": "vendor/package1",
19+
"source": { "url": "https://example.com/repo1.git", "type": "git" }
20+
}
21+
],
22+
"packages-dev": [
23+
{
24+
"name": "vendor/package2",
25+
"source": { "url": "https://example.com/repo2.git", "type": "git" }
26+
}
27+
]
28+
}
29+
JSON;
30+
31+
$tmpFile = \tmpfile();
32+
\fwrite($tmpFile, $json);
33+
$path = \stream_get_meta_data($tmpFile)['uri'];
34+
$file = new \SplFileObject($path);
35+
36+
$manager = $this->createMock(RepositoryManager::class);
37+
$manager->expects($this->once())
38+
->method('addAll')
39+
->with($this->callback(function ($repositories) {
40+
return 2 === \count($repositories)
41+
&& $repositories[0] instanceof Repository
42+
&& 'https://example.com/repo1.git' === $repositories[0]->getUrl()
43+
&& 'git' === $repositories[0]->getType()
44+
&& $repositories[1] instanceof Repository
45+
&& 'https://example.com/repo2.git' === $repositories[1]->getUrl()
46+
&& 'git' === $repositories[1]->getType();
47+
}));
48+
49+
$processor = new LockProcessor($manager);
50+
$processor->processFile($file);
51+
52+
\fclose($tmpFile);
53+
}
54+
55+
public function testGetRepositoriesFiltersInvalidPackages(): void
56+
{
57+
$manager = $this->createMock(RepositoryManager::class);
58+
$processor = new LockProcessor($manager);
59+
60+
$packages = [
61+
(object) [
62+
'name' => 'vendor/valid',
63+
'source' => (object) ['url' => 'https://example.com/repo.git', 'type' => 'git'],
64+
],
65+
(object) [
66+
'name' => 'vendor/invalid',
67+
'source' => (object) ['url' => '', 'type' => ''],
68+
],
69+
(object) [
70+
'name' => 'vendor/nosource',
71+
],
72+
];
73+
74+
$method = new \ReflectionMethod(LockProcessor::class, 'getRepositories');
75+
$repositories = $method->invoke($processor, $packages);
76+
77+
$this->assertCount(1, $repositories);
78+
$this->assertSame('https://example.com/repo.git', $repositories[0]->getUrl());
79+
$this->assertSame('git', $repositories[0]->getType());
80+
}
81+
}

0 commit comments

Comments
 (0)