stryker-mutator / stryker-net

Mutation testing for .NET core and .NET framework!
https://stryker-mutator.io
Apache License 2.0
1.75k stars 176 forks source link

Cannot inject mutation error appeared in 3.13 #2825

Closed cfuehrmann closed 5 months ago

cfuehrmann commented 6 months ago

Describe the bug A "cannot inject mutation" error has appeared in version 3.13. The error did not occur with 3.12.1. The console output of dotnet stryker --log-to-file is here:

CannotInjectMutation.log

The (main?) log file written by Stryker is here:

log-20240108.txt

Expected behavior Stryker should succeed on the .NET project in question.

Desktop (please complete the following information):

Additional context I have a .NET 6 solution containing a dozen projects, on several of which I run Stryker. Since the update from Stryker 3.12 to 3.13, one of those projects causes Stryker to fail with "cannot inject mutation". Apparently when Stryker tries to mutate a .First() into a .FirstOrDefault(). Possibly important: The mutated statements are inside an anonymous function inside a LINQ Select:

            .Select(group =>
            {
                var employeePerson = group.First().TimeCorrection!.Employee.Person;
                return new WorkflowTimeCorrectionsReadResponse
                {
                    EmployeeFirstName = employeePerson?.FirstName,
dupdob commented 6 months ago

thank you for opening an issue for this. I may have a workaround for you: could add a Stryker comments above the problematic line ? such as : // Stryker disable once all or // Stryker disable once Linq

The mutant will still be generated but no attempt to inject it should happen. It will be reported as Ignored. once you have pinpointed the offending line(s) could you please share the whole method so we can reproduce the problem and fix it properly ?

CrouseN commented 6 months ago

I've encountered a similar issue with .NET 6 and Flurl. The error is "Cannot inject mutation '$""' in 'IsSuccessStatusCode' because we cannot find the original code." and has a stack trace similar to the OP. It is interesting that this is taking that bool (IsSuccessStatusCode) from the System.Net.Http.HttpResponseMessage and trying to change it to a string. I hope this helps in investigating this issue. Thank you for the workaround.

dupdob commented 6 months ago

@CrouseN : thanks for the report. Could you please share the method where you encounter the issue ? What happens here is what we call a 'mutation leak', i.e. a mutation from some syntax construct was not injected at the right place and then Stryker to inject it on some other bit of code. This is due to the fact that mutation generation and mutation injection do not happen in a close sequence (for reasons that are long to explain).

cfuehrmann commented 6 months ago

@dupdob Thanks for you quick response! I did as you requested. The file with the problem has three similar methods, below you can see one of them. The problematic line is the one containing group.First(), directly underneath the // Stryker disable once all, which I just added. That suppression, when added in all three above-mentioned methods, does indeed remove the problem. (Unlike your suggestion above, the suppression line contains no colon. With a colon, it did not work.)

    private List<WorkflowTimePermissionsReadResponse> GetTimePermissionResponse(
        IEnumerable<WorkflowRequestsReadOpenApi> openApiResponse)
    {
        return openApiResponse
            .Where(_ => true)
            .Where(response => response.Entitlement is not null)
            .GroupBy(request => request.Entitlement!.Employee.Id)
            .Select(group =>
            {
                // Stryker disable once all
                var employeePerson = group.First().Entitlement!.Employee.Person;
                return new WorkflowTimePermissionsReadResponse
                {
                    EmployeeFirstName = employeePerson?.FirstName,
                    EmployeeMiddleName = employeePerson?.MiddleName,
                    EmployeeLastName = employeePerson?.LastName,
                    EmployeeId = group.Key,
                    PendingRequests = group
                        .Where(request => request.RequestState is WorkflowRequestStateOpenApi.Open)
                        .GroupBy(request => request.Status)
                        .SelectMany(grouping => grouping.ToList())
                        .Select(timePermission => new WorkflowTimePermissionResponse
                        {
                            RequestId = timePermission.Id,
                            RequestStatus = WorkflowConverter.FromOpenApi(timePermission.Status,
                                _logger),
                            RequestedOn = timePermission.CreationDate,
                            FromDay = DateOnly.FromDateTime(timePermission.Entitlement!.FromDate),
                            ToDay = DateOnly.FromDateTime(timePermission.Entitlement.ToDate),
                            RequesterComment = timePermission.RequestorComment,
                            AvailableActions = timePermission.AvailableActions
                                .Select(action => WorkflowConverter.FromOpenApi(action,
                                    _logger))
                                .ToList(),
                            TimePermissionName = timePermission.Entitlement.Entitlement.Name,
                            TimePermissionValue =
                                timePermission.RequestedAction is WorkflowRequestedActionOpenApi.Create
                        }).ToList(),
                    ApprovedRequests = group
                        .Where(request => request.RequestState is WorkflowRequestStateOpenApi.Done)
                        .GroupBy(request => request.Status)
                        .Where(statusGroup =>
                            statusGroup.Key is WorkflowRequestStatusOpenApi.Agreed
                                or WorkflowRequestStatusOpenApi.Approved)
                        .SelectMany(grouping => grouping.ToList())
                        .Select(timePermission => new WorkflowTimePermissionResponse
                        {
                            RequestId = timePermission.Id,
                            RequestStatus = WorkflowConverter.FromOpenApi(timePermission.Status,
                                _logger),
                            RequestedOn = timePermission.CreationDate,
                            FromDay = DateOnly.FromDateTime(timePermission.Entitlement!.FromDate),
                            ToDay = DateOnly.FromDateTime(timePermission.Entitlement.ToDate),
                            RequesterComment = timePermission.RequestorComment,
                            AvailableActions = timePermission.AvailableActions
                                .Select(action => WorkflowConverter.FromOpenApi(action,
                                    _logger))
                                .ToList(),
                            TimePermissionName = timePermission.Entitlement.Entitlement.Name,
                            TimePermissionValue =
                                timePermission.RequestedAction is WorkflowRequestedActionOpenApi.Create
                        }).ToList(),
                    DeniedRequests = group
                        .Where(request => request.RequestState is WorkflowRequestStateOpenApi.Done)
                        .GroupBy(request => request.Status)
                        .Where(statusGroup => statusGroup.Key is
                            WorkflowRequestStatusOpenApi.Rejected)
                        .SelectMany(grouping => grouping.ToList())
                        .Select(timePermission => new WorkflowTimePermissionResponse
                        {
                            RequestId = timePermission.Id,
                            RequestStatus = WorkflowConverter.FromOpenApi(timePermission.Status,
                                _logger),
                            RequestedOn = timePermission.CreationDate,
                            FromDay = DateOnly.FromDateTime(timePermission.Entitlement!.FromDate),
                            ToDay = DateOnly.FromDateTime(timePermission.Entitlement.ToDate),
                            RequesterComment = timePermission.RequestorComment,
                            AvailableActions = timePermission.AvailableActions
                                .Select(action => WorkflowConverter.FromOpenApi(action,
                                    _logger))
                                .ToList(),
                            TimePermissionName = timePermission.Entitlement.Entitlement.Name,
                            TimePermissionValue =
                                timePermission.RequestedAction is WorkflowRequestedActionOpenApi.Create
                        }).ToList()
                };
            }).ToList();
    }
dupdob commented 6 months ago

Thanks for the sample and correcting my work around (did provide it on a memory basis, failed to verify the proper syntax). I think I have enough information to try and reproduce it. Will keep you informed on my progress

dupdob commented 6 months ago

I just opened a PR with the (very) likely fix for this. Thanks for your helpful contributions

dupdob commented 5 months ago

note that I have no valid explanation of why this issue appears in V3.13. My understanding is that it is a long standing issue.

cfuehrmann commented 5 months ago

note that I have no valid explanation of why this issue appears in V3.13. My understanding is that it is a long standing issue.

That's strange, thanks for letting me know. I really tested with V3.12, and the error did not occur.

dupdob commented 5 months ago

my guess is that this problem was hidden by some other limitation/issue, that has been lifted/fix in V 3.13

cfuehrmann commented 5 months ago

Now that dotnet-stryker 3.13.2 is available, I could test it. The problem I had is indeed fixed. Thanks!