Skip to content

Commit 4316915

Browse files
CI: Setup CI tools and CI (#151)
* 🔨 Add PHPStan and make it pass * 🔧 Add and configure php_codesniffer, fix lints * 🔧 Set php 8.3 in composer.json * 👷 Add CI to run code style and static analysis
1 parent ce6233f commit 4316915

8 files changed

Lines changed: 170 additions & 50 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: continuous integration
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- main
7+
push:
8+
branches:
9+
- main
10+
11+
jobs:
12+
continuous-integration:
13+
runs-on: ubuntu-24.04
14+
steps:
15+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
16+
17+
- name: Setup PHP
18+
uses: shivammathur/setup-php@9e72090525849c5e82e596468b86eb55e9cc5401
19+
with:
20+
php-version: '8.3'
21+
tools: composer
22+
23+
- name: Get Composer Cache Directory
24+
id: composer-cache
25+
run: |
26+
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
27+
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684
28+
with:
29+
path: ${{ steps.composer-cache.outputs.dir }}
30+
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
31+
restore-keys: |
32+
${{ runner.os }}-composer-
33+
34+
- run: composer install
35+
- run: composer lint
36+
- run: composer phpstan

.github/workflows/smoketest.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ on: [push, pull_request, workflow_dispatch]
55
jobs:
66
test:
77
name: Tests
8-
runs-on: ubuntu-22.04
8+
runs-on: ubuntu-24.04
99
steps:
1010
- name: Checkout code
1111
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683

composer.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,22 @@
88
}
99
},
1010
"require": {
11+
"php": "^8.3",
1112
"phpunit/phpunit": "^10.5.29"
13+
},
14+
"require-dev": {
15+
"phpstan/phpstan": "^2.1",
16+
"slevomat/coding-standard": "^8.14.1",
17+
"squizlabs/php_codesniffer": "^3.12"
18+
},
19+
"scripts": {
20+
"phpstan": "phpstan analyse --configuration phpstan.neon --memory-limit=2G",
21+
"lint": "phpcs",
22+
"lint:fix": "phpcbf"
23+
},
24+
"config": {
25+
"allow-plugins": {
26+
"dealerdirect/phpcodesniffer-composer-installer": true
27+
}
1228
}
1329
}

phpcs.xml.dist

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?xml version="1.0"?>
2+
<ruleset
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:noNamespaceSchemaLocation="vendor/squizlabs/php_codesniffer/phpcs.xsd"
5+
name="Exercism PHP Coding Standard"
6+
>
7+
<description>Coding standard for Exercism PHP test runner</description>
8+
9+
<!-- Expect all files are UTF-8 -->
10+
<arg name="encoding" value="utf-8" />
11+
12+
<!-- Show sniffs (it's easy to find solutions knowing the code) -->
13+
<arg value="s" />
14+
15+
<!-- A TAB is 4 chars wide (does not replace them, for calculation only!) -->
16+
<arg name="tab-width" value="4" />
17+
18+
<!-- Run on 60 files in parallel -->
19+
<arg name="parallel" value="60" />
20+
21+
<file>src</file>
22+
23+
<!-- Include all sniffs in the PSR12 -->
24+
<rule ref="PSR12" />
25+
<rule ref="SlevomatCodingStandard.TypeHints.DeclareStrictTypes">
26+
<properties>
27+
<property name="linesCountBeforeDeclare" value="1" />
28+
<property name="linesCountAfterDeclare" value="1" />
29+
<property name="spacesCountAroundEqualsSign" value="0" />
30+
</properties>
31+
</rule>
32+
</ruleset>

phpstan.neon

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
includes:
2+
- phar://phpstan.phar/conf/bleedingEdge.neon
3+
4+
parameters:
5+
level: max
6+
paths:
7+
- src/
8+
treatPhpDocTypesAsCertain: false

src/Extension.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,28 @@
1010
use PHPUnit\Runner\Extension\ParameterCollection;
1111
use PHPUnit\TextUI\Configuration\Configuration;
1212

13+
use function assert;
14+
use function getenv;
15+
use function is_string;
16+
1317
final class Extension implements ExtensionInterface
1418
{
1519
public function bootstrap(
1620
Configuration $configuration,
1721
Facade $facade,
1822
ParameterCollection $parameters,
1923
): void {
20-
$outFileName = \getenv('EXERCISM_RESULT_FILE');
24+
$outFileName = getenv('EXERCISM_RESULT_FILE');
2125
if (empty($outFileName)) {
2226
$outFileName = $parameters->get('outFileName');
2327
}
28+
assert(is_string($outFileName));
2429

25-
$exerciseDir = \getenv('EXERCISM_EXERCISE_DIR');
30+
$exerciseDir = getenv('EXERCISM_EXERCISE_DIR');
2631
if (empty($exerciseDir)) {
27-
$exerciseDir = \getenv('PWD');
32+
$exerciseDir = getenv('PWD');
2833
}
34+
assert(is_string($exerciseDir));
2935

3036
$facade->registerTracer(new Tracer($outFileName, $exerciseDir));
3137
}

src/Result.php

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@
66

77
use JsonSerializable;
88

9+
use function str_replace;
10+
911
/**
1012
* Represents a test result for Exercism
13+
*
1114
* @see https://exercism.org/docs/building/tooling/test-runners/interface#h-per-test
1215
*/
1316
final class Result implements JsonSerializable
@@ -51,23 +54,19 @@ public function jsonSerialize(): mixed
5154

5255
if ($this->userOutput !== '') {
5356
// In 2024 some innocent ASCII code still fools displays and editors.
54-
$result['output'] = \str_replace(
55-
[
56-
"\u{7F}", // Delete
57-
],
57+
$result['output'] = str_replace(
58+
"\u{7F}", // Delete
5859
"\u{FFFD}", // Unicode substitute for invalid characters
59-
$this->userOutput
60+
$this->userOutput,
6061
);
6162
}
6263

6364
if ($this->phpUnitMessage !== '') {
6465
// In 2024 some innocent ASCII code still fools displays and editors.
65-
$result['message'] = \str_replace(
66-
[
67-
"\u{7F}", // Delete
68-
],
66+
$result['message'] = str_replace(
67+
"\u{7F}", // Delete
6968
"\u{FFFD}", // Unicode substitute for invalid characters
70-
$this->phpUnitMessage
69+
$this->phpUnitMessage,
7170
);
7271
}
7372

src/Tracer.php

Lines changed: 59 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,39 @@
1616
use PHPUnit\Event\Tracer\Tracer as TracerInterface;
1717
use ReflectionClass;
1818

19+
use function array_key_last;
20+
use function array_map;
21+
use function array_slice;
22+
use function assert;
23+
use function file;
24+
use function file_put_contents;
25+
use function implode;
26+
use function is_array;
27+
use function is_string;
28+
use function json_encode;
29+
use function preg_match;
30+
use function str_replace;
31+
use function str_starts_with;
32+
use function substr;
33+
use function trim;
34+
35+
use const JSON_INVALID_UTF8_SUBSTITUTE;
36+
use const JSON_PRETTY_PRINT;
37+
1938
final class Tracer implements TracerInterface
2039
{
2140
/** Enable to add all events to result JSON */
22-
private const DEBUG_ALL_EVENTS = false;
41+
private const bool DEBUG_ALL_EVENTS = false;
2342

2443
/** Enable to pretty print result JSON */
25-
private const DEBUG_PRETTY_JSON = false;
44+
private const bool DEBUG_PRETTY_JSON = false;
2645

2746
/**
2847
* Represents the result of the test run for Exercism
48+
*
2949
* @see https://exercism.org/docs/building/tooling/test-runners/interface#h-top-level
30-
* @var array{version: int, status: string, tests: list<Result>, messsage: string}
50+
*
51+
* @var array{version: int, status: string, tests: list<Result>, messsage?: string}
3152
*/
3253
private array $result = [
3354
'version' => 3,
@@ -44,19 +65,18 @@ public function __construct(
4465

4566
public function trace(Event $event): void
4667
{
47-
match (\get_class($event)) {
68+
match ($event::class) {
4869
Passed::class => $this->addTestPassed($event),
4970
Failed::class => $this->addTestFailed($event),
5071
Errored::class => $this->addTestErrored($event),
5172
BeforeFirstTestMethodErrored::class => $this->addBeforeFirstTestMethodErrored($event),
5273
PrintedUnexpectedOutput::class => $this->addTestOutput($event),
53-
default => self:: DEBUG_ALL_EVENTS
74+
default => self::DEBUG_ALL_EVENTS // @phpstan-ignore ternary.alwaysFalse
5475
? $this->addUnhandledEvent($event)
5576
: true
5677
,
5778
};
5879

59-
6080
if ($event instanceof Finished) {
6181
$this->saveResults();
6282
}
@@ -73,8 +93,9 @@ private function addUnhandledEvent(Event $event): void
7393

7494
private function addTestPassed(Passed $event): void
7595
{
76-
/** @var TestMethod */
96+
/** @var TestMethod $testMethod */
7797
$testMethod = $event->test();
98+
assert($testMethod instanceof TestMethod);
7899

79100
$this->result['tests'][] = new Result(
80101
$testMethod->testDox()->prettifiedMethodName(),
@@ -86,14 +107,15 @@ private function addTestPassed(Passed $event): void
86107

87108
private function addTestFailed(Failed $event): void
88109
{
89-
/** @var TestMethod */
110+
/** @var TestMethod $testMethod */
90111
$testMethod = $event->test();
112+
assert($testMethod instanceof TestMethod);
91113

92-
$phpUnitMessage = \trim($event->throwable()->asString());
93-
$phpUnitMessage = \str_replace(
114+
$phpUnitMessage = trim($event->throwable()->asString());
115+
$phpUnitMessage = str_replace(
94116
$this->exerciseDir . '/',
95117
'',
96-
$phpUnitMessage
118+
$phpUnitMessage,
97119
);
98120
$phpUnitMessage = $testMethod->nameWithClass() . "\n" . $phpUnitMessage;
99121

@@ -109,14 +131,15 @@ private function addTestFailed(Failed $event): void
109131

110132
private function addTestErrored(Errored $event): void
111133
{
112-
/** @var TestMethod */
134+
/** @var TestMethod $testMethod */
113135
$testMethod = $event->test();
136+
assert($testMethod instanceof TestMethod);
114137

115-
$phpUnitMessage = \trim($event->throwable()->asString());
116-
$phpUnitMessage = \str_replace(
138+
$phpUnitMessage = trim($event->throwable()->asString());
139+
$phpUnitMessage = str_replace(
117140
$this->exerciseDir . '/',
118141
'',
119-
$phpUnitMessage
142+
$phpUnitMessage,
120143
);
121144
$phpUnitMessage = $testMethod->nameWithClass() . "\n" . $phpUnitMessage;
122145

@@ -132,11 +155,11 @@ private function addTestErrored(Errored $event): void
132155

133156
private function addBeforeFirstTestMethodErrored(BeforeFirstTestMethodErrored $event): void
134157
{
135-
$phpUnitMessage = \trim($event->throwable()->asString());
136-
$phpUnitMessage = \str_replace(
158+
$phpUnitMessage = trim($event->throwable()->asString());
159+
$phpUnitMessage = str_replace(
137160
$this->exerciseDir . '/',
138161
'',
139-
$phpUnitMessage
162+
$phpUnitMessage,
140163
);
141164

142165
$this->result['status'] = 'error';
@@ -147,8 +170,8 @@ private function addTestOutput(PrintedUnexpectedOutput $event): void
147170
{
148171
// This must rely on the sequence of events!
149172

150-
/** @var Result */
151-
$lastTest = $this->result['tests'][\array_key_last($this->result['tests'])];
173+
/** @var Result $lastTest */
174+
$lastTest = $this->result['tests'][array_key_last($this->result['tests'])];
152175
$lastTest->setUserOutput($event->output());
153176
}
154177

@@ -161,12 +184,12 @@ private function saveResults(): void
161184
}
162185
}
163186

164-
\file_put_contents(
187+
file_put_contents(
165188
$this->outFileName,
166-
\json_encode(
189+
json_encode(
167190
$this->result,
168191
JSON_INVALID_UTF8_SUBSTITUTE
169-
| (self::DEBUG_PRETTY_JSON ? JSON_PRETTY_PRINT : 0)
192+
| (self::DEBUG_PRETTY_JSON ? JSON_PRETTY_PRINT : 0), // @phpstan-ignore ternary.alwaysFalse
170193
) . "\n",
171194
);
172195
}
@@ -186,22 +209,22 @@ private function methodCode(TestMethod $testMethod): string
186209
$start = $reflectionMethod->getStartLine() - 1 + 2;
187210
$length = $reflectionMethod->getEndLine() - 1 - $start;
188211

189-
$codeLines = \array_slice(
190-
\file($reflectionMethod->getFileName()),
191-
$start,
192-
$length,
193-
);
212+
$testFileName = $reflectionMethod->getFileName();
213+
assert(is_string($testFileName));
214+
$testCodeLines = file($testFileName);
215+
assert(is_array($testCodeLines));
216+
217+
$codeLines = array_slice($testCodeLines, $start, $length);
194218

195219
// Unindent lines 2 levels of 4 spaces each (if possible)
196-
$codeLines = \array_map(
197-
fn ($line) => \str_starts_with($line, ' ')
198-
? \substr($line, 2 * 4)
199-
: $line
200-
,
220+
$codeLines = array_map(
221+
static fn ($line) => str_starts_with($line, ' ')
222+
? substr($line, 2 * 4)
223+
: $line,
201224
$codeLines,
202225
);
203226

204-
return \implode('', $codeLines);
227+
return implode('', $codeLines);
205228
}
206229

207230
private function taskId(TestMethod $testMethod): int
@@ -213,8 +236,8 @@ private function taskId(TestMethod $testMethod): int
213236
return 0;
214237
}
215238

216-
$matches=[];
217-
$matchCount = \preg_match('/@task_id\s+(\d+)/', $docComment, $matches);
239+
$matches = [];
240+
$matchCount = preg_match('/@task_id\s+(\d+)/', $docComment, $matches);
218241

219242
return $matchCount >= 1 ? (int)$matches[1] : 0;
220243
}

0 commit comments

Comments
 (0)