Skip to content

Commit 5862564

Browse files
committed
fix tests command env injection and coverage-safe behavior
1 parent 626b33b commit 5862564

21 files changed

Lines changed: 539 additions & 323 deletions

src/Config/ComposerDependencyAnalyserConfig.php

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
namespace FastForward\DevTools\Config;
2121

22+
use FastForward\DevTools\Environment\Environment;
23+
use FastForward\DevTools\Environment\EnvironmentInterface;
2224
use FastForward\DevTools\Path\DevToolsPathResolver;
2325
use ShipMonk\ComposerDependencyAnalyser\Config\Configuration;
2426
use ShipMonk\ComposerDependencyAnalyser\Config\ErrorType;
@@ -86,14 +88,18 @@ final class ComposerDependencyAnalyserConfig
8688
* Creates the default Composer Dependency Analyser configuration.
8789
*
8890
* @param callable|null $customize optional callback to customize the configuration
91+
* @param EnvironmentInterface|null $environment optional environment reader used to resolve overrides
8992
*
9093
* @return Configuration the configured analyser configuration
9194
*/
92-
public static function configure(?callable $customize = null): Configuration
93-
{
95+
public static function configure(
96+
?callable $customize = null,
97+
?EnvironmentInterface $environment = null,
98+
): Configuration {
99+
$environment ??= new Environment();
94100
$configuration = new Configuration();
95101

96-
if (! self::shouldShowShadowDependencies()) {
102+
if (! self::shouldShowShadowDependencies($environment)) {
97103
self::applyIgnoresShadowDependencies($configuration);
98104
}
99105

@@ -128,11 +134,15 @@ public static function applyIgnoresShadowDependencies(Configuration $configurati
128134
/**
129135
* Determines whether shadow dependency reports SHOULD remain visible.
130136
*
137+
* @param EnvironmentInterface|null $environment optional environment reader used to resolve overrides
138+
*
131139
* @return bool
132140
*/
133-
public static function shouldShowShadowDependencies(): bool
141+
public static function shouldShowShadowDependencies(?EnvironmentInterface $environment = null): bool
134142
{
135-
return '1' === getenv(self::ENV_SHOW_SHADOW_DEPENDENCIES);
143+
$environment ??= new Environment();
144+
145+
return '1' === $environment->get(self::ENV_SHOW_SHADOW_DEPENDENCIES);
136146
}
137147

138148
/**

src/Console/Command/ReportsCommand.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
use FastForward\DevTools\Process\ProcessBuilderInterface;
2828
use FastForward\DevTools\Process\ProcessQueueInterface;
2929
use FastForward\DevTools\Path\ManagedWorkspace;
30+
use Psr\Log\LogLevel;
3031
use Symfony\Component\Console\Attribute\AsCommand;
3132
use Symfony\Component\Console\Command\Command;
3233
use Symfony\Component\Console\Input\InputInterface;
@@ -197,7 +198,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
197198
$this->log(
198199
'Skipping coverage report because no tests directory or PHP source files were detected.',
199200
$input,
200-
logLevel: 'warning',
201+
logLevel: LogLevel::WARNING,
201202
);
202203
}
203204

src/Console/Command/TestsCommand.php

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use FastForward\DevTools\Console\Input\HasCacheOption;
2525
use FastForward\DevTools\Console\Input\HasJsonOption;
2626
use FastForward\DevTools\Composer\Json\ComposerJsonInterface;
27+
use FastForward\DevTools\Environment\EnvironmentInterface;
2728
use FastForward\DevTools\Filesystem\FilesystemInterface;
2829
use FastForward\DevTools\Path\DevToolsPathResolver;
2930
use FastForward\DevTools\PhpUnit\Bootstrap\BootstrapShimGenerator;
@@ -60,9 +61,9 @@ final class TestsCommand extends Command
6061
use HasJsonOption;
6162
use LogsCommandResults;
6263

63-
private const string AGENT_ENVIRONMENT_VARIABLE = 'AI_AGENT';
64+
public const string AGENT_ENVIRONMENT_VARIABLE = 'AI_AGENT';
6465

65-
private const string AGENT_ENVIRONMENT_VALUE = 'fast-forward/dev-tools';
66+
public const string AGENT_ENVIRONMENT_VALUE = 'fast-forward/dev-tools';
6667

6768
private const string PROCESS_LABEL = 'Running PHPUnit Tests';
6869

@@ -71,6 +72,8 @@ final class TestsCommand extends Command
7172
*/
7273
public const string CONFIG = 'phpunit.xml';
7374

75+
public const string ENV_MINIMUM_COVERAGE = 'FAST_FORWARD_MIN_COVERAGE';
76+
7477
/**
7578
* @param CoverageSummaryLoaderInterface $coverageSummaryLoader the loader used for `coverage-php` summaries
7679
* @param ComposerJsonInterface $composer the composer.json reader for autoload information
@@ -80,6 +83,7 @@ final class TestsCommand extends Command
8083
* @param ProcessBuilderInterface $processBuilder the builder used to assemble the PHPUnit process
8184
* @param ProcessQueueInterface $processQueue the queue used to execute PHPUnit
8285
* @param ProjectCapabilitiesResolverInterface $projectCapabilitiesResolver the project capability resolver
86+
* @param EnvironmentInterface $environment the environment resolver for CLI-scoped flags
8387
*/
8488
public function __construct(
8589
private readonly CoverageSummaryLoaderInterface $coverageSummaryLoader,
@@ -90,6 +94,7 @@ public function __construct(
9094
private readonly ProcessBuilderInterface $processBuilder,
9195
private readonly ProcessQueueInterface $processQueue,
9296
private readonly ProjectCapabilitiesResolverInterface $projectCapabilitiesResolver,
97+
private readonly EnvironmentInterface $environment,
9398
) {
9499
parent::__construct();
95100
}
@@ -193,7 +198,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
193198

194199
if (! $projectCapabilities->canRunTests()) {
195200
return $this->success(
196-
'Skipping PHPUnit tests because no tests directory or PHP source files were detected.',
201+
'Skipping PHPUnit tests because no Composer-autoloaded PHP source files were detected.',
197202
$input,
198203
[
199204
'output' => $processOutput,
@@ -249,14 +254,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int
249254
$result = $this->processQueue->run($processOutput);
250255
$processResultContext = $this->resolveProcessResultContext($processOutput, $result, $structuredOutput);
251256

252-
if (self::SUCCESS !== $result || null === $minimumCoverage || null === $coverageReportPath) {
253-
if (self::SUCCESS === $result) {
254-
return $this->success('PHPUnit tests completed successfully.', $input, $processResultContext);
255-
}
256-
257+
if (self::SUCCESS !== $result) {
257258
return $this->failure('PHPUnit tests failed.', $input, $processResultContext);
258259
}
259260

261+
if (null === $minimumCoverage || null === $coverageReportPath) {
262+
return $this->success('PHPUnit tests completed successfully.', $input, $processResultContext);
263+
}
264+
260265
[$validationResult, $message, $coverageContext] = $this->validateMinimumCoverage(
261266
$coverageReportPath,
262267
$minimumCoverage,
@@ -314,10 +319,9 @@ private function resolveProcessResultContext(
314319
private function forceAgentReporter(Process $process): void
315320
{
316321
$env = $process->getEnv();
322+
$parentAgentEnvironment = $this->environment->get(self::AGENT_ENVIRONMENT_VARIABLE);
317323

318-
if (\array_key_exists(self::AGENT_ENVIRONMENT_VARIABLE, $env) || false !== getenv(
319-
self::AGENT_ENVIRONMENT_VARIABLE
320-
)) {
324+
if (\array_key_exists(self::AGENT_ENVIRONMENT_VARIABLE, $env) || null !== $parentAgentEnvironment) {
321325
return;
322326
}
323327

@@ -515,9 +519,15 @@ private function resolveMinimumCoverage(InputInterface $input): ?float
515519
$minimumCoverage = $input->getOption('min-coverage');
516520

517521
if (null === $minimumCoverage) {
522+
$minimumCoverage = $this->resolveMinimumCoverageFromEnvironment();
523+
}
524+
525+
if (false === $minimumCoverage || '' === trim((string) $minimumCoverage)) {
518526
return null;
519527
}
520528

529+
$minimumCoverage = trim((string) $minimumCoverage);
530+
521531
if (! is_numeric($minimumCoverage)) {
522532
throw new InvalidArgumentException('The --min-coverage option MUST be a numeric percentage.');
523533
}
@@ -531,6 +541,16 @@ private function resolveMinimumCoverage(InputInterface $input): ?float
531541
return $minimumCoverage;
532542
}
533543

544+
/**
545+
* Resolves minimum-coverage value from injected environment abstraction.
546+
*
547+
* @return string|false|null the configured coverage threshold or a falsey fallback
548+
*/
549+
private function resolveMinimumCoverageFromEnvironment(): ?string
550+
{
551+
return $this->environment->get(self::ENV_MINIMUM_COVERAGE);
552+
}
553+
534554
/**
535555
* @param InputInterface $input the raw parameter definitions
536556
* @param ProcessBuilderInterface $processBuilder the process builder to extend with coverage arguments

src/Console/DevTools.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
*/
4747
final class DevTools extends Application
4848
{
49+
public const string ENV_AUTO_UPDATE = 'FAST_FORWARD_AUTO_UPDATE';
50+
4951
private const string LOGO = <<<'LOGO'
5052
____ _____ _
5153
| _ \ _____ _|_ _|__ ___ | |___
@@ -201,7 +203,7 @@ private function configureWorkspaceDirectory(InputInterface $input): void
201203
*/
202204
private function runAutoUpdateWhenRequested(InputInterface $input, OutputInterface $output): void
203205
{
204-
$autoUpdateMode = $this->environment->get('FAST_FORWARD_AUTO_UPDATE', '');
206+
$autoUpdateMode = $this->environment->get(self::ENV_AUTO_UPDATE, '');
205207

206208
if (! $input->hasParameterOption('--auto-update', true) && ! $this->isTruthyAutoUpdateMode($autoUpdateMode)) {
207209
return;

src/Path/ManagedWorkspace.php

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
namespace FastForward\DevTools\Path;
2121

22+
use FastForward\DevTools\Environment\Environment;
23+
use FastForward\DevTools\Environment\EnvironmentInterface;
2224
use Symfony\Component\Filesystem\Path;
2325

2426
/**
@@ -80,10 +82,14 @@ final class ManagedWorkspace
8082
*
8183
* @param string $path the optional relative segment to append under the managed output root
8284
* @param string $baseDir the optional repository root used to resolve the managed workspace path
85+
* @param EnvironmentInterface|null $environment explicit environment reader used to resolve FAST_FORWARD_WORKSPACE_DIR
8386
*/
84-
public static function getOutputDirectory(string $path = '', string $baseDir = ''): string
85-
{
86-
$baseDir = self::getWorkspaceRoot($baseDir);
87+
public static function getOutputDirectory(
88+
string $path = '',
89+
string $baseDir = '',
90+
?EnvironmentInterface $environment = null,
91+
): string {
92+
$baseDir = self::getWorkspaceRoot(baseDir: $baseDir, environment: $environment);
8793

8894
return '' === $path
8995
? $baseDir
@@ -99,10 +105,14 @@ public static function getOutputDirectory(string $path = '', string $baseDir = '
99105
*
100106
* @param string $path the optional relative cache segment to append under the managed cache root
101107
* @param string $baseDir the optional repository root used to resolve the managed cache path
108+
* @param EnvironmentInterface|null $environment explicit environment reader used to resolve FAST_FORWARD_WORKSPACE_DIR
102109
*/
103-
public static function getCacheDirectory(string $path = '', string $baseDir = ''): string
104-
{
105-
$baseDir = self::getOutputDirectory(self::CACHE_ROOT, $baseDir);
110+
public static function getCacheDirectory(
111+
string $path = '',
112+
string $baseDir = '',
113+
?EnvironmentInterface $environment = null,
114+
): string {
115+
$baseDir = self::getOutputDirectory(path: self::CACHE_ROOT, baseDir: $baseDir, environment: $environment);
106116

107117
return '' === $path
108118
? $baseDir
@@ -117,12 +127,17 @@ public static function getCacheDirectory(string $path = '', string $baseDir = ''
117127
* under that base directory while absolute workspaces are used as-is.
118128
*
119129
* @param string $baseDir the optional repository root used to resolve a relative workspace
130+
* @param EnvironmentInterface|null $environment explicit environment reader used to resolve FAST_FORWARD_WORKSPACE_DIR
120131
*/
121-
public static function getWorkspaceRoot(string $baseDir = ''): string
122-
{
123-
$workspaceRoot = getenv(self::ENV_WORKSPACE_DIR);
132+
public static function getWorkspaceRoot(
133+
string $baseDir = '',
134+
?EnvironmentInterface $environment = null,
135+
): string {
136+
$environment ??= new Environment();
124137

125-
if (false === $workspaceRoot || '' === $workspaceRoot) {
138+
$workspaceRoot = $environment->get(self::ENV_WORKSPACE_DIR);
139+
140+
if (null === $workspaceRoot || '' === $workspaceRoot) {
126141
$workspaceRoot = self::WORKSPACE_ROOT;
127142
}
128143

@@ -138,17 +153,25 @@ public static function getWorkspaceRoot(string $baseDir = ''): string
138153
* should skip generated artifacts during source scans.
139154
*
140155
* @param string $baseDir the optional repository root used to relativize absolute workspace paths
156+
* @param EnvironmentInterface|null $environment explicit environment reader used for tests
141157
*/
142-
public static function getProjectRelativeWorkspaceRoot(string $baseDir = ''): ?string
143-
{
144-
$workspaceRoot = self::getWorkspaceRoot();
145-
146-
if (! Path::isAbsolute($workspaceRoot)) {
147-
return $workspaceRoot;
158+
public static function getProjectRelativeWorkspaceRoot(
159+
string $baseDir = '',
160+
?EnvironmentInterface $environment = null,
161+
): ?string {
162+
$environment ??= new Environment();
163+
$workspaceRoot = $environment->get(self::ENV_WORKSPACE_DIR);
164+
165+
if (null === $workspaceRoot || '' === $workspaceRoot) {
166+
$workspaceRoot = self::WORKSPACE_ROOT;
148167
}
149168

150169
if ('' === $baseDir) {
151-
return null;
170+
return Path::isAbsolute($workspaceRoot) ? null : $workspaceRoot;
171+
}
172+
173+
if (! Path::isAbsolute($workspaceRoot)) {
174+
return $workspaceRoot;
152175
}
153176

154177
$baseDir = Path::canonicalize($baseDir);

src/Path/WorkingProjectPathResolver.php

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
namespace FastForward\DevTools\Path;
2121

22+
use FastForward\DevTools\Environment\EnvironmentInterface;
2223
use Symfony\Component\Finder\Finder;
2324
use Symfony\Component\Filesystem\Path;
2425

@@ -64,14 +65,17 @@ public static function getProjectPath(string $path = ''): string
6465
* Returns the project directories that static-analysis and coding-style tooling SHOULD skip.
6566
*
6667
* @param string $baseDir the optional repository base directory used to materialize absolute paths
68+
* @param EnvironmentInterface|null $environment explicit environment reader used to resolve FAST_FORWARD_WORKSPACE_DIR
6769
*
6870
* @return list<string>
6971
*/
70-
public static function getToolingExcludedDirectories(string $baseDir = ''): array
71-
{
72+
public static function getToolingExcludedDirectories(
73+
string $baseDir = '',
74+
?EnvironmentInterface $environment = null,
75+
): array {
7276
$directories = [];
7377

74-
foreach (self::getToolingExcludedDirectoryNames($baseDir) as $excludedDirectory) {
78+
foreach (self::getToolingExcludedDirectoryNames($baseDir, $environment) as $excludedDirectory) {
7579
$directories[] = Path::join($baseDir, $excludedDirectory);
7680
}
7781

@@ -82,13 +86,16 @@ public static function getToolingExcludedDirectories(string $baseDir = ''): arra
8286
* Returns PHP source files that tooling SHOULD inspect without traversing generated directories.
8387
*
8488
* @param string $baseDir the optional repository base directory used to materialize absolute paths
89+
* @param EnvironmentInterface|null $environment explicit environment reader used to resolve FAST_FORWARD_WORKSPACE_DIR
8590
*
8691
* @return list<string>
8792
*/
88-
public static function getToolingSourcePaths(string $baseDir = ''): array
89-
{
93+
public static function getToolingSourcePaths(
94+
string $baseDir = '',
95+
?EnvironmentInterface $environment = null,
96+
): array {
9097
$workingDirectory = '' === $baseDir ? getcwd() : $baseDir;
91-
$excludedDirectories = self::getToolingExcludedDirectoryNames($workingDirectory);
98+
$excludedDirectories = self::getToolingExcludedDirectoryNames($workingDirectory, $environment);
9299
$finder = Finder::create()
93100
->files()
94101
->name('*.php')
@@ -115,13 +122,19 @@ public static function getToolingSourcePaths(string $baseDir = ''): array
115122
* Returns repository-relative directories ignored by tooling.
116123
*
117124
* @param string $baseDir the optional repository base directory used to relativize a custom workspace
125+
* @param EnvironmentInterface|null $environment explicit environment reader used to resolve FAST_FORWARD_WORKSPACE_DIR
118126
*
119127
* @return list<string>
120128
*/
121-
private static function getToolingExcludedDirectoryNames(string $baseDir = ''): array
122-
{
129+
private static function getToolingExcludedDirectoryNames(
130+
string $baseDir = '',
131+
?EnvironmentInterface $environment = null,
132+
): array {
123133
$directories = self::TOOLING_EXCLUDED_DIRECTORIES;
124-
$workspaceRoot = ManagedWorkspace::getProjectRelativeWorkspaceRoot($baseDir);
134+
$workspaceRoot = ManagedWorkspace::getProjectRelativeWorkspaceRoot(
135+
baseDir: $baseDir,
136+
environment: $environment
137+
);
125138

126139
if (null !== $workspaceRoot && ! \in_array($workspaceRoot, $directories, true)) {
127140
$directories[] = $workspaceRoot;

src/Project/ProjectCapabilities.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,6 @@ public function canGenerateWiki(): bool
128128
*/
129129
public function canRunTests(): bool
130130
{
131-
return $this->hasTestsPath || $this->hasPhpSourceFiles;
131+
return $this->hasPhpSourceFiles;
132132
}
133133
}

0 commit comments

Comments
 (0)