Skip to content

Commit 57b1752

Browse files
committed
[tests] Expand coverage fixtures and helper branches (#133)
1 parent 60e9e99 commit 57b1752

13 files changed

Lines changed: 502 additions & 26 deletions

composer.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,10 @@
9191
"autoload-dev": {
9292
"psr-4": {
9393
"FastForward\\DevTools\\Tests\\": "tests/"
94-
}
94+
},
95+
"classmap": [
96+
"tests/Fixtures/"
97+
]
9598
},
9699
"bin": "bin/dev-tools",
97100
"config": {

tests/Changelog/Document/ChangelogDocumentTest.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,7 @@ public function promoteUnreleasedWillMergeWithAnExistingPublishedVersion(): void
9090
#[Test]
9191
public function documentAccessorsWillResolveExpectedReleaseVariants(): void
9292
{
93-
$document = new ChangelogDocument([
94-
new ChangelogRelease('1.2.0', '2026-04-19'),
95-
]);
93+
$document = new ChangelogDocument([new ChangelogRelease('1.2.0', '2026-04-19')]);
9694

9795
self::assertSame(ChangelogDocument::UNRELEASED_VERSION, $document->getUnreleased()->getVersion());
9896
self::assertNull($document->getRelease('9.9.9'));
@@ -172,7 +170,9 @@ public function releaseHelpersWillNormalizeEntriesAndSupportImmutableMutators():
172170

173171
$updated = $release
174172
->withEntry(ChangelogEntryType::Fixed, 'Repair behavior')
175-
->withEntries([ChangelogEntryType::Security->value => ['Security hardening']])
173+
->withEntries([
174+
ChangelogEntryType::Security->value => ['Security hardening'],
175+
])
176176
->withDate('2026-04-20');
177177

178178
self::assertSame('2026-04-20', $updated->getDate());

tests/Changelog/Parser/ChangelogParserTest.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,60 @@ public function parseWillExtractReleaseSectionsAndEntries(): void
6969
self::assertSame(['Add release preparation workflow'], $document->getUnreleased()->getEntries()['Added']);
7070
self::assertSame('2026-04-01', $document->getRelease('1.0.0')?->getDate());
7171
}
72+
73+
/**
74+
* @return void
75+
*/
76+
#[Test]
77+
public function parseWillReturnDefaultDocumentForEmptyContents(): void
78+
{
79+
$document = (new ChangelogParser())->parse(" \n\n");
80+
81+
self::assertSame([ChangelogDocument::UNRELEASED_VERSION], array_map(
82+
static fn(ChangelogRelease $release): string => $release->getVersion(),
83+
$document->getReleases(),
84+
));
85+
}
86+
87+
/**
88+
* @return void
89+
*/
90+
#[Test]
91+
public function parseWillReturnDefaultDocumentWhenNoReleaseHeadingsExist(): void
92+
{
93+
$document = (new ChangelogParser())->parse("# Changelog\n\nThis file has no release headings yet.\n");
94+
95+
self::assertSame(ChangelogDocument::UNRELEASED_VERSION, $document->getUnreleased()->getVersion());
96+
self::assertFalse($document->getUnreleased()->hasEntries());
97+
}
98+
99+
/**
100+
* @return void
101+
*/
102+
#[Test]
103+
public function parseWillIgnoreUnsupportedLinesAndDeduplicateEntriesWithinASection(): void
104+
{
105+
$document = (new ChangelogParser())->parse(<<<'MD'
106+
## [Unreleased]
107+
108+
### Added
109+
110+
Intro line that should be ignored
111+
- Add sync command
112+
- Add sync command
113+
*
114+
115+
### Fixed
116+
117+
- Repair coverage report
118+
-
119+
MD);
120+
121+
self::assertSame(['Add sync command'], $document->getUnreleased()->getEntriesFor(ChangelogEntryType::Added));
122+
self::assertSame(
123+
['Repair coverage report'],
124+
$document->getUnreleased()->getEntriesFor(ChangelogEntryType::Fixed)
125+
);
126+
self::assertSame([], $document->getUnreleased()->getEntriesFor(ChangelogEntryType::Security));
127+
}
72128
}

tests/Changelog/Renderer/MarkdownRendererTest.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,4 +148,68 @@ public function renderWillKeepOnlyOneBlankLineBetweenAnEmptyUnreleasedSectionAnd
148148
self::assertStringContainsString("## [Unreleased]\n\n## [1.2.0] - 2026-04-19", $output);
149149
self::assertStringNotContainsString("## [Unreleased]\n\n\n## [1.2.0] - 2026-04-19", $output);
150150
}
151+
152+
/**
153+
* @return void
154+
*/
155+
#[Test]
156+
public function renderWillOmitReferencesWhenRepositoryUrlIsMissingOrBlank(): void
157+
{
158+
$document = new ChangelogDocument([
159+
(new ChangelogRelease('1.2.0', '2026-04-19'))->withEntry(
160+
ChangelogEntryType::Added,
161+
'Ship changelog automation',
162+
),
163+
]);
164+
165+
$renderer = new MarkdownRenderer();
166+
167+
self::assertStringNotContainsString('[1.2.0]:', $renderer->render($document));
168+
self::assertStringNotContainsString('[1.2.0]:', $renderer->render($document, ' '));
169+
}
170+
171+
/**
172+
* @return void
173+
*/
174+
#[Test]
175+
public function renderWillNormalizeSshRepositoryUrlsAndTrimTrailingGitSuffix(): void
176+
{
177+
$document = new ChangelogDocument([
178+
(new ChangelogRelease('1.2.0', '2026-04-19'))->withEntry(
179+
ChangelogEntryType::Added,
180+
'Ship changelog automation',
181+
),
182+
]);
183+
184+
$output = (new MarkdownRenderer())->render($document, 'ssh://git@github.com/php-fast-forward/dev-tools.git');
185+
186+
self::assertStringContainsString(
187+
'[unreleased]: https://github.com/php-fast-forward/dev-tools/compare/v1.2.0...HEAD',
188+
$output,
189+
);
190+
self::assertStringContainsString(
191+
'[1.2.0]: https://github.com/php-fast-forward/dev-tools/releases/tag/v1.2.0',
192+
$output,
193+
);
194+
}
195+
196+
/**
197+
* @return void
198+
*/
199+
#[Test]
200+
public function renderWillOmitReferencesWhenOnlyUnreleasedSectionExists(): void
201+
{
202+
$output = (new MarkdownRenderer())->render(
203+
new ChangelogDocument([
204+
(new ChangelogRelease(ChangelogDocument::UNRELEASED_VERSION))->withEntry(
205+
ChangelogEntryType::Added,
206+
'Pending change',
207+
),
208+
]),
209+
'https://github.com/php-fast-forward/dev-tools'
210+
);
211+
212+
self::assertStringNotContainsString('[unreleased]:', $output);
213+
self::assertStringNotContainsString('releases/tag', $output);
214+
}
151215
}

tests/Console/Command/TestsCommandTest.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use FastForward\DevTools\PhpUnit\Coverage\CoverageSummaryLoaderInterface;
2727
use FastForward\DevTools\Process\ProcessBuilder;
2828
use FastForward\DevTools\Process\ProcessQueueInterface;
29+
use RuntimeException;
2930
use PHPUnit\Framework\Attributes\CoversClass;
3031
use PHPUnit\Framework\Attributes\Test;
3132
use PHPUnit\Framework\Attributes\UsesClass;
@@ -328,6 +329,23 @@ public function executeWithCoverageBelowMinimumWillReturnFailure(): void
328329
self::assertSame(TestsCommand::FAILURE, $this->invokeExecute());
329330
}
330331

332+
/**
333+
* @return void
334+
*/
335+
#[Test]
336+
public function executeWithFilterWillForwardTheFilterToPhpUnit(): void
337+
{
338+
$this->willQueueProcessMatching(static fn(Process $process): bool => str_contains(
339+
$process->getCommandLine(),
340+
'--filter=TestsCommandTest',
341+
));
342+
343+
$this->input->getOption('filter')
344+
->willReturn('TestsCommandTest');
345+
346+
$this->invokeExecute();
347+
}
348+
331349
/**
332350
* @return void
333351
*/
@@ -345,6 +363,50 @@ public function executeWithInvalidMinCoverageWillReturnFailure(): void
345363
self::assertSame(TestsCommand::FAILURE, $this->invokeExecute());
346364
}
347365

366+
/**
367+
* @return void
368+
*/
369+
#[Test]
370+
public function executeWithOutOfRangeMinCoverageWillReturnFailure(): void
371+
{
372+
$this->output->writeln(Argument::type('string'))
373+
->will(static function (): void {});
374+
$this->output->writeln(Argument::containingString('between 0 and 100'))
375+
->shouldBeCalled();
376+
377+
$this->input->getOption('min-coverage')
378+
->willReturn('101');
379+
380+
self::assertSame(TestsCommand::FAILURE, $this->invokeExecute());
381+
}
382+
383+
/**
384+
* @return void
385+
*/
386+
#[Test]
387+
public function executeWillReturnFailureWhenCoverageSummaryCannotBeLoaded(): void
388+
{
389+
$coverageReportPath = getcwd() . '/tmp/cache/phpunit/coverage.php';
390+
391+
$this->willQueueProcessMatching(static fn(Process $process): bool => str_contains(
392+
$process->getCommandLine(),
393+
'--coverage-php=' . $coverageReportPath,
394+
));
395+
396+
$this->output->writeln(Argument::type('string'))
397+
->will(static function (): void {});
398+
$this->output->writeln('<error>coverage report missing</error>')
399+
->shouldBeCalledOnce();
400+
401+
$this->coverageSummaryLoader->load($coverageReportPath)
402+
->willThrow(new RuntimeException('coverage report missing'));
403+
404+
$this->input->getOption('min-coverage')
405+
->willReturn('80');
406+
407+
self::assertSame(TestsCommand::FAILURE, $this->invokeExecute());
408+
}
409+
348410
/**
349411
* @return void
350412
*/

tests/Console/CommandLoader/DevToolsCommandLoaderTest.php

Lines changed: 99 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
use PHPUnit\Framework\TestCase;
3030
use Prophecy\Argument;
3131
use Prophecy\PhpUnit\ProphecyTrait;
32+
use Prophecy\Prophecy\ObjectProphecy;
3233
use Psr\Container\ContainerInterface;
3334
use Symfony\Component\Console\Command\Command;
3435
use Symfony\Component\Finder\Finder;
@@ -40,6 +41,31 @@ final class DevToolsCommandLoaderTest extends TestCase
4041
{
4142
use ProphecyTrait;
4243

44+
/**
45+
* @var ObjectProphecy<FinderFactoryInterface>
46+
*/
47+
private ObjectProphecy $finderFactory;
48+
49+
/**
50+
* @var ObjectProphecy<Finder>
51+
*/
52+
private ObjectProphecy $finder;
53+
54+
/**
55+
* @var ObjectProphecy<ContainerInterface>
56+
*/
57+
private ObjectProphecy $container;
58+
59+
/**
60+
* @return void
61+
*/
62+
protected function setUp(): void
63+
{
64+
$this->finderFactory = $this->prophesize(FinderFactoryInterface::class);
65+
$this->finder = $this->prophesize(Finder::class);
66+
$this->container = $this->prophesize(ContainerInterface::class);
67+
}
68+
4369
/**
4470
* @return void
4571
*/
@@ -49,30 +75,89 @@ public function constructorWillRegisterOnlyInstantiableCommands(): void
4975
$commandDirectory = \dirname(__DIR__, 3) . '/src/Console/Command';
5076
$command = $this->prophesize(Command::class);
5177

52-
$finderFactory = $this->prophesize(FinderFactoryInterface::class);
53-
$finder = $this->prophesize(Finder::class);
54-
$finderFactory->create()
55-
->willReturn($finder->reveal())
78+
$this->finderFactory->create()
79+
->willReturn($this->finder->reveal())
5680
->shouldBeCalledOnce();
57-
$finder->files()
58-
->willReturn($finder->reveal())
81+
$this->finder->files()
82+
->willReturn($this->finder->reveal())
5983
->shouldBeCalled();
60-
$finder->in(Argument::type('string'))->willReturn($finder->reveal())->shouldBeCalled();
61-
$finder->name('*.php')
62-
->willReturn($finder->reveal())
84+
$this->finder->in(Argument::type('string'))->willReturn($this->finder->reveal())->shouldBeCalled();
85+
$this->finder->name('*.php')
86+
->willReturn($this->finder->reveal())
6387
->shouldBeCalled();
64-
$finder->getIterator()
88+
$this->finder->getIterator()
6589
->willReturn(new ArrayIterator([
6690
new SplFileInfo($commandDirectory . '/CodeStyleCommand.php', '', 'CodeStyleCommand.php'),
6791
]))->shouldBeCalled();
6892

69-
$container = $this->prophesize(ContainerInterface::class);
70-
$container->has(CodeStyleCommand::class)->willReturn(true)->shouldBeCalled();
71-
$container->get(CodeStyleCommand::class)->willReturn($command->reveal())->shouldBeCalled();
93+
$this->container->has(CodeStyleCommand::class)->willReturn(true)->shouldBeCalled();
94+
$this->container->get(CodeStyleCommand::class)->willReturn($command->reveal())->shouldBeCalled();
7295

73-
$loader = new DevToolsCommandLoader($finderFactory->reveal(), $container->reveal());
96+
$loader = new DevToolsCommandLoader($this->finderFactory->reveal(), $this->container->reveal());
7497

7598
self::assertTrue($loader->has('code-style'));
7699
self::assertSame($command->reveal(), $loader->get('code-style'));
77100
}
101+
102+
/**
103+
* @return void
104+
*/
105+
#[Test]
106+
public function constructorWillSkipClassesWithoutAsCommandAttribute(): void
107+
{
108+
$commandDirectory = \dirname(__DIR__, 3) . '/src/Console/Command';
109+
110+
$this->finderFactory->create()
111+
->willReturn($this->finder->reveal())
112+
->shouldBeCalledOnce();
113+
$this->finder->files()
114+
->willReturn($this->finder->reveal())
115+
->shouldBeCalled();
116+
$this->finder->in(Argument::type('string'))->willReturn($this->finder->reveal())->shouldBeCalled();
117+
$this->finder->name('*.php')
118+
->willReturn($this->finder->reveal())
119+
->shouldBeCalled();
120+
$this->finder->getIterator()
121+
->willReturn(new ArrayIterator([
122+
new SplFileInfo($commandDirectory . '/FixtureWithoutAsCommand.php', '', 'FixtureWithoutAsCommand.php'),
123+
]))->shouldBeCalled();
124+
125+
$loader = new DevToolsCommandLoader($this->finderFactory->reveal(), $this->container->reveal());
126+
127+
self::assertFalse($loader->has('abstract'));
128+
}
129+
130+
/**
131+
* @return void
132+
*/
133+
#[Test]
134+
public function constructorWillSkipNonInstantiableAndNonCommandClasses(): void
135+
{
136+
$commandDirectory = \dirname(__DIR__, 3) . '/src/Console/Command';
137+
138+
$this->finderFactory->create()
139+
->willReturn($this->finder->reveal())
140+
->shouldBeCalledOnce();
141+
$this->finder->files()
142+
->willReturn($this->finder->reveal())
143+
->shouldBeCalled();
144+
$this->finder->in(Argument::type('string'))->willReturn($this->finder->reveal())->shouldBeCalled();
145+
$this->finder->name('*.php')
146+
->willReturn($this->finder->reveal())
147+
->shouldBeCalled();
148+
$this->finder->getIterator()
149+
->willReturn(new ArrayIterator([
150+
new SplFileInfo($commandDirectory . '/FixtureAbstractCommand.php', '', 'FixtureAbstractCommand.php'),
151+
new SplFileInfo(
152+
$commandDirectory . '/FixtureWithoutCommandParent.php',
153+
'',
154+
'FixtureWithoutCommandParent.php'
155+
),
156+
]))->shouldBeCalled();
157+
158+
$loader = new DevToolsCommandLoader($this->finderFactory->reveal(), $this->container->reveal());
159+
160+
self::assertFalse($loader->has('fixture-abstract'));
161+
self::assertFalse($loader->has('fixture-without-command-parent'));
162+
}
78163
}

0 commit comments

Comments
 (0)