Skip to content

Commit ea6ca7d

Browse files
authored
Suppress find()/findOrCreate()/loadInto() method overrides on Cake 5.3.6+ (#446)
* Suppress find()/findOrCreate()/loadInto() overrides on Cake 5.3.6+ Builds on PR #445. After cakephp/cakephp#19438 lands, the parent Table::find(), Table::findOrCreate() and Table::loadInto() propagate TEntity through their return types, so the matching plugin overrides become redundant — and the parent's PersistenceFailedException declaration on findOrCreate() finally survives in static analysis. A dedicated supportsEntityTemplateFindFamily() helper splits the find-family propagation from the class-level template gate: the template itself landed in CakePHP 5.3.4 (cakephp/cakephp#19388), while the find-family annotations are slated for 5.3.6. Until #19438 actually ships, the helper's version check is a placeholder that MUST be tightened to the real release before this PR comes out of draft. Per-method gating: * find(): suppressed when tableEntityQuery emission would otherwise fire AND the find-family parent annotations are available. Parent now returns SelectQuery<TEntity|array>, which is strictly more truthful than the plugin's previous SelectQuery<TEntity> override since find() legitimately can return rows as arrays. * findOrCreate(): suppressed when the gates hold. Kept in genericsInParam=detailed mode because the plugin override narrows the $search argument beyond what the parent provides. * loadInto(): suppressed in concreteEntitiesInParam=strict mode when the gates hold; the parent's TEntity|array<TEntity> annotation matches the override exactly. Also lowers supportsEntityTemplate()'s version check from 5.4.0 to 5.3.4 (the actual release that shipped #19388). The previous gate was set anticipating 5.4 but the feature shipped earlier in the 5.3.x line. Tests cover the new gating in three regression dimensions: * tableEntityQuery + entity-template gates met → find() override suppressed (and kept when tableBehaviors excludes extends); * concreteEntitiesInParam=strict + entity-template gates met → loadInto() override suppressed (and kept on a custom-parent table where the template can't be assumed); * entity-template present but find-family support NOT yet present (the realistic in-between state between Cake 5.3.4 and 5.3.6) → find()/findOrCreate()/loadInto() overrides kept, other methods still suppressed. * Revert supportsEntityTemplate() gate back to 5.4.0 Lowering the gate to 5.3.4 was out of scope for this PR and broke fixture-comparison tests on CI (which installs Cake 5.3.5). Fixture tests not using the mock emit '@extends Table<array{}, \Cake\ORM\Entity>' that the saved fixtures don't have. The gate-lowering was a factual correction but needs its own PR with all the affected fixtures regenerated. * Lower supportsEntityTemplate() gate to the actual ship version (5.3.4) The TEntity template was added in cakephp/cakephp#19388 and first released in CakePHP 5.3.4 — not 5.4 as the previous gate anticipated. Test isolation: * _getAnnotatorMock() now forces supportsEntityTemplate=false so the legacy-path fixture comparisons stay stable regardless of the installed Cake version. The entity-template path keeps its own _getEntityTemplateAnnotatorMock() that forces it true. * AnnotateCommandTest's CI-mode tests run the real annotator via exec() — no mocking possible. The Awesome plugin Tables are pre-annotated with the @extends line the 5.3.4+ annotator emits; the affected tests skip on Cake < 5.3.4 where the annotator would want to remove that annotation. * Suppress marshalling-family overrides under the entity-template gate newEntity / newEntities / patchEntity / patchEntities all resolve their return types via the parent's TEntity (or method-level TPatchedEntity) on Cake 5.3.4+, so the plugin overrides only carry parameter-narrowing weight on top of that — and the narrowing is only meaningful when the user has opted into it. Per-method gates: * newEntity / newEntities: keep override in detailed mode (narrows array<string, mixed> for the data parameter). Suppress everywhere else — the override's User[]|ResultSetInterface<User> return shape is also technically wrong, since newEntities actually returns array<TEntity> (no ResultSet involvement). * patchEntity: keep override in detailed mode AND in concreteEntitiesInParam mode (where the entity parameter is narrowed to the concrete entity class). * patchEntities: same as patchEntity — kept under concreteEntitiesInParam for symmetry, even though the iterable narrowing only kicks in when generics or strict are also on. Tests cover the matrix: detailed mode keeps narrowed data parameter, concrete mode keeps narrowed entity parameter for patchEntity and patchEntities, non-narrowing scenarios suppress all four overrides.
1 parent 0978666 commit ea6ca7d

6 files changed

Lines changed: 276 additions & 19 deletions

File tree

src/Annotator/ModelAnnotator.php

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -246,28 +246,43 @@ protected function buildAnnotations(array $associations, string $entity, array $
246246
// `TEntity` unless they re-declare it themselves).
247247
//
248248
// Per-method gates also account for parameter narrowing the parent doesn't
249-
// provide (detailed `$finder` on `get()`, concrete `$entity` on `saveOrFail()`),
250-
// so we keep those overrides when the user has opted into narrowing.
249+
// provide (detailed `$finder` on `get()`, detailed `$search` on `findOrCreate()`,
250+
// concrete `$entity` on `save()`/`saveOrFail()`), so we keep those overrides
251+
// when the user has opted into narrowing.
252+
//
253+
// `find()`/`findOrCreate()`/`loadInto()` were only updated to flow `TEntity`
254+
// in cakephp/cakephp#19438 — a strictly later change than the class template
255+
// itself — so they have their own feature gate via
256+
// `supportsEntityTemplateFindFamily()`.
251257
$entityTemplateEmitted = $this->supportsEntityTemplate()
252258
&& in_array(static::BEHAVIOR_EXTENDS, $this->_config[static::TABLE_BEHAVIORS], true)
253259
&& ($parentClass === '' || ltrim($parentClass, '\\') === 'Cake\\ORM\\Table');
260+
$entityTemplateFindFamily = $entityTemplateEmitted && $this->supportsEntityTemplateFindFamily();
254261

255262
if (!$entityTemplateEmitted) {
256263
$annotations[] = "@method {$fullClassName} newEmptyEntity()";
257264
}
258-
$annotations[] = "@method {$fullClassName} newEntity({$dataType} \$data, {$optionsType} \$options = [])";
259-
$annotations[] = "@method {$fullClassNameCollection} newEntities({$dataListType} \$data, {$optionsType} \$options = [])";
265+
if (!$entityTemplateEmitted || $detailed) {
266+
$annotations[] = "@method {$fullClassName} newEntity({$dataType} \$data, {$optionsType} \$options = [])";
267+
$annotations[] = "@method {$fullClassNameCollection} newEntities({$dataListType} \$data, {$optionsType} \$options = [])";
268+
}
260269

261270
if (!$entityTemplateEmitted || $detailed) {
262271
$annotations[] = "@method {$fullClassName} get(mixed \$primaryKey, {$finderType} \$finder = 'all', \Psr\SimpleCache\CacheInterface|string|null \$cache = null, \Closure|string|null \$cacheKey = null, mixed ...\$args)";
263272
}
264-
if (Configure::read('IdeHelper.tableEntityQuery')) {
273+
if (Configure::read('IdeHelper.tableEntityQuery') && !$entityTemplateFindFamily) {
265274
$annotations[] = "@method \Cake\ORM\Query\SelectQuery<{$fullClassName}> find(string \$type = 'all', mixed ...\$args)";
266275
}
267-
$annotations[] = "@method {$fullClassName} findOrCreate({$findOrCreateSearchType} \$search, ?callable \$callback = null, {$optionsType} \$options = [])";
276+
if (!$entityTemplateFindFamily || $detailed) {
277+
$annotations[] = "@method {$fullClassName} findOrCreate({$findOrCreateSearchType} \$search, ?callable \$callback = null, {$optionsType} \$options = [])";
278+
}
268279

269-
$annotations[] = "@method {$fullClassName} patchEntity({$entityInterface} \$entity, {$dataType} \$data, {$optionsType} \$options = [])";
270-
$annotations[] = "@method {$fullClassNameCollection} patchEntities({$iterable} \$entities, {$dataListType} \$data, {$optionsType} \$options = [])";
280+
if (!$entityTemplateEmitted || $detailed || $concrete) {
281+
$annotations[] = "@method {$fullClassName} patchEntity({$entityInterface} \$entity, {$dataType} \$data, {$optionsType} \$options = [])";
282+
}
283+
if (!$entityTemplateEmitted || $detailed || $concrete) {
284+
$annotations[] = "@method {$fullClassNameCollection} patchEntities({$iterable} \$entities, {$dataListType} \$data, {$optionsType} \$options = [])";
285+
}
271286

272287
if (!$entityTemplateEmitted || $concrete) {
273288
$annotations[] = "@method {$fullClassName}|false save({$entityInterface} \$entity, {$optionsType} \$options = [])";
@@ -284,7 +299,7 @@ protected function buildAnnotations(array $associations, string $entity, array $
284299
$annotations[] = "@method {$resultSetInterfaceCollection}|false deleteMany({$iterable} \$entities, {$optionsType} \$options = [])";
285300
$annotations[] = "@method {$resultSetInterfaceCollection} deleteManyOrFail({$iterable} \$entities, {$optionsType} \$options = [])";
286301

287-
if ($strict) {
302+
if ($strict && !$entityTemplateFindFamily) {
288303
$annotations[] = "@method {$fullClassName}|array<{$fullClassName}> loadInto({$fullClassName}|array<{$fullClassName}> \$entities, array \$contain)";
289304
}
290305
}
@@ -569,12 +584,31 @@ protected function addBehaviorExtends(array $result, array $behaviors, string $p
569584
}
570585

571586
/**
572-
* Whether `\Cake\ORM\Table` declares a second `TEntity` template parameter (CakePHP 5.4+).
587+
* Whether `\Cake\ORM\Table` declares a second `TEntity` template parameter.
588+
*
589+
* Introduced via cakephp/cakephp#19388, first released in CakePHP 5.3.4.
573590
*
574591
* @return bool
575592
*/
576593
protected function supportsEntityTemplate(): bool {
577-
return version_compare(Configure::version(), '5.4.0', '>=');
594+
return version_compare(Configure::version(), '5.3.4', '>=');
595+
}
596+
597+
/**
598+
* Whether `\Cake\ORM\Table::find()` / `findOrCreate()` / `loadInto()` propagate
599+
* the entity template `TEntity` through their `@return`.
600+
*
601+
* This is a strictly later change than {@see static::supportsEntityTemplate()} —
602+
* the class-level template was added separately from the find-family annotations.
603+
*
604+
* Requires cakephp/cakephp#19438, currently milestoned for CakePHP 5.3.6. The
605+
* version gate below MUST be set to the actual release containing that change
606+
* before this is mark-able as non-draft.
607+
*
608+
* @return bool
609+
*/
610+
protected function supportsEntityTemplateFindFamily(): bool {
611+
return version_compare(Configure::version(), '5.3.6', '>=');
578612
}
579613

580614
}

tests/TestCase/Annotator/ModelAnnotatorTest.php

Lines changed: 219 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,17 @@ protected function _getAnnotatorMock(array $params): ModelAnnotator {
126126
AbstractAnnotator::CONFIG_VERBOSE => true,
127127
];
128128

129-
return $this->getMockBuilder(ModelAnnotator::class)->onlyMethods(['storeFile'])->setConstructorArgs([$this->io, $params])->getMock();
129+
// Force the legacy (no-entity-template) path so fixture-comparison tests stay
130+
// stable regardless of which CakePHP version is installed. The entity-template
131+
// path is exercised separately via _getEntityTemplateAnnotatorMock().
132+
$mock = $this->getMockBuilder(ModelAnnotator::class)
133+
->onlyMethods(['storeFile', 'supportsEntityTemplate', 'supportsEntityTemplateFindFamily'])
134+
->setConstructorArgs([$this->io, $params])
135+
->getMock();
136+
$mock->method('supportsEntityTemplate')->willReturn(false);
137+
$mock->method('supportsEntityTemplateFindFamily')->willReturn(false);
138+
139+
return $mock;
130140
}
131141

132142
/**
@@ -141,10 +151,32 @@ protected function _getEntityTemplateAnnotatorMock(array $params): ModelAnnotato
141151
];
142152

143153
$mock = $this->getMockBuilder(ModelAnnotator::class)
144-
->onlyMethods(['storeFile', 'supportsEntityTemplate'])
154+
->onlyMethods(['storeFile', 'supportsEntityTemplate', 'supportsEntityTemplateFindFamily'])
145155
->setConstructorArgs([$this->io, $params])
146156
->getMock();
147157
$mock->method('supportsEntityTemplate')->willReturn(true);
158+
$mock->method('supportsEntityTemplateFindFamily')->willReturn(true);
159+
160+
return $mock;
161+
}
162+
163+
/**
164+
* @param array $params
165+
* @return \IdeHelper\Annotator\ModelAnnotator|\PHPUnit\Framework\MockObject\MockObject
166+
*/
167+
protected function _getEntityTemplateWithoutFindFamilyAnnotatorMock(array $params): ModelAnnotator {
168+
$params += [
169+
AbstractAnnotator::CONFIG_REMOVE => true,
170+
AbstractAnnotator::CONFIG_DRY_RUN => true,
171+
AbstractAnnotator::CONFIG_VERBOSE => true,
172+
];
173+
174+
$mock = $this->getMockBuilder(ModelAnnotator::class)
175+
->onlyMethods(['storeFile', 'supportsEntityTemplate', 'supportsEntityTemplateFindFamily'])
176+
->setConstructorArgs([$this->io, $params])
177+
->getMock();
178+
$mock->method('supportsEntityTemplate')->willReturn(true);
179+
$mock->method('supportsEntityTemplateFindFamily')->willReturn(false);
148180

149181
return $mock;
150182
}
@@ -395,7 +427,7 @@ public function testAnnotateWithEntityTemplate() {
395427
$annotator->annotate($path);
396428

397429
$output = $this->out->output();
398-
$this->assertTextContains(' -> 14 annotations added', $output);
430+
$this->assertTextContains(' -> 9 annotations added', $output);
399431
}
400432

401433
/**
@@ -426,6 +458,11 @@ public function testAnnotateWithEntityTemplateKeepsOverridesWhenExtendsDisabled(
426458
$this->assertStringContainsString('saveOrFail(', $capture);
427459
$this->assertStringContainsString('newEmptyEntity()', $capture);
428460
$this->assertStringContainsString(' get(mixed $primaryKey', $capture);
461+
$this->assertStringContainsString('findOrCreate(', $capture);
462+
$this->assertStringContainsString('newEntity(', $capture);
463+
$this->assertStringContainsString('newEntities(', $capture);
464+
$this->assertStringContainsString('patchEntity(', $capture);
465+
$this->assertStringContainsString('patchEntities(', $capture);
429466
}
430467

431468
/**
@@ -452,6 +489,9 @@ public function testAnnotateWithEntityTemplateKeepsOverridesForCustomParent() {
452489
$this->assertStringContainsString('saveOrFail(', $capture);
453490
$this->assertStringContainsString('newEmptyEntity()', $capture);
454491
$this->assertStringContainsString(' get(mixed $primaryKey', $capture);
492+
$this->assertStringContainsString('findOrCreate(', $capture);
493+
$this->assertStringContainsString('newEntity(', $capture);
494+
$this->assertStringContainsString('patchEntity(', $capture);
455495
}
456496

457497
/**
@@ -480,8 +520,14 @@ public function testAnnotateWithEntityTemplateKeepsSaveOverridesWhenConcreteEnti
480520

481521
$this->assertStringContainsString('save(\TestApp\Model\Entity\BarBar $entity', $capture);
482522
$this->assertStringContainsString('saveOrFail(\TestApp\Model\Entity\BarBar $entity', $capture);
523+
// `patchEntity()` and `patchEntities()` are also kept under `concreteEntitiesInParam`.
524+
$this->assertStringContainsString('patchEntity(\TestApp\Model\Entity\BarBar $entity', $capture);
525+
$this->assertStringContainsString('patchEntities(', $capture);
483526
// `newEmptyEntity` remains suppressed because it has no parameters to narrow.
484527
$this->assertStringNotContainsString('newEmptyEntity()', $capture);
528+
// `newEntity` / `newEntities` aren't entity-typed in their parameters, so they
529+
// remain suppressed in non-detailed mode.
530+
$this->assertStringNotContainsString('newEntity(', $capture);
485531
}
486532

487533
/**
@@ -508,6 +554,176 @@ public function testAnnotateWithEntityTemplateKeepsGetWhenDetailedGenerics() {
508554
$annotator->annotate(APP . 'Model/Table/BarBarsTable.php');
509555

510556
$this->assertStringContainsString('get(mixed $primaryKey, array<string, mixed>|string', $capture);
557+
// detailed mode also narrows `$search` on `findOrCreate()`, so its override stays.
558+
$this->assertStringContainsString('findOrCreate(\Cake\ORM\Query\SelectQuery<\TestApp\Model\Entity\BarBar>|callable|array<string, mixed>', $capture);
559+
// detailed mode narrows `$data` on the marshalling family, so their overrides stay.
560+
$this->assertStringContainsString('newEntity(array<string, mixed> $data', $capture);
561+
$this->assertStringContainsString('newEntities(array<array<string, mixed>> $data', $capture);
562+
$this->assertStringContainsString('patchEntity(', $capture);
563+
$this->assertStringContainsString('patchEntities(', $capture);
564+
}
565+
566+
/**
567+
* `IdeHelper.tableEntityQuery` historically emits a `find()` @method override
568+
* narrowed to `SelectQuery<Entity>`. With the entity-template emitted, the
569+
* parent's `find()` already returns `SelectQuery<TEntity|array>` — so we drop
570+
* the override on Cake 5.4+ (the `|array` fallback is actually more truthful
571+
* since `find()` can be hydration-disabled).
572+
*
573+
* @return void
574+
*/
575+
public function testAnnotateWithEntityTemplateSuppressesTableEntityQueryFindOverride() {
576+
Configure::write('IdeHelper.tableEntityQuery', true);
577+
578+
$annotator = $this->_getEntityTemplateAnnotatorMock([]);
579+
580+
$capture = '';
581+
$annotator->expects($this->once())
582+
->method('storeFile')
583+
->with($this->anything(), $this->callback(function ($value) use (&$capture) {
584+
$capture = $value;
585+
586+
return true;
587+
}));
588+
589+
$annotator->annotate(APP . 'Model/Table/BarBarsTable.php');
590+
591+
$this->assertStringNotContainsString('SelectQuery<\TestApp\Model\Entity\BarBar> find(', $capture);
592+
}
593+
594+
/**
595+
* Regression: when the entity-template guards do NOT hold (e.g. `tableBehaviors=mixin`
596+
* so no @extends is emitted), the `find()` override under `tableEntityQuery` must
597+
* still be emitted — the parent generic isn't available to resolve from.
598+
*
599+
* @return void
600+
*/
601+
public function testAnnotateWithEntityTemplateKeepsTableEntityQueryFindOverrideWhenExtendsDisabled() {
602+
Configure::write('IdeHelper.tableEntityQuery', true);
603+
Configure::write('IdeHelper.tableBehaviors', 'mixin');
604+
605+
$annotator = $this->_getEntityTemplateAnnotatorMock([]);
606+
607+
$capture = '';
608+
$annotator->expects($this->once())
609+
->method('storeFile')
610+
->with($this->anything(), $this->callback(function ($value) use (&$capture) {
611+
$capture = $value;
612+
613+
return true;
614+
}));
615+
616+
$annotator->annotate(APP . 'Model/Table/BarBarsTable.php');
617+
618+
$this->assertStringContainsString('SelectQuery<\TestApp\Model\Entity\BarBar> find(', $capture);
619+
}
620+
621+
/**
622+
* `loadInto()` is only emitted in `concreteEntitiesInParam=strict` mode. With
623+
* the entity-template emitted, the parent's `loadInto()` already declares
624+
* `TEntity|array<TEntity>` for both param and return, so the override is
625+
* redundant on Cake 5.4+ (after cakephp/cakephp#19438).
626+
*
627+
* @return void
628+
*/
629+
public function testAnnotateWithEntityTemplateSuppressesStrictLoadIntoOverride() {
630+
Configure::write('IdeHelper.concreteEntitiesInParam', 'strict');
631+
632+
$annotator = $this->_getEntityTemplateAnnotatorMock([]);
633+
634+
$capture = '';
635+
$annotator->expects($this->once())
636+
->method('storeFile')
637+
->with($this->anything(), $this->callback(function ($value) use (&$capture) {
638+
$capture = $value;
639+
640+
return true;
641+
}));
642+
643+
$annotator->annotate(APP . 'Model/Table/BarBarsTable.php');
644+
645+
$this->assertStringNotContainsString('loadInto(', $capture);
646+
}
647+
648+
/**
649+
* Regression: on CakePHP versions that have the entity template (5.3.4+) but
650+
* NOT the find-family TEntity propagation (pre-#19438), the find/findOrCreate/
651+
* loadInto overrides must still be emitted even though the entity template
652+
* is otherwise present. The other entity-returning methods (`get`/`saveOrFail`
653+
* etc.) remain suppressed because their TEntity propagation landed earlier.
654+
*
655+
* @return void
656+
*/
657+
public function testAnnotateWithEntityTemplateKeepsFindAndFindOrCreateWithoutFindFamilySupport() {
658+
Configure::write('IdeHelper.tableEntityQuery', true);
659+
660+
$annotator = $this->_getEntityTemplateWithoutFindFamilyAnnotatorMock([]);
661+
662+
$capture = '';
663+
$annotator->expects($this->once())
664+
->method('storeFile')
665+
->with($this->anything(), $this->callback(function ($value) use (&$capture) {
666+
$capture = $value;
667+
668+
return true;
669+
}));
670+
671+
$annotator->annotate(APP . 'Model/Table/BarBarsTable.php');
672+
673+
$this->assertStringContainsString('SelectQuery<\TestApp\Model\Entity\BarBar> find(', $capture);
674+
$this->assertStringContainsString('findOrCreate(', $capture);
675+
// Non-find-family methods are still suppressed because the entity template
676+
// itself IS present and their parent annotations already carry TEntity.
677+
$this->assertStringNotContainsString('newEmptyEntity()', $capture);
678+
$this->assertStringNotContainsString('saveOrFail(', $capture);
679+
}
680+
681+
/**
682+
* Mirrors the previous test for `loadInto()`, which only emits in strict mode.
683+
*
684+
* @return void
685+
*/
686+
public function testAnnotateWithEntityTemplateKeepsStrictLoadIntoWithoutFindFamilySupport() {
687+
Configure::write('IdeHelper.concreteEntitiesInParam', 'strict');
688+
689+
$annotator = $this->_getEntityTemplateWithoutFindFamilyAnnotatorMock([]);
690+
691+
$capture = '';
692+
$annotator->expects($this->once())
693+
->method('storeFile')
694+
->with($this->anything(), $this->callback(function ($value) use (&$capture) {
695+
$capture = $value;
696+
697+
return true;
698+
}));
699+
700+
$annotator->annotate(APP . 'Model/Table/BarBarsTable.php');
701+
702+
$this->assertStringContainsString('loadInto(', $capture);
703+
}
704+
705+
/**
706+
* Regression: in strict mode without the entity-template guards (e.g. on a
707+
* custom-parent table), the `loadInto()` override is still needed.
708+
*
709+
* @return void
710+
*/
711+
public function testAnnotateWithEntityTemplateKeepsStrictLoadIntoOverrideForCustomParent() {
712+
Configure::write('IdeHelper.concreteEntitiesInParam', 'strict');
713+
714+
$annotator = $this->_getEntityTemplateAnnotatorMock([]);
715+
716+
$capture = '';
717+
$annotator->method('storeFile')
718+
->with($this->anything(), $this->callback(function ($value) use (&$capture) {
719+
$capture = $value;
720+
721+
return true;
722+
}));
723+
724+
$annotator->annotate(APP . 'Model/Table/BarBarsAbstractTable.php');
725+
726+
$this->assertStringContainsString('loadInto(', $capture);
511727
}
512728

513729
}

tests/TestCase/Command/AnnotateCommandTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,11 @@ public function testAll() {
145145
* @return void
146146
*/
147147
public function testAllCiModeNoChanges() {
148+
// The Awesome plugin's tables are pre-annotated with `@extends Table<..., TEntity>`,
149+
// which only the CakePHP 5.3.4+ annotator emits. On older Cake versions the annotator
150+
// would want to remove those annotations, breaking the no-changes check.
151+
$this->skipIf(version_compare(Configure::version(), '5.3.4', '<'), 'Requires CakePHP 5.3.4+ (entity-template support).');
152+
148153
$this->exec('annotate all -d -v --ci -p Awesome');
149154
$this->assertExitSuccess($this->_out->output());
150155
}
@@ -180,6 +185,9 @@ public static function provideSubcommandsForCiModeTest() {
180185
#[DataProvider('provideSubcommandsForCiModeTest')]
181186
public function testIndividualSubcommandCiModeNoChanges(string $subcommand): void {
182187
$this->skipIf($subcommand === 'view', 'View does not support the plugin parameter');
188+
// See testAllCiModeNoChanges() — the Awesome plugin tables are pre-annotated for the
189+
// CakePHP 5.3.4+ annotator output.
190+
$this->skipIf($subcommand === 'models' && version_compare(Configure::version(), '5.3.4', '<'), 'Models pre-annotation requires CakePHP 5.3.4+.');
183191

184192
$this->exec('annotate ' . $subcommand . ' -d -v --ci -p Awesome');
185193
$this->assertExitSuccess($this->_out->output());

0 commit comments

Comments
 (0)