Adding infection to an existing project and publishing the results to Gitlab merge requests
Tags: php, testing, devops, codequality
Published: 2021-12-09 15:46:46

---
title: Adding infection to an existing project and publishing the results to Gitlab merge requests
published: true
description: In this blog post I want to describe how we recently added infection to our development workflow in an existing project hosted on Gitlab.
tags: php, testing, devops, codequality
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x109hmytzjopwgc2m7yt.png
---

In this blog post I want to describe how we recently added [infection](https://infection.github.io/) to our development workflow in an existing project. The project has a code coverage of ~40% and we have ~27k LLOC (measured with `phploc src | grep LLOC`). If you're not familiar with infection or mutation testing in general yet, I can recommend [this blog post](https://medium.com/@maks_rafalko/infection-mutation-testing-framework-c9ccf02eefd1).

Adding infection to a greenfield project or some small module is not that hard: You can just add it to your CI pipeline together with a hard-limit via the `--min-msi` & `--min-covered-msi` flags and raise the bar from time to time, until you reach 100%.

When adding infection to an existing project it's a bit different and it was in fact what was hindering us from using that: How could we really make it part of our development process without (a) having a too strict check and (b) without risking that the reported problems will just be ignored, because there are obviuosly so many of them in the beginning?

We decided to try out a pragramtic solution: We run infection only for changed files in a merge request and publish the results directly in the merge request as a note, so that the author and the reviewers see all mutations.

## Adding infection to the project

We added infection to the docker image we use in CI like so:
```
RUN wget https://github.com/infection/infection/releases/download/0.25.0/infection.phar \
    && wget https://github.com/infection/infection/releases/download/0.25.0/infection.phar.asc \
    && chmod +x infection.phar \
    && mv infection.phar /usr/local/bin/infection
```

Have a look at different ways to install infection [here](https://infection.github.io/guide/installation.html)

Our `infection.json` is pretty basic, but it's important to have the different outputs in the `logs` section configured:
```
{
  "$schema": "https://raw.githubusercontent.com/infection/infection/0.25.0/resources/schema.json",
  "source": {
    "directories": [
      "src/Pyz"
    ],
    "excludes": [
      "DependencyProvider.php",
      "BusinessFactory.php"
    ]
  },
  "logs": {
    "text": "infection.log",
    "summary": "summary.log",
    "json": "infection-log.json",
    "perMutator": "per-mutator.md"
  },
  "mutators": {
    "@default": true
  }
}
```
Your source directory is probably just `src/` and you will need to adapt the `excludes` as well.

Infection allows you to limit which files it should mutate with [the `--git-diff-filter` option](https://infection.github.io/guide/command-line-options.html#git-diff-filter). We use that like so in our CI:
```
- git fetch --depth=1 origin master
- infection --git-diff-filter=AM || true
```

This will execute infection in the pipeline and write the output also to the configured log files (`infection.log`, `summary.log` & `per-mutator.md`): ![output of infection command in CI job](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x109hmytzjopwgc2m7yt.png)

## Publishing the results as a comment in the merge request

To make it a bit easier for the author and the reviewers, we added a php script that publishes these results as a comment directly in the merge request.

You need [a Project Access Token](https://docs.gitlab.com/ee/user/project/settings/project_access_tokens.html) in order to use the Gitlab API for stuff like this. You can create one at `https://<your-gitlab-domain>/<group>/<project>/-/settings/access_tokens`. Copy the token to a safe place and make sure that it's available as a environment variable during the CI job.

We've choosen [this GitLab PHP API Client](https://github.com/GitLabPHP/Client) for our project:
```
composer require --dev "m4tthumphrey/php-gitlab-api:^11.4" "guzzlehttp/guzzle:^7.2" "http-interop/http-factory-guzzle:^1.0"
```
Now here's the script that reads the log files and sends them to the related merge request:
```php
<?php

declare(strict_types = 1);

require_once 'vendor/autoload.php';

$projectId = (int)getenv('CI_PROJECT_ID');
if ($projectId === 0) {
    die('CI_PROJECT_ID is missing!');
}
$mergeRequestId = (int)getenv('CI_MERGE_REQUEST_IID');
if ($mergeRequestId === 0) {
    die('CI_MERGE_REQUEST_IID missing!');
}
$authToken = (string)getenv('GITLAB_AUTH_TOKEN');
if ($authToken === '') {
    die('GITLAB_AUTH_TOKEN missing!');
}

$client = new Gitlab\Client();
$client->setUrl('<your-gitlab-domain>');
$client->authenticate($authToken, Gitlab\Client::AUTH_HTTP_TOKEN);
if (!function_exists('str_starts_with')) {
    function str_starts_with(string $haystack, string $needle): bool
    {
        return strpos($haystack, $needle) === 0;
    }
}

/* ---------------------------------------------- */

$data = json_decode(file_get_contents('infection-log.json'), true);
$identifierText = '<details><summary>infection results 📋</summary>';
$noteBody = $identifierText . PHP_EOL . PHP_EOL;

$noteBody .= '| metric | value |' . PHP_EOL;
$noteBody .= '| ------ | ----- |' . PHP_EOL;
$noteBody .= '| Mutation Score Indicator (MSI) | ' . $data['stats']['msi'] . '% |' . PHP_EOL;
$noteBody .= '| Mutation Code Coverage | ' . $data['stats']['mutationCodeCoverage'] . '% |' . PHP_EOL;
$noteBody .= '| Covered Code MSI | ' . $data['stats']['coveredCodeMsi'] . '% |' . PHP_EOL;

$noteBody .= '```' . file_get_contents('summary.log') . '```' . PHP_EOL;
$noteBody .= '```' . file_get_contents('infection.log') . '```' . PHP_EOL;
$noteBody .= file_get_contents('per-mutator.md') . PHP_EOL;

$noteBody .= PHP_EOL . '</details>';

/* ---------------------------------------------- */

$notes = $client->mergeRequests()->showNotes($projectId, $mergeRequestId);
foreach ($notes as $note) {
    if (str_starts_with($note['body'], $identifierText)) {
        $noteId = $note['id'];
    }
}
if (isset($noteId)) {
    $notes = $client->mergeRequests()->updateNote($projectId, $mergeRequestId, $noteId, $noteBody);
    echo 'updated';
} else {
    $notes = $client->mergeRequests()->addNote($projectId, $mergeRequestId, $noteBody);
    echo 'added';
}

echo PHP_EOL;
```
The variables `CI_PROJECT_ID` and `CI_MERGE_REQUEST_IID` are automatically available in every Gitlab CI job, you just need to make sure that the auth token you generated is available in `GITLAB_AUTH_TOKEN`. Do not forget to replace `<your-gitlab-domain>` with the real url.

The script adds only one note per merge request, which will be updated if new results are generated. The comment will look like this: ![published results as a comment in the merge request](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6m41fnobrydlp2zpnkyd.png)

You can use this of course for all kinds of reports, I think we'll add `davidrjonas/composer-lock-diff` next.

----

This setup allows us to see escaped mutants in every merge request and improve our codebase step-by-step. I hope you enjoyed it, feel free to ask any questions here or on [twitter](https://twitter.com/AlfredBez).