Skip to content

Commit 3f33637

Browse files
pfazzifain182
andauthored
Add baseline feature (#314)
* WIP add baseline feature * Refactor test * Baseline is now json * Add docs in the readme file * Improve UX * Refactor * rename --set-baseline in --generate-baseline * generate baseline with default name * fix style * baseline with default filename is used automatically * show which baseline is used * adds --skip-baseline option * README updated Co-authored-by: Pietro Campagnano <[email protected]>
1 parent 4311a21 commit 3f33637

File tree

5 files changed

+221
-11
lines changed

5 files changed

+221
-11
lines changed

README.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,37 @@ phparkitect check --config=/project/yourConfigFile.php
5858

5959
By default, a progress bar will show the status of the ongoing analysis.
6060

61+
### Using a baseline file
62+
63+
If there are a lot of violations in your codebase and you can't fix them now,
64+
you can use the baseline feature to instruct the tool to ignore past violations.
65+
66+
To create a baseline file, run the `check` command with the `generate-baseline` parameter as follows:
67+
68+
```
69+
phparkitect check --generate-baseline
70+
```
71+
This will create a `phparkitect-baseline.json`, if you want a different file name you can do it with:
72+
```
73+
phparkitect check --generate-baseline=my-baseline.json
74+
```
75+
76+
It will produce a json file with the current list of violations.
77+
78+
If is present a baseline file with the default name will be used automatically.
79+
80+
To use a different baseline file, run the `check` command with the `use-baseline` parameter as follows:
81+
82+
```
83+
phparkitect check --use-baseline=my-baseline.json
84+
```
85+
86+
To avoid using the default baseline file, you can use the `skip-baseline` option:
87+
88+
```
89+
phparkitect check --skip-baseline
90+
```
91+
6192
## Configuration
6293

6394
Example of configuration file `phparkitect.php`
@@ -93,7 +124,6 @@ return static function (Config $config): void {
93124
};
94125
```
95126

96-
97127
# Available rules
98128

99129
**Hint**: If you want to test how a Rule work, you can use the command like `phparkitect debug:expression <RuleName> <arguments>` to check which class satisfy the rule in your current folder.

src/CLI/Command/Check.php

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,17 @@
2121
class Check extends Command
2222
{
2323
private const CONFIG_FILENAME_PARAM = 'config';
24-
2524
private const TARGET_PHP_PARAM = 'target-php-version';
26-
2725
private const STOP_ON_FAILURE_PARAM = 'stop-on-failure';
26+
private const USE_BASELINE_PARAM = 'use-baseline';
27+
private const SKIP_BASELINE_PARAM = 'skip-baseline';
2828

29-
private const DEFAULT_FILENAME = 'phparkitect.php';
29+
private const GENERATE_BASELINE_PARAM = 'generate-baseline';
30+
private const DEFAULT_RULES_FILENAME = 'phparkitect.php';
3031

31-
private const SUCCESS_CODE = 0;
32+
private const DEFAULT_BASELINE_FILENAME = 'phparkitect-baseline.json';
3233

34+
private const SUCCESS_CODE = 0;
3335
private const ERROR_CODE = 1;
3436

3537
public function __construct()
@@ -59,6 +61,25 @@ protected function configure(): void
5961
's',
6062
InputOption::VALUE_NONE,
6163
'Stop on failure'
64+
)
65+
->addOption(
66+
self::GENERATE_BASELINE_PARAM,
67+
'g',
68+
InputOption::VALUE_OPTIONAL,
69+
'Generate a file containing the current errors',
70+
false
71+
)
72+
->addOption(
73+
self::USE_BASELINE_PARAM,
74+
'b',
75+
InputOption::VALUE_REQUIRED,
76+
'Ignore errors in baseline file'
77+
)
78+
->addOption(
79+
self::SKIP_BASELINE_PARAM,
80+
'k',
81+
InputOption::VALUE_NONE,
82+
'Don\'t use the default baseline'
6283
);
6384
}
6485

@@ -71,6 +92,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int
7192
try {
7293
$verbose = $input->getOption('verbose');
7394
$stopOnFailure = $input->getOption(self::STOP_ON_FAILURE_PARAM);
95+
$useBaseline = $input->getOption(self::USE_BASELINE_PARAM);
96+
$skipBaseline = $input->getOption(self::SKIP_BASELINE_PARAM);
97+
98+
if (true !== $skipBaseline && !$useBaseline && file_exists(self::DEFAULT_BASELINE_FILENAME)) {
99+
$useBaseline = self::DEFAULT_BASELINE_FILENAME;
100+
}
101+
102+
if ($useBaseline && !file_exists($useBaseline)) {
103+
$output->writeln('<error>Baseline file not found.</error>');
104+
105+
return self::ERROR_CODE;
106+
}
107+
$output->writeln('<info>Baseline found: '.$useBaseline.'</info>');
108+
109+
$generateBaseline = $input->getOption(self::GENERATE_BASELINE_PARAM);
74110

75111
/** @var string|null $phpVersion */
76112
$phpVersion = $input->getOption('target-php-version');
@@ -93,6 +129,25 @@ protected function execute(InputInterface $input, OutputInterface $output): int
93129
} catch (FailOnFirstViolationException $e) {
94130
}
95131
$violations = $runner->getViolations();
132+
133+
if (false !== $generateBaseline) {
134+
if (null === $generateBaseline) {
135+
$generateBaseline = self::DEFAULT_BASELINE_FILENAME;
136+
}
137+
$this->saveBaseline($generateBaseline, $violations);
138+
139+
$output->writeln('<info>Baseline file \''.$generateBaseline.'\'created!</info>');
140+
$this->printExecutionTime($output, $startTime);
141+
142+
return self::SUCCESS_CODE;
143+
}
144+
145+
if ($useBaseline) {
146+
$baseline = $this->loadBaseline($useBaseline);
147+
148+
$violations->remove($baseline);
149+
}
150+
96151
if ($violations->count() > 0) {
97152
$this->printViolations($violations, $output);
98153
$this->printExecutionTime($output, $startTime);
@@ -149,12 +204,22 @@ protected function printExecutionTime(OutputInterface $output, float $startTime)
149204
$output->writeln('<info>Execution time: '.$executionTime."s</info>\n");
150205
}
151206

207+
private function loadBaseline(string $filename): Violations
208+
{
209+
return Violations::fromJson(file_get_contents($filename));
210+
}
211+
212+
private function saveBaseline(string $filename, Violations $violations): void
213+
{
214+
file_put_contents($filename, json_encode($violations, \JSON_PRETTY_PRINT));
215+
}
216+
152217
private function getConfigFilename(InputInterface $input): string
153218
{
154219
$filename = $input->getOption(self::CONFIG_FILENAME_PARAM);
155220

156221
if (null === $filename) {
157-
$filename = self::DEFAULT_FILENAME;
222+
$filename = self::DEFAULT_RULES_FILENAME;
158223
}
159224

160225
Assert::file($filename, 'Config file not found');

src/Rules/Violation.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
namespace Arkitect\Rules;
66

7-
class Violation
7+
class Violation implements \JsonSerializable
88
{
99
/** @var string */
1010
private $fqcn;
@@ -46,4 +46,14 @@ public function getLine(): ?int
4646
{
4747
return $this->line;
4848
}
49+
50+
public function jsonSerialize(): array
51+
{
52+
return get_object_vars($this);
53+
}
54+
55+
public static function fromJson(array $json): self
56+
{
57+
return new self($json['fqcn'], $json['error'], $json['line']);
58+
}
4959
}

src/Rules/Violations.php

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
use Arkitect\Exceptions\FailOnFirstViolationException;
88
use Arkitect\Exceptions\IndexNotFoundException;
99

10-
class Violations implements \IteratorAggregate, \Countable
10+
class Violations implements \IteratorAggregate, \Countable, \JsonSerializable
1111
{
1212
/**
1313
* @var Violation[]
@@ -24,6 +24,19 @@ public function __construct(bool $stopOnFailure = false)
2424
$this->stopOnFailure = $stopOnFailure;
2525
}
2626

27+
public static function fromJson(string $json): self
28+
{
29+
$json = json_decode($json, true);
30+
31+
$instance = new self($json['stopOnFailure']);
32+
33+
$instance->violations = array_map(function (array $json): Violation {
34+
return Violation::fromJson($json);
35+
}, $json['violations']);
36+
37+
return $instance;
38+
}
39+
2740
public function add(Violation $violation): void
2841
{
2942
$this->violations[] = $violation;
@@ -92,4 +105,20 @@ public function toArray(): array
92105
{
93106
return $this->violations;
94107
}
108+
109+
public function remove(self $violations): void
110+
{
111+
$this->violations = array_udiff(
112+
$this->violations,
113+
$violations->violations,
114+
function (Violation $a, Violation $b): int {
115+
return $a == $b ? 0 : 1;
116+
}
117+
);
118+
}
119+
120+
public function jsonSerialize(): array
121+
{
122+
return get_object_vars($this);
123+
}
95124
}

tests/E2E/Cli/CheckCommandTest.php

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,20 @@ class CheckCommandTest extends TestCase
1414

1515
const ERROR_CODE = 1;
1616

17+
/** @var string */
18+
private $customBaselineFilename = __DIR__.'/my-baseline.json';
19+
private $defaultBaselineFilename = 'phparkitect-baseline.json';
20+
21+
protected function tearDown(): void
22+
{
23+
if (file_exists($this->customBaselineFilename)) {
24+
unlink($this->customBaselineFilename);
25+
}
26+
if (file_exists($this->defaultBaselineFilename)) {
27+
unlink($this->defaultBaselineFilename);
28+
}
29+
}
30+
1731
public function test_app_returns_error_with_multiple_violations(): void
1832
{
1933
$cmdTester = $this->runCheck(__DIR__.'/../_fixtures/configMvc.php');
@@ -86,15 +100,77 @@ public function test_bug_yield(): void
86100
$this->assertCheckHasErrors($cmdTester, $expectedErrors);
87101
}
88102

89-
protected function runCheck($configFilePath = null, bool $stopOnFailure = null): ApplicationTester
103+
public function test_baseline(): void
104+
{
105+
$configFilePath = __DIR__.'/../_fixtures/configMvcForYieldBug.php';
106+
107+
// Produce the baseline
108+
109+
$this->runCheck($configFilePath, null, null, $this->customBaselineFilename);
110+
111+
// Check it detects error if baseline is not used
112+
113+
$cmdTester = $this->runCheck($configFilePath, null, null);
114+
115+
$this->assertCheckHasErrors($cmdTester);
116+
117+
// Check it ignores error if baseline is used
118+
119+
$cmdTester = $this->runCheck($configFilePath, null, $this->customBaselineFilename);
120+
$this->assertCheckHasSuccess($cmdTester);
121+
}
122+
123+
public function test_baseline_with_default_filename_is_enabled_automatically(): void
90124
{
125+
$configFilePath = __DIR__.'/../_fixtures/configMvcForYieldBug.php';
126+
127+
// Produce the baseline
128+
129+
$this->runCheck($configFilePath, null, null, null);
130+
131+
// Check it ignores error if baseline is used
132+
133+
$cmdTester = $this->runCheck($configFilePath, null, null);
134+
$this->assertCheckHasSuccess($cmdTester);
135+
}
136+
137+
public function test_you_can_ignore_the_default_baseline(): void
138+
{
139+
$configFilePath = __DIR__.'/../_fixtures/configMvcForYieldBug.php';
140+
141+
// Produce the baseline
142+
$this->runCheck($configFilePath, null, null, null);
143+
144+
// Check it ignores the default baseline
145+
$cmdTester = $this->runCheck($configFilePath, null, null, false, true);
146+
$this->assertCheckHasErrors($cmdTester);
147+
}
148+
149+
protected function runCheck(
150+
$configFilePath = null,
151+
bool $stopOnFailure = null,
152+
?string $useBaseline = null,
153+
$generateBaseline = false,
154+
bool $skipBaseline = false
155+
): ApplicationTester {
91156
$input = ['check'];
92157
if (null !== $configFilePath) {
93158
$input['--config'] = $configFilePath;
94159
}
95160
if (null !== $stopOnFailure) {
96161
$input['--stop-on-failure'] = true;
97162
}
163+
if (null !== $useBaseline) {
164+
$input['--use-baseline'] = $useBaseline;
165+
}
166+
if ($skipBaseline) {
167+
$input['--skip-baseline'] = true;
168+
}
169+
170+
// false = option not set, null = option set but without value, string = option with value
171+
if (false !== $generateBaseline) {
172+
$input['--generate-baseline'] = $generateBaseline;
173+
}
98174

99175
$app = new PhpArkitectApplication();
100176
$app->setAutoExit(false);
@@ -127,7 +203,7 @@ protected function assertCheckHasNoErrorsLike(ApplicationTester $applicationTest
127203

128204
protected function assertCheckHasSuccess(ApplicationTester $applicationTester): void
129205
{
130-
$this->assertEquals(self::SUCCESS_CODE, $applicationTester->getStatusCode());
131-
$this->assertStringNotContainsString('ERRORS!', $applicationTester->getDisplay());
206+
$this->assertEquals(self::SUCCESS_CODE, $applicationTester->getStatusCode(), 'Command failed: '.$applicationTester->getDisplay());
207+
$this->assertStringNotContainsString('ERRORS!', $applicationTester->getDisplay(), 'Error message not expected in successful execution');
132208
}
133209
}

0 commit comments

Comments
 (0)