Skip to content

Add tests to kill escaped mutants (#529)#545

Open
dbuhonov wants to merge 55 commits intoyiisoft:masterfrom
dbuhonov:feature/529-mutant-part-1
Open

Add tests to kill escaped mutants (#529)#545
dbuhonov wants to merge 55 commits intoyiisoft:masterfrom
dbuhonov:feature/529-mutant-part-1

Conversation

@dbuhonov
Copy link

Q A
Is bugfix?
New feature?
Breaks BC?
Fixed issues part of #529

@codecov
Copy link

codecov bot commented Mar 13, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.93%. Comparing base (e686a15) to head (dc1ccc0).
⚠️ Report is 2 commits behind head on master.

Additional details and impacted files
@@              Coverage Diff              @@
##              master     #545      +/-   ##
=============================================
- Coverage     100.00%   99.93%   -0.07%     
  Complexity       655      655              
=============================================
  Files             43       43              
  Lines           1629     1605      -24     
=============================================
- Hits            1629     1604      -25     
- Misses             0        1       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@dbuhonov dbuhonov force-pushed the feature/529-mutant-part-1 branch from ec926d0 to 27f1743 Compare March 13, 2026 09:09
dbuhonov added 27 commits March 13, 2026 13:41
@samdark samdark requested a review from Copilot March 18, 2026 20:38
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to increase mutation testing score (Issue #529) by adding/adjusting unit tests around ActiveRecord/ActiveQuery edge-cases (relations population/reset, upsert behavior, event dispatching, array access, etc.) and updating Infection configuration.

Changes:

  • Added new tests covering previously untested branches/edge-cases in ActiveRecord, ActiveQuery, EventsTrait, Arrayable/ArrayAccess traits, RepositoryTrait, and MagicActiveRecord behavior.
  • Introduced additional test stubs to exercise deep via() relations, event-enabled models, and ArrayAccess edge-cases involving name collisions between properties and relations.
  • Updated infection.json.dist to ignore several specific mutators/locations.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/Stubs/MagicActiveRecord/CategoryWithNameRelationArrayAccess.php New stub to test ArrayAccess behavior when a relation name collides with a column/property name.
tests/Stubs/ActiveRecord/OrderItemWithDeepViaProfile.php New stub to test deep via() chains and relation population behavior.
tests/Stubs/ActiveRecord/CustomerEventsModel.php New stub enabling EventsTrait on a Customer derivative for event dispatch tests.
tests/Stubs/ActiveRecord/OrderItemWithConstructor.php Doc comment fix to match the class name.
tests/RepositoryTraitTest.php Adds coverage for find() behavior when condition is null (params should be ignored).
tests/MagicActiveRecordTest.php Adds coverage for new-record save paths and property capability checks.
tests/EventsTraitTest.php Adds coverage for delete prevention and “after” event dispatching + handler behavior.
tests/Driver/Pgsql/ActiveQueryTest.php Adds PG-specific assertions for ModelRelationFilter condition types (array/json/scalar).
tests/ArrayableTraitTest.php Adds coverage for extraFields() behavior when relations are populated.
tests/ArrayAccessTraitTest.php Expands coverage for ArrayAccess edge-cases (property vs relation, magic properties, dependent-relation reset).
tests/ActiveRecordTest.php Adds multiple tests for relation reset, populateProperties behavior, cloning semantics, linking/unlinking edge-cases, counters, overrides, and upsert defaults.
tests/ActiveQueryTest.php Adds broad coverage around helper internals, joinWith parsing/behavior, relation population, via behavior, cloning, and filter application.
infection.json.dist Adds multiple mutator ignore rules / regex-based ignores.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +17 to +55
"ConcatOperandRemoval": {
"ignore": [
"Yiisoft\\ActiveRecord\\Trait\\MagicRelationsTrait::relationQuery",
"Yiisoft\\ActiveRecord\\Trait\\MagicRelationsTrait::relationNames"
]
},
"FalseValue": {
"ignore": [
"Yiisoft\\ActiveRecord\\Event\\Handler\\SetValueOnUpdate::beforeUpsert"
]
},
"LogicalAnd": {
"ignore": [
"Yiisoft\\ActiveRecord\\Internal\\JoinsWithBuilder::joinWithRelations"
]
},
"MatchArmRemoval": {
"ignore": [
"Yiisoft\\ActiveRecord\\Event\\Handler\\SetValueOnUpdate::beforeUpsert"
]
},
"MethodCallRemoval": {
"ignore": [
"Yiisoft\\ActiveRecord\\Internal\\JoinsWithBuilder::build"
]
},
"TrueValue": {
"ignore": [
"Yiisoft\\ActiveRecord\\ActiveRecord::upsertInternal"
],
"ignoreSourceCodeByRegex": [
"protected function upsertInternal\\(\\?array \\$insertProperties = null, array\\|bool \\$updateProperties = true\\): void"
]
},
"UnwrapArrayValues": {
"ignore": [
"Yiisoft\\ActiveRecord\\Internal\\JoinsWithBuilder::build"
]
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These exclusions are for survivors that are not obvious from the test diff itself, so I moved them into infection.json.dist intentionally to make them explicit and visible rather than leaving them hidden in the report only.

@dbuhonov dbuhonov marked this pull request as ready for review March 19, 2026 13:00
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds/extends PHPUnit coverage across ActiveRecord, ActiveQuery, events, array access/arrayable behavior, and several internal helpers to improve mutation score (Issue #529) by exercising previously untested branches and edge-cases.

Changes:

  • Added many new tests around relation population/reset, upsert/link/unlink edge cases, event dispatching, and query/join/via behaviors.
  • Introduced multiple new test stub ActiveRecord/ActiveQuery classes to support targeted scenarios (deep via relations, composite PKs, overridden internals, etc.).
  • Updated Infection configuration with explicit mutator ignores for specific survivors.

Reviewed changes

Copilot reviewed 27 out of 27 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/Stubs/MagicActiveRecord/CategoryWithNameRelationArrayAccess.php New stub for ArrayAccess + magic relation name collision scenarios.
tests/Stubs/ActiveRecord/OrderWithCustomerProfileViaCustomerRelation.php New stub to exercise nested “via” relation dependency handling.
tests/Stubs/ActiveRecord/OrderItemWithDeepViaProfile.php New stub to cover deep via chain relation population.
tests/Stubs/ActiveRecord/OrderItemWithConstructor.php Docblock fix for stub class name.
tests/Stubs/ActiveRecord/EmployeeWithPrototypeDossierRelation.php New stub for prototype-cloned relation target and stricter PK checks.
tests/Stubs/ActiveRecord/CustomerWithUpdateInternalOverride.php New stub to test overridable updateInternal().
tests/Stubs/ActiveRecord/CustomerWithRefreshInternalOverride.php New stub to test overridable refreshInternal().
tests/Stubs/ActiveRecord/CustomerWithOverriddenRelationQuery.php New stub to test overridable relation query creation.
tests/Stubs/ActiveRecord/CustomerWithDeleteInternalOverride.php New stub to test overridable deleteInternal().
tests/Stubs/ActiveRecord/CustomerSetValueOnUpdateUpsert.php New stub for SetValueOnUpdate behavior during upsert.
tests/Stubs/ActiveRecord/CustomerEventsModel.php New stub enabling EventsTrait on Customer.
tests/Stubs/ActiveRecord/CompositePrimaryKeyDossier.php New stub for composite PK relation/linking scenarios.
tests/Stubs/ActiveQuery/SingleModelArrayActiveQuery.php New stub to ensure one() path doesn’t fall back to all().
tests/Stubs/ActiveQuery/OverriddenPrimaryTableNameActiveQuery.php New stub to cover overriding primary table resolution.
tests/Stubs/ActiveQuery/OverriddenCreateModelsActiveQuery.php New stub to cover overriding model creation from rows.
tests/Stubs/ActiveQuery/MissingLinkValuesActiveQuery.php New stub for missing link values during relation population.
tests/Stubs/ActiveQuery/CreateModelsExceptionOnEmptyRowsActiveQuery.php New stub ensuring empty populate doesn’t call createModels().
tests/Stubs/ActiveQuery/AlternativeActiveQuery.php New stub for alternate ActiveQuery implementation.
tests/RepositoryTraitTest.php Added coverage for find() behavior with null condition/params handling.
tests/MagicActiveRecordTest.php Added tests for save/update behavior and property accessor edge-cases.
tests/EventsTraitTest.php Expanded event dispatch/handler behavior coverage (after-events, soft delete, defaults, etc.).
tests/Driver/Pgsql/ActiveQueryTest.php Added PG-specific assertions for ModelRelationFilter conditions.
tests/ArrayableTraitTest.php Added extraFields() coverage.
tests/ArrayAccessTraitTest.php Expanded ArrayAccess tests for properties vs relations and magic properties.
tests/ActiveRecordTest.php Added extensive relation reset/dependency, PK error message, upsert/link/unlink/counters, override hooks tests.
tests/ActiveQueryTest.php Added extensive coverage for populate/dedup, joinWith validation, viaTable callable, relation population internals.
infection.json.dist Added explicit mutator ignore configuration for remaining survivors.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

dbuhonov and others added 2 commits March 20, 2026 00:04
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@samdark samdark requested review from Tigrov and vjik March 19, 2026 20:08
@dbuhonov
Copy link
Author

Up review pls


$dossier = clone $dossierPrototype;
$dossier->set('summary', 'Strict primary key validation');
if ($this->db()->getDriverName() !== 'sqlsrv') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why skip sqlsrv here?


$this->assertSame(2, $dossier->get('department_id'));
$this->assertSame(2, $dossier->get('employee_id'));
$this->assertNotNull($dossier->get('id'));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it be equal to 100?

Comment on lines +2041 to +2049
public function testMarkPropertyChangedWithEmptyNameDoesNotModifyOldValues(): void
{
$customer = new Customer();
$customer->assignOldValues(['' => 'sentinel', 'name' => 'user1']);

$customer->markPropertyChanged('');

$this->assertSame(['' => 'sentinel', 'name' => 'user1'], $customer->oldValues());
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this is a bug and $customer->markPropertyChanged(''); should change it.

Comment on lines +2338 to +2382
public function testCreateRelationQueryCanBeOverridden(): void
{
$customer = new CustomerWithOverriddenRelationQuery();

$query = $customer->relationQuery('profile');

$this->assertNotSame(ActiveQuery::class, $query::class);
}

public function testCreateRelationQueryIsProtected(): void
{
$method = new ReflectionMethod(AbstractActiveRecord::class, 'createRelationQuery');

$this->assertTrue($method->isProtected());
}

public function testDeleteInternalCanBeOverridden(): void
{
$this->reloadFixtureAfterTest();

$customer = CustomerWithDeleteInternalOverride::query()->findByPk(1);

$this->assertSame(77, $customer->delete());
$this->assertNotNull(Customer::query()->findByPk(1));
}

public function testRefreshInternalCanBeOverridden(): void
{
$customer = CustomerWithRefreshInternalOverride::query()->findByPk(1);

$this->assertTrue($customer->refresh());
$this->assertSame('refreshed-via-override', $customer->getName());
}

public function testUpdateInternalCanBeOverridden(): void
{
$this->reloadFixtureAfterTest();

$customer = CustomerWithUpdateInternalOverride::query()->findByPk(1);

$customer->setName('should-not-hit-db');

$this->assertSame(91, $customer->update());
$this->assertSame('user1', Customer::query()->findByPk(1)->getName());
}
Copy link
Member

@Tigrov Tigrov Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where the fundamentals of OOP (inheritance) are tested. This looks redundant, but if it is necessary, perhaps it would be better to create a separate class in which to define all methods which can be overridden.

Comment on lines +28 to +35
public function testOffsetExistsDoesNotTreatPropertyAsRelation(): void
{
$model = new CustomerArrayAccessModel();
$model->name = 'test';

$this->assertTrue(isset($model['name']));
$this->assertFalse($model->isRelationPopulated('name'));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test looks like testOffsetGetReturnsPropertyValue()

Suggested change
public function testOffsetExistsDoesNotTreatPropertyAsRelation(): void
{
$model = new CustomerArrayAccessModel();
$model->name = 'test';
$this->assertTrue(isset($model['name']));
$this->assertFalse($model->isRelationPopulated('name'));
}

Comment on lines +232 to +233
$this->assertSame('customer c!', $tableName);
$this->assertSame('customer c!', $alias);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't it looks like a bug?

Comment on lines +242 to +243
$this->assertSame($from, $tableName);
$this->assertSame($from, $alias);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't it look like a bug?

{
$query = Customer::query();
$method = new ReflectionMethod(ActiveQuery::class, 'removeDuplicatedRows');
$method->setAccessible(true);
Copy link
Member

@Tigrov Tigrov Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not required
https://www.php.net/manual/en/reflectionmethod.setaccessible.php

Note: As of PHP 8.1.0, calling this method has no effect; all methods are invokable by default.

Suggested change
$method->setAccessible(true);

Comment on lines +269 to +284
public function testCreateModelsCanBeOverridden(): void
{
$query = new OverriddenCreateModelsActiveQuery(Customer::class);

$this->assertSame(
[['overridden' => true, 'rows' => [['id' => 1]]]],
$query->populate([['id' => 1]]),
);
}

public function testGetPrimaryTableNameCanBeOverridden(): void
{
$query = new OverriddenPrimaryTableNameActiveQuery(Customer::class);

$this->assertSame(['{{profile}}' => '{{profile}}'], $query->getTablesUsedInFrom());
}
Copy link
Member

@Tigrov Tigrov Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where the fundamentals of OOP (inheritance) are tested. This looks redundant, but if it is necessary, perhaps it would be better to create a separate class in which to define all methods which can be overridden.

Comment on lines +362 to +380
public function testModelRelationFilterCompositeArrayModelsUseQualifiedColumnNames(): void
{
$query = Dossier::query()
->from(['d' => 'dossier'])
->join('INNER JOIN', 'employee e', '1=1')
->link(['department_id' => 'department_id', 'employee_id' => 'employee_id']);

ModelRelationFilter::apply($query, [
['department_id' => 2],
]);

$where = $query->getWhere();

$this->assertInstanceOf(In::class, $where);
$this->assertSame(
[['d.department_id' => 2, 'd.employee_id' => null]],
array_values($where->values),
);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like testModelRelationFilterCompositeKeysFillMissingValuesWithNull()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants