Skip to content

Progress report#1469

Merged
hcoles merged 3 commits into
hcoles:masterfrom
eatdrinksleepcode:progress-report
May 25, 2026
Merged

Progress report#1469
hcoles merged 3 commits into
hcoles:masterfrom
eatdrinksleepcode:progress-report

Conversation

@eatdrinksleepcode

Copy link
Copy Markdown
Contributor

The mutation analysis phase can take a long time, but there's no indication of how far along it is. This adds periodic progress logging that reports how many mutation test units have completed out of the total, giving a rough sense of how much time remains.

  • Adds a scheduled progress reporter to MutationAnalysisExecutor that logs completion count at a configurable interval
  • Exposes progressInterval (in seconds) as a configuration option via CLI and Maven plugin
  • Defaults to disabled (0) for backwards compatibility; opt in by setting a positive interval

@hcoles

hcoles commented May 22, 2026

Copy link
Copy Markdown
Owner

Thanks for this.

I think it should be possible to implement this as a org.pitest.mutationtest.MutationResultListener.

This would require far less code change, and would automatically make it a configurable feature across all build tools, including gradle.

The org.pitest.mutationtest.MutationResultListenerFactory allow a feature string to be configured, so this could be turned on with, for example

+progress.

Features can also accept simple parameters, so the interval could be set with

+progress(interval[42])

The examples of MutationResultListenerFactory in the codebase don't demonstrate how the feature strings work (features were retrofitted to the interface for the benefit of external plugins). But if you look at the MutationInterceptorFactory there are several examples.

@eatdrinksleepcode

Copy link
Copy Markdown
Contributor Author

Funny enough, after I implemented this I wanted to test it with my existing Gradle projects, and I realized the Gradle plugin only allows known configuration options. I then figured out that it does allow specifying arbitrary features, so I implemented a ConfigurationUpdater that works exactly like you mentioned: +progress(interval[42]). But it felt like a workaround for a Gradle plugin limitation, so I kept that part local and did not include it.

I'll have a look at the MutationResultListener approach.

@eatdrinksleepcode

Copy link
Copy Markdown
Contributor Author

@hcoles The downside of the MutationResultListener approach is that we lose out on the denominator. MutationAnalysisExecutor knows the total number of mutation units before it starts, so although all units are not equal in size, the progress report has a useful percentage component. With MutationResultListener we could only report that absolute progress is being made. That helps show that the analysis is not stalled, but it doesn't give the same sense of percentage progress that I was going for.

One solution would be to add the total number of mutation units to the ListenerArguments data structure that is passed to MutationResultListenerFactory.getListener. The total has already been calculated before ListenerArguments.

Another option would be to expose more information about the scan on the ClassMutationResults instance that is passed to MutationResultListener; either as individual properties, or as a MutationAnalysisInfo object that has information about the scan, or even a MutationAnalysisResults that aggregates the results so far. It's not the worst approach, but feels a bit hackish.

@eatdrinksleepcode eatdrinksleepcode force-pushed the progress-report branch 2 times, most recently from 92badc2 to 7b63005 Compare May 22, 2026 17:51
The previous implementation conflated output format listeners (selected
by name) with feature-activated listeners in a single predicate. This
caused the unknown-format error check to be bypassed when any
feature-activated listener factory was on the classpath.
Enabled with +progress or +progress(interval[N]) where N is seconds
between reports (defaults to 30). Counts individual mutations completed
and writes to stdout on a timer, giving a rough sense of how far the
analysis phase has progressed.
A feature-managed listener whose name happened to match an output
format string would silently satisfy the validation check without
actually producing output. Only legacy-mode factories should be
eligible for output format name matching.

Also renames isFeatureActivated to isActivatedByFeature for clarity.

@eatdrinksleepcode eatdrinksleepcode left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@hcoles I updated the implementation using a MutationResultListener as you suggested. It's definitely cleaner and does not require modifying core classes. The only downside is the loss of the denominator.


final Collection<MutationResultListenerFactory> outputFormatListeners = FCollection
.filter(listeners, matchesOutputFormat(this.options.getOutputFormats()));
if (outputFormatListeners.size() < this.options.getOutputFormats().size()) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Because there is now a MutationResultListenerFactory that does not use the LEGACY_MODE feature, this guard did not fire correctly when an invalid output format was specified, because there was 1 requested output format and 1 listener in the resulting collection (it was just the wrong listener). The corresponding test in SettingsFactoryTest caught the problem. Output formats are now verified against LEGACY_MODE listeners only; then feature-managed listeners are added in.


import static org.assertj.core.api.Assertions.assertThat;

public class ProgressListenerFactoryTest {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Other listener factories don't have tests, but I felt like these are simple enough and have enough value to add. Can remove if you don't think they are worth keeping.

aMutationTestResult().build(),
aMutationTestResult().build(),
aMutationTestResult().build()));
Thread.sleep(1500);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't love that this test has to sleep for a 1.5 seconds. One alternative is to express the interval in ProgressListener in milliseconds, and convert the values in ProgressListenerFactory. Then at least this test could sleep for a much shorter time. That's actually what I did in the previous version of this PR. But ultimately I decided the confusion of expressing the value with different units in different places wasn't worth it.

@hcoles hcoles merged commit 94834f9 into hcoles:master May 25, 2026
6 checks passed
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.

2 participants