Skip to content

Commit 1b28597

Browse files
authored
Merge pull request #3 from krakphp/exit-on-changes
Fail on Changes
2 parents 37c1c0b + 7c0f354 commit 1b28597

4 files changed

Lines changed: 97 additions & 11 deletions

File tree

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,25 @@ The generate option allows you to control which generators get used for the spec
240240

241241
The list of generator names you can use are: `constructor,from-validated-array,to-array,getters,withers`
242242

243+
### Continuous Integration Setup
244+
245+
If you are committing your struct gen changes directly in the repo, then you'll want to make sure that struct-gen was properly run and tested before any commit is merged into the mainline branch.
246+
247+
In that case, you'll want to use the `--fail-on-changes` flag while running struct gen during your CI testing pipeline. This will fail with exit code 1 if any changes are detected which in most CI pipelines will fail the build ensuring that the developer submitting the patch has tested with the latest changes and didn't modify any of the generated files.
248+
249+
Example:
250+
```yaml
251+
- composer struct-gen:generate --fail-on-changes
252+
```
253+
254+
If you are generating the structs into an external file via the `generatedPath` option and are ignoring that file in your CVS, then you'll want to make sure to run struct-gen **before you run composer install**.
255+
256+
```yaml
257+
- composer struct-gen:generate -vv
258+
# some point later
259+
- composer install --no-dev -o
260+
```
261+
243262
## Why use static generation?
244263

245264
Most libraries for DTOs or structs are not IDE friendly and give access to helpful methods for a struct via runtime magic and reflection. Not only is there a slight performance hit with these methods, it's difficult to get typesafe while also working well with ides and static analysis tools.

src/Command/GenerateStructsCommand.php

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
use Symfony\Component\Console\Output\OutputInterface;
1212
use Symfony\Component\Finder\Finder;
1313
use function Krak\StructGen\{
14+
detectChangesInGeneratedStructsForFiles,
1415
generateStructsExternallyForFiles,
1516
generateStructsForFiles,
16-
traversePhpFiles
17-
};
17+
traversePhpFiles};
1818

1919
final class GenerateStructsCommand extends BaseCommand
2020
{
@@ -30,23 +30,27 @@ protected function configure() {
3030
->setDescription('Generate struct info for any matching classes')
3131
->addArgument('paths', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Path globs to search and list directories', $this->config->paths())
3232
->addOption('generated-path', 'g', InputOption::VALUE_REQUIRED, 'If set, the file path to generate the structs into', $this->config->generatedPath())
33+
->addOption('fail-on-changes', 'f', InputOption::VALUE_NONE, 'Ensures no changes are detected on generated structs by exiting with a failure if changes are detected.')
3334
->addOption('basic-traverse-files', 'b', InputOption::VALUE_NONE, 'By default, this command will use symfony finder to traverse the list of paths which support glob patterns. If you do not want to include symfony finder, you can use the basic traverse files feature.');
3435
}
3536

3637
protected function execute(InputInterface $input, OutputInterface $output) {
3738
$paths = $input->getArgument('paths') ?: $this->config->paths();
3839
$generatedPath = $input->getOption('generated-path');
3940
$basicTraverseFiles = $input->getOption('basic-traverse-files');
41+
$failOnChanges = $input->getOption('fail-on-changes');
4042
$files = $basicTraverseFiles ? traversePhpFiles($paths) : (function() use ($paths) {
4143
$finder = new Finder();
4244
$finder->files()->name('*.php')->in($paths);
4345
return $finder;
4446
})();
4547

4648
if ($generatedPath) {
47-
generateStructsExternallyForFiles($files, $generatedPath, new ConsoleLogger($output));
49+
$res = generateStructsExternallyForFiles($files, $generatedPath, new ConsoleLogger($output));
4850
} else {
49-
generateStructsForFiles($files, new ConsoleLogger($output));
51+
$res = generateStructsForFiles($files, new ConsoleLogger($output));
5052
}
53+
54+
return $failOnChanges && $res->hasChanges() ? 1 : 0;
5155
}
5256
}

src/struct-gen.php

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,27 +23,43 @@ function traversePhpFiles(iterable $paths): iterable {
2323
}
2424
}
2525

26+
final class GeneratedStructsForFilesResult {
27+
private $hasChanges;
28+
29+
public function __construct(bool $hasChanges) {
30+
$this->hasChanges = $hasChanges;
31+
}
32+
33+
public function hasChanges(): bool {
34+
return $this->hasChanges;
35+
}
36+
}
37+
2638
/**
2739
* @param \SplFileInfo[] $files
2840
* @param ?callable(string): string $generateStruct
2941
*/
30-
function generateStructsForFiles(iterable $files, LoggerInterface $logger = null, ?callable $generateStruct = null): void {
42+
function generateStructsForFiles(iterable $files, LoggerInterface $logger = null, ?callable $generateStruct = null): GeneratedStructsForFilesResult {
3143
$logger = $logger ?: new NullLogger();
3244
$generateStruct = $generateStruct ?: new GenerateStruct();
45+
$hasChanges = false;
3346
foreach ($files as $file) {
3447
$logger->info('Generate Structs for File: ' . $file->getPathname());
3548
$original = file_get_contents($file->getPathname());
3649
$updated = $generateStruct(GenerateStructArgs::inline($original))->code();
37-
if ($original === $updated) {
50+
if (trim($original) === trim($updated)) {
3851
$logger->debug('No changes detected.');
3952
} else {
4053
$logger->info("New Struct Info: \n". $updated);
4154
file_put_contents($file->getPathname(), $updated);
55+
$hasChanges = true;
4256
}
4357
}
58+
59+
return new GeneratedStructsForFilesResult($hasChanges);
4460
}
4561

46-
function generateStructsExternallyForFiles(iterable $files, string $generatedFilePath, LoggerInterface $logger = null, ?callable $generateStruct = null): void {
62+
function generateStructsExternallyForFiles(iterable $files, string $generatedFilePath, LoggerInterface $logger = null, ?callable $generateStruct = null): GeneratedStructsForFilesResult {
4763
$logger = $logger ?: new NullLogger();
4864
$generateStruct = $generateStruct ?: new GenerateStruct();
4965
$ast = [];
@@ -53,6 +69,20 @@ function generateStructsExternallyForFiles(iterable $files, string $generatedFil
5369
$ast = array_merge($ast, $generateStruct(GenerateStructArgs::external($original))->ast());
5470
}
5571

72+
$newContents = (new \PhpParser\PrettyPrinter\Standard())->prettyPrintFile($ast);
73+
74+
// perform md5 hash to try and keep memory down so we don't need to load both strings in memory
75+
$currentHash = file_exists($generatedFilePath) ? md5_file($generatedFilePath) : '';
76+
$newHash = md5($newContents);
77+
78+
if ($currentHash !== $newHash) {
79+
$logger->debug("Changes detected: old hash = $currentHash, new hash = $newHash");
80+
} else {
81+
$logger->debug("No changes detected: hash $currentHash");
82+
}
83+
5684
$logger->info('Writing generated structs into: ' . $generatedFilePath);
57-
file_put_contents($generatedFilePath, (new \PhpParser\PrettyPrinter\Standard())->prettyPrintFile($ast));
85+
file_put_contents($generatedFilePath, $newContents);
86+
87+
return new GeneratedStructsForFilesResult($currentHash !== $newHash);
5888
}

test/Feature/StructGenTest.php

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,9 @@ public function provide_for_can_generate_struct_files_externally() {
7474

7575
/** @test */
7676
public function can_generate_structs_for_files() {
77-
$tplPath = __DIR__ . '/Fixtures/generated-from-paths-template.php';
77+
$this->given_generated_from_paths_is_initialized_with('template');
7878
$expectedPath = __DIR__ . '/Fixtures/generated-from-paths-expected.php';
79-
copy($tplPath, self::GENERATED_FILE_PATH);
80-
generateStructsForFiles(traversePhpFiles([__DIR__ . '/Fixtures/generated-from-paths.php']));
79+
generateStructsForFiles(traversePhpFiles([self::GENERATED_FILE_PATH]));
8180
$this->assertEquals(
8281
trim(file_get_contents($expectedPath)),
8382
trim(file_get_contents(self::GENERATED_FILE_PATH))
@@ -98,6 +97,40 @@ public function can_generate_structs_externally_for_files() {
9897
);
9998
}
10099

100+
/**
101+
* @depends can_generate_structs_externally_for_files
102+
* @test
103+
*/
104+
public function generating_structs_inline_for_files_can_detect_changes() {
105+
// set of files is different, should result in changed output
106+
$res = generateStructsExternallyForFiles(traversePhpFiles([
107+
__DIR__ . '/Fixtures/generate-struct-externally-for-files-test-cases/Acme.php',
108+
__DIR__ . '/Fixtures/generate-struct-externally-for-files-test-cases/GlobalClass.php',
109+
]), __DIR__ . '/Fixtures/generate-struct-externally-for-files-test-cases/.generated.php');
110+
$this->assertEquals(true, $res->hasChanges());
111+
}
112+
113+
/**
114+
* @dataProvider provide_for_generating_structs_externally_for_files_can_detect_changes
115+
* @test
116+
*/
117+
public function generating_structs_externally_for_files_can_detect_changes(string $type, bool $hasChanges) {
118+
$this->given_generated_from_paths_is_initialized_with($type);
119+
$res = generateStructsForFiles(traversePhpFiles([self::GENERATED_FILE_PATH]));
120+
$this->assertEquals($hasChanges, $res->hasChanges());
121+
}
122+
123+
public function provide_for_generating_structs_externally_for_files_can_detect_changes() {
124+
yield 'has-changes' => ['template', true];
125+
yield 'no-changes' => ['expected', false];
126+
}
127+
128+
/** can be template or expected */
129+
private function given_generated_from_paths_is_initialized_with(string $type = 'template') {
130+
$tplPath = __DIR__ . "/Fixtures/generated-from-paths-{$type}.php";
131+
copy($tplPath, self::GENERATED_FILE_PATH);
132+
}
133+
101134
/** @test */
102135
public function files_without_any_matching_classes_are_left_untouched() {
103136
$genStruct = new GenerateStruct();

0 commit comments

Comments
 (0)