From 538efe4f0d77b3181df3cf2746c88bb7e13ec5be Mon Sep 17 00:00:00 2001 From: Martin Welgemoed Date: Wed, 25 Aug 2021 13:55:52 +0200 Subject: [PATCH 01/26] First version of the retry logic for dealing with compiler errors --- .../command/runner/ProcessTestRunner.scala | 2 +- .../stryker4s/run/ProcessTestRunnerTest.scala | 6 +- core/src/main/scala/stryker4s/Stryker4s.scala | 38 ++++++++-- .../main/scala/stryker4s/model/Mutant.scala | 15 +++- .../stryker4s/model/MutantRunResult.scala | 2 + .../scala/stryker4s/model/MutatedFile.scala | 37 ++++++++- .../scala/stryker4s/mutants/Mutator.scala | 28 +++++-- .../applymutants/CoverageMatchBuilder.scala | 4 +- .../mutants/applymutants/MatchBuilder.scala | 76 ++++++++++--------- .../applymutants/StatementTransformer.scala | 2 +- .../mutants/findmutants/MutantFinder.scala | 12 ++- .../mutants/findmutants/MutantMatcher.scala | 15 +++- .../report/mapper/MutantRunResultMapper.scala | 15 ++-- .../scala/stryker4s/run/MutantRunner.scala | 37 ++++++--- .../scala/stryker4s/run/Stryker4sRunner.scala | 11 +-- .../test/scala/stryker4s/Stryker4sTest.scala | 11 +-- .../mutants/AddAllMutationsTest.scala | 4 +- .../CoverageMatchBuilderTest.scala | 6 +- .../applymutants/MatchBuilderTest.scala | 31 ++++---- .../StatementTransformerTest.scala | 11 ++- .../findmutants/MutantMatcherTest.scala | 2 +- .../mapper/MutantRunResultMapperTest.scala | 4 +- .../stryker4s/run/MutantRunnerTest.scala | 12 +-- .../scala/stryker4s/testutil/TestData.scala | 4 +- .../testutil/stubs/TestRunnerStub.scala | 4 +- .../stryker4s/sbt/Stryker4sSbtRunner.scala | 44 +++++++++-- .../sbt/runner/LegacySbtTestRunner.scala | 2 +- .../sbt/runner/ProcessTestRunner.scala | 2 +- 28 files changed, 297 insertions(+), 140 deletions(-) diff --git a/command-runner/src/main/scala/stryker4s/command/runner/ProcessTestRunner.scala b/command-runner/src/main/scala/stryker4s/command/runner/ProcessTestRunner.scala index 24591efda..5cd38eb81 100644 --- a/command-runner/src/main/scala/stryker4s/command/runner/ProcessTestRunner.scala +++ b/command-runner/src/main/scala/stryker4s/command/runner/ProcessTestRunner.scala @@ -18,7 +18,7 @@ class ProcessTestRunner(command: Command, processRunner: ProcessRunner, tmpDir: } def runMutant(mutant: Mutant): IO[MutantRunResult] = { - val id = mutant.id + val id = mutant.id.globalId processRunner(command, tmpDir, ("ACTIVE_MUTATION", id.toString)).map { case Success(0) => Survived(mutant) case Success(_) => Killed(mutant) diff --git a/command-runner/src/test/scala/stryker4s/run/ProcessTestRunnerTest.scala b/command-runner/src/test/scala/stryker4s/run/ProcessTestRunnerTest.scala index e73a89b61..6313f32ec 100644 --- a/command-runner/src/test/scala/stryker4s/run/ProcessTestRunnerTest.scala +++ b/command-runner/src/test/scala/stryker4s/run/ProcessTestRunnerTest.scala @@ -22,7 +22,7 @@ class ProcessTestRunnerTest extends Stryker4sIOSuite with MockitoIOSuite with Lo it("should return a Survived mutant on an exitcode 0 process") { val testProcessRunner = TestProcessRunner(Success(0)) - val mutant = Mutant(0, q"4", q"5", EmptyString) + val mutant = Mutant(MutantId(0), q"4", q"5", EmptyString) processTestRunner(testProcessRunner).runMutant(mutant).asserting { result => result shouldBe a[Survived] testProcessRunner.timesCalled.next() should equal(1) @@ -31,7 +31,7 @@ class ProcessTestRunnerTest extends Stryker4sIOSuite with MockitoIOSuite with Lo it("should return a Killed mutant on an exitcode 1 process") { val testProcessRunner = TestProcessRunner(Success(1)) - val mutant = Mutant(0, q"4", q"5", EmptyString) + val mutant = Mutant(MutantId(0), q"4", q"5", EmptyString) processTestRunner(testProcessRunner).runMutant(mutant).asserting { result => result shouldBe a[Killed] testProcessRunner.timesCalled.next() should equal(1) @@ -41,7 +41,7 @@ class ProcessTestRunnerTest extends Stryker4sIOSuite with MockitoIOSuite with Lo it("should return a TimedOut mutant on a TimedOut process") { val exception = new TimeoutException("Test") val testProcessRunner = TestProcessRunner(Failure(exception)) - val mutant = Mutant(0, q"4", q"5", EmptyString) + val mutant = Mutant(MutantId(0), q"4", q"5", EmptyString) processTestRunner(testProcessRunner).runMutant(mutant).asserting { result => result shouldBe a[TimedOut] testProcessRunner.timesCalled.next() should equal(1) diff --git a/core/src/main/scala/stryker4s/Stryker4s.scala b/core/src/main/scala/stryker4s/Stryker4s.scala index c01d4668f..de7c24d71 100644 --- a/core/src/main/scala/stryker4s/Stryker4s.scala +++ b/core/src/main/scala/stryker4s/Stryker4s.scala @@ -1,21 +1,47 @@ package stryker4s import cats.effect.IO +import mutationtesting.MetricsResult import stryker4s.config.Config import stryker4s.files.MutatesFileResolver +import stryker4s.model.MutantId import stryker4s.mutants.Mutator import stryker4s.run.MutantRunner import stryker4s.run.threshold.{ScoreStatus, ThresholdChecker} -class Stryker4s(fileSource: MutatesFileResolver, mutator: Mutator, runner: MutantRunner)(implicit config: Config) { +case class CompileError(msg: String, path: String, line: Integer) { + override def toString: String = s"$path:L$line: '$msg'" +} +case class MutationCompilationFailed(errors: Seq[CompileError]) extends RuntimeException(s"Compilation failed: $errors") + +class Stryker4s(fileSource: MutatesFileResolver, mutatorFactory: () => Mutator, runner: MutantRunner)(implicit + config: Config +) { def run(): IO[ScoreStatus] = { - val filesToMutate = fileSource.files + runWithRetry(List.empty) + .map(metrics => ThresholdChecker.determineScoreStatus(metrics.mutationScore)) + } + //Retries the run, after removing the non-compiling mutants + def runWithRetry(nonCompilingMutants: Seq[MutantId]): IO[MetricsResult] = { + //Recreate the mutator from the factory, otherwise the second run's ids will not start at 0 + val mutator = mutatorFactory() + val filesToMutate = fileSource.files for { - mutatedFiles <- mutator.mutate(filesToMutate) - metrics <- runner(mutatedFiles) - scoreStatus = ThresholdChecker.determineScoreStatus(metrics.mutationScore) - } yield scoreStatus + mutatedFiles <- mutator.mutate(filesToMutate, nonCompilingMutants) + metrics <- runner(mutatedFiles, nonCompilingMutants).handleErrorWith { + //If a compiler error occurs, retry once without the lines that gave an error + case MutationCompilationFailed(errs) if nonCompilingMutants.isEmpty => + val nonCompilingMutations = mutator.errorsToIds(errs, mutatedFiles) + if (nonCompilingMutations.nonEmpty) { + runWithRetry(nonCompilingMutations) + } else { + IO.raiseError(new RuntimeException("Unable to see which mutations caused the compiler failure")) + } + //Something else went wrong vOv + case e => IO.raiseError(e) + } + } yield metrics } } diff --git a/core/src/main/scala/stryker4s/model/Mutant.scala b/core/src/main/scala/stryker4s/model/Mutant.scala index d8e3ed326..b4b6d1cfe 100644 --- a/core/src/main/scala/stryker4s/model/Mutant.scala +++ b/core/src/main/scala/stryker4s/model/Mutant.scala @@ -4,4 +4,17 @@ import scala.meta.{Term, Tree} import stryker4s.extension.mutationtype.Mutation -final case class Mutant(id: Int, original: Term, mutated: Term, mutationType: Mutation[_ <: Tree]) +//The globalId is used in mutation switching and generating reports, but it varies between runs +//The file and idInFile is stable, and used for finding compiler errors +case class MutantId(globalId: Int, file: String, idInFile: Int) { + def sameMutation(otherMutId: MutantId): Boolean = { + otherMutId.idInFile == this.idInFile && otherMutId.file == this.file + } +} + +object MutantId { + //Initially mutants are created with just a globalId, the file information is added later on + def apply(globalId: Int): MutantId = new MutantId(globalId, "", -1) +} + +final case class Mutant(id: MutantId, original: Term, mutated: Term, mutationType: Mutation[_ <: Tree]) diff --git a/core/src/main/scala/stryker4s/model/MutantRunResult.scala b/core/src/main/scala/stryker4s/model/MutantRunResult.scala index 1f40f5e52..d237bbb96 100644 --- a/core/src/main/scala/stryker4s/model/MutantRunResult.scala +++ b/core/src/main/scala/stryker4s/model/MutantRunResult.scala @@ -22,3 +22,5 @@ final case class NoCoverage(mutant: Mutant, description: Option[String] = None) final case class Error(mutant: Mutant, description: Option[String] = None) extends MutantRunResult final case class Ignored(mutant: Mutant, description: Option[String] = None) extends MutantRunResult + +final case class CompilerError(mutant: Mutant, description: Option[String] = None) extends MutantRunResult diff --git a/core/src/main/scala/stryker4s/model/MutatedFile.scala b/core/src/main/scala/stryker4s/model/MutatedFile.scala index 0634b07b7..29ab2a512 100644 --- a/core/src/main/scala/stryker4s/model/MutatedFile.scala +++ b/core/src/main/scala/stryker4s/model/MutatedFile.scala @@ -2,6 +2,39 @@ package stryker4s.model import fs2.io.file.Path -import scala.meta.Tree +import scala.meta._ -final case class MutatedFile(fileOrigin: Path, tree: Tree, mutants: Seq[Mutant], excludedMutants: Int) +final case class MutatedFile( + fileOrigin: Path, + tree: Tree, + mutants: Seq[Mutant], + mutationStatements: Seq[(MutantId, Tree)], + excludedMutants: Int +) { + + def mutatedSource: String = { + tree.syntax + } + + //Returns a map of line numbers to the mutant on that line + //Contains only mutated lines, and the same mutant can stretch over multiple multiple lines + //This logic is not very fast, because it has to search the entire tree, that's why it's lazy + lazy val mutantLineNumbers: Map[Int, MutantId] = { + val statementToMutIdMap = mutationStatements.map { case (mutantId, mutationStatement) => + mutationStatement.structure -> mutantId + }.toMap + + mutatedSource + .parse[Stat] //Parse as a standalone statement, used in unit tests and conceivable in some real code + .orElse(mutatedSource.parse[Source]) //Parse as a complete scala source file + .get //If both failed something has gone very badly wrong, give up + .collect { + case node if statementToMutIdMap.contains(node.structure) => + val mutId = statementToMutIdMap(node.structure) + //+1 because scalameta uses zero-indexed line numbers + (node.pos.startLine to node.pos.endLine).map(i => i + 1 -> mutId) + } + .flatten + .toMap + } +} diff --git a/core/src/main/scala/stryker4s/mutants/Mutator.scala b/core/src/main/scala/stryker4s/mutants/Mutator.scala index e41ff9114..697cbe930 100644 --- a/core/src/main/scala/stryker4s/mutants/Mutator.scala +++ b/core/src/main/scala/stryker4s/mutants/Mutator.scala @@ -4,28 +4,40 @@ import cats.effect.IO import cats.syntax.functor._ import fs2.Stream import fs2.io.file.Path +import stryker4s.CompileError import stryker4s.config.Config import stryker4s.extension.StreamExtensions._ import stryker4s.log.Logger -import stryker4s.model.{MutatedFile, MutationsInSource, SourceTransformations} +import stryker4s.model.{MutantId, MutatedFile, MutationsInSource, SourceTransformations} import stryker4s.mutants.applymutants.{MatchBuilder, StatementTransformer} import stryker4s.mutants.findmutants.MutantFinder -import scala.meta.Tree - class Mutator(mutantFinder: MutantFinder, transformer: StatementTransformer, matchBuilder: MatchBuilder)(implicit config: Config, log: Logger ) { - def mutate(files: Stream[IO, Path]): IO[Seq[MutatedFile]] = { + //Given compiler errors, return the mutants that caused it + def errorsToIds(compileError: Seq[CompileError], files: Seq[MutatedFile]): Seq[MutantId] = { + compileError.flatMap { err => + files + //Find the file that the compiler error came from + .find(_.fileOrigin.toString.endsWith(err.path)) + //Find the mutant case statement that cased the compiler error + .flatMap(file => file.mutantLineNumbers.get(err.line)) + } + } + + def mutate(files: Stream[IO, Path], nonCompilingMutants: Seq[MutantId] = Seq.empty): IO[Seq[MutatedFile]] = { files .parEvalMapUnordered(config.concurrency)(p => findMutants(p).tupleLeft(p)) .map { case (file, mutationsInSource) => - val transformed = transformStatements(mutationsInSource) - val builtTree = buildMatches(transformed) + val validMutants = + mutationsInSource.mutants.filterNot(mut => nonCompilingMutants.exists(_.sameMutation(mut.id))) + val transformed = transformStatements(mutationsInSource.copy(mutants = validMutants)) + val (builtTree, mutations) = buildMatches(transformed) - MutatedFile(file, builtTree, mutationsInSource.mutants, mutationsInSource.excluded) + MutatedFile(file, builtTree, mutationsInSource.mutants, mutations, mutationsInSource.excluded) } .filterNot(mutatedFile => mutatedFile.mutants.isEmpty && mutatedFile.excludedMutants == 0) .compile @@ -44,7 +56,7 @@ class Mutator(mutantFinder: MutantFinder, transformer: StatementTransformer, mat /** Step 3: Build pattern matches from transformed trees */ - private def buildMatches(transformedMutantsInSource: SourceTransformations): Tree = + private def buildMatches(transformedMutantsInSource: SourceTransformations) = matchBuilder.buildNewSource(transformedMutantsInSource) private def logMutationResult(mutatedFiles: Iterable[MutatedFile]): IO[Unit] = { diff --git a/core/src/main/scala/stryker4s/mutants/applymutants/CoverageMatchBuilder.scala b/core/src/main/scala/stryker4s/mutants/applymutants/CoverageMatchBuilder.scala index d47779bbd..ffc29c763 100644 --- a/core/src/main/scala/stryker4s/mutants/applymutants/CoverageMatchBuilder.scala +++ b/core/src/main/scala/stryker4s/mutants/applymutants/CoverageMatchBuilder.scala @@ -11,7 +11,7 @@ class CoverageMatchBuilder(mutationContext: ActiveMutationContext)(implicit log: // sbt-stryker4s-testrunner matches on Int instead of Option[Int] override def mutantToCase(mutant: Mutant): Case = - super.buildCase(mutant.mutated, p"${mutant.id}") + super.buildCase(mutant.mutated, p"${mutant.id.globalId}") override def defaultCase(transformedMutant: TransformedMutants): Case = withCoverage(super.defaultCase(transformedMutant), transformedMutant.mutantStatements) @@ -20,7 +20,7 @@ class CoverageMatchBuilder(mutationContext: ActiveMutationContext)(implicit log: * switch. `coverMutant` always returns true */ private def withCoverage(caze: Case, mutants: List[Mutant]): Case = { - val coverageCond = q"_root_.stryker4s.coverage.coverMutant(..${mutants.map(_.id).map(Lit.Int(_))})" + val coverageCond = q"_root_.stryker4s.coverage.coverMutant(..${mutants.map(_.id.globalId).map(Lit.Int(_))})" caze.copy(cond = Some(coverageCond)) } } diff --git a/core/src/main/scala/stryker4s/mutants/applymutants/MatchBuilder.scala b/core/src/main/scala/stryker4s/mutants/applymutants/MatchBuilder.scala index db7b25ba3..e75319cb8 100644 --- a/core/src/main/scala/stryker4s/mutants/applymutants/MatchBuilder.scala +++ b/core/src/main/scala/stryker4s/mutants/applymutants/MatchBuilder.scala @@ -3,50 +3,52 @@ package stryker4s.mutants.applymutants import stryker4s.extension.TreeExtensions.{IsEqualExtension, TransformOnceExtension} import stryker4s.extension.exception.UnableToBuildPatternMatchException import stryker4s.log.Logger -import stryker4s.model.{Mutant, SourceTransformations, TransformedMutants} +import stryker4s.model.{Mutant, MutantId, SourceTransformations, TransformedMutants} import stryker4s.mutants.applymutants.ActiveMutationContext.ActiveMutationContext import scala.meta._ import scala.util.{Failure, Success} class MatchBuilder(mutationContext: ActiveMutationContext)(implicit log: Logger) { - def buildNewSource(transformedStatements: SourceTransformations): Tree = { + def buildNewSource(transformedStatements: SourceTransformations): (Tree, Seq[(MutantId, Case)]) = { val source = transformedStatements.source - groupTransformedStatements(transformedStatements).foldLeft(source: Tree) { (rest, mutants) => - val origStatement = mutants.originalStatement + groupTransformedStatements(transformedStatements).foldLeft((source, Seq.empty): (Tree, Seq[(MutantId, Case)])) { + (rest, mutants) => + val origStatement = mutants.originalStatement - var isTransformed = false - rest transformOnce { - case found if found.isEqual(origStatement) && found.pos == origStatement.pos => - isTransformed = true - buildMatch(mutants) - } match { - case Success(value) if isTransformed => value - case Success(value) => - log.warn( - s"Failed to add mutation(s) ${mutants.mutantStatements.map(_.id).mkString(", ")} to new mutated code" - ) - log.warn( - s"The code that failed to mutate was: [$origStatement] at ${origStatement.pos.input}:${origStatement.pos.startLine + 1}:${origStatement.pos.startColumn + 1}" - ) - log.warn("This mutation will likely show up as Survived") - log.warn( - "Please open an issue on github with sample code of the mutation that failed: https://github.com/stryker-mutator/stryker4s/issues/new" - ) - value - case Failure(exception) => - log.error(s"Failed to construct pattern match: original statement [$origStatement]") - log.error(s"Failed mutation(s) ${mutants.mutantStatements.mkString(",")}.") - log.error( - s"at ${origStatement.pos.input}:${origStatement.pos.startLine + 1}:${origStatement.pos.startColumn + 1}" - ) - log.error("This is likely an issue on Stryker4s's end, please enable debug logging and restart Stryker4s.") - log.debug("Please open an issue on github: https://github.com/stryker-mutator/stryker4s/issues/new") - log.debug("Please be so kind to copy the stacktrace into the issue", exception) + var isTransformed = false + rest._1 transformOnce { + case found if found.isEqual(origStatement) && found.pos == origStatement.pos => + isTransformed = true + buildMatch(mutants) + } match { + case Success(value) if isTransformed => + (value, rest._2 ++ mutants.mutantStatements.map(mut => (mut.id, mutantToCase(mut)))) + case Success(value) => + log.warn( + s"Failed to add mutation(s) ${mutants.mutantStatements.map(_.id.globalId).mkString(", ")} to new mutated code" + ) + log.warn( + s"The code that failed to mutate was: [$origStatement] at ${origStatement.pos.input}:${origStatement.pos.startLine + 1}:${origStatement.pos.startColumn + 1}" + ) + log.warn("This mutation will likely show up as Survived") + log.warn( + "Please open an issue on github with sample code of the mutation that failed: https://github.com/stryker-mutator/stryker4s/issues/new" + ) + (value, rest._2) + case Failure(exception) => + log.error(s"Failed to construct pattern match: original statement [$origStatement]") + log.error(s"Failed mutation(s) ${mutants.mutantStatements.mkString(",")}.") + log.error( + s"at ${origStatement.pos.input}:${origStatement.pos.startLine + 1}:${origStatement.pos.startColumn + 1}" + ) + log.error("This is likely an issue on Stryker4s's end, please enable debug logging and restart Stryker4s.") + log.debug("Please open an issue on github: https://github.com/stryker-mutator/stryker4s/issues/new") + log.debug("Please be so kind to copy the stacktrace into the issue", exception) - throw UnableToBuildPatternMatchException() - } + throw UnableToBuildPatternMatchException() + } } } @@ -57,7 +59,7 @@ class MatchBuilder(mutationContext: ActiveMutationContext)(implicit log: Logger) } protected def mutantToCase(mutant: Mutant): Case = - buildCase(mutant.mutated, p"Some(${mutant.id})") + buildCase(mutant.mutated, p"Some(${mutant.id.globalId})") protected def defaultCase(transformedMutant: TransformedMutants): Case = buildCase(transformedMutant.originalStatement, p"_") @@ -72,6 +74,8 @@ class MatchBuilder(mutationContext: ActiveMutationContext)(implicit log: Logger) } .map { case (originalStatement, mutants) => TransformedMutants(originalStatement, mutants.toList) } .toSeq - .sortBy(_.mutantStatements.head.id) // Should be sorted so tree transformations are applied in order of discovery + .sortBy( + _.mutantStatements.head.id.globalId + ) // Should be sorted so tree transformations are applied in order of discovery } } diff --git a/core/src/main/scala/stryker4s/mutants/applymutants/StatementTransformer.scala b/core/src/main/scala/stryker4s/mutants/applymutants/StatementTransformer.scala index 2a821cd4a..ba06e44fa 100644 --- a/core/src/main/scala/stryker4s/mutants/applymutants/StatementTransformer.scala +++ b/core/src/main/scala/stryker4s/mutants/applymutants/StatementTransformer.scala @@ -11,7 +11,7 @@ class StatementTransformer { .groupBy(mutant => mutant.original) .map { case (original, mutants) => transformMutant(original, mutants) } .toSeq - .sortBy(_.mutantStatements.map(_.id).max) + .sortBy(_.mutantStatements.map(_.id.globalId).max) SourceTransformations(source, transformedMutants) } diff --git a/core/src/main/scala/stryker4s/mutants/findmutants/MutantFinder.scala b/core/src/main/scala/stryker4s/mutants/findmutants/MutantFinder.scala index b421164ef..98354e34c 100644 --- a/core/src/main/scala/stryker4s/mutants/findmutants/MutantFinder.scala +++ b/core/src/main/scala/stryker4s/mutants/findmutants/MutantFinder.scala @@ -6,7 +6,7 @@ import fs2.io.file.Path import stryker4s.config.Config import stryker4s.extension.FileExtensions._ import stryker4s.log.Logger -import stryker4s.model.{Mutant, MutationExcluded, MutationsInSource, RegexParseError} +import stryker4s.model.{Mutant, MutantId, MutationExcluded, MutationsInSource, RegexParseError} import scala.meta.parsers.XtensionParseInputLike import scala.meta.{Dialect, Parsed, Source} @@ -15,7 +15,15 @@ class MutantFinder(matcher: MutantMatcher)(implicit config: Config, log: Logger) def mutantsInFile(filePath: Path): IO[MutationsInSource] = for { parsedSource <- parseFile(filePath) (included, excluded) <- IO(findMutants(parsedSource)) - } yield MutationsInSource(parsedSource, included, excluded) + } yield MutationsInSource(parsedSource, addFileInfo(filePath.toString, included), excluded) + + def addFileInfo(filePath: String, mutants: Seq[Mutant]): Seq[Mutant] = { + mutants.zipWithIndex.map { case (mut, numInFile) => + //Assign the file and the index of the mutation in the file to the MutantId + //This is used later to trace potential compiler errors back to the mutation + mut.copy(id = MutantId(mut.id.globalId, filePath, numInFile)) + } + } def findMutants(source: Source): (Seq[Mutant], Int) = { val (ignored, included) = source.collect(matcher.allMatchers).flatten.partitionEither(identity) diff --git a/core/src/main/scala/stryker4s/mutants/findmutants/MutantMatcher.scala b/core/src/main/scala/stryker4s/mutants/findmutants/MutantMatcher.scala index 27b87a6e6..4a9af9ebf 100644 --- a/core/src/main/scala/stryker4s/mutants/findmutants/MutantMatcher.scala +++ b/core/src/main/scala/stryker4s/mutants/findmutants/MutantMatcher.scala @@ -6,7 +6,7 @@ import stryker4s.config.Config import stryker4s.extension.PartialFunctionOps._ import stryker4s.extension.TreeExtensions.{GetMods, PathToRoot, TreeIsInExtension} import stryker4s.extension.mutationtype._ -import stryker4s.model.{IgnoredMutationReason, Mutant, MutationExcluded} +import stryker4s.model.{IgnoredMutationReason, Mutant, MutantId, MutationExcluded} import scala.meta._ @@ -115,8 +115,17 @@ class MutantMatcher()(implicit config: Config) { .map { mutated => if (matchExcluded(mutated) || isSuppressedByAnnotation(mutated, original)) Left(MutationExcluded()) - else - Right(Mutant(ids.next(), original, mutationToTerm(mutated), mutated)) + else { + Right( + Mutant( + //The file and idInFile are left empty for now, they're assigned later + MutantId(globalId = ids.next()), + original, + mutationToTerm(mutated), + mutated + ) + ) + } } } diff --git a/core/src/main/scala/stryker4s/report/mapper/MutantRunResultMapper.scala b/core/src/main/scala/stryker4s/report/mapper/MutantRunResultMapper.scala index 746eec441..be987ac44 100644 --- a/core/src/main/scala/stryker4s/report/mapper/MutantRunResultMapper.scala +++ b/core/src/main/scala/stryker4s/report/mapper/MutantRunResultMapper.scala @@ -39,7 +39,7 @@ trait MutantRunResultMapper { private def toMutantResult(runResult: MutantRunResult): MutantResult = { val mutant = runResult.mutant MutantResult( - mutant.id.toString, + mutant.id.globalId.toString, mutant.mutationType.mutationName, mutant.mutated.syntax, toLocation(mutant.original.pos), @@ -56,12 +56,13 @@ trait MutantRunResultMapper { private def toMutantStatus(mutant: MutantRunResult): MutantStatus = mutant match { - case _: Survived => MutantStatus.Survived - case _: Killed => MutantStatus.Killed - case _: NoCoverage => MutantStatus.NoCoverage - case _: TimedOut => MutantStatus.Timeout - case _: Error => MutantStatus.RuntimeError - case _: Ignored => MutantStatus.Ignored + case _: Survived => MutantStatus.Survived + case _: Killed => MutantStatus.Killed + case _: NoCoverage => MutantStatus.NoCoverage + case _: TimedOut => MutantStatus.Timeout + case _: Error => MutantStatus.RuntimeError + case _: Ignored => MutantStatus.Ignored + case _: CompilerError => MutantStatus.CompileError } private def fileContentAsString(path: Path)(implicit config: Config): String = diff --git a/core/src/main/scala/stryker4s/run/MutantRunner.scala b/core/src/main/scala/stryker4s/run/MutantRunner.scala index a8b2e9adb..4529cd831 100644 --- a/core/src/main/scala/stryker4s/run/MutantRunner.scala +++ b/core/src/main/scala/stryker4s/run/MutantRunner.scala @@ -26,17 +26,18 @@ class MutantRunner( )(implicit config: Config, log: Logger) extends MutantRunResultMapper { - def apply(mutatedFiles: Seq[MutatedFile]): IO[MetricsResult] = + def apply(mutatedFiles: Seq[MutatedFile], nonCompilingMutants: Seq[MutantId]): IO[MetricsResult] = { prepareEnv(mutatedFiles) .flatMap(createTestRunnerPool) .use { testRunnerPool => testRunnerPool.loan .use(initialTestRun) .flatMap { coverageExclusions => - runMutants(mutatedFiles, testRunnerPool, coverageExclusions).timed + runMutants(mutatedFiles, testRunnerPool, coverageExclusions, nonCompilingMutants).timed } } .flatMap(t => createAndReportResults(t._1, t._2)) + } def createAndReportResults(duration: FiniteDuration, runResults: Map[Path, Seq[MutantRunResult]]) = for { time <- IO.realTime @@ -88,7 +89,7 @@ class MutantRunner( .createDirectories(targetPath.parent.get) .as((mutatedFile, targetPath)) }.map { case (mutatedFile, targetPath) => - Stream(mutatedFile.tree.syntax) + Stream(mutatedFile.mutatedSource) .covary[IO] .through(text.utf8.encode) .through(Files[IO].writeAll(targetPath)) @@ -97,27 +98,41 @@ class MutantRunner( private def runMutants( mutatedFiles: Seq[MutatedFile], testRunnerPool: TestRunnerPool, - coverageExclusions: CoverageExclusions + coverageExclusions: CoverageExclusions, + nonCompilingMutants: Seq[MutantId] ): IO[Map[Path, Seq[MutantRunResult]]] = { val allMutants = mutatedFiles.flatMap(m => m.mutants.toList.map(m.fileOrigin.relativePath -> _)) - val (staticMutants, rest) = allMutants.partition(m => coverageExclusions.staticMutants.contains(m._2.id)) + val (staticMutants, rest) = allMutants.partition(m => coverageExclusions.staticMutants.contains(m._2.id.globalId)) + + val (compilerErrorMutants, rest2) = + rest.partition(mut => nonCompilingMutants.exists(_.sameMutation(mut._2.id))) + val (noCoverageMutants, testableMutants) = - rest.partition(m => coverageExclusions.hasCoverage && !coverageExclusions.coveredMutants.contains(m._2.id)) + rest2.partition(m => + coverageExclusions.hasCoverage && !coverageExclusions.coveredMutants.contains(m._2.id.globalId) + ) if (noCoverageMutants.nonEmpty) { log.info( s"${noCoverageMutants.size} mutants detected as having no code coverage. They will be skipped and marked as NoCoverage" ) - log.debug(s"NoCoverage mutant ids are: ${noCoverageMutants.map(_._2.id).mkString(", ")}") + log.debug(s"NoCoverage mutant ids are: ${noCoverageMutants.map(_._2.id.globalId).mkString(", ")}") } if (staticMutants.nonEmpty) { log.info( s"${staticMutants.size} mutants detected as static. They will be skipped and marked as Ignored" ) - log.debug(s"Static mutant ids are: ${staticMutants.map(_._2.id).mkString(", ")}") + log.debug(s"Static mutant ids are: ${staticMutants.map(_._2.id.globalId).mkString(", ")}") + } + + if (compilerErrorMutants.nonEmpty) { + log.info( + s"${compilerErrorMutants.size} mutants gave a compiler error. They will be marked as such in the report." + ) + log.debug(s"Non-compiling mutant ids are: ${compilerErrorMutants.map(_._2.id.globalId).mkString(", ")}") } def mapPureMutants[K, V, VV](l: Seq[(K, V)], f: V => VV) = @@ -127,6 +142,8 @@ class MutantRunner( val static = mapPureMutants(staticMutants, staticMutant(_)) // Map all no-coverage mutants val noCoverage = mapPureMutants(noCoverageMutants, (m: Mutant) => NoCoverage(m)) + // Map all no-compiling mutants + val noCompiling = mapPureMutants(compilerErrorMutants, (m: Mutant) => CompilerError(m)) // Run all testable mutants val totalTestableMutants = testableMutants.size @@ -140,14 +157,14 @@ class MutantRunner( // Back to per-file structure implicit val pathOrdering: Ordering[Path] = implicitly[Ordering[nio.file.Path]].on[Path](_.toNioPath) - (static ++ noCoverage ++ testedMutants) + (static ++ noCoverage ++ noCompiling ++ testedMutants) .fold(SortedMap.empty[Path, Seq[MutantRunResult]]) { case (resultsMap, (path, result)) => val results = resultsMap.getOrElse(path, Seq.empty) :+ result resultsMap + (path -> results) } .compile .lastOrError - .map(_.map { case (k, v) => (k -> v.sortBy(_.mutant.id)) }) + .map(_.map { case (k, v) => (k -> v.sortBy(_.mutant.id.globalId)) }) } def initialTestRun(testRunner: TestRunner): IO[CoverageExclusions] = { diff --git a/core/src/main/scala/stryker4s/run/Stryker4sRunner.scala b/core/src/main/scala/stryker4s/run/Stryker4sRunner.scala index 6bf68b14c..f96ee7c5f 100644 --- a/core/src/main/scala/stryker4s/run/Stryker4sRunner.scala +++ b/core/src/main/scala/stryker4s/run/Stryker4sRunner.scala @@ -26,11 +26,12 @@ abstract class Stryker4sRunner(implicit log: Logger) { val stryker4s = new Stryker4s( resolveMutatesFileSource, - new Mutator( - new MutantFinder(new MutantMatcher), - new StatementTransformer, - resolveMatchBuilder - ), + () => + new Mutator( + new MutantFinder(new MutantMatcher), + new StatementTransformer, + resolveMatchBuilder + ), new MutantRunner(createTestRunnerPool, resolveFilesFileSource, new AggregateReporter(resolveReporters())) ) diff --git a/core/src/test/scala/stryker4s/Stryker4sTest.scala b/core/src/test/scala/stryker4s/Stryker4sTest.scala index 8efeb9643..1bf4c21c5 100644 --- a/core/src/test/scala/stryker4s/Stryker4sTest.scala +++ b/core/src/test/scala/stryker4s/Stryker4sTest.scala @@ -36,11 +36,12 @@ class Stryker4sTest extends Stryker4sIOSuite with MockitoIOSuite with Inside wit val sut = new Stryker4s( testSourceCollector, - new Mutator( - new MutantFinder(new MutantMatcher), - new StatementTransformer, - new MatchBuilder(ActiveMutationContext.sysProps) - ), + () => + new Mutator( + new MutantFinder(new MutantMatcher), + new StatementTransformer, + new MatchBuilder(ActiveMutationContext.sysProps) + ), testMutantRunner ) diff --git a/core/src/test/scala/stryker4s/mutants/AddAllMutationsTest.scala b/core/src/test/scala/stryker4s/mutants/AddAllMutationsTest.scala index a3d63d234..7ff006e12 100644 --- a/core/src/test/scala/stryker4s/mutants/AddAllMutationsTest.scala +++ b/core/src/test/scala/stryker4s/mutants/AddAllMutationsTest.scala @@ -86,8 +86,8 @@ class AddAllMutationsTest extends Stryker4sSuite with LogMatchers { transformed.transformedStatements .flatMap(_.mutantStatements) .foreach { mutantStatement => - mutatedTree - .find(p"Some(${Lit.Int(mutantStatement.id)})") + mutatedTree._1 + .find(p"Some(${Lit.Int(mutantStatement.id.globalId)})") .getOrElse( fail { val mutant = foundMutants.find(_.id == mutantStatement.id).get diff --git a/core/src/test/scala/stryker4s/mutants/applymutants/CoverageMatchBuilderTest.scala b/core/src/test/scala/stryker4s/mutants/applymutants/CoverageMatchBuilderTest.scala index c5f83c361..4ba2ea9cd 100644 --- a/core/src/test/scala/stryker4s/mutants/applymutants/CoverageMatchBuilderTest.scala +++ b/core/src/test/scala/stryker4s/mutants/applymutants/CoverageMatchBuilderTest.scala @@ -2,7 +2,7 @@ package stryker4s.mutants.applymutants import stryker4s.extension.TreeExtensions.IsEqualExtension import stryker4s.extension.mutationtype.GreaterThan -import stryker4s.model.{Mutant, TransformedMutants} +import stryker4s.model.{Mutant, MutantId, TransformedMutants} import stryker4s.scalatest.LogMatchers import stryker4s.testutil.Stryker4sSuite @@ -15,7 +15,7 @@ class CoverageMatchBuilderTest extends Stryker4sSuite with LogMatchers { val ids = Iterator.from(0) val originalStatement = q"x >= 15" val mutants = List(q"x > 15", q"x <= 15") - .map(Mutant(ids.next(), originalStatement, _, GreaterThan)) + .map(Mutant(MutantId(ids.next()), originalStatement, _, GreaterThan)) val sut = new CoverageMatchBuilder(ActiveMutationContext.testRunner) // Act @@ -30,7 +30,7 @@ class CoverageMatchBuilderTest extends Stryker4sSuite with LogMatchers { val ids = Iterator.from(0) val originalStatement = q"x >= 15" val mutants = List(q"x > 15", q"x <= 15") - .map(Mutant(ids.next(), originalStatement, _, GreaterThan)) + .map(Mutant(MutantId(ids.next()), originalStatement, _, GreaterThan)) val sut = new CoverageMatchBuilder(ActiveMutationContext.testRunner) // Act diff --git a/core/src/test/scala/stryker4s/mutants/applymutants/MatchBuilderTest.scala b/core/src/test/scala/stryker4s/mutants/applymutants/MatchBuilderTest.scala index 7568baec4..26eca8e3f 100644 --- a/core/src/test/scala/stryker4s/mutants/applymutants/MatchBuilderTest.scala +++ b/core/src/test/scala/stryker4s/mutants/applymutants/MatchBuilderTest.scala @@ -1,11 +1,10 @@ package stryker4s.mutants.applymutants import scala.meta._ - import stryker4s.extension.TreeExtensions._ import stryker4s.extension.exception.UnableToBuildPatternMatchException import stryker4s.extension.mutationtype._ -import stryker4s.model.{Mutant, SourceTransformations, TransformedMutants} +import stryker4s.model.{Mutant, MutantId, SourceTransformations, TransformedMutants} import stryker4s.scalatest.LogMatchers import stryker4s.testutil.Stryker4sSuite @@ -17,7 +16,7 @@ class MatchBuilderTest extends Stryker4sSuite with LogMatchers { val ids = Iterator.from(0) val originalStatement = q"x >= 15" val mutants = List(q"x > 15", q"x <= 15") - .map(Mutant(ids.next(), originalStatement, _, GreaterThan)) + .map(Mutant(MutantId(ids.next()), originalStatement, _, GreaterThan)) val sut = new MatchBuilder(ActiveMutationContext.testRunner) // Act @@ -53,7 +52,7 @@ class MatchBuilderTest extends Stryker4sSuite with LogMatchers { // Assert "Failed to construct pattern match: original statement [true]" shouldBe loggedAsError - "Failed mutation(s) Mutant(0,true,false,EmptyString)." shouldBe loggedAsError + "Failed mutation(s) Mutant(MutantId(0,,-1),true,false,EmptyString)." shouldBe loggedAsError "at Input.String(\"class Foo { def foo = true }\"):1:23" shouldBe loggedAsError "This is likely an issue on Stryker4s's end, please enable debug logging and restart Stryker4s." shouldBe loggedAsError @@ -66,12 +65,12 @@ class MatchBuilderTest extends Stryker4sSuite with LogMatchers { val source = source"""class Foo { def bar: Boolean = 15 > 14 }""" val failedMutants = List( - Mutant(0, q"foo", q"bar", GreaterThan), - Mutant(1, q"baz", q"qux", GreaterThan) + Mutant(MutantId(0), q"foo", q"bar", GreaterThan), + Mutant(MutantId(1), q"baz", q"qux", GreaterThan) ) val successfulMutants = List( - Mutant(2, q">", q"15 < 14", GreaterThan), - Mutant(3, q">", q"15 <= 14", GreaterThan) + Mutant(MutantId(2), q">", q"15 < 14", GreaterThan), + Mutant(MutantId(3), q">", q"15 <= 14", GreaterThan) ) val transformed = TransformedMutants(q"14 < 15", failedMutants) val successfulTransformed = TransformedMutants(source.find(q"15 > 14").value, successfulMutants) @@ -79,7 +78,7 @@ class MatchBuilderTest extends Stryker4sSuite with LogMatchers { val sut = new MatchBuilder(ActiveMutationContext.testRunner) // Act - val result = sut.buildNewSource(transStatements) + val (result, _) = sut.buildNewSource(transStatements) // Assert val expected = source"""class Foo { @@ -110,7 +109,7 @@ class MatchBuilderTest extends Stryker4sSuite with LogMatchers { val sut = new MatchBuilder(ActiveMutationContext.sysProps) // Act - val result = sut.buildNewSource(transStatements) + val (result, _) = sut.buildNewSource(transStatements) // Assert val expected = @@ -140,7 +139,7 @@ class MatchBuilderTest extends Stryker4sSuite with LogMatchers { val sut = new MatchBuilder(ActiveMutationContext.testRunner) // Act - val result = sut.buildNewSource(transformedStatements) + val (result, _) = sut.buildNewSource(transformedStatements) // Assert val expected = @@ -180,7 +179,7 @@ class MatchBuilderTest extends Stryker4sSuite with LogMatchers { val sut = new MatchBuilder(ActiveMutationContext.testRunner) // Act - val result = sut.buildNewSource(transformedStatements) + val (result, _) = sut.buildNewSource(transformedStatements) // Assert val expected = @@ -220,7 +219,7 @@ class MatchBuilderTest extends Stryker4sSuite with LogMatchers { val sut = new MatchBuilder(ActiveMutationContext.testRunner) // Act - val result = sut.buildNewSource(transformedStatements) + val (result, _) = sut.buildNewSource(transformedStatements) // Assert val expected = source""" @@ -273,7 +272,7 @@ class MatchBuilderTest extends Stryker4sSuite with LogMatchers { val sut = new MatchBuilder(ActiveMutationContext.testRunner) // Act - val result = sut.buildNewSource(transformedStatements) + val (result, _) = sut.buildNewSource(transformedStatements) // Assert val expected = source"""class Foo() { @@ -318,7 +317,7 @@ class MatchBuilderTest extends Stryker4sSuite with LogMatchers { val sut = new MatchBuilder(ActiveMutationContext.testRunner) // Act - val result = sut.buildNewSource(transformedStatements) + val (result, _) = sut.buildNewSource(transformedStatements) // Assert val expected = source"""class Foo() { @@ -379,7 +378,7 @@ class MatchBuilderTest extends Stryker4sSuite with LogMatchers { val topStatement = source.find(origStatement).value.topStatement() val mutant = mutants .map(m => topStatement.transformOnce { case orig if orig.isEqual(origStatement) => m }.get) - .map(m => Mutant(ids.next(), topStatement, m.asInstanceOf[Term], mutation)) + .map(m => Mutant(MutantId(ids.next()), topStatement, m.asInstanceOf[Term], mutation)) .toList TransformedMutants(topStatement, mutant) diff --git a/core/src/test/scala/stryker4s/mutants/applymutants/StatementTransformerTest.scala b/core/src/test/scala/stryker4s/mutants/applymutants/StatementTransformerTest.scala index d146c3a74..1b79015d6 100644 --- a/core/src/test/scala/stryker4s/mutants/applymutants/StatementTransformerTest.scala +++ b/core/src/test/scala/stryker4s/mutants/applymutants/StatementTransformerTest.scala @@ -1,11 +1,10 @@ package stryker4s.mutants.applymutants import scala.meta._ - import stryker4s.extension.ImplicitMutationConversion.mutationToTree import stryker4s.extension.TreeExtensions._ import stryker4s.extension.mutationtype._ -import stryker4s.model.Mutant +import stryker4s.model.{Mutant, MutantId} import stryker4s.testutil.Stryker4sSuite class StatementTransformerTest extends Stryker4sSuite { @@ -64,7 +63,7 @@ class StatementTransformerTest extends Stryker4sSuite { val originalTopTree = q"val x: Boolean = 15 >= 5" val originalTree = originalTopTree.find(q">=").value val mutants = List(EqualTo, GreaterThan, LesserThanEqualTo) - .map(Mutant(0, originalTree, _, GreaterThanEqualTo)) + .map(Mutant(MutantId(0), originalTree, _, GreaterThanEqualTo)) // Act val transformedMutant = sut.transformMutant(originalTree, mutants) @@ -84,7 +83,7 @@ class StatementTransformerTest extends Stryker4sSuite { val source = "object Foo { def bar: Boolean = 15 >= 4 }".parse[Source].get val origTree = source.find(q">=").value val mutants = List(EqualTo, GreaterThan, LesserThanEqualTo) - .map(Mutant(0, origTree, _, GreaterThanEqualTo)) + .map(Mutant(MutantId(0), origTree, _, GreaterThanEqualTo)) // Act val result = sut.transformSource(source, mutants) @@ -103,11 +102,11 @@ class StatementTransformerTest extends Stryker4sSuite { val firstOrigTree = source.find(q">=").value val firstMutants: Seq[Mutant] = List(EqualTo, GreaterThan, LesserThanEqualTo) - .map(Mutant(0, firstOrigTree, _, GreaterThanEqualTo)) + .map(Mutant(MutantId(0), firstOrigTree, _, GreaterThanEqualTo)) val secOrigTree = source.find(q"<").value val secondMutants: Seq[Mutant] = List(LesserThanEqualTo, GreaterThan, EqualTo) - .map(Mutant(0, secOrigTree, _, GreaterThanEqualTo)) + .map(Mutant(MutantId(0), secOrigTree, _, GreaterThanEqualTo)) val statements = firstMutants ++ secondMutants diff --git a/core/src/test/scala/stryker4s/mutants/findmutants/MutantMatcherTest.scala b/core/src/test/scala/stryker4s/mutants/findmutants/MutantMatcherTest.scala index 748e6fc5d..bdaa7e2da 100644 --- a/core/src/test/scala/stryker4s/mutants/findmutants/MutantMatcherTest.scala +++ b/core/src/test/scala/stryker4s/mutants/findmutants/MutantMatcherTest.scala @@ -540,7 +540,7 @@ class MutantMatcherTest extends Stryker4sSuite { val mutants = sut.TermExtensions(GreaterThan).~~>(LesserThan, GreaterThanEqualTo, EqualTo).collect { case Right(v) => v } - mutants.map(_.id) should contain theSameElementsAs List(0, 1, 2) + mutants.map(_.id.globalId) should contain theSameElementsAs List(0, 1, 2) } } diff --git a/core/src/test/scala/stryker4s/report/mapper/MutantRunResultMapperTest.scala b/core/src/test/scala/stryker4s/report/mapper/MutantRunResultMapperTest.scala index f9870ff08..333aaf823 100644 --- a/core/src/test/scala/stryker4s/report/mapper/MutantRunResultMapperTest.scala +++ b/core/src/test/scala/stryker4s/report/mapper/MutantRunResultMapperTest.scala @@ -7,7 +7,7 @@ import stryker4s.config.{Config, Thresholds => ConfigThresholds} import stryker4s.extension.FileExtensions._ import stryker4s.extension.ImplicitMutationConversion._ import stryker4s.extension.mutationtype._ -import stryker4s.model.{Killed, Mutant, Survived} +import stryker4s.model.{Killed, Mutant, MutantId, Survived} import stryker4s.scalatest.FileUtil import stryker4s.testutil.Stryker4sSuite @@ -78,6 +78,6 @@ class MutantRunResultMapperTest extends Stryker4sSuite with Inside { import scala.meta._ val parsed = file.toNioPath.parse[Source] val foundOrig = parsed.get.find(original).value - Mutant(id, foundOrig, category.tree, category) + Mutant(MutantId(id), foundOrig, category.tree, category) } } diff --git a/core/src/test/scala/stryker4s/run/MutantRunnerTest.scala b/core/src/test/scala/stryker4s/run/MutantRunnerTest.scala index 10ff8620a..90dcc2a1d 100644 --- a/core/src/test/scala/stryker4s/run/MutantRunnerTest.scala +++ b/core/src/test/scala/stryker4s/run/MutantRunnerTest.scala @@ -3,7 +3,7 @@ package stryker4s.run import org.mockito.captor.ArgCaptor import stryker4s.config.Config import stryker4s.extension.mutationtype.EmptyString -import stryker4s.model.{Killed, Mutant, MutatedFile, Survived} +import stryker4s.model.{Killed, Mutant, MutantId, MutatedFile, Survived} import stryker4s.report.{FinishedRunEvent, Reporter} import stryker4s.scalatest.{FileUtil, LogMatchers} import stryker4s.testutil.stubs.{TestFileResolver, TestRunnerStub} @@ -21,17 +21,17 @@ class MutantRunnerTest extends Stryker4sIOSuite with MockitoIOSuite with LogMatc val reporterMock = mock[Reporter] whenF(reporterMock.onRunFinished(any[FinishedRunEvent])).thenReturn(()) when(reporterMock.mutantTested).thenReturn(_.drain) - val mutant = Mutant(3, q"0", q"zero", EmptyString) - val secondMutant = Mutant(1, q"1", q"one", EmptyString) - val thirdMutant = Mutant(2, q"5", q"5", EmptyString) + val mutant = Mutant(MutantId(3, "scalaFiles/simpleFile.scala", 3), q"0", q"zero", EmptyString) + val secondMutant = Mutant(MutantId(1, "scalaFiles/simpleFile.scala", 1), q"1", q"one", EmptyString) + val thirdMutant = Mutant(MutantId(2, "scalaFiles/simpleFile.scala", 2), q"5", q"5", EmptyString) val testRunner = TestRunnerStub.withResults(Killed(mutant), Killed(secondMutant), Survived(thirdMutant)) val sut = new MutantRunner(testRunner, fileCollectorMock, reporterMock) val file = FileUtil.getResource("scalaFiles/simpleFile.scala") val mutants = Seq(mutant, secondMutant, thirdMutant) - val mutatedFile = MutatedFile(file, q"def foo = 4", mutants, 0) + val mutatedFile = MutatedFile(file, q"def foo = 4", mutants, Seq.empty, 0) - sut(List(mutatedFile)).asserting { result => + sut(List(mutatedFile), Seq.empty).asserting { result => val captor = ArgCaptor[FinishedRunEvent] verify(reporterMock, times(1)).onRunFinished(captor.capture) val runReport = captor.value.report.files.loneElement diff --git a/core/src/test/scala/stryker4s/testutil/TestData.scala b/core/src/test/scala/stryker4s/testutil/TestData.scala index 841ff46a0..eec5c2b10 100644 --- a/core/src/test/scala/stryker4s/testutil/TestData.scala +++ b/core/src/test/scala/stryker4s/testutil/TestData.scala @@ -4,14 +4,14 @@ import fs2.io.file.Path import mutationtesting.{Metrics, MetricsResult, MutationTestResult, Thresholds} import stryker4s.config.Config import stryker4s.extension.mutationtype.GreaterThan -import stryker4s.model.Mutant +import stryker4s.model.{Mutant, MutantId} import stryker4s.report.FinishedRunEvent import scala.concurrent.duration._ import scala.meta._ trait TestData { - def createMutant = Mutant(0, q"<", q">", GreaterThan) + def createMutant = Mutant(MutantId(0), q"<", q">", GreaterThan) def createMutationTestResult = MutationTestResult(thresholds = Thresholds(100, 0), files = Map.empty) diff --git a/core/src/test/scala/stryker4s/testutil/stubs/TestRunnerStub.scala b/core/src/test/scala/stryker4s/testutil/stubs/TestRunnerStub.scala index 44edd3ac3..77619ec44 100644 --- a/core/src/test/scala/stryker4s/testutil/stubs/TestRunnerStub.scala +++ b/core/src/test/scala/stryker4s/testutil/stubs/TestRunnerStub.scala @@ -5,7 +5,7 @@ import cats.effect.{IO, Resource} import cats.syntax.applicativeError._ import fs2.io.file.Path import stryker4s.extension.mutationtype.LesserThan -import stryker4s.model.{InitialTestRunResult, Killed, Mutant, MutantRunResult, NoCoverageInitialTestRun} +import stryker4s.model.{InitialTestRunResult, Killed, Mutant, MutantId, MutantRunResult, NoCoverageInitialTestRun} import stryker4s.run.{ResourcePool, TestRunner} import scala.meta._ @@ -24,7 +24,7 @@ class TestRunnerStub(results: Seq[() => MutantRunResult]) extends TestRunner { object TestRunnerStub { - def resource = withResults(Killed(Mutant(0, q">", q"<", LesserThan))) + def resource = withResults(Killed(Mutant(MutantId(0), q">", q"<", LesserThan))) def withResults(mutants: MutantRunResult*) = (_: Path) => ResourcePool(NonEmptyList.of(Resource.pure[IO, TestRunner](new TestRunnerStub(mutants.map(() => _))))) diff --git a/sbt/src/main/scala/stryker4s/sbt/Stryker4sSbtRunner.scala b/sbt/src/main/scala/stryker4s/sbt/Stryker4sSbtRunner.scala index 47c81ad06..46bb8d7eb 100644 --- a/sbt/src/main/scala/stryker4s/sbt/Stryker4sSbtRunner.scala +++ b/sbt/src/main/scala/stryker4s/sbt/Stryker4sSbtRunner.scala @@ -6,9 +6,11 @@ import fs2.io.file.Path import sbt.Keys._ import sbt._ import sbt.internal.LogManager +import stryker4s.{CompileError, MutationCompilationFailed} import stryker4s.config.{Config, TestFilter} import stryker4s.extension.FileExtensions._ import stryker4s.extension.exception.TestSetupException +import stryker4s.files.{FilesFileResolver, MutatesFileResolver, SbtFilesResolver, SbtMutatesResolver} import stryker4s.log.Logger import stryker4s.mutants.applymutants.ActiveMutationContext.ActiveMutationContext import stryker4s.mutants.applymutants.{ActiveMutationContext, CoverageMatchBuilder, MatchBuilder} @@ -18,10 +20,6 @@ import stryker4s.sbt.runner.{LegacySbtTestRunner, SbtTestRunner} import java.io.{File => JFile, PrintStream} import scala.concurrent.duration.FiniteDuration -import stryker4s.files.SbtMutatesResolver -import stryker4s.files.MutatesFileResolver -import stryker4s.files.FilesFileResolver -import stryker4s.files.SbtFilesResolver /** This Runner run Stryker mutations in a single SBT session * @@ -78,13 +76,47 @@ class Stryker4sSbtRunner( "io.stryker-mutator" %% "sbt-stryker4s-testrunner" % stryker4sVersion ) val newState = extracted.appendWithSession(fullSettings, state) - def extractTaskValue[T](task: TaskKey[T], name: String) = - Project.runTask(task, newState) match { + + def extractTaskValue[T](task: TaskKey[T], name: String) = { + + val ret = Project.runTask(task, newState) match { case Some((_, Value(result))) => result case other => log.debug(s"Expected $name but got $other") throw new TestSetupException(name) } + ret + } + + //SBT returns any errors as a Incomplete case class, which can contain other Incomplete instances + //You have to recursively search through them to get the real exception + def getRootCause(i: Incomplete): Seq[Throwable] = { + i.directCause match { + case None => + i.causes.flatMap(getRootCause) + case Some(cause) => + cause +: i.causes.flatMap(getRootCause) + } + } + + //See if the mutations compile, and if not extract the errors and throw an exception + Project.runTask(Compile / Keys.compile, newState) match { + case Some((_, Inc(cause))) => + val compileErrors = (getRootCause(cause) collect { case exception: sbt.internal.inc.CompileFailed => + exception.problems.flatMap { e => + for { + path <- e.position().sourceFile().asScala + pathStr = path.toPath.toAbsolutePath.toString.replace(tmpDir.toString, "") + line <- e.position().line().asScala + } yield CompileError(e.message(), pathStr, line) + }.toSeq + }).flatten.toList + + if (compileErrors.nonEmpty) { + throw MutationCompilationFailed(compileErrors) + } + case _ => + } val classpath = extractTaskValue(Test / fullClasspath, "classpath").map(_.data.getPath()) diff --git a/sbt/src/main/scala/stryker4s/sbt/runner/LegacySbtTestRunner.scala b/sbt/src/main/scala/stryker4s/sbt/runner/LegacySbtTestRunner.scala index e4905fb47..ce58578b7 100644 --- a/sbt/src/main/scala/stryker4s/sbt/runner/LegacySbtTestRunner.scala +++ b/sbt/src/main/scala/stryker4s/sbt/runner/LegacySbtTestRunner.scala @@ -23,7 +23,7 @@ class LegacySbtTestRunner(initialState: State, settings: Seq[Def.Setting[_]], ex def runMutant(mutant: Mutant): IO[MutantRunResult] = { val mutationState = - extracted.appendWithSession(settings :+ mutationSetting(mutant.id), initialState) + extracted.appendWithSession(settings :+ mutationSetting(mutant.id.globalId), initialState) runTests( mutationState, onError = { diff --git a/sbt/src/main/scala/stryker4s/sbt/runner/ProcessTestRunner.scala b/sbt/src/main/scala/stryker4s/sbt/runner/ProcessTestRunner.scala index a140853d1..a62dbf931 100644 --- a/sbt/src/main/scala/stryker4s/sbt/runner/ProcessTestRunner.scala +++ b/sbt/src/main/scala/stryker4s/sbt/runner/ProcessTestRunner.scala @@ -21,7 +21,7 @@ import scala.util.control.NonFatal class ProcessTestRunner(testProcess: TestRunnerConnection) extends TestRunner { override def runMutant(mutant: Mutant): IO[MutantRunResult] = { - val message = StartTestRun(mutant.id) + val message = StartTestRun(mutant.id.globalId) testProcess.sendMessage(message) map { case _: TestsSuccessful => Survived(mutant) case _: TestsUnsuccessful => Killed(mutant) From 6b871b2fe84ca9af91a04e64ffd52ebc83232bac Mon Sep 17 00:00:00 2001 From: Martin Welgemoed Date: Fri, 27 Aug 2021 12:14:22 +0200 Subject: [PATCH 02/26] Removed somewhat hacky second file id for mutants --- core/src/main/scala/stryker4s/Stryker4s.scala | 18 ++--- .../main/scala/stryker4s/model/Mutant.scala | 13 +-- .../scala/stryker4s/model/MutatedFile.scala | 24 +----- .../stryker4s/model/MutationsInSource.scala | 2 +- .../scala/stryker4s/mutants/Mutator.scala | 79 ++++++++++++++----- .../mutants/findmutants/MutantFinder.scala | 12 +-- .../scala/stryker4s/run/MutantRunner.scala | 15 ++-- .../applymutants/MatchBuilderTest.scala | 2 +- .../stryker4s/run/MutantRunnerTest.scala | 8 +- 9 files changed, 86 insertions(+), 87 deletions(-) diff --git a/core/src/main/scala/stryker4s/Stryker4s.scala b/core/src/main/scala/stryker4s/Stryker4s.scala index de7c24d71..7b2d409c0 100644 --- a/core/src/main/scala/stryker4s/Stryker4s.scala +++ b/core/src/main/scala/stryker4s/Stryker4s.scala @@ -4,7 +4,6 @@ import cats.effect.IO import mutationtesting.MetricsResult import stryker4s.config.Config import stryker4s.files.MutatesFileResolver -import stryker4s.model.MutantId import stryker4s.mutants.Mutator import stryker4s.run.MutantRunner import stryker4s.run.threshold.{ScoreStatus, ThresholdChecker} @@ -24,21 +23,18 @@ class Stryker4s(fileSource: MutatesFileResolver, mutatorFactory: () => Mutator, } //Retries the run, after removing the non-compiling mutants - def runWithRetry(nonCompilingMutants: Seq[MutantId]): IO[MetricsResult] = { + def runWithRetry(compilerErrors: Seq[CompileError]): IO[MetricsResult] = { //Recreate the mutator from the factory, otherwise the second run's ids will not start at 0 val mutator = mutatorFactory() val filesToMutate = fileSource.files for { - mutatedFiles <- mutator.mutate(filesToMutate, nonCompilingMutants) - metrics <- runner(mutatedFiles, nonCompilingMutants).handleErrorWith { + mutatedFiles <- mutator.mutate(filesToMutate, compilerErrors) + metrics <- runner(mutatedFiles).handleErrorWith { //If a compiler error occurs, retry once without the lines that gave an error - case MutationCompilationFailed(errs) if nonCompilingMutants.isEmpty => - val nonCompilingMutations = mutator.errorsToIds(errs, mutatedFiles) - if (nonCompilingMutations.nonEmpty) { - runWithRetry(nonCompilingMutations) - } else { - IO.raiseError(new RuntimeException("Unable to see which mutations caused the compiler failure")) - } + case MutationCompilationFailed(errs) if compilerErrors.isEmpty => + runWithRetry(errs) + case MutationCompilationFailed(_) => + IO.raiseError(new RuntimeException("Tried and failed to remove non-compiling mutants")) //Something else went wrong vOv case e => IO.raiseError(e) } diff --git a/core/src/main/scala/stryker4s/model/Mutant.scala b/core/src/main/scala/stryker4s/model/Mutant.scala index b4b6d1cfe..217249142 100644 --- a/core/src/main/scala/stryker4s/model/Mutant.scala +++ b/core/src/main/scala/stryker4s/model/Mutant.scala @@ -4,17 +4,8 @@ import scala.meta.{Term, Tree} import stryker4s.extension.mutationtype.Mutation -//The globalId is used in mutation switching and generating reports, but it varies between runs -//The file and idInFile is stable, and used for finding compiler errors -case class MutantId(globalId: Int, file: String, idInFile: Int) { - def sameMutation(otherMutId: MutantId): Boolean = { - otherMutId.idInFile == this.idInFile && otherMutId.file == this.file - } -} - -object MutantId { - //Initially mutants are created with just a globalId, the file information is added later on - def apply(globalId: Int): MutantId = new MutantId(globalId, "", -1) +case class MutantId(globalId: Int) extends AnyVal { + override def toString: String = globalId.toString } final case class Mutant(id: MutantId, original: Term, mutated: Term, mutationType: Mutation[_ <: Tree]) diff --git a/core/src/main/scala/stryker4s/model/MutatedFile.scala b/core/src/main/scala/stryker4s/model/MutatedFile.scala index 29ab2a512..6a5a98f64 100644 --- a/core/src/main/scala/stryker4s/model/MutatedFile.scala +++ b/core/src/main/scala/stryker4s/model/MutatedFile.scala @@ -8,33 +8,11 @@ final case class MutatedFile( fileOrigin: Path, tree: Tree, mutants: Seq[Mutant], - mutationStatements: Seq[(MutantId, Tree)], + nonCompilingMutants: Seq[Mutant], excludedMutants: Int ) { def mutatedSource: String = { tree.syntax } - - //Returns a map of line numbers to the mutant on that line - //Contains only mutated lines, and the same mutant can stretch over multiple multiple lines - //This logic is not very fast, because it has to search the entire tree, that's why it's lazy - lazy val mutantLineNumbers: Map[Int, MutantId] = { - val statementToMutIdMap = mutationStatements.map { case (mutantId, mutationStatement) => - mutationStatement.structure -> mutantId - }.toMap - - mutatedSource - .parse[Stat] //Parse as a standalone statement, used in unit tests and conceivable in some real code - .orElse(mutatedSource.parse[Source]) //Parse as a complete scala source file - .get //If both failed something has gone very badly wrong, give up - .collect { - case node if statementToMutIdMap.contains(node.structure) => - val mutId = statementToMutIdMap(node.structure) - //+1 because scalameta uses zero-indexed line numbers - (node.pos.startLine to node.pos.endLine).map(i => i + 1 -> mutId) - } - .flatten - .toMap - } } diff --git a/core/src/main/scala/stryker4s/model/MutationsInSource.scala b/core/src/main/scala/stryker4s/model/MutationsInSource.scala index a93368aa8..41812d833 100644 --- a/core/src/main/scala/stryker4s/model/MutationsInSource.scala +++ b/core/src/main/scala/stryker4s/model/MutationsInSource.scala @@ -2,4 +2,4 @@ package stryker4s.model import scala.meta.Source -final case class MutationsInSource(source: Source, mutants: Seq[Mutant], excluded: Int) +final case class MutationsInSource(source: Source, mutants: Seq[Mutant], excluded: Int, fileName: String) diff --git a/core/src/main/scala/stryker4s/mutants/Mutator.scala b/core/src/main/scala/stryker4s/mutants/Mutator.scala index 697cbe930..91fe7f333 100644 --- a/core/src/main/scala/stryker4s/mutants/Mutator.scala +++ b/core/src/main/scala/stryker4s/mutants/Mutator.scala @@ -12,32 +12,20 @@ import stryker4s.model.{MutantId, MutatedFile, MutationsInSource, SourceTransfor import stryker4s.mutants.applymutants.{MatchBuilder, StatementTransformer} import stryker4s.mutants.findmutants.MutantFinder +import scala.meta._ + class Mutator(mutantFinder: MutantFinder, transformer: StatementTransformer, matchBuilder: MatchBuilder)(implicit config: Config, log: Logger ) { - //Given compiler errors, return the mutants that caused it - def errorsToIds(compileError: Seq[CompileError], files: Seq[MutatedFile]): Seq[MutantId] = { - compileError.flatMap { err => - files - //Find the file that the compiler error came from - .find(_.fileOrigin.toString.endsWith(err.path)) - //Find the mutant case statement that cased the compiler error - .flatMap(file => file.mutantLineNumbers.get(err.line)) - } - } - - def mutate(files: Stream[IO, Path], nonCompilingMutants: Seq[MutantId] = Seq.empty): IO[Seq[MutatedFile]] = { + def mutate(files: Stream[IO, Path], compileErrors: Seq[CompileError] = Seq.empty): IO[Seq[MutatedFile]] = { files .parEvalMapUnordered(config.concurrency)(p => findMutants(p).tupleLeft(p)) .map { case (file, mutationsInSource) => - val validMutants = - mutationsInSource.mutants.filterNot(mut => nonCompilingMutants.exists(_.sameMutation(mut.id))) - val transformed = transformStatements(mutationsInSource.copy(mutants = validMutants)) - val (builtTree, mutations) = buildMatches(transformed) - - MutatedFile(file, builtTree, mutationsInSource.mutants, mutations, mutationsInSource.excluded) + val errorsInThisFile = compileErrors.filter(err => file.toString.endsWith(err.path)) + println(s"$file $errorsInThisFile $compileErrors ${file.toString}") + mutateFile(file, mutationsInSource, errorsInThisFile) } .filterNot(mutatedFile => mutatedFile.mutants.isEmpty && mutatedFile.excludedMutants == 0) .compile @@ -45,6 +33,61 @@ class Mutator(mutantFinder: MutantFinder, transformer: StatementTransformer, mat .flatTap(logMutationResult) } + private def mutateFile( + file: Path, + mutationsInSource: MutationsInSource, + compileErrors: Seq[CompileError] + ): MutatedFile = { + val transformed = transformStatements(mutationsInSource) + val (builtTree, mutations) = buildMatches(transformed) + + //If there are any compiler errors (i.e. we're currently retrying the mutation) + //Then we take the original tree built that didn't compile + //And then we search inside it to translate the compile errors to mutants + //Finally we rebuild it from scratch without those mutants + //This is not very performant, but you only pay the cost if there actually is a compiler error + val mutatedFile = MutatedFile(file, builtTree, mutationsInSource.mutants, Seq.empty, mutationsInSource.excluded) + if (compileErrors.isEmpty) { + mutatedFile + } else { + val nonCompiligIds = errorsToIds(compileErrors, mutatedFile.mutatedSource, mutations) + val (nonCompilingMutants, compilingMutants) = + mutationsInSource.mutants.partition(mut => nonCompiligIds.contains(mut.id)) + + val transformed = transformStatements(mutationsInSource.copy(mutants = compilingMutants)) + val (builtTree, _) = buildMatches(transformed) + MutatedFile(file, builtTree, compilingMutants, nonCompilingMutants, mutationsInSource.excluded) + } + } + + //Given compiler errors, return the mutants that caused it + private def errorsToIds( + compileErrors: Seq[CompileError], + mutatedFile: String, + mutants: Seq[(MutantId, Case)] + ): Seq[MutantId] = { + val statementToMutIdMap = mutants.map { case (mutantId, mutationStatement) => + mutationStatement.structure -> mutantId + }.toMap + + val lineToMutantId: Map[Int, MutantId] = mutatedFile + .parse[Stat] //Parse as a standalone statement, used in unit tests and conceivable in some real code + .orElse(mutatedFile.parse[Source]) //Parse as a complete scala source file + .get //If both failed something has gone very badly wrong, give up + .collect { + case node if statementToMutIdMap.contains(node.structure) => + val mutId = statementToMutIdMap(node.structure) + //+1 because scalameta uses zero-indexed line numbers + (node.pos.startLine to node.pos.endLine).map(i => i + 1 -> mutId) + } + .flatten + .toMap + + compileErrors.flatMap { err => + lineToMutantId.get(err.line) + } + } + /** Step 1: Find mutants in the found files */ private def findMutants(file: Path): IO[MutationsInSource] = mutantFinder.mutantsInFile(file) diff --git a/core/src/main/scala/stryker4s/mutants/findmutants/MutantFinder.scala b/core/src/main/scala/stryker4s/mutants/findmutants/MutantFinder.scala index 98354e34c..286720ab1 100644 --- a/core/src/main/scala/stryker4s/mutants/findmutants/MutantFinder.scala +++ b/core/src/main/scala/stryker4s/mutants/findmutants/MutantFinder.scala @@ -6,7 +6,7 @@ import fs2.io.file.Path import stryker4s.config.Config import stryker4s.extension.FileExtensions._ import stryker4s.log.Logger -import stryker4s.model.{Mutant, MutantId, MutationExcluded, MutationsInSource, RegexParseError} +import stryker4s.model.{Mutant, MutationExcluded, MutationsInSource, RegexParseError} import scala.meta.parsers.XtensionParseInputLike import scala.meta.{Dialect, Parsed, Source} @@ -15,15 +15,7 @@ class MutantFinder(matcher: MutantMatcher)(implicit config: Config, log: Logger) def mutantsInFile(filePath: Path): IO[MutationsInSource] = for { parsedSource <- parseFile(filePath) (included, excluded) <- IO(findMutants(parsedSource)) - } yield MutationsInSource(parsedSource, addFileInfo(filePath.toString, included), excluded) - - def addFileInfo(filePath: String, mutants: Seq[Mutant]): Seq[Mutant] = { - mutants.zipWithIndex.map { case (mut, numInFile) => - //Assign the file and the index of the mutation in the file to the MutantId - //This is used later to trace potential compiler errors back to the mutation - mut.copy(id = MutantId(mut.id.globalId, filePath, numInFile)) - } - } + } yield MutationsInSource(parsedSource, included, excluded, filePath.toString) def findMutants(source: Source): (Seq[Mutant], Int) = { val (ignored, included) = source.collect(matcher.allMatchers).flatten.partitionEither(identity) diff --git a/core/src/main/scala/stryker4s/run/MutantRunner.scala b/core/src/main/scala/stryker4s/run/MutantRunner.scala index 4529cd831..cd7d3276e 100644 --- a/core/src/main/scala/stryker4s/run/MutantRunner.scala +++ b/core/src/main/scala/stryker4s/run/MutantRunner.scala @@ -26,14 +26,14 @@ class MutantRunner( )(implicit config: Config, log: Logger) extends MutantRunResultMapper { - def apply(mutatedFiles: Seq[MutatedFile], nonCompilingMutants: Seq[MutantId]): IO[MetricsResult] = { + def apply(mutatedFiles: Seq[MutatedFile]): IO[MetricsResult] = { prepareEnv(mutatedFiles) .flatMap(createTestRunnerPool) .use { testRunnerPool => testRunnerPool.loan .use(initialTestRun) .flatMap { coverageExclusions => - runMutants(mutatedFiles, testRunnerPool, coverageExclusions, nonCompilingMutants).timed + runMutants(mutatedFiles, testRunnerPool, coverageExclusions).timed } } .flatMap(t => createAndReportResults(t._1, t._2)) @@ -98,22 +98,21 @@ class MutantRunner( private def runMutants( mutatedFiles: Seq[MutatedFile], testRunnerPool: TestRunnerPool, - coverageExclusions: CoverageExclusions, - nonCompilingMutants: Seq[MutantId] + coverageExclusions: CoverageExclusions ): IO[Map[Path, Seq[MutantRunResult]]] = { val allMutants = mutatedFiles.flatMap(m => m.mutants.toList.map(m.fileOrigin.relativePath -> _)) val (staticMutants, rest) = allMutants.partition(m => coverageExclusions.staticMutants.contains(m._2.id.globalId)) - val (compilerErrorMutants, rest2) = - rest.partition(mut => nonCompilingMutants.exists(_.sameMutation(mut._2.id))) - val (noCoverageMutants, testableMutants) = - rest2.partition(m => + rest.partition(m => coverageExclusions.hasCoverage && !coverageExclusions.coveredMutants.contains(m._2.id.globalId) ) + val compilerErrorMutants = + mutatedFiles.flatMap(m => m.nonCompilingMutants.toList.map(m.fileOrigin.relativePath -> _)) + if (noCoverageMutants.nonEmpty) { log.info( s"${noCoverageMutants.size} mutants detected as having no code coverage. They will be skipped and marked as NoCoverage" diff --git a/core/src/test/scala/stryker4s/mutants/applymutants/MatchBuilderTest.scala b/core/src/test/scala/stryker4s/mutants/applymutants/MatchBuilderTest.scala index 26eca8e3f..94a5421fc 100644 --- a/core/src/test/scala/stryker4s/mutants/applymutants/MatchBuilderTest.scala +++ b/core/src/test/scala/stryker4s/mutants/applymutants/MatchBuilderTest.scala @@ -52,7 +52,7 @@ class MatchBuilderTest extends Stryker4sSuite with LogMatchers { // Assert "Failed to construct pattern match: original statement [true]" shouldBe loggedAsError - "Failed mutation(s) Mutant(MutantId(0,,-1),true,false,EmptyString)." shouldBe loggedAsError + "Failed mutation(s) Mutant(0,true,false,EmptyString)." shouldBe loggedAsError "at Input.String(\"class Foo { def foo = true }\"):1:23" shouldBe loggedAsError "This is likely an issue on Stryker4s's end, please enable debug logging and restart Stryker4s." shouldBe loggedAsError diff --git a/core/src/test/scala/stryker4s/run/MutantRunnerTest.scala b/core/src/test/scala/stryker4s/run/MutantRunnerTest.scala index 90dcc2a1d..fbb8e0557 100644 --- a/core/src/test/scala/stryker4s/run/MutantRunnerTest.scala +++ b/core/src/test/scala/stryker4s/run/MutantRunnerTest.scala @@ -21,9 +21,9 @@ class MutantRunnerTest extends Stryker4sIOSuite with MockitoIOSuite with LogMatc val reporterMock = mock[Reporter] whenF(reporterMock.onRunFinished(any[FinishedRunEvent])).thenReturn(()) when(reporterMock.mutantTested).thenReturn(_.drain) - val mutant = Mutant(MutantId(3, "scalaFiles/simpleFile.scala", 3), q"0", q"zero", EmptyString) - val secondMutant = Mutant(MutantId(1, "scalaFiles/simpleFile.scala", 1), q"1", q"one", EmptyString) - val thirdMutant = Mutant(MutantId(2, "scalaFiles/simpleFile.scala", 2), q"5", q"5", EmptyString) + val mutant = Mutant(MutantId(3), q"0", q"zero", EmptyString) + val secondMutant = Mutant(MutantId(1), q"1", q"one", EmptyString) + val thirdMutant = Mutant(MutantId(2), q"5", q"5", EmptyString) val testRunner = TestRunnerStub.withResults(Killed(mutant), Killed(secondMutant), Survived(thirdMutant)) val sut = new MutantRunner(testRunner, fileCollectorMock, reporterMock) @@ -31,7 +31,7 @@ class MutantRunnerTest extends Stryker4sIOSuite with MockitoIOSuite with LogMatc val mutants = Seq(mutant, secondMutant, thirdMutant) val mutatedFile = MutatedFile(file, q"def foo = 4", mutants, Seq.empty, 0) - sut(List(mutatedFile), Seq.empty).asserting { result => + sut(List(mutatedFile)).asserting { result => val captor = ArgCaptor[FinishedRunEvent] verify(reporterMock, times(1)).onRunFinished(captor.capture) val runReport = captor.value.report.files.loneElement From 8cd27e6207a2b0708c9b90efe082f73f3dfefecb Mon Sep 17 00:00:00 2001 From: Martin Welgemoed Date: Fri, 27 Aug 2021 14:13:10 +0200 Subject: [PATCH 03/26] Restructured retry flow for compiler errors --- .../command/Stryker4sCommandRunner.scala | 5 +- core/src/main/scala/stryker4s/Stryker4s.scala | 30 ++-------- .../scala/stryker4s/model/CompileError.scala | 7 +++ .../stryker4s/model/MutantRunResult.scala | 2 +- .../scala/stryker4s/mutants/Mutator.scala | 4 +- .../report/mapper/MutantRunResultMapper.scala | 14 ++--- .../scala/stryker4s/run/MutantRunner.scala | 42 +++++++++---- .../scala/stryker4s/run/Stryker4sRunner.scala | 11 ++-- .../stryker4s/run/MutantRunnerTest.scala | 3 +- .../testutil/stubs/TestRunnerStub.scala | 2 +- .../stryker4s/sbt/Stryker4sSbtRunner.scala | 59 ++++++++++--------- 11 files changed, 96 insertions(+), 83 deletions(-) create mode 100644 core/src/main/scala/stryker4s/model/CompileError.scala diff --git a/command-runner/src/main/scala/stryker4s/command/Stryker4sCommandRunner.scala b/command-runner/src/main/scala/stryker4s/command/Stryker4sCommandRunner.scala index 8e7a31ccc..2e0b50204 100644 --- a/command-runner/src/main/scala/stryker4s/command/Stryker4sCommandRunner.scala +++ b/command-runner/src/main/scala/stryker4s/command/Stryker4sCommandRunner.scala @@ -7,6 +7,7 @@ import stryker4s.command.config.ProcessRunnerConfig import stryker4s.command.runner.ProcessTestRunner import stryker4s.config.Config import stryker4s.log.Logger +import stryker4s.model.CompileError import stryker4s.mutants.applymutants.ActiveMutationContext import stryker4s.mutants.applymutants.ActiveMutationContext.ActiveMutationContext import stryker4s.run.process.ProcessRunner @@ -21,12 +22,12 @@ class Stryker4sCommandRunner(processRunnerConfig: ProcessRunnerConfig, timeout: override def resolveTestRunners( tmpDir: Path - )(implicit config: Config): NonEmptyList[Resource[IO, TestRunner]] = { + )(implicit config: Config): Either[NonEmptyList[CompileError], NonEmptyList[Resource[IO, TestRunner]]] = { val innerTestRunner = Resource.pure[IO, TestRunner](new ProcessTestRunner(processRunnerConfig.testRunner, ProcessRunner(), tmpDir)) val withTimeout = TestRunner.timeoutRunner(timeout, innerTestRunner) - NonEmptyList.of(withTimeout) + Right(NonEmptyList.of(withTimeout)) } } diff --git a/core/src/main/scala/stryker4s/Stryker4s.scala b/core/src/main/scala/stryker4s/Stryker4s.scala index 7b2d409c0..cb4f826ae 100644 --- a/core/src/main/scala/stryker4s/Stryker4s.scala +++ b/core/src/main/scala/stryker4s/Stryker4s.scala @@ -1,43 +1,23 @@ package stryker4s import cats.effect.IO -import mutationtesting.MetricsResult import stryker4s.config.Config import stryker4s.files.MutatesFileResolver import stryker4s.mutants.Mutator import stryker4s.run.MutantRunner import stryker4s.run.threshold.{ScoreStatus, ThresholdChecker} -case class CompileError(msg: String, path: String, line: Integer) { - override def toString: String = s"$path:L$line: '$msg'" -} -case class MutationCompilationFailed(errors: Seq[CompileError]) extends RuntimeException(s"Compilation failed: $errors") - class Stryker4s(fileSource: MutatesFileResolver, mutatorFactory: () => Mutator, runner: MutantRunner)(implicit config: Config ) { def run(): IO[ScoreStatus] = { - runWithRetry(List.empty) - .map(metrics => ThresholdChecker.determineScoreStatus(metrics.mutationScore)) - } - - //Retries the run, after removing the non-compiling mutants - def runWithRetry(compilerErrors: Seq[CompileError]): IO[MetricsResult] = { - //Recreate the mutator from the factory, otherwise the second run's ids will not start at 0 - val mutator = mutatorFactory() val filesToMutate = fileSource.files + val mutator = mutatorFactory() + for { - mutatedFiles <- mutator.mutate(filesToMutate, compilerErrors) - metrics <- runner(mutatedFiles).handleErrorWith { - //If a compiler error occurs, retry once without the lines that gave an error - case MutationCompilationFailed(errs) if compilerErrors.isEmpty => - runWithRetry(errs) - case MutationCompilationFailed(_) => - IO.raiseError(new RuntimeException("Tried and failed to remove non-compiling mutants")) - //Something else went wrong vOv - case e => IO.raiseError(e) - } - } yield metrics + metrics <- runner(errors => mutator.mutate(filesToMutate, errors)) + scoreStatus = ThresholdChecker.determineScoreStatus(metrics.mutationScore) + } yield scoreStatus } } diff --git a/core/src/main/scala/stryker4s/model/CompileError.scala b/core/src/main/scala/stryker4s/model/CompileError.scala new file mode 100644 index 000000000..f5c9c4f89 --- /dev/null +++ b/core/src/main/scala/stryker4s/model/CompileError.scala @@ -0,0 +1,7 @@ +package stryker4s.model + +//This class is used to contain information about mutants that did not compile +//It essentially exists so that we don't have to pass around the SBT specific compiler exception +case class CompileError(msg: String, path: String, line: Integer) { + override def toString: String = s"$path:L$line: '$msg'" +} diff --git a/core/src/main/scala/stryker4s/model/MutantRunResult.scala b/core/src/main/scala/stryker4s/model/MutantRunResult.scala index d237bbb96..85d9302ac 100644 --- a/core/src/main/scala/stryker4s/model/MutantRunResult.scala +++ b/core/src/main/scala/stryker4s/model/MutantRunResult.scala @@ -23,4 +23,4 @@ final case class Error(mutant: Mutant, description: Option[String] = None) exten final case class Ignored(mutant: Mutant, description: Option[String] = None) extends MutantRunResult -final case class CompilerError(mutant: Mutant, description: Option[String] = None) extends MutantRunResult +final case class NotCompiling(mutant: Mutant, description: Option[String] = None) extends MutantRunResult diff --git a/core/src/main/scala/stryker4s/mutants/Mutator.scala b/core/src/main/scala/stryker4s/mutants/Mutator.scala index 91fe7f333..d9e4c1bde 100644 --- a/core/src/main/scala/stryker4s/mutants/Mutator.scala +++ b/core/src/main/scala/stryker4s/mutants/Mutator.scala @@ -4,11 +4,10 @@ import cats.effect.IO import cats.syntax.functor._ import fs2.Stream import fs2.io.file.Path -import stryker4s.CompileError import stryker4s.config.Config import stryker4s.extension.StreamExtensions._ import stryker4s.log.Logger -import stryker4s.model.{MutantId, MutatedFile, MutationsInSource, SourceTransformations} +import stryker4s.model.{CompileError, MutantId, MutatedFile, MutationsInSource, SourceTransformations} import stryker4s.mutants.applymutants.{MatchBuilder, StatementTransformer} import stryker4s.mutants.findmutants.MutantFinder @@ -24,7 +23,6 @@ class Mutator(mutantFinder: MutantFinder, transformer: StatementTransformer, mat .parEvalMapUnordered(config.concurrency)(p => findMutants(p).tupleLeft(p)) .map { case (file, mutationsInSource) => val errorsInThisFile = compileErrors.filter(err => file.toString.endsWith(err.path)) - println(s"$file $errorsInThisFile $compileErrors ${file.toString}") mutateFile(file, mutationsInSource, errorsInThisFile) } .filterNot(mutatedFile => mutatedFile.mutants.isEmpty && mutatedFile.excludedMutants == 0) diff --git a/core/src/main/scala/stryker4s/report/mapper/MutantRunResultMapper.scala b/core/src/main/scala/stryker4s/report/mapper/MutantRunResultMapper.scala index be987ac44..18691a2bb 100644 --- a/core/src/main/scala/stryker4s/report/mapper/MutantRunResultMapper.scala +++ b/core/src/main/scala/stryker4s/report/mapper/MutantRunResultMapper.scala @@ -56,13 +56,13 @@ trait MutantRunResultMapper { private def toMutantStatus(mutant: MutantRunResult): MutantStatus = mutant match { - case _: Survived => MutantStatus.Survived - case _: Killed => MutantStatus.Killed - case _: NoCoverage => MutantStatus.NoCoverage - case _: TimedOut => MutantStatus.Timeout - case _: Error => MutantStatus.RuntimeError - case _: Ignored => MutantStatus.Ignored - case _: CompilerError => MutantStatus.CompileError + case _: Survived => MutantStatus.Survived + case _: Killed => MutantStatus.Killed + case _: NoCoverage => MutantStatus.NoCoverage + case _: TimedOut => MutantStatus.Timeout + case _: Error => MutantStatus.RuntimeError + case _: Ignored => MutantStatus.Ignored + case _: NotCompiling => MutantStatus.CompileError } private def fileContentAsString(path: Path)(implicit config: Config): String = diff --git a/core/src/main/scala/stryker4s/run/MutantRunner.scala b/core/src/main/scala/stryker4s/run/MutantRunner.scala index cd7d3276e..1c5e16d33 100644 --- a/core/src/main/scala/stryker4s/run/MutantRunner.scala +++ b/core/src/main/scala/stryker4s/run/MutantRunner.scala @@ -1,5 +1,6 @@ package stryker4s.run +import cats.data.NonEmptyList import cats.effect.{IO, Resource} import cats.syntax.functor._ import fs2.io.file.{Files, Path} @@ -11,7 +12,7 @@ import stryker4s.extension.StreamExtensions._ import stryker4s.extension.exception.InitialTestRunFailedException import stryker4s.files.FilesFileResolver import stryker4s.log.Logger -import stryker4s.model._ +import stryker4s.model.{CompileError, _} import stryker4s.report.mapper.MutantRunResultMapper import stryker4s.report.{FinishedRunEvent, MutantTestedEvent, Reporter} @@ -20,23 +21,40 @@ import scala.collection.immutable.SortedMap import scala.concurrent.duration._ class MutantRunner( - createTestRunnerPool: Path => Resource[IO, TestRunnerPool], + createTestRunnerPool: Path => Either[NonEmptyList[CompileError], Resource[IO, TestRunnerPool]], fileResolver: FilesFileResolver, reporter: Reporter )(implicit config: Config, log: Logger) extends MutantRunResultMapper { - def apply(mutatedFiles: Seq[MutatedFile]): IO[MetricsResult] = { - prepareEnv(mutatedFiles) - .flatMap(createTestRunnerPool) - .use { testRunnerPool => - testRunnerPool.loan - .use(initialTestRun) - .flatMap { coverageExclusions => - runMutants(mutatedFiles, testRunnerPool, coverageExclusions).timed + def apply(mutateFiles: Seq[CompileError] => IO[Seq[MutatedFile]]): IO[MetricsResult] = { + mutateFiles(Seq.empty).flatMap { mutatedFiles => + run(mutatedFiles) + .flatMap { + case Right(metrics) => IO(Right(metrics)) + case Left(errors) => + mutateFiles(errors.toList).flatMap(run) + } + .map(_.getOrElse(throw new RuntimeException(s"Unable to remove non-compiling mutants"))) + } + } + + def run(mutatedFiles: Seq[MutatedFile]): IO[Either[NonEmptyList[CompileError], MetricsResult]] = { + prepareEnv(mutatedFiles).use { path => + createTestRunnerPool(path) match { + case Left(errs) => IO(Left(errs)) + case Right(testRunnerPoolResource) => + testRunnerPoolResource.use { testRunnerPool => + testRunnerPool.loan + .use(initialTestRun) + .flatMap { coverageExclusions => + runMutants(mutatedFiles, testRunnerPool, coverageExclusions).timed + } + .flatMap(t => createAndReportResults(t._1, t._2)) + .map(Right(_)) } } - .flatMap(t => createAndReportResults(t._1, t._2)) + } } def createAndReportResults(duration: FiniteDuration, runResults: Map[Path, Seq[MutantRunResult]]) = for { @@ -142,7 +160,7 @@ class MutantRunner( // Map all no-coverage mutants val noCoverage = mapPureMutants(noCoverageMutants, (m: Mutant) => NoCoverage(m)) // Map all no-compiling mutants - val noCompiling = mapPureMutants(compilerErrorMutants, (m: Mutant) => CompilerError(m)) + val noCompiling = mapPureMutants(compilerErrorMutants, (m: Mutant) => NotCompiling(m)) // Run all testable mutants val totalTestableMutants = testableMutants.size diff --git a/core/src/main/scala/stryker4s/run/Stryker4sRunner.scala b/core/src/main/scala/stryker4s/run/Stryker4sRunner.scala index f96ee7c5f..3a1d44e72 100644 --- a/core/src/main/scala/stryker4s/run/Stryker4sRunner.scala +++ b/core/src/main/scala/stryker4s/run/Stryker4sRunner.scala @@ -3,9 +3,8 @@ package stryker4s.run import cats.data.NonEmptyList import cats.effect.{IO, Resource} import fs2.io.file.Path -import stryker4s.Stryker4s import stryker4s.config._ -import stryker4s.files.{ConfigFilesResolver, DiskFileIO, FilesFileResolver, GlobFileResolver, MutatesFileResolver} +import stryker4s.files._ import stryker4s.log.Logger import stryker4s.mutants.Mutator import stryker4s.mutants.applymutants.ActiveMutationContext.ActiveMutationContext @@ -15,6 +14,8 @@ import stryker4s.report._ import stryker4s.report.dashboard.DashboardConfigProvider import stryker4s.run.process.ProcessRunner import stryker4s.run.threshold.ScoreStatus +import stryker4s.Stryker4s +import stryker4s.model.CompileError import sttp.client3.SttpBackend import sttp.client3.httpclient.fs2.HttpClientFs2Backend @@ -22,7 +23,7 @@ abstract class Stryker4sRunner(implicit log: Logger) { def run(): IO[ScoreStatus] = { implicit val config: Config = ConfigReader.readConfig() - val createTestRunnerPool = (path: Path) => ResourcePool(resolveTestRunners(path)) + val createTestRunnerPool = (path: Path) => resolveTestRunners(path).map(ResourcePool(_)) val stryker4s = new Stryker4s( resolveMutatesFileSource, @@ -60,7 +61,9 @@ abstract class Stryker4sRunner(implicit log: Logger) { def resolveMatchBuilder(implicit config: Config): MatchBuilder = new MatchBuilder(mutationActivation) - def resolveTestRunners(tmpDir: Path)(implicit config: Config): NonEmptyList[Resource[IO, stryker4s.run.TestRunner]] + def resolveTestRunners(tmpDir: Path)(implicit + config: Config + ): Either[NonEmptyList[CompileError], NonEmptyList[Resource[IO, stryker4s.run.TestRunner]]] def resolveMutatesFileSource(implicit config: Config): MutatesFileResolver = new GlobFileResolver( diff --git a/core/src/test/scala/stryker4s/run/MutantRunnerTest.scala b/core/src/test/scala/stryker4s/run/MutantRunnerTest.scala index fbb8e0557..0f65d840d 100644 --- a/core/src/test/scala/stryker4s/run/MutantRunnerTest.scala +++ b/core/src/test/scala/stryker4s/run/MutantRunnerTest.scala @@ -1,5 +1,6 @@ package stryker4s.run +import cats.effect.IO import org.mockito.captor.ArgCaptor import stryker4s.config.Config import stryker4s.extension.mutationtype.EmptyString @@ -31,7 +32,7 @@ class MutantRunnerTest extends Stryker4sIOSuite with MockitoIOSuite with LogMatc val mutants = Seq(mutant, secondMutant, thirdMutant) val mutatedFile = MutatedFile(file, q"def foo = 4", mutants, Seq.empty, 0) - sut(List(mutatedFile)).asserting { result => + sut(_ => IO(List(mutatedFile))).asserting { result => val captor = ArgCaptor[FinishedRunEvent] verify(reporterMock, times(1)).onRunFinished(captor.capture) val runReport = captor.value.report.files.loneElement diff --git a/core/src/test/scala/stryker4s/testutil/stubs/TestRunnerStub.scala b/core/src/test/scala/stryker4s/testutil/stubs/TestRunnerStub.scala index 77619ec44..a4a506300 100644 --- a/core/src/test/scala/stryker4s/testutil/stubs/TestRunnerStub.scala +++ b/core/src/test/scala/stryker4s/testutil/stubs/TestRunnerStub.scala @@ -27,5 +27,5 @@ object TestRunnerStub { def resource = withResults(Killed(Mutant(MutantId(0), q">", q"<", LesserThan))) def withResults(mutants: MutantRunResult*) = (_: Path) => - ResourcePool(NonEmptyList.of(Resource.pure[IO, TestRunner](new TestRunnerStub(mutants.map(() => _))))) + Right(ResourcePool(NonEmptyList.of(Resource.pure[IO, TestRunner](new TestRunnerStub(mutants.map(() => _)))))) } diff --git a/sbt/src/main/scala/stryker4s/sbt/Stryker4sSbtRunner.scala b/sbt/src/main/scala/stryker4s/sbt/Stryker4sSbtRunner.scala index 46bb8d7eb..70b284808 100644 --- a/sbt/src/main/scala/stryker4s/sbt/Stryker4sSbtRunner.scala +++ b/sbt/src/main/scala/stryker4s/sbt/Stryker4sSbtRunner.scala @@ -6,12 +6,12 @@ import fs2.io.file.Path import sbt.Keys._ import sbt._ import sbt.internal.LogManager -import stryker4s.{CompileError, MutationCompilationFailed} import stryker4s.config.{Config, TestFilter} import stryker4s.extension.FileExtensions._ import stryker4s.extension.exception.TestSetupException import stryker4s.files.{FilesFileResolver, MutatesFileResolver, SbtFilesResolver, SbtMutatesResolver} import stryker4s.log.Logger +import stryker4s.model.CompileError import stryker4s.mutants.applymutants.ActiveMutationContext.ActiveMutationContext import stryker4s.mutants.applymutants.{ActiveMutationContext, CoverageMatchBuilder, MatchBuilder} import stryker4s.run.{Stryker4sRunner, TestRunner} @@ -43,7 +43,7 @@ class Stryker4sSbtRunner( def resolveTestRunners( tmpDir: Path - )(implicit config: Config): NonEmptyList[Resource[IO, TestRunner]] = { + )(implicit config: Config): Either[NonEmptyList[CompileError], NonEmptyList[Resource[IO, TestRunner]]] = { def setupLegacySbtTestRunner( settings: Seq[Def.Setting[_]], extracted: Extracted @@ -67,7 +67,7 @@ class Stryker4sSbtRunner( def setupSbtTestRunner( settings: Seq[Def.Setting[_]], extracted: Extracted - ): NonEmptyList[Resource[IO, TestRunner]] = { + ): Either[NonEmptyList[CompileError], NonEmptyList[Resource[IO, TestRunner]]] = { val stryker4sVersion = this.getClass().getPackage().getImplementationVersion() log.debug(s"Resolved stryker4s version $stryker4sVersion") @@ -99,8 +99,8 @@ class Stryker4sSbtRunner( } } - //See if the mutations compile, and if not extract the errors and throw an exception - Project.runTask(Compile / Keys.compile, newState) match { + //See if the mutations compile, and if not extract the errors + val compilerErrors = Project.runTask(Compile / Keys.compile, newState) match { case Some((_, Inc(cause))) => val compileErrors = (getRootCause(cause) collect { case exception: sbt.internal.inc.CompileFailed => exception.problems.flatMap { e => @@ -112,33 +112,37 @@ class Stryker4sSbtRunner( }.toSeq }).flatten.toList - if (compileErrors.nonEmpty) { - throw MutationCompilationFailed(compileErrors) - } + NonEmptyList.fromList(compileErrors) case _ => + None } - val classpath = extractTaskValue(Test / fullClasspath, "classpath").map(_.data.getPath()) + compilerErrors match { + case Some(errors) => Left(errors) + case None => + val classpath = extractTaskValue(Test / fullClasspath, "classpath").map(_.data.getPath()) - val javaOpts = extractTaskValue(Test / javaOptions, "javaOptions") + val javaOpts = extractTaskValue(Test / javaOptions, "javaOptions") - val frameworks = extractTaskValue(Test / loadedTestFrameworks, "test frameworks").values.toSeq + val frameworks = extractTaskValue(Test / loadedTestFrameworks, "test frameworks").values.toSeq - val testGroups = extractTaskValue(Test / testGrouping, "testGrouping").map { group => - if (config.testFilter.isEmpty) group - else { - val testFilter = new TestFilter() - val filteredTests = group.tests.filter(t => testFilter.filter(t.name)) - group.copy(tests = filteredTests) - } - } + val testGroups = extractTaskValue(Test / testGrouping, "testGrouping").map { group => + if (config.testFilter.isEmpty) group + else { + val testFilter = new TestFilter() + val filteredTests = group.tests.filter(t => testFilter.filter(t.name)) + group.copy(tests = filteredTests) + } + } - log.info(s"Creating ${config.concurrency} test-runners") - val portStart = 13336 - val portRanges = NonEmptyList.fromListUnsafe((1 to config.concurrency).map(_ + portStart).toList) + log.info(s"Creating ${config.concurrency} test-runners") + val portStart = 13336 + val portRanges = NonEmptyList.fromListUnsafe((1 to config.concurrency).map(_ + portStart).toList) - portRanges.map { port => - SbtTestRunner.create(classpath, javaOpts, frameworks, testGroups, port, sharedTimeout) + val testRunners = portRanges.map { port => + SbtTestRunner.create(classpath, javaOpts, frameworks, testGroups, port, sharedTimeout) + } + Right(testRunners) } } @@ -187,9 +191,10 @@ class Stryker4sSbtRunner( val (settings, extracted) = extractSbtProject(tmpDir) - if (config.legacyTestRunner) - setupLegacySbtTestRunner(settings, extracted) - else + if (config.legacyTestRunner) { + //No compiler error handling in the legacy runner + Right(setupLegacySbtTestRunner(settings, extracted)) + } else setupSbtTestRunner(settings, extracted) } From 4a338bc838d3529a2df9dc23237fe3fc379498db Mon Sep 17 00:00:00 2001 From: Martin Welgemoed Date: Fri, 27 Aug 2021 14:41:00 +0200 Subject: [PATCH 04/26] Removed parse[Stat], because it always deals with complete source files --- core/src/main/scala/stryker4s/mutants/Mutator.scala | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/src/main/scala/stryker4s/mutants/Mutator.scala b/core/src/main/scala/stryker4s/mutants/Mutator.scala index d9e4c1bde..c0b6a9c07 100644 --- a/core/src/main/scala/stryker4s/mutants/Mutator.scala +++ b/core/src/main/scala/stryker4s/mutants/Mutator.scala @@ -7,7 +7,7 @@ import fs2.io.file.Path import stryker4s.config.Config import stryker4s.extension.StreamExtensions._ import stryker4s.log.Logger -import stryker4s.model.{CompileError, MutantId, MutatedFile, MutationsInSource, SourceTransformations} +import stryker4s.model._ import stryker4s.mutants.applymutants.{MatchBuilder, StatementTransformer} import stryker4s.mutants.findmutants.MutantFinder @@ -48,9 +48,9 @@ class Mutator(mutantFinder: MutantFinder, transformer: StatementTransformer, mat if (compileErrors.isEmpty) { mutatedFile } else { - val nonCompiligIds = errorsToIds(compileErrors, mutatedFile.mutatedSource, mutations) + val nonCompilingIds = errorsToIds(compileErrors, mutatedFile.mutatedSource, mutations) val (nonCompilingMutants, compilingMutants) = - mutationsInSource.mutants.partition(mut => nonCompiligIds.contains(mut.id)) + mutationsInSource.mutants.partition(mut => nonCompilingIds.contains(mut.id)) val transformed = transformStatements(mutationsInSource.copy(mutants = compilingMutants)) val (builtTree, _) = buildMatches(transformed) @@ -69,9 +69,9 @@ class Mutator(mutantFinder: MutantFinder, transformer: StatementTransformer, mat }.toMap val lineToMutantId: Map[Int, MutantId] = mutatedFile - .parse[Stat] //Parse as a standalone statement, used in unit tests and conceivable in some real code - .orElse(mutatedFile.parse[Source]) //Parse as a complete scala source file - .get //If both failed something has gone very badly wrong, give up + //Parsing the mutated tree again as a string is the only way to get the position info of the mutated statements + .parse[Source] + .getOrElse(throw new RuntimeException(s"Failed to parse $mutatedFile to remove non-compiling mutants")) .collect { case node if statementToMutIdMap.contains(node.structure) => val mutId = statementToMutIdMap(node.structure) From e03136abae04222f9b79e40ce30077b588e43b8b Mon Sep 17 00:00:00 2001 From: Martin Welgemoed Date: Fri, 27 Aug 2021 14:57:40 +0200 Subject: [PATCH 05/26] Improved code style in MatchBuilder --- core/src/main/scala/stryker4s/mutants/Mutator.scala | 2 +- .../mutants/applymutants/MatchBuilder.scala | 12 ++++++------ .../stryker4s/mutants/AddAllMutationsTest.scala | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/core/src/main/scala/stryker4s/mutants/Mutator.scala b/core/src/main/scala/stryker4s/mutants/Mutator.scala index c0b6a9c07..47ff596a3 100644 --- a/core/src/main/scala/stryker4s/mutants/Mutator.scala +++ b/core/src/main/scala/stryker4s/mutants/Mutator.scala @@ -62,7 +62,7 @@ class Mutator(mutantFinder: MutantFinder, transformer: StatementTransformer, mat private def errorsToIds( compileErrors: Seq[CompileError], mutatedFile: String, - mutants: Seq[(MutantId, Case)] + mutants: Seq[(MutantId, Tree)] ): Seq[MutantId] = { val statementToMutIdMap = mutants.map { case (mutantId, mutationStatement) => mutationStatement.structure -> mutantId diff --git a/core/src/main/scala/stryker4s/mutants/applymutants/MatchBuilder.scala b/core/src/main/scala/stryker4s/mutants/applymutants/MatchBuilder.scala index e75319cb8..c9c9ded1d 100644 --- a/core/src/main/scala/stryker4s/mutants/applymutants/MatchBuilder.scala +++ b/core/src/main/scala/stryker4s/mutants/applymutants/MatchBuilder.scala @@ -10,21 +10,21 @@ import scala.meta._ import scala.util.{Failure, Success} class MatchBuilder(mutationContext: ActiveMutationContext)(implicit log: Logger) { - def buildNewSource(transformedStatements: SourceTransformations): (Tree, Seq[(MutantId, Case)]) = { + def buildNewSource(transformedStatements: SourceTransformations): (Tree, Seq[(MutantId, Tree)]) = { val source = transformedStatements.source - groupTransformedStatements(transformedStatements).foldLeft((source, Seq.empty): (Tree, Seq[(MutantId, Case)])) { - (rest, mutants) => + groupTransformedStatements(transformedStatements).foldLeft((source, Seq.empty): (Tree, Seq[(MutantId, Tree)])) { + case ((tree, mutantCases), mutants) => val origStatement = mutants.originalStatement var isTransformed = false - rest._1 transformOnce { + tree transformOnce { case found if found.isEqual(origStatement) && found.pos == origStatement.pos => isTransformed = true buildMatch(mutants) } match { case Success(value) if isTransformed => - (value, rest._2 ++ mutants.mutantStatements.map(mut => (mut.id, mutantToCase(mut)))) + (value, mutantCases ++ mutants.mutantStatements.map(mut => (mut.id, mutantToCase(mut)))) case Success(value) => log.warn( s"Failed to add mutation(s) ${mutants.mutantStatements.map(_.id.globalId).mkString(", ")} to new mutated code" @@ -36,7 +36,7 @@ class MatchBuilder(mutationContext: ActiveMutationContext)(implicit log: Logger) log.warn( "Please open an issue on github with sample code of the mutation that failed: https://github.com/stryker-mutator/stryker4s/issues/new" ) - (value, rest._2) + (value, mutantCases) case Failure(exception) => log.error(s"Failed to construct pattern match: original statement [$origStatement]") log.error(s"Failed mutation(s) ${mutants.mutantStatements.mkString(",")}.") diff --git a/core/src/test/scala/stryker4s/mutants/AddAllMutationsTest.scala b/core/src/test/scala/stryker4s/mutants/AddAllMutationsTest.scala index 7ff006e12..17f961c1b 100644 --- a/core/src/test/scala/stryker4s/mutants/AddAllMutationsTest.scala +++ b/core/src/test/scala/stryker4s/mutants/AddAllMutationsTest.scala @@ -82,11 +82,11 @@ class AddAllMutationsTest extends Stryker4sSuite with LogMatchers { val source = source"class Foo { $tree }" val foundMutants = source.collect(new MutantMatcher().allMatchers).flatten.collect { case Right(v) => v } val transformed = new StatementTransformer().transformSource(source, foundMutants) - val mutatedTree = new MatchBuilder(ActiveMutationContext.testRunner).buildNewSource(transformed) + val (mutatedTree, _) = new MatchBuilder(ActiveMutationContext.testRunner).buildNewSource(transformed) transformed.transformedStatements .flatMap(_.mutantStatements) .foreach { mutantStatement => - mutatedTree._1 + mutatedTree .find(p"Some(${Lit.Int(mutantStatement.id.globalId)})") .getOrElse( fail { From 71897b0470066ba94a74ddfca84db640f13e46b2 Mon Sep 17 00:00:00 2001 From: Martin Welgemoed Date: Fri, 27 Aug 2021 15:13:50 +0200 Subject: [PATCH 06/26] Now only tracks mutant case statements after a compiler error --- .../scala/stryker4s/mutants/Mutator.scala | 14 ++-- .../mutants/applymutants/MatchBuilder.scala | 72 +++++++++---------- .../mutants/AddAllMutationsTest.scala | 2 +- .../applymutants/MatchBuilderTest.scala | 14 ++-- 4 files changed, 50 insertions(+), 52 deletions(-) diff --git a/core/src/main/scala/stryker4s/mutants/Mutator.scala b/core/src/main/scala/stryker4s/mutants/Mutator.scala index 47ff596a3..aadb89304 100644 --- a/core/src/main/scala/stryker4s/mutants/Mutator.scala +++ b/core/src/main/scala/stryker4s/mutants/Mutator.scala @@ -37,7 +37,7 @@ class Mutator(mutantFinder: MutantFinder, transformer: StatementTransformer, mat compileErrors: Seq[CompileError] ): MutatedFile = { val transformed = transformStatements(mutationsInSource) - val (builtTree, mutations) = buildMatches(transformed) + val builtTree = buildMatches(transformed) //If there are any compiler errors (i.e. we're currently retrying the mutation) //Then we take the original tree built that didn't compile @@ -48,12 +48,12 @@ class Mutator(mutantFinder: MutantFinder, transformer: StatementTransformer, mat if (compileErrors.isEmpty) { mutatedFile } else { - val nonCompilingIds = errorsToIds(compileErrors, mutatedFile.mutatedSource, mutations) + val nonCompilingIds = errorsToIds(compileErrors, mutatedFile.mutatedSource, mutationsInSource.mutants) val (nonCompilingMutants, compilingMutants) = mutationsInSource.mutants.partition(mut => nonCompilingIds.contains(mut.id)) val transformed = transformStatements(mutationsInSource.copy(mutants = compilingMutants)) - val (builtTree, _) = buildMatches(transformed) + val builtTree = buildMatches(transformed) MutatedFile(file, builtTree, compilingMutants, nonCompilingMutants, mutationsInSource.excluded) } } @@ -62,10 +62,10 @@ class Mutator(mutantFinder: MutantFinder, transformer: StatementTransformer, mat private def errorsToIds( compileErrors: Seq[CompileError], mutatedFile: String, - mutants: Seq[(MutantId, Tree)] + mutants: Seq[Mutant] ): Seq[MutantId] = { - val statementToMutIdMap = mutants.map { case (mutantId, mutationStatement) => - mutationStatement.structure -> mutantId + val statementToMutIdMap = mutants.map { mutant => + matchBuilder.mutantToCase(mutant).structure -> mutant.id }.toMap val lineToMutantId: Map[Int, MutantId] = mutatedFile @@ -73,7 +73,7 @@ class Mutator(mutantFinder: MutantFinder, transformer: StatementTransformer, mat .parse[Source] .getOrElse(throw new RuntimeException(s"Failed to parse $mutatedFile to remove non-compiling mutants")) .collect { - case node if statementToMutIdMap.contains(node.structure) => + case node: Case if statementToMutIdMap.contains(node.structure) => val mutId = statementToMutIdMap(node.structure) //+1 because scalameta uses zero-indexed line numbers (node.pos.startLine to node.pos.endLine).map(i => i + 1 -> mutId) diff --git a/core/src/main/scala/stryker4s/mutants/applymutants/MatchBuilder.scala b/core/src/main/scala/stryker4s/mutants/applymutants/MatchBuilder.scala index c9c9ded1d..d0811a982 100644 --- a/core/src/main/scala/stryker4s/mutants/applymutants/MatchBuilder.scala +++ b/core/src/main/scala/stryker4s/mutants/applymutants/MatchBuilder.scala @@ -3,52 +3,50 @@ package stryker4s.mutants.applymutants import stryker4s.extension.TreeExtensions.{IsEqualExtension, TransformOnceExtension} import stryker4s.extension.exception.UnableToBuildPatternMatchException import stryker4s.log.Logger -import stryker4s.model.{Mutant, MutantId, SourceTransformations, TransformedMutants} +import stryker4s.model.{Mutant, SourceTransformations, TransformedMutants} import stryker4s.mutants.applymutants.ActiveMutationContext.ActiveMutationContext import scala.meta._ import scala.util.{Failure, Success} class MatchBuilder(mutationContext: ActiveMutationContext)(implicit log: Logger) { - def buildNewSource(transformedStatements: SourceTransformations): (Tree, Seq[(MutantId, Tree)]) = { + def buildNewSource(transformedStatements: SourceTransformations): Tree = { val source = transformedStatements.source - groupTransformedStatements(transformedStatements).foldLeft((source, Seq.empty): (Tree, Seq[(MutantId, Tree)])) { - case ((tree, mutantCases), mutants) => - val origStatement = mutants.originalStatement + groupTransformedStatements(transformedStatements).foldLeft(source: Tree) { (rest, mutants) => + val origStatement = mutants.originalStatement - var isTransformed = false - tree transformOnce { - case found if found.isEqual(origStatement) && found.pos == origStatement.pos => - isTransformed = true - buildMatch(mutants) - } match { - case Success(value) if isTransformed => - (value, mutantCases ++ mutants.mutantStatements.map(mut => (mut.id, mutantToCase(mut)))) - case Success(value) => - log.warn( - s"Failed to add mutation(s) ${mutants.mutantStatements.map(_.id.globalId).mkString(", ")} to new mutated code" - ) - log.warn( - s"The code that failed to mutate was: [$origStatement] at ${origStatement.pos.input}:${origStatement.pos.startLine + 1}:${origStatement.pos.startColumn + 1}" - ) - log.warn("This mutation will likely show up as Survived") - log.warn( - "Please open an issue on github with sample code of the mutation that failed: https://github.com/stryker-mutator/stryker4s/issues/new" - ) - (value, mutantCases) - case Failure(exception) => - log.error(s"Failed to construct pattern match: original statement [$origStatement]") - log.error(s"Failed mutation(s) ${mutants.mutantStatements.mkString(",")}.") - log.error( - s"at ${origStatement.pos.input}:${origStatement.pos.startLine + 1}:${origStatement.pos.startColumn + 1}" - ) - log.error("This is likely an issue on Stryker4s's end, please enable debug logging and restart Stryker4s.") - log.debug("Please open an issue on github: https://github.com/stryker-mutator/stryker4s/issues/new") - log.debug("Please be so kind to copy the stacktrace into the issue", exception) + var isTransformed = false + rest transformOnce { + case found if found.isEqual(origStatement) && found.pos == origStatement.pos => + isTransformed = true + buildMatch(mutants) + } match { + case Success(value) if isTransformed => value + case Success(value) => + log.warn( + s"Failed to add mutation(s) ${mutants.mutantStatements.map(_.id).mkString(", ")} to new mutated code" + ) + log.warn( + s"The code that failed to mutate was: [$origStatement] at ${origStatement.pos.input}:${origStatement.pos.startLine + 1}:${origStatement.pos.startColumn + 1}" + ) + log.warn("This mutation will likely show up as Survived") + log.warn( + "Please open an issue on github with sample code of the mutation that failed: https://github.com/stryker-mutator/stryker4s/issues/new" + ) + value + case Failure(exception) => + log.error(s"Failed to construct pattern match: original statement [$origStatement]") + log.error(s"Failed mutation(s) ${mutants.mutantStatements.mkString(",")}.") + log.error( + s"at ${origStatement.pos.input}:${origStatement.pos.startLine + 1}:${origStatement.pos.startColumn + 1}" + ) + log.error("This is likely an issue on Stryker4s's end, please enable debug logging and restart Stryker4s.") + log.debug("Please open an issue on github: https://github.com/stryker-mutator/stryker4s/issues/new") + log.debug("Please be so kind to copy the stacktrace into the issue", exception) - throw UnableToBuildPatternMatchException() - } + throw UnableToBuildPatternMatchException() + } } } @@ -58,7 +56,7 @@ class MatchBuilder(mutationContext: ActiveMutationContext)(implicit log: Logger) q"($mutationContext match { ..case $cases })" } - protected def mutantToCase(mutant: Mutant): Case = + def mutantToCase(mutant: Mutant): Case = buildCase(mutant.mutated, p"Some(${mutant.id.globalId})") protected def defaultCase(transformedMutant: TransformedMutants): Case = diff --git a/core/src/test/scala/stryker4s/mutants/AddAllMutationsTest.scala b/core/src/test/scala/stryker4s/mutants/AddAllMutationsTest.scala index 17f961c1b..abb83ca2a 100644 --- a/core/src/test/scala/stryker4s/mutants/AddAllMutationsTest.scala +++ b/core/src/test/scala/stryker4s/mutants/AddAllMutationsTest.scala @@ -82,7 +82,7 @@ class AddAllMutationsTest extends Stryker4sSuite with LogMatchers { val source = source"class Foo { $tree }" val foundMutants = source.collect(new MutantMatcher().allMatchers).flatten.collect { case Right(v) => v } val transformed = new StatementTransformer().transformSource(source, foundMutants) - val (mutatedTree, _) = new MatchBuilder(ActiveMutationContext.testRunner).buildNewSource(transformed) + val mutatedTree = new MatchBuilder(ActiveMutationContext.testRunner).buildNewSource(transformed) transformed.transformedStatements .flatMap(_.mutantStatements) .foreach { mutantStatement => diff --git a/core/src/test/scala/stryker4s/mutants/applymutants/MatchBuilderTest.scala b/core/src/test/scala/stryker4s/mutants/applymutants/MatchBuilderTest.scala index 94a5421fc..5a6aa5c5f 100644 --- a/core/src/test/scala/stryker4s/mutants/applymutants/MatchBuilderTest.scala +++ b/core/src/test/scala/stryker4s/mutants/applymutants/MatchBuilderTest.scala @@ -78,7 +78,7 @@ class MatchBuilderTest extends Stryker4sSuite with LogMatchers { val sut = new MatchBuilder(ActiveMutationContext.testRunner) // Act - val (result, _) = sut.buildNewSource(transStatements) + val result = sut.buildNewSource(transStatements) // Assert val expected = source"""class Foo { @@ -109,7 +109,7 @@ class MatchBuilderTest extends Stryker4sSuite with LogMatchers { val sut = new MatchBuilder(ActiveMutationContext.sysProps) // Act - val (result, _) = sut.buildNewSource(transStatements) + val result = sut.buildNewSource(transStatements) // Assert val expected = @@ -139,7 +139,7 @@ class MatchBuilderTest extends Stryker4sSuite with LogMatchers { val sut = new MatchBuilder(ActiveMutationContext.testRunner) // Act - val (result, _) = sut.buildNewSource(transformedStatements) + val result = sut.buildNewSource(transformedStatements) // Assert val expected = @@ -179,7 +179,7 @@ class MatchBuilderTest extends Stryker4sSuite with LogMatchers { val sut = new MatchBuilder(ActiveMutationContext.testRunner) // Act - val (result, _) = sut.buildNewSource(transformedStatements) + val result = sut.buildNewSource(transformedStatements) // Assert val expected = @@ -219,7 +219,7 @@ class MatchBuilderTest extends Stryker4sSuite with LogMatchers { val sut = new MatchBuilder(ActiveMutationContext.testRunner) // Act - val (result, _) = sut.buildNewSource(transformedStatements) + val result = sut.buildNewSource(transformedStatements) // Assert val expected = source""" @@ -272,7 +272,7 @@ class MatchBuilderTest extends Stryker4sSuite with LogMatchers { val sut = new MatchBuilder(ActiveMutationContext.testRunner) // Act - val (result, _) = sut.buildNewSource(transformedStatements) + val result = sut.buildNewSource(transformedStatements) // Assert val expected = source"""class Foo() { @@ -317,7 +317,7 @@ class MatchBuilderTest extends Stryker4sSuite with LogMatchers { val sut = new MatchBuilder(ActiveMutationContext.testRunner) // Act - val (result, _) = sut.buildNewSource(transformedStatements) + val result = sut.buildNewSource(transformedStatements) // Assert val expected = source"""class Foo() { From 6751f300283586e274527d46988f781d969b568a Mon Sep 17 00:00:00 2001 From: Martin Welgemoed Date: Fri, 27 Aug 2021 15:19:46 +0200 Subject: [PATCH 07/26] Fixed compiler error in Maven test --- .../stryker4s/maven/runner/MavenTestRunnerTest.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/maven/src/test/scala/stryker4s/maven/runner/MavenTestRunnerTest.scala b/maven/src/test/scala/stryker4s/maven/runner/MavenTestRunnerTest.scala index 60dfe6e7f..3358add56 100644 --- a/maven/src/test/scala/stryker4s/maven/runner/MavenTestRunnerTest.scala +++ b/maven/src/test/scala/stryker4s/maven/runner/MavenTestRunnerTest.scala @@ -9,7 +9,7 @@ import org.mockito.captor.ArgCaptor import org.mockito.scalatest.MockitoSugar import stryker4s.config.Config import stryker4s.extension.mutationtype.LesserThan -import stryker4s.model.{Killed, Mutant, NoCoverageInitialTestRun, Survived} +import stryker4s.model.{Killed, Mutant, MutantId, NoCoverageInitialTestRun, Survived} import stryker4s.testutil.Stryker4sSuite import java.{util => ju} @@ -81,7 +81,7 @@ class MavenTestRunnerTest extends Stryker4sSuite with MockitoSugar { when(invokerMock.execute(any[InvocationRequest])).thenReturn(mockResult) val sut = new MavenTestRunner(new MavenProject(), invokerMock, properties, goals) - val result = sut.runMutant(Mutant(1, q">", q"<", LesserThan)).unsafeRunSync() + val result = sut.runMutant(Mutant(MutantId(1), q">", q"<", LesserThan)).unsafeRunSync() result shouldBe a[Killed] } @@ -93,7 +93,7 @@ class MavenTestRunnerTest extends Stryker4sSuite with MockitoSugar { when(invokerMock.execute(any[InvocationRequest])).thenReturn(mockResult) val sut = new MavenTestRunner(new MavenProject(), invokerMock, properties, goals) - val result = sut.runMutant(Mutant(1, q">", q"<", LesserThan)).unsafeRunSync() + val result = sut.runMutant(Mutant(MutantId(1), q">", q"<", LesserThan)).unsafeRunSync() result shouldBe a[Survived] } @@ -109,7 +109,7 @@ class MavenTestRunnerTest extends Stryker4sSuite with MockitoSugar { val sut = new MavenTestRunner(project, invokerMock, project.getProperties(), goals) - sut.runMutant(Mutant(1, q">", q"<", LesserThan)).unsafeRunSync() + sut.runMutant(Mutant(MutantId(1), q">", q"<", LesserThan)).unsafeRunSync() verify(invokerMock).execute(captor) val invokedRequest = captor.value @@ -132,7 +132,7 @@ class MavenTestRunnerTest extends Stryker4sSuite with MockitoSugar { mavenProject.getActiveProfiles.add(profile) val sut = new MavenTestRunner(mavenProject, invokerMock, properties, goals) - sut.runMutant(Mutant(1, q">", q"<", LesserThan)).unsafeRunSync() + sut.runMutant(Mutant(MutantId(1), q">", q"<", LesserThan)).unsafeRunSync() verify(invokerMock).execute(captor) val invokedRequest = captor.value From 5b5bb3768b6c5c8bd677475742614adc3eb74ab6 Mon Sep 17 00:00:00 2001 From: Martin Welgemoed Date: Wed, 1 Sep 2021 15:58:02 +0200 Subject: [PATCH 08/26] Fixed more maven compiler errors --- .../main/scala/stryker4s/maven/Stryker4sMavenRunner.scala | 5 +++-- .../scala/stryker4s/maven/Stryker4sMavenRunnerTest.scala | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/maven/src/main/scala/stryker4s/maven/Stryker4sMavenRunner.scala b/maven/src/main/scala/stryker4s/maven/Stryker4sMavenRunner.scala index c2ec622aa..70bab01fd 100644 --- a/maven/src/main/scala/stryker4s/maven/Stryker4sMavenRunner.scala +++ b/maven/src/main/scala/stryker4s/maven/Stryker4sMavenRunner.scala @@ -8,6 +8,7 @@ import org.apache.maven.shared.invoker.Invoker import stryker4s.config.Config import stryker4s.log.Logger import stryker4s.maven.runner.MavenTestRunner +import stryker4s.model.CompileError import stryker4s.mutants.applymutants.ActiveMutationContext.{envVar, ActiveMutationContext} import stryker4s.run.Stryker4sRunner @@ -19,14 +20,14 @@ class Stryker4sMavenRunner(project: MavenProject, invoker: Invoker)(implicit log override def resolveTestRunners( tmpDir: Path - )(implicit config: Config): NonEmptyList[Resource[IO, MavenTestRunner]] = { + )(implicit config: Config): Either[NonEmptyList[CompileError], NonEmptyList[Resource[IO, MavenTestRunner]]] = { val goals = List("test") val properties = new Properties(project.getProperties) setTestProperties(properties, config.testFilter) invoker.setWorkingDirectory(tmpDir.toNioPath.toFile()) - NonEmptyList.of(Resource.pure[IO, MavenTestRunner](new MavenTestRunner(project, invoker, properties, goals))) + Right(NonEmptyList.of(Resource.pure[IO, MavenTestRunner](new MavenTestRunner(project, invoker, properties, goals)))) } private def setTestProperties(properties: Properties, testFilter: Seq[String]): Unit = { diff --git a/maven/src/test/scala/stryker4s/maven/Stryker4sMavenRunnerTest.scala b/maven/src/test/scala/stryker4s/maven/Stryker4sMavenRunnerTest.scala index 9dba154b4..a8f312f28 100644 --- a/maven/src/test/scala/stryker4s/maven/Stryker4sMavenRunnerTest.scala +++ b/maven/src/test/scala/stryker4s/maven/Stryker4sMavenRunnerTest.scala @@ -24,6 +24,7 @@ class Stryker4sMavenRunnerTest extends Stryker4sSuite with MockitoSugar { sut .resolveTestRunners(tmpDir) + .right.get .head .use(result => { verify(invokerMock).setWorkingDirectory(eqTo(tmpDir.toNioPath.toFile())) @@ -42,6 +43,7 @@ class Stryker4sMavenRunnerTest extends Stryker4sSuite with MockitoSugar { sut .resolveTestRunners(tmpDir) + .right.get .head .use(result => { result.properties.getProperty("test") should equal(expectedTestFilter.mkString(", ")) @@ -62,6 +64,7 @@ class Stryker4sMavenRunnerTest extends Stryker4sSuite with MockitoSugar { sut .resolveTestRunners(tmpDir) + .right.get .head .use(result => IO.pure(result.properties.getProperty("test") should equal(s"*OtherTest, $expectedTestFilter"))) .unsafeRunSync() @@ -78,6 +81,7 @@ class Stryker4sMavenRunnerTest extends Stryker4sSuite with MockitoSugar { sut .resolveTestRunners(tmpDir) + .right.get .head .use(result => IO.pure(result.properties.getProperty("wildcardSuites") should equal(s"*OtherTest,$expectedTestFilter")) From d2bf7a8ab030cca08f7234ec4a9a479a73f37914 Mon Sep 17 00:00:00 2001 From: Martin Welgemoed Date: Wed, 1 Sep 2021 16:13:05 +0200 Subject: [PATCH 09/26] Fixed scalafmt issues on maven plugin --- .../stryker4s/maven/Stryker4sMavenRunnerTest.scala | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/maven/src/test/scala/stryker4s/maven/Stryker4sMavenRunnerTest.scala b/maven/src/test/scala/stryker4s/maven/Stryker4sMavenRunnerTest.scala index a8f312f28..f747d446f 100644 --- a/maven/src/test/scala/stryker4s/maven/Stryker4sMavenRunnerTest.scala +++ b/maven/src/test/scala/stryker4s/maven/Stryker4sMavenRunnerTest.scala @@ -24,7 +24,8 @@ class Stryker4sMavenRunnerTest extends Stryker4sSuite with MockitoSugar { sut .resolveTestRunners(tmpDir) - .right.get + .right + .get .head .use(result => { verify(invokerMock).setWorkingDirectory(eqTo(tmpDir.toNioPath.toFile())) @@ -43,7 +44,8 @@ class Stryker4sMavenRunnerTest extends Stryker4sSuite with MockitoSugar { sut .resolveTestRunners(tmpDir) - .right.get + .right + .get .head .use(result => { result.properties.getProperty("test") should equal(expectedTestFilter.mkString(", ")) @@ -64,7 +66,8 @@ class Stryker4sMavenRunnerTest extends Stryker4sSuite with MockitoSugar { sut .resolveTestRunners(tmpDir) - .right.get + .right + .get .head .use(result => IO.pure(result.properties.getProperty("test") should equal(s"*OtherTest, $expectedTestFilter"))) .unsafeRunSync() @@ -81,7 +84,8 @@ class Stryker4sMavenRunnerTest extends Stryker4sSuite with MockitoSugar { sut .resolveTestRunners(tmpDir) - .right.get + .right + .get .head .use(result => IO.pure(result.properties.getProperty("wildcardSuites") should equal(s"*OtherTest,$expectedTestFilter")) From b544805076a030e2624f36b8b300ba9b1f3cadbe Mon Sep 17 00:00:00 2001 From: Martin Welgemoed Date: Mon, 27 Sep 2021 16:25:53 +0200 Subject: [PATCH 10/26] Using IO.pure() instead of the apply --- core/src/main/scala/stryker4s/run/MutantRunner.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala/stryker4s/run/MutantRunner.scala b/core/src/main/scala/stryker4s/run/MutantRunner.scala index 1c5e16d33..a856bfed4 100644 --- a/core/src/main/scala/stryker4s/run/MutantRunner.scala +++ b/core/src/main/scala/stryker4s/run/MutantRunner.scala @@ -3,6 +3,7 @@ package stryker4s.run import cats.data.NonEmptyList import cats.effect.{IO, Resource} import cats.syntax.functor._ +import cats.syntax.either._ import fs2.io.file.{Files, Path} import fs2.{text, Pipe, Stream} import mutationtesting.{Metrics, MetricsResult} @@ -31,7 +32,7 @@ class MutantRunner( mutateFiles(Seq.empty).flatMap { mutatedFiles => run(mutatedFiles) .flatMap { - case Right(metrics) => IO(Right(metrics)) + case Right(metrics) => IO.pure(metrics.asRight) case Left(errors) => mutateFiles(errors.toList).flatMap(run) } @@ -42,7 +43,7 @@ class MutantRunner( def run(mutatedFiles: Seq[MutatedFile]): IO[Either[NonEmptyList[CompileError], MetricsResult]] = { prepareEnv(mutatedFiles).use { path => createTestRunnerPool(path) match { - case Left(errs) => IO(Left(errs)) + case Left(errs) => IO.pure(errs.asLeft) case Right(testRunnerPoolResource) => testRunnerPoolResource.use { testRunnerPool => testRunnerPool.loan From 9eb0f2a3269d67571954e9b7b77f0fbbe124740a Mon Sep 17 00:00:00 2001 From: Martin Welgemoed Date: Mon, 27 Sep 2021 16:29:52 +0200 Subject: [PATCH 11/26] Using CompileError as MutantRunResult and renamed the model class to CompilerErrMsg to avoid confusion --- .../stryker4s/command/Stryker4sCommandRunner.scala | 4 ++-- .../model/{CompileError.scala => CompilerErrMsg.scala} | 2 +- .../main/scala/stryker4s/model/MutantRunResult.scala | 2 +- core/src/main/scala/stryker4s/mutants/Mutator.scala | 6 +++--- .../report/mapper/MutantRunResultMapper.scala | 2 +- core/src/main/scala/stryker4s/run/MutantRunner.scala | 10 +++++----- .../src/main/scala/stryker4s/run/Stryker4sRunner.scala | 4 ++-- .../main/scala/stryker4s/sbt/Stryker4sSbtRunner.scala | 8 ++++---- 8 files changed, 19 insertions(+), 19 deletions(-) rename core/src/main/scala/stryker4s/model/{CompileError.scala => CompilerErrMsg.scala} (78%) diff --git a/command-runner/src/main/scala/stryker4s/command/Stryker4sCommandRunner.scala b/command-runner/src/main/scala/stryker4s/command/Stryker4sCommandRunner.scala index 2e0b50204..0b18f247f 100644 --- a/command-runner/src/main/scala/stryker4s/command/Stryker4sCommandRunner.scala +++ b/command-runner/src/main/scala/stryker4s/command/Stryker4sCommandRunner.scala @@ -7,7 +7,7 @@ import stryker4s.command.config.ProcessRunnerConfig import stryker4s.command.runner.ProcessTestRunner import stryker4s.config.Config import stryker4s.log.Logger -import stryker4s.model.CompileError +import stryker4s.model.CompilerErrMsg import stryker4s.mutants.applymutants.ActiveMutationContext import stryker4s.mutants.applymutants.ActiveMutationContext.ActiveMutationContext import stryker4s.run.process.ProcessRunner @@ -22,7 +22,7 @@ class Stryker4sCommandRunner(processRunnerConfig: ProcessRunnerConfig, timeout: override def resolveTestRunners( tmpDir: Path - )(implicit config: Config): Either[NonEmptyList[CompileError], NonEmptyList[Resource[IO, TestRunner]]] = { + )(implicit config: Config): Either[NonEmptyList[CompilerErrMsg], NonEmptyList[Resource[IO, TestRunner]]] = { val innerTestRunner = Resource.pure[IO, TestRunner](new ProcessTestRunner(processRunnerConfig.testRunner, ProcessRunner(), tmpDir)) diff --git a/core/src/main/scala/stryker4s/model/CompileError.scala b/core/src/main/scala/stryker4s/model/CompilerErrMsg.scala similarity index 78% rename from core/src/main/scala/stryker4s/model/CompileError.scala rename to core/src/main/scala/stryker4s/model/CompilerErrMsg.scala index f5c9c4f89..45f1984e9 100644 --- a/core/src/main/scala/stryker4s/model/CompileError.scala +++ b/core/src/main/scala/stryker4s/model/CompilerErrMsg.scala @@ -2,6 +2,6 @@ package stryker4s.model //This class is used to contain information about mutants that did not compile //It essentially exists so that we don't have to pass around the SBT specific compiler exception -case class CompileError(msg: String, path: String, line: Integer) { +case class CompilerErrMsg(msg: String, path: String, line: Integer) { override def toString: String = s"$path:L$line: '$msg'" } diff --git a/core/src/main/scala/stryker4s/model/MutantRunResult.scala b/core/src/main/scala/stryker4s/model/MutantRunResult.scala index 85d9302ac..e3f824f40 100644 --- a/core/src/main/scala/stryker4s/model/MutantRunResult.scala +++ b/core/src/main/scala/stryker4s/model/MutantRunResult.scala @@ -23,4 +23,4 @@ final case class Error(mutant: Mutant, description: Option[String] = None) exten final case class Ignored(mutant: Mutant, description: Option[String] = None) extends MutantRunResult -final case class NotCompiling(mutant: Mutant, description: Option[String] = None) extends MutantRunResult +final case class CompileError(mutant: Mutant, description: Option[String] = None) extends MutantRunResult diff --git a/core/src/main/scala/stryker4s/mutants/Mutator.scala b/core/src/main/scala/stryker4s/mutants/Mutator.scala index aadb89304..acd56ed40 100644 --- a/core/src/main/scala/stryker4s/mutants/Mutator.scala +++ b/core/src/main/scala/stryker4s/mutants/Mutator.scala @@ -18,7 +18,7 @@ class Mutator(mutantFinder: MutantFinder, transformer: StatementTransformer, mat log: Logger ) { - def mutate(files: Stream[IO, Path], compileErrors: Seq[CompileError] = Seq.empty): IO[Seq[MutatedFile]] = { + def mutate(files: Stream[IO, Path], compileErrors: Seq[CompilerErrMsg] = Seq.empty): IO[Seq[MutatedFile]] = { files .parEvalMapUnordered(config.concurrency)(p => findMutants(p).tupleLeft(p)) .map { case (file, mutationsInSource) => @@ -34,7 +34,7 @@ class Mutator(mutantFinder: MutantFinder, transformer: StatementTransformer, mat private def mutateFile( file: Path, mutationsInSource: MutationsInSource, - compileErrors: Seq[CompileError] + compileErrors: Seq[CompilerErrMsg] ): MutatedFile = { val transformed = transformStatements(mutationsInSource) val builtTree = buildMatches(transformed) @@ -60,7 +60,7 @@ class Mutator(mutantFinder: MutantFinder, transformer: StatementTransformer, mat //Given compiler errors, return the mutants that caused it private def errorsToIds( - compileErrors: Seq[CompileError], + compileErrors: Seq[CompilerErrMsg], mutatedFile: String, mutants: Seq[Mutant] ): Seq[MutantId] = { diff --git a/core/src/main/scala/stryker4s/report/mapper/MutantRunResultMapper.scala b/core/src/main/scala/stryker4s/report/mapper/MutantRunResultMapper.scala index 18691a2bb..3748b268c 100644 --- a/core/src/main/scala/stryker4s/report/mapper/MutantRunResultMapper.scala +++ b/core/src/main/scala/stryker4s/report/mapper/MutantRunResultMapper.scala @@ -62,7 +62,7 @@ trait MutantRunResultMapper { case _: TimedOut => MutantStatus.Timeout case _: Error => MutantStatus.RuntimeError case _: Ignored => MutantStatus.Ignored - case _: NotCompiling => MutantStatus.CompileError + case _: CompileError => MutantStatus.CompileError } private def fileContentAsString(path: Path)(implicit config: Config): String = diff --git a/core/src/main/scala/stryker4s/run/MutantRunner.scala b/core/src/main/scala/stryker4s/run/MutantRunner.scala index a856bfed4..5d85d2478 100644 --- a/core/src/main/scala/stryker4s/run/MutantRunner.scala +++ b/core/src/main/scala/stryker4s/run/MutantRunner.scala @@ -13,7 +13,7 @@ import stryker4s.extension.StreamExtensions._ import stryker4s.extension.exception.InitialTestRunFailedException import stryker4s.files.FilesFileResolver import stryker4s.log.Logger -import stryker4s.model.{CompileError, _} +import stryker4s.model.{CompilerErrMsg, _} import stryker4s.report.mapper.MutantRunResultMapper import stryker4s.report.{FinishedRunEvent, MutantTestedEvent, Reporter} @@ -22,13 +22,13 @@ import scala.collection.immutable.SortedMap import scala.concurrent.duration._ class MutantRunner( - createTestRunnerPool: Path => Either[NonEmptyList[CompileError], Resource[IO, TestRunnerPool]], + createTestRunnerPool: Path => Either[NonEmptyList[CompilerErrMsg], Resource[IO, TestRunnerPool]], fileResolver: FilesFileResolver, reporter: Reporter )(implicit config: Config, log: Logger) extends MutantRunResultMapper { - def apply(mutateFiles: Seq[CompileError] => IO[Seq[MutatedFile]]): IO[MetricsResult] = { + def apply(mutateFiles: Seq[CompilerErrMsg] => IO[Seq[MutatedFile]]): IO[MetricsResult] = { mutateFiles(Seq.empty).flatMap { mutatedFiles => run(mutatedFiles) .flatMap { @@ -40,7 +40,7 @@ class MutantRunner( } } - def run(mutatedFiles: Seq[MutatedFile]): IO[Either[NonEmptyList[CompileError], MetricsResult]] = { + def run(mutatedFiles: Seq[MutatedFile]): IO[Either[NonEmptyList[CompilerErrMsg], MetricsResult]] = { prepareEnv(mutatedFiles).use { path => createTestRunnerPool(path) match { case Left(errs) => IO.pure(errs.asLeft) @@ -161,7 +161,7 @@ class MutantRunner( // Map all no-coverage mutants val noCoverage = mapPureMutants(noCoverageMutants, (m: Mutant) => NoCoverage(m)) // Map all no-compiling mutants - val noCompiling = mapPureMutants(compilerErrorMutants, (m: Mutant) => NotCompiling(m)) + val noCompiling = mapPureMutants(compilerErrorMutants, (m: Mutant) => CompileError(m)) // Run all testable mutants val totalTestableMutants = testableMutants.size diff --git a/core/src/main/scala/stryker4s/run/Stryker4sRunner.scala b/core/src/main/scala/stryker4s/run/Stryker4sRunner.scala index 3a1d44e72..3c7c57888 100644 --- a/core/src/main/scala/stryker4s/run/Stryker4sRunner.scala +++ b/core/src/main/scala/stryker4s/run/Stryker4sRunner.scala @@ -15,7 +15,7 @@ import stryker4s.report.dashboard.DashboardConfigProvider import stryker4s.run.process.ProcessRunner import stryker4s.run.threshold.ScoreStatus import stryker4s.Stryker4s -import stryker4s.model.CompileError +import stryker4s.model.CompilerErrMsg import sttp.client3.SttpBackend import sttp.client3.httpclient.fs2.HttpClientFs2Backend @@ -63,7 +63,7 @@ abstract class Stryker4sRunner(implicit log: Logger) { def resolveTestRunners(tmpDir: Path)(implicit config: Config - ): Either[NonEmptyList[CompileError], NonEmptyList[Resource[IO, stryker4s.run.TestRunner]]] + ): Either[NonEmptyList[CompilerErrMsg], NonEmptyList[Resource[IO, stryker4s.run.TestRunner]]] def resolveMutatesFileSource(implicit config: Config): MutatesFileResolver = new GlobFileResolver( diff --git a/sbt/src/main/scala/stryker4s/sbt/Stryker4sSbtRunner.scala b/sbt/src/main/scala/stryker4s/sbt/Stryker4sSbtRunner.scala index 70b284808..030a2a5ef 100644 --- a/sbt/src/main/scala/stryker4s/sbt/Stryker4sSbtRunner.scala +++ b/sbt/src/main/scala/stryker4s/sbt/Stryker4sSbtRunner.scala @@ -11,7 +11,7 @@ import stryker4s.extension.FileExtensions._ import stryker4s.extension.exception.TestSetupException import stryker4s.files.{FilesFileResolver, MutatesFileResolver, SbtFilesResolver, SbtMutatesResolver} import stryker4s.log.Logger -import stryker4s.model.CompileError +import stryker4s.model.CompilerErrMsg import stryker4s.mutants.applymutants.ActiveMutationContext.ActiveMutationContext import stryker4s.mutants.applymutants.{ActiveMutationContext, CoverageMatchBuilder, MatchBuilder} import stryker4s.run.{Stryker4sRunner, TestRunner} @@ -43,7 +43,7 @@ class Stryker4sSbtRunner( def resolveTestRunners( tmpDir: Path - )(implicit config: Config): Either[NonEmptyList[CompileError], NonEmptyList[Resource[IO, TestRunner]]] = { + )(implicit config: Config): Either[NonEmptyList[CompilerErrMsg], NonEmptyList[Resource[IO, TestRunner]]] = { def setupLegacySbtTestRunner( settings: Seq[Def.Setting[_]], extracted: Extracted @@ -67,7 +67,7 @@ class Stryker4sSbtRunner( def setupSbtTestRunner( settings: Seq[Def.Setting[_]], extracted: Extracted - ): Either[NonEmptyList[CompileError], NonEmptyList[Resource[IO, TestRunner]]] = { + ): Either[NonEmptyList[CompilerErrMsg], NonEmptyList[Resource[IO, TestRunner]]] = { val stryker4sVersion = this.getClass().getPackage().getImplementationVersion() log.debug(s"Resolved stryker4s version $stryker4sVersion") @@ -108,7 +108,7 @@ class Stryker4sSbtRunner( path <- e.position().sourceFile().asScala pathStr = path.toPath.toAbsolutePath.toString.replace(tmpDir.toString, "") line <- e.position().line().asScala - } yield CompileError(e.message(), pathStr, line) + } yield CompilerErrMsg(e.message(), pathStr, line) }.toSeq }).flatten.toList From fae89bf97f19794105aad0deb06172d036a2863b Mon Sep 17 00:00:00 2001 From: Martin Welgemoed Date: Tue, 28 Sep 2021 11:38:05 +0200 Subject: [PATCH 12/26] Using UnableToFixCompilerErrorsException instead of RuntimeException --- .../extension/exception/stryker4sException.scala | 10 ++++++++++ .../src/main/scala/stryker4s/run/MutantRunner.scala | 13 +++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/core/src/main/scala/stryker4s/extension/exception/stryker4sException.scala b/core/src/main/scala/stryker4s/extension/exception/stryker4sException.scala index c89ec60ed..e52c0fd4f 100644 --- a/core/src/main/scala/stryker4s/extension/exception/stryker4sException.scala +++ b/core/src/main/scala/stryker4s/extension/exception/stryker4sException.scala @@ -1,5 +1,7 @@ package stryker4s.extension.exception +import stryker4s.model.CompilerErrMsg + import scala.util.control.NoStackTrace sealed abstract class Stryker4sException(message: String) extends Exception(message) @@ -14,3 +16,11 @@ final case class TestSetupException(name: String) ) final case class MutationRunFailedException(message: String) extends Stryker4sException(message) + +final case class UnableToFixCompilerErrorsException(errs: List[CompilerErrMsg]) + extends Stryker4sException( + "Unable to remove non-compiling mutants in the mutated-files. As a work-around you can exclude them in the stryker.conf. Please report this issue at https://github.com/stryker-mutator/stryker4s/issues\n" + + errs + .map(err => s"File: ${err.path} Line ${err.line}: '${err.msg}'") + .mkString("\n") + ) diff --git a/core/src/main/scala/stryker4s/run/MutantRunner.scala b/core/src/main/scala/stryker4s/run/MutantRunner.scala index 5d85d2478..4f7e7b232 100644 --- a/core/src/main/scala/stryker4s/run/MutantRunner.scala +++ b/core/src/main/scala/stryker4s/run/MutantRunner.scala @@ -2,15 +2,15 @@ package stryker4s.run import cats.data.NonEmptyList import cats.effect.{IO, Resource} -import cats.syntax.functor._ import cats.syntax.either._ +import cats.syntax.functor._ import fs2.io.file.{Files, Path} import fs2.{text, Pipe, Stream} import mutationtesting.{Metrics, MetricsResult} import stryker4s.config.Config import stryker4s.extension.FileExtensions._ import stryker4s.extension.StreamExtensions._ -import stryker4s.extension.exception.InitialTestRunFailedException +import stryker4s.extension.exception.{InitialTestRunFailedException, UnableToFixCompilerErrorsException} import stryker4s.files.FilesFileResolver import stryker4s.log.Logger import stryker4s.model.{CompilerErrMsg, _} @@ -33,10 +33,15 @@ class MutantRunner( run(mutatedFiles) .flatMap { case Right(metrics) => IO.pure(metrics.asRight) - case Left(errors) => + case Left(errors) => + //Retry once with the non-compiling mutants removed mutateFiles(errors.toList).flatMap(run) } - .map(_.getOrElse(throw new RuntimeException(s"Unable to remove non-compiling mutants"))) + .flatMap { + case Right(metrics) => IO.pure(metrics) + //Failed at remove the non-compiling mutants + case Left(errs) => IO.raiseError(UnableToFixCompilerErrorsException(errs.toList)) + } } } From c77d70d97f1c357c86dc2694eaf919d19a31c554 Mon Sep 17 00:00:00 2001 From: Martin Welgemoed Date: Tue, 28 Sep 2021 11:56:23 +0200 Subject: [PATCH 13/26] Removed unneeded fatory logic for the Mutator --- core/src/main/scala/stryker4s/Stryker4s.scala | 3 +-- .../extension/exception/stryker4sException.scala | 4 ++-- .../scala/stryker4s/run/Stryker4sRunner.scala | 15 +++++++-------- core/src/test/scala/stryker4s/Stryker4sTest.scala | 11 +++++------ 4 files changed, 15 insertions(+), 18 deletions(-) diff --git a/core/src/main/scala/stryker4s/Stryker4s.scala b/core/src/main/scala/stryker4s/Stryker4s.scala index cb4f826ae..83da915d7 100644 --- a/core/src/main/scala/stryker4s/Stryker4s.scala +++ b/core/src/main/scala/stryker4s/Stryker4s.scala @@ -7,13 +7,12 @@ import stryker4s.mutants.Mutator import stryker4s.run.MutantRunner import stryker4s.run.threshold.{ScoreStatus, ThresholdChecker} -class Stryker4s(fileSource: MutatesFileResolver, mutatorFactory: () => Mutator, runner: MutantRunner)(implicit +class Stryker4s(fileSource: MutatesFileResolver, mutator: Mutator, runner: MutantRunner)(implicit config: Config ) { def run(): IO[ScoreStatus] = { val filesToMutate = fileSource.files - val mutator = mutatorFactory() for { metrics <- runner(errors => mutator.mutate(filesToMutate, errors)) diff --git a/core/src/main/scala/stryker4s/extension/exception/stryker4sException.scala b/core/src/main/scala/stryker4s/extension/exception/stryker4sException.scala index e52c0fd4f..d9a5273e6 100644 --- a/core/src/main/scala/stryker4s/extension/exception/stryker4sException.scala +++ b/core/src/main/scala/stryker4s/extension/exception/stryker4sException.scala @@ -19,8 +19,8 @@ final case class MutationRunFailedException(message: String) extends Stryker4sEx final case class UnableToFixCompilerErrorsException(errs: List[CompilerErrMsg]) extends Stryker4sException( - "Unable to remove non-compiling mutants in the mutated-files. As a work-around you can exclude them in the stryker.conf. Please report this issue at https://github.com/stryker-mutator/stryker4s/issues\n" + "Unable to remove non-compiling mutants in the mutated files. As a work-around you can exclude them in the stryker.conf. Please report this issue at https://github.com/stryker-mutator/stryker4s/issues\n" + errs - .map(err => s"File: ${err.path} Line ${err.line}: '${err.msg}'") + .map(err => s"${err.path}: '${err.msg}'") .mkString("\n") ) diff --git a/core/src/main/scala/stryker4s/run/Stryker4sRunner.scala b/core/src/main/scala/stryker4s/run/Stryker4sRunner.scala index 3c7c57888..903fa515b 100644 --- a/core/src/main/scala/stryker4s/run/Stryker4sRunner.scala +++ b/core/src/main/scala/stryker4s/run/Stryker4sRunner.scala @@ -3,9 +3,11 @@ package stryker4s.run import cats.data.NonEmptyList import cats.effect.{IO, Resource} import fs2.io.file.Path +import stryker4s.Stryker4s import stryker4s.config._ import stryker4s.files._ import stryker4s.log.Logger +import stryker4s.model.CompilerErrMsg import stryker4s.mutants.Mutator import stryker4s.mutants.applymutants.ActiveMutationContext.ActiveMutationContext import stryker4s.mutants.applymutants.{MatchBuilder, StatementTransformer} @@ -14,8 +16,6 @@ import stryker4s.report._ import stryker4s.report.dashboard.DashboardConfigProvider import stryker4s.run.process.ProcessRunner import stryker4s.run.threshold.ScoreStatus -import stryker4s.Stryker4s -import stryker4s.model.CompilerErrMsg import sttp.client3.SttpBackend import sttp.client3.httpclient.fs2.HttpClientFs2Backend @@ -27,12 +27,11 @@ abstract class Stryker4sRunner(implicit log: Logger) { val stryker4s = new Stryker4s( resolveMutatesFileSource, - () => - new Mutator( - new MutantFinder(new MutantMatcher), - new StatementTransformer, - resolveMatchBuilder - ), + new Mutator( + new MutantFinder(new MutantMatcher), + new StatementTransformer, + resolveMatchBuilder + ), new MutantRunner(createTestRunnerPool, resolveFilesFileSource, new AggregateReporter(resolveReporters())) ) diff --git a/core/src/test/scala/stryker4s/Stryker4sTest.scala b/core/src/test/scala/stryker4s/Stryker4sTest.scala index 1bf4c21c5..8efeb9643 100644 --- a/core/src/test/scala/stryker4s/Stryker4sTest.scala +++ b/core/src/test/scala/stryker4s/Stryker4sTest.scala @@ -36,12 +36,11 @@ class Stryker4sTest extends Stryker4sIOSuite with MockitoIOSuite with Inside wit val sut = new Stryker4s( testSourceCollector, - () => - new Mutator( - new MutantFinder(new MutantMatcher), - new StatementTransformer, - new MatchBuilder(ActiveMutationContext.sysProps) - ), + new Mutator( + new MutantFinder(new MutantMatcher), + new StatementTransformer, + new MatchBuilder(ActiveMutationContext.sysProps) + ), testMutantRunner ) From 161a8d969ff7be512e534d441ec4f34ffcec3715 Mon Sep 17 00:00:00 2001 From: Martin Welgemoed Date: Tue, 28 Sep 2021 16:53:03 +0200 Subject: [PATCH 14/26] Moved mutation rollback logic into its own class --- .../scala/stryker4s/mutants/Mutator.scala | 72 ++++++------------- .../stryker4s/mutants/RollbackHandler.scala | 58 +++++++++++++++ .../scala/stryker4s/run/Stryker4sRunner.scala | 4 +- 3 files changed, 80 insertions(+), 54 deletions(-) create mode 100644 core/src/main/scala/stryker4s/mutants/RollbackHandler.scala diff --git a/core/src/main/scala/stryker4s/mutants/Mutator.scala b/core/src/main/scala/stryker4s/mutants/Mutator.scala index acd56ed40..91aaf13f1 100644 --- a/core/src/main/scala/stryker4s/mutants/Mutator.scala +++ b/core/src/main/scala/stryker4s/mutants/Mutator.scala @@ -11,19 +11,26 @@ import stryker4s.model._ import stryker4s.mutants.applymutants.{MatchBuilder, StatementTransformer} import stryker4s.mutants.findmutants.MutantFinder -import scala.meta._ - -class Mutator(mutantFinder: MutantFinder, transformer: StatementTransformer, matchBuilder: MatchBuilder)(implicit +class Mutator( + mutantFinder: MutantFinder, + transformer: StatementTransformer, + matchBuilder: MatchBuilder +)(implicit config: Config, log: Logger ) { + //Logic for dealing with compiler errors and removing non-compiling mutants from files + private val rollbackHandler: RollbackHandler = new RollbackHandler(matchBuilder) + def mutate(files: Stream[IO, Path], compileErrors: Seq[CompilerErrMsg] = Seq.empty): IO[Seq[MutatedFile]] = { files .parEvalMapUnordered(config.concurrency)(p => findMutants(p).tupleLeft(p)) .map { case (file, mutationsInSource) => - val errorsInThisFile = compileErrors.filter(err => file.toString.endsWith(err.path)) - mutateFile(file, mutationsInSource, errorsInThisFile) + if (compileErrors.isEmpty) + mutateFile(file, mutationsInSource) + else + rollbackHandler.rollbackNonCompilingMutants(file, mutationsInSource, mutateFile, compileErrors) } .filterNot(mutatedFile => mutatedFile.mutants.isEmpty && mutatedFile.excludedMutants == 0) .compile @@ -33,57 +40,18 @@ class Mutator(mutantFinder: MutantFinder, transformer: StatementTransformer, mat private def mutateFile( file: Path, - mutationsInSource: MutationsInSource, - compileErrors: Seq[CompilerErrMsg] + mutationsInSource: MutationsInSource ): MutatedFile = { val transformed = transformStatements(mutationsInSource) val builtTree = buildMatches(transformed) - //If there are any compiler errors (i.e. we're currently retrying the mutation) - //Then we take the original tree built that didn't compile - //And then we search inside it to translate the compile errors to mutants - //Finally we rebuild it from scratch without those mutants - //This is not very performant, but you only pay the cost if there actually is a compiler error - val mutatedFile = MutatedFile(file, builtTree, mutationsInSource.mutants, Seq.empty, mutationsInSource.excluded) - if (compileErrors.isEmpty) { - mutatedFile - } else { - val nonCompilingIds = errorsToIds(compileErrors, mutatedFile.mutatedSource, mutationsInSource.mutants) - val (nonCompilingMutants, compilingMutants) = - mutationsInSource.mutants.partition(mut => nonCompilingIds.contains(mut.id)) - - val transformed = transformStatements(mutationsInSource.copy(mutants = compilingMutants)) - val builtTree = buildMatches(transformed) - MutatedFile(file, builtTree, compilingMutants, nonCompilingMutants, mutationsInSource.excluded) - } - } - - //Given compiler errors, return the mutants that caused it - private def errorsToIds( - compileErrors: Seq[CompilerErrMsg], - mutatedFile: String, - mutants: Seq[Mutant] - ): Seq[MutantId] = { - val statementToMutIdMap = mutants.map { mutant => - matchBuilder.mutantToCase(mutant).structure -> mutant.id - }.toMap - - val lineToMutantId: Map[Int, MutantId] = mutatedFile - //Parsing the mutated tree again as a string is the only way to get the position info of the mutated statements - .parse[Source] - .getOrElse(throw new RuntimeException(s"Failed to parse $mutatedFile to remove non-compiling mutants")) - .collect { - case node: Case if statementToMutIdMap.contains(node.structure) => - val mutId = statementToMutIdMap(node.structure) - //+1 because scalameta uses zero-indexed line numbers - (node.pos.startLine to node.pos.endLine).map(i => i + 1 -> mutId) - } - .flatten - .toMap - - compileErrors.flatMap { err => - lineToMutantId.get(err.line) - } + MutatedFile( + fileOrigin = file, + tree = builtTree, + mutants = mutationsInSource.mutants, + nonCompilingMutants = Seq.empty, + excludedMutants = mutationsInSource.excluded + ) } /** Step 1: Find mutants in the found files diff --git a/core/src/main/scala/stryker4s/mutants/RollbackHandler.scala b/core/src/main/scala/stryker4s/mutants/RollbackHandler.scala new file mode 100644 index 000000000..1d79e02c6 --- /dev/null +++ b/core/src/main/scala/stryker4s/mutants/RollbackHandler.scala @@ -0,0 +1,58 @@ +package stryker4s.mutants + +import fs2.io.file.Path +import stryker4s.model._ +import stryker4s.mutants.applymutants.MatchBuilder + +import scala.meta.{Case, Source, _} + +class RollbackHandler(matchBuilder: MatchBuilder) { + def rollbackNonCompilingMutants( + file: Path, + mutationsInSource: MutationsInSource, + mutateFile: (Path, MutationsInSource) => MutatedFile, + compileErrors: Seq[CompilerErrMsg] + ): MutatedFile = { + //If there are any compiler errors (i.e. we're currently retrying the mutation with the bad ones rolled back) + //Then we take the original tree built that didn't compile + //And then we search inside it to translate the compile errors to mutants + //Finally we rebuild it from scratch without those mutants + //This is not very performant, but you only pay the cost if there actually is a compiler error + val originalFile = mutateFile(file, mutationsInSource) + + val nonCompilingIds = errorsToIds(compileErrors, originalFile.mutatedSource, mutationsInSource.mutants) + val (nonCompilingMutants, compilingMutants) = + mutationsInSource.mutants.partition(mut => nonCompilingIds.contains(mut.id)) + + mutateFile(file, mutationsInSource.copy(mutants = compilingMutants)).copy(nonCompilingMutants = nonCompilingMutants) + } + + //Given compiler errors, return the mutants that caused it by searching for the matching case statement at that line + private def errorsToIds( + compileErrors: Seq[CompilerErrMsg], + mutatedFile: String, + mutants: Seq[Mutant] + ): Seq[MutantId] = { + val statementToMutIdMap = mutants.map { mutant => + matchBuilder.mutantToCase(mutant).structure -> mutant.id + }.toMap + + val lineToMutantId: Map[Int, MutantId] = mutatedFile + //Parsing the mutated tree again as a string is the only way to get the position info of the mutated statements + .parse[Source] + .getOrElse(throw new RuntimeException(s"Failed to parse $mutatedFile to remove non-compiling mutants")) + .collect { + case node: Case if statementToMutIdMap.contains(node.structure) => + val mutId = statementToMutIdMap(node.structure) + //+1 because scalameta uses zero-indexed line numbers + (node.pos.startLine to node.pos.endLine).map(i => i + 1 -> mutId) + } + .flatten + .toMap + + compileErrors.flatMap { err => + lineToMutantId.get(err.line) + } + } + +} diff --git a/core/src/main/scala/stryker4s/run/Stryker4sRunner.scala b/core/src/main/scala/stryker4s/run/Stryker4sRunner.scala index 903fa515b..fe6be8a33 100644 --- a/core/src/main/scala/stryker4s/run/Stryker4sRunner.scala +++ b/core/src/main/scala/stryker4s/run/Stryker4sRunner.scala @@ -3,11 +3,9 @@ package stryker4s.run import cats.data.NonEmptyList import cats.effect.{IO, Resource} import fs2.io.file.Path -import stryker4s.Stryker4s import stryker4s.config._ import stryker4s.files._ import stryker4s.log.Logger -import stryker4s.model.CompilerErrMsg import stryker4s.mutants.Mutator import stryker4s.mutants.applymutants.ActiveMutationContext.ActiveMutationContext import stryker4s.mutants.applymutants.{MatchBuilder, StatementTransformer} @@ -16,6 +14,8 @@ import stryker4s.report._ import stryker4s.report.dashboard.DashboardConfigProvider import stryker4s.run.process.ProcessRunner import stryker4s.run.threshold.ScoreStatus +import stryker4s.Stryker4s +import stryker4s.model.CompilerErrMsg import sttp.client3.SttpBackend import sttp.client3.httpclient.fs2.HttpClientFs2Backend From 465ee74e0adb48eb062c9b4c61e958c6666a6b92 Mon Sep 17 00:00:00 2001 From: Martin Welgemoed Date: Thu, 30 Sep 2021 16:37:46 +0200 Subject: [PATCH 15/26] Fixed merge issue --- .../stryker4s/sbt/Stryker4sSbtRunner.scala | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/sbt/src/main/scala/stryker4s/sbt/Stryker4sSbtRunner.scala b/sbt/src/main/scala/stryker4s/sbt/Stryker4sSbtRunner.scala index 1ca1a86e8..4cdaa8adc 100644 --- a/sbt/src/main/scala/stryker4s/sbt/Stryker4sSbtRunner.scala +++ b/sbt/src/main/scala/stryker4s/sbt/Stryker4sSbtRunner.scala @@ -136,19 +136,23 @@ class Stryker4sSbtRunner( } val concurrency = if (config.debug.debugTestRunner) { - log.warn( - "'debug.debug-test-runner' config is 'true', creating 1 test-runner with debug arguments enabled on port 8000." - ) - 1 - } else {log.info(s"Creating ${config.concurrency} test-runners") - config.concurrency - }val portStart = 13336 + log.warn( + "'debug.debug-test-runner' config is 'true', creating 1 test-runner with debug arguments enabled on port 8000." + ) + 1 + } else { + log.info(s"Creating ${config.concurrency} test-runners") + config.concurrency + } + + val portStart = 13336 val portRanges = NonEmptyList.fromListUnsafe((1 to concurrency).map(_ + portStart).toList) - val testRunners = portRanges.map { port => - SbtTestRunner.create(classpath, javaOpts, frameworks, testGroups, port, sharedTimeout) - } - Right(testRunners) + Right( + portRanges.map { port => + SbtTestRunner.create(classpath, javaOpts, frameworks, testGroups, port, sharedTimeout) + } + ) } } From 10e94586ed7a484d2bd7996178f84560697be4b8 Mon Sep 17 00:00:00 2001 From: Martin Welgemoed Date: Thu, 30 Sep 2021 10:20:11 +0200 Subject: [PATCH 16/26] Cleanup and tests for rollback logic --- .../mutants/findmutants/MutantMatcher.scala | 1 - .../scala/stryker4s/run/MutantRunner.scala | 3 +- .../exception/Stryker4sExceptionTest.scala | 58 +++++++++++++++++++ .../stryker4s/model/CompilerErrMsgTest.scala | 15 +++++ .../scala/stryker4s/model/MutantIdTest.scala | 12 ++++ .../stryker4s/model/MutatedFileTest.scala | 20 +++++++ .../stryker4s/run/MutantRunnerTest.scala | 49 +++++++++++++++- .../testutil/stubs/TestRunnerStub.scala | 24 +++++++- 8 files changed, 176 insertions(+), 6 deletions(-) create mode 100644 core/src/test/scala/stryker4s/extension/exception/Stryker4sExceptionTest.scala create mode 100644 core/src/test/scala/stryker4s/model/CompilerErrMsgTest.scala create mode 100644 core/src/test/scala/stryker4s/model/MutantIdTest.scala create mode 100644 core/src/test/scala/stryker4s/model/MutatedFileTest.scala diff --git a/core/src/main/scala/stryker4s/mutants/findmutants/MutantMatcher.scala b/core/src/main/scala/stryker4s/mutants/findmutants/MutantMatcher.scala index 4a9af9ebf..a8b3729e2 100644 --- a/core/src/main/scala/stryker4s/mutants/findmutants/MutantMatcher.scala +++ b/core/src/main/scala/stryker4s/mutants/findmutants/MutantMatcher.scala @@ -118,7 +118,6 @@ class MutantMatcher()(implicit config: Config) { else { Right( Mutant( - //The file and idInFile are left empty for now, they're assigned later MutantId(globalId = ids.next()), original, mutationToTerm(mutated), diff --git a/core/src/main/scala/stryker4s/run/MutantRunner.scala b/core/src/main/scala/stryker4s/run/MutantRunner.scala index 4f7e7b232..4a7342ba1 100644 --- a/core/src/main/scala/stryker4s/run/MutantRunner.scala +++ b/core/src/main/scala/stryker4s/run/MutantRunner.scala @@ -33,7 +33,8 @@ class MutantRunner( run(mutatedFiles) .flatMap { case Right(metrics) => IO.pure(metrics.asRight) - case Left(errors) => + case Left(errors) => + log.info("Attempting to remove mutants that gave a compile error...") //Retry once with the non-compiling mutants removed mutateFiles(errors.toList).flatMap(run) } diff --git a/core/src/test/scala/stryker4s/extension/exception/Stryker4sExceptionTest.scala b/core/src/test/scala/stryker4s/extension/exception/Stryker4sExceptionTest.scala new file mode 100644 index 000000000..941b0d374 --- /dev/null +++ b/core/src/test/scala/stryker4s/extension/exception/Stryker4sExceptionTest.scala @@ -0,0 +1,58 @@ +package stryker4s.extension.exception + +import stryker4s.model.CompilerErrMsg +import stryker4s.testutil.Stryker4sSuite + +class Stryker4sExceptionTest extends Stryker4sSuite { + describe("UnableToBuildPatternMatchException") { + it("should have the correct message") { + UnableToBuildPatternMatchException().getMessage shouldBe "Unable to build pattern match" + } + } + + describe("InitialTestRunFailedException") { + it("should have the correct message") { + InitialTestRunFailedException("testMsg").getMessage shouldBe "testMsg" + } + } + + describe("TestSetupException") { + it("should have the correct message ") { + TestSetupException("ProjectName").getMessage shouldBe + "Could not setup mutation testing environment. Unable to resolve project ProjectName. This could be due to compile errors or misconfiguration of Stryker4s. See debug logs for more information." + } + } + + describe("MutationRunFailedException") { + it("should have the correct message") { + MutationRunFailedException("xyz").getMessage shouldBe "xyz" + } + } + describe("UnableToFixCompilerErrorsException") { + it("should have a nicely formatted message") { + val errs = List( + CompilerErrMsg( + msg = "value forall is not a member of object java.nio.file.Files", + path = "/src/main/scala/com/company/strykerTest/TestObj1.scala", + line = 123 + ), + CompilerErrMsg( + msg = "something something types", + path = "/src/main/scala/com/company/strykerTest/TestObj1.scala", + line = 2 + ), + CompilerErrMsg( + msg = "yet another error with symbols $#'%%$~@1", + path = "/src/main/scala/com/company/strykerTest/TestObj2.scala", + line = 10000 + ) + ) + + UnableToFixCompilerErrorsException(errs).getMessage shouldBe + """Unable to remove non-compiling mutants in the mutated files. As a work-around you can exclude them in the stryker.conf. Please report this issue at https://github.com/stryker-mutator/stryker4s/issues + |/src/main/scala/com/company/strykerTest/TestObj1.scala: 'value forall is not a member of object java.nio.file.Files' + |/src/main/scala/com/company/strykerTest/TestObj1.scala: 'something something types' + |/src/main/scala/com/company/strykerTest/TestObj2.scala: 'yet another error with symbols $#'%%$~@1'""".stripMargin + } + } +} diff --git a/core/src/test/scala/stryker4s/model/CompilerErrMsgTest.scala b/core/src/test/scala/stryker4s/model/CompilerErrMsgTest.scala new file mode 100644 index 000000000..bf461bc12 --- /dev/null +++ b/core/src/test/scala/stryker4s/model/CompilerErrMsgTest.scala @@ -0,0 +1,15 @@ +package stryker4s.model + +import stryker4s.testutil.Stryker4sSuite + +class CompilerErrMsgTest extends Stryker4sSuite { + describe("CompilerErrMsgTest") { + it("should have a nicely formatted toString") { + CompilerErrMsg( + msg = "value forall is not a member of object java.nio.file.Files", + path = "/src/main/scala/com/company/strykerTest/TestObj1.scala", + line = 123 + ).toString shouldBe "/src/main/scala/com/company/strykerTest/TestObj1.scala:L123: 'value forall is not a member of object java.nio.file.Files'" + } + } +} diff --git a/core/src/test/scala/stryker4s/model/MutantIdTest.scala b/core/src/test/scala/stryker4s/model/MutantIdTest.scala new file mode 100644 index 000000000..9d6c12a06 --- /dev/null +++ b/core/src/test/scala/stryker4s/model/MutantIdTest.scala @@ -0,0 +1,12 @@ +package stryker4s.model + +import stryker4s.testutil.Stryker4sSuite + +class MutantIdTest extends Stryker4sSuite { + describe("MutantId") { + it("should have a toString that returns a number") { + MutantId(1234).toString shouldBe "1234" + MutantId(-1).toString shouldBe "-1" + } + } +} diff --git a/core/src/test/scala/stryker4s/model/MutatedFileTest.scala b/core/src/test/scala/stryker4s/model/MutatedFileTest.scala new file mode 100644 index 000000000..2a9181543 --- /dev/null +++ b/core/src/test/scala/stryker4s/model/MutatedFileTest.scala @@ -0,0 +1,20 @@ +package stryker4s.model + +import fs2.io.file.Path +import stryker4s.testutil.Stryker4sSuite + +import scala.meta._ + +class MutatedFileTest extends Stryker4sSuite { + describe("MutatedFile") { + it("should return the new source codes as a string") { + MutatedFile( + Path("/blah/test"), + q"class blah(x: String) { def hi() = x }", + Seq.empty, + Seq.empty, + 0 + ).mutatedSource shouldBe "class blah(x: String) { def hi() = x }" + } + } +} diff --git a/core/src/test/scala/stryker4s/run/MutantRunnerTest.scala b/core/src/test/scala/stryker4s/run/MutantRunnerTest.scala index 0f65d840d..be503e7f5 100644 --- a/core/src/test/scala/stryker4s/run/MutantRunnerTest.scala +++ b/core/src/test/scala/stryker4s/run/MutantRunnerTest.scala @@ -4,7 +4,7 @@ import cats.effect.IO import org.mockito.captor.ArgCaptor import stryker4s.config.Config import stryker4s.extension.mutationtype.EmptyString -import stryker4s.model.{Killed, Mutant, MutantId, MutatedFile, Survived} +import stryker4s.model._ import stryker4s.report.{FinishedRunEvent, Reporter} import stryker4s.scalatest.{FileUtil, LogMatchers} import stryker4s.testutil.stubs.{TestFileResolver, TestRunnerStub} @@ -48,5 +48,52 @@ class MutantRunnerTest extends Stryker4sIOSuite with MockitoIOSuite with LogMatc result.survived shouldBe 1 } } + + it("should return a mutationScore of 66.67 when 2 of 3 mutants are killed and 1 doesn't compile.") { + val fileCollectorMock = new TestFileResolver(Seq.empty) + val reporterMock = mock[Reporter] + whenF(reporterMock.onRunFinished(any[FinishedRunEvent])).thenReturn(()) + when(reporterMock.mutantTested).thenReturn(_.drain) + + val mutant = Mutant(MutantId(3), q"0", q"zero", EmptyString) + val secondMutant = Mutant(MutantId(1), q"1", q"one", EmptyString) + val thirdMutant = Mutant(MutantId(2), q"5", q"5", EmptyString) + val nonCompilingMutant = Mutant(MutantId(4), q"7", q"2", EmptyString) + + val errs = List(CompilerErrMsg("blah", "xyz", 123)) + val testRunner = + TestRunnerStub.withInitialCompilerError( + errs, + Killed(mutant), + Killed(secondMutant), + Survived(thirdMutant), + CompileError(nonCompilingMutant) + ) + + val sut = new MutantRunner(testRunner, fileCollectorMock, reporterMock) + val file = FileUtil.getResource("scalaFiles/simpleFile.scala") + val mutants = Seq(mutant, secondMutant, thirdMutant) + val mutatedFile = MutatedFile(file, q"def foo = 4", mutants, Seq(nonCompilingMutant), 0) + + sut(_ => IO(List(mutatedFile))).asserting { result => + val captor = ArgCaptor[FinishedRunEvent] + verify(reporterMock, times(1)).onRunFinished(captor.capture) + val runReport = captor.value.report.files.loneElement + + "Setting up mutated environment..." shouldBe loggedAsInfo + "Starting initial test run..." shouldBe loggedAsInfo + "Initial test run succeeded! Testing mutants..." shouldBe loggedAsInfo + "Attempting to remove mutants that gave a compile error..." shouldBe loggedAsInfo + + runReport._1 shouldBe "simpleFile.scala" + runReport._2.mutants.map(_.id) shouldBe List("1", "2", "3", "4") + result.mutationScore shouldBe ((2d / 3d) * 100) + result.totalMutants shouldBe 4 + result.totalInvalid shouldBe 1 + result.killed shouldBe 2 + result.survived shouldBe 1 + result.compileErrors shouldBe 1 + } + } } } diff --git a/core/src/test/scala/stryker4s/testutil/stubs/TestRunnerStub.scala b/core/src/test/scala/stryker4s/testutil/stubs/TestRunnerStub.scala index a4a506300..10c581825 100644 --- a/core/src/test/scala/stryker4s/testutil/stubs/TestRunnerStub.scala +++ b/core/src/test/scala/stryker4s/testutil/stubs/TestRunnerStub.scala @@ -5,8 +5,8 @@ import cats.effect.{IO, Resource} import cats.syntax.applicativeError._ import fs2.io.file.Path import stryker4s.extension.mutationtype.LesserThan -import stryker4s.model.{InitialTestRunResult, Killed, Mutant, MutantId, MutantRunResult, NoCoverageInitialTestRun} -import stryker4s.run.{ResourcePool, TestRunner} +import stryker4s.model._ +import stryker4s.run.{ResourcePool, TestRunner, TestRunnerPool} import scala.meta._ @@ -26,6 +26,24 @@ object TestRunnerStub { def resource = withResults(Killed(Mutant(MutantId(0), q">", q"<", LesserThan))) - def withResults(mutants: MutantRunResult*) = (_: Path) => + def withResults(mutants: MutantRunResult*) = (_: Path) => makeResults(mutants) + + def withInitialCompilerError( + errs: List[CompilerErrMsg], + mutants: MutantRunResult* + ): Path => Either[NonEmptyList[CompilerErrMsg], Resource[IO, TestRunnerPool]] = { + var firstRun = true + (_: Path) => + if (firstRun) { + firstRun = false + Left(NonEmptyList.fromListUnsafe(errs)) + } else { + makeResults(mutants) + } + } + + private def makeResults( + mutants: Seq[MutantRunResult] + ): Either[NonEmptyList[CompilerErrMsg], Resource[IO, TestRunnerPool]] = Right(ResourcePool(NonEmptyList.of(Resource.pure[IO, TestRunner](new TestRunnerStub(mutants.map(() => _)))))) } From 18202be8fa4e9d088bff38e0bbddff50eced1399 Mon Sep 17 00:00:00 2001 From: Martin Welgemoed Date: Thu, 30 Sep 2021 16:32:14 +0200 Subject: [PATCH 17/26] Added test and performance improvement in the RollbackHandler --- .../stryker4s/mutants/RollbackHandler.scala | 3 +- .../resources/rollbackTest/TestObj1.scala | 16 ++++ .../TestObj1MutatedWithoutForall.scala | 36 +++++++++ .../resources/rollbackTest/TestObj2.scala | 9 +++ .../rollbackTest/TestObj2Mutated.scala | 13 ++++ .../stryker4s/mutants/RollbackTest.scala | 73 +++++++++++++++++++ 6 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 core/src/test/resources/rollbackTest/TestObj1.scala create mode 100644 core/src/test/resources/rollbackTest/TestObj1MutatedWithoutForall.scala create mode 100644 core/src/test/resources/rollbackTest/TestObj2.scala create mode 100644 core/src/test/resources/rollbackTest/TestObj2Mutated.scala create mode 100644 core/src/test/scala/stryker4s/mutants/RollbackTest.scala diff --git a/core/src/main/scala/stryker4s/mutants/RollbackHandler.scala b/core/src/main/scala/stryker4s/mutants/RollbackHandler.scala index 1d79e02c6..e9542a2f0 100644 --- a/core/src/main/scala/stryker4s/mutants/RollbackHandler.scala +++ b/core/src/main/scala/stryker4s/mutants/RollbackHandler.scala @@ -20,7 +20,8 @@ class RollbackHandler(matchBuilder: MatchBuilder) { //This is not very performant, but you only pay the cost if there actually is a compiler error val originalFile = mutateFile(file, mutationsInSource) - val nonCompilingIds = errorsToIds(compileErrors, originalFile.mutatedSource, mutationsInSource.mutants) + val errorsInThisFile = compileErrors.filter(err => file.toString.endsWith(err.path)) + val nonCompilingIds = errorsToIds(errorsInThisFile, originalFile.mutatedSource, mutationsInSource.mutants) val (nonCompilingMutants, compilingMutants) = mutationsInSource.mutants.partition(mut => nonCompilingIds.contains(mut.id)) diff --git a/core/src/test/resources/rollbackTest/TestObj1.scala b/core/src/test/resources/rollbackTest/TestObj1.scala new file mode 100644 index 000000000..fb7887b33 --- /dev/null +++ b/core/src/test/resources/rollbackTest/TestObj1.scala @@ -0,0 +1,16 @@ +package stryker4s.mutants + +import java.nio.file.{Files, Paths} + +object TestObj1 { + def test2(a: String): Boolean = { + Files.exists(Paths.get(a)) //Should not get mutated! + } + + def least(a: Int, b: Int): Int = { + (a, b) match { + case (a, b) if a < b => a + case (a, b) if a == b => 0 + } + } +} diff --git a/core/src/test/resources/rollbackTest/TestObj1MutatedWithoutForall.scala b/core/src/test/resources/rollbackTest/TestObj1MutatedWithoutForall.scala new file mode 100644 index 000000000..a27c19f74 --- /dev/null +++ b/core/src/test/resources/rollbackTest/TestObj1MutatedWithoutForall.scala @@ -0,0 +1,36 @@ +package stryker4s.mutants +import java.nio.file.{Files, Paths} +object TestObj1 { + def test2(a: String): Boolean = { + Files.exists(Paths.get(a)) + } + def least(a: Int, b: Int): Int = { + _root_.scala.sys.props.get("ACTIVE_MUTATION").map(_root_.java.lang.Integer.parseInt(_)) match { + case Some(1) => + (a, b) match { + case (a, b) if a <= b => a + case (a, b) if a == b => 0 + } + case Some(2) => + (a, b) match { + case (a, b) if a > b => a + case (a, b) if a == b => 0 + } + case Some(3) => + (a, b) match { + case (a, b) if a == b => a + case (a, b) if a == b => 0 + } + case Some(4) => + (a, b) match { + case (a, b) if a < b => a + case (a, b) if a != b => 0 + } + case _ => + (a, b) match { + case (a, b) if a < b => a + case (a, b) if a == b => 0 + } + } + } +} diff --git a/core/src/test/resources/rollbackTest/TestObj2.scala b/core/src/test/resources/rollbackTest/TestObj2.scala new file mode 100644 index 000000000..287ccf801 --- /dev/null +++ b/core/src/test/resources/rollbackTest/TestObj2.scala @@ -0,0 +1,9 @@ +package stryker4s.mutants + +object TestObj2 { + //spacing to get different mutant on exact same line as in TestObj1 + + def str(a: String): Boolean = { + a == "blah" + } +} diff --git a/core/src/test/resources/rollbackTest/TestObj2Mutated.scala b/core/src/test/resources/rollbackTest/TestObj2Mutated.scala new file mode 100644 index 000000000..d75f51fc1 --- /dev/null +++ b/core/src/test/resources/rollbackTest/TestObj2Mutated.scala @@ -0,0 +1,13 @@ +package stryker4s.mutants +object TestObj2 { + def str(a: String): Boolean = { + _root_.scala.sys.props.get("ACTIVE_MUTATION").map(_root_.java.lang.Integer.parseInt(_)) match { + case Some(5) => + a != "blah" + case Some(6) => + a == "" + case _ => + a == "blah" + } + } +} diff --git a/core/src/test/scala/stryker4s/mutants/RollbackTest.scala b/core/src/test/scala/stryker4s/mutants/RollbackTest.scala new file mode 100644 index 000000000..40aa0e35c --- /dev/null +++ b/core/src/test/scala/stryker4s/mutants/RollbackTest.scala @@ -0,0 +1,73 @@ +package stryker4s.mutants + +import stryker4s.config.Config +import stryker4s.extension.mutationtype.Forall +import stryker4s.model.CompilerErrMsg +import stryker4s.mutants.applymutants.{ActiveMutationContext, MatchBuilder, StatementTransformer} +import stryker4s.mutants.findmutants.{MutantFinder, MutantMatcher} +import stryker4s.scalatest.{FileUtil, LogMatchers} +import stryker4s.testutil.Stryker4sIOSuite +import stryker4s.testutil.stubs.TestFileResolver + +import scala.meta._ + +class RollbackTest extends Stryker4sIOSuite with LogMatchers { + describe("RollbackHandler") { + it("should remove a non-compiling mutant") { + implicit val conf: Config = Config.default.copy( + baseDir = FileUtil.getResource("rollbackTest"), + concurrency = 1 //Concurrency 1 to make output order predictable + ) + + val testObj1Path = FileUtil.getResource("rollbackTest/TestObj1.scala") + val testObj2Path = FileUtil.getResource("rollbackTest/TestObj2.scala") + + val testFiles = Seq(testObj1Path, testObj2Path) + + val testSourceCollector = new TestFileResolver(testFiles) + + val mutator = new Mutator( + new MutantFinder(new MutantMatcher), + new StatementTransformer, + new MatchBuilder(ActiveMutationContext.sysProps) + ) + + val errs = List( + CompilerErrMsg("value forall is not a member of object java.nio.file.Files", "rollbackTest/TestObj1.scala", 7) + ) + + val ret = mutator.mutate(testSourceCollector.files, errs) + + ret.asserting { files => + val testObj1Mutated = files.head + val testObj2Mutated = files.last + + testObj1Mutated.fileOrigin shouldBe testObj1Path + testObj1Mutated.tree.structure shouldBe FileUtil + .getResource("rollbackTest/TestObj1MutatedWithoutForall.scala") + .toNioPath + .parse[Source] + .get + .structure + + testObj1Mutated.mutants.size shouldBe 4 + + testObj1Mutated.nonCompilingMutants.size shouldBe 1 + testObj1Mutated.nonCompilingMutants.head.mutationType shouldBe Forall + + testObj1Mutated.excludedMutants shouldBe 0 + + testObj2Mutated.fileOrigin shouldBe testObj2Path + testObj2Mutated.tree.structure shouldBe FileUtil + .getResource("rollbackTest/TestObj2Mutated.scala") + .toNioPath + .parse[Source] + .get + .structure + testObj2Mutated.mutants.size shouldBe 2 + testObj2Mutated.nonCompilingMutants.size shouldBe 0 + testObj2Mutated.excludedMutants shouldBe 0 + } + } + } +} From ee05a0b4a1e518f1dd4ff7a8e5223fa8a3852149 Mon Sep 17 00:00:00 2001 From: Martin Welgemoed Date: Fri, 1 Oct 2021 11:31:47 +0200 Subject: [PATCH 18/26] Added sbt scripted test for compiler errors --- .../src/main/scala/example/TestObj1.scala | 17 +++++++++++++++++ .../src/main/scala/example/TestObj2.scala | 9 +++++++++ .../src/test/scala/example/TestObj1Spec.scala | 19 +++++++++++++++++++ .../src/test/scala/example/TestObj2Spec.scala | 11 +++++++++++ 4 files changed, 56 insertions(+) create mode 100644 sbt/src/sbt-test/sbt-stryker4s/test-1/src/main/scala/example/TestObj1.scala create mode 100644 sbt/src/sbt-test/sbt-stryker4s/test-1/src/main/scala/example/TestObj2.scala create mode 100644 sbt/src/sbt-test/sbt-stryker4s/test-1/src/test/scala/example/TestObj1Spec.scala create mode 100644 sbt/src/sbt-test/sbt-stryker4s/test-1/src/test/scala/example/TestObj2Spec.scala diff --git a/sbt/src/sbt-test/sbt-stryker4s/test-1/src/main/scala/example/TestObj1.scala b/sbt/src/sbt-test/sbt-stryker4s/test-1/src/main/scala/example/TestObj1.scala new file mode 100644 index 000000000..1740192e8 --- /dev/null +++ b/sbt/src/sbt-test/sbt-stryker4s/test-1/src/main/scala/example/TestObj1.scala @@ -0,0 +1,17 @@ +package example + +import java.nio.file.{Files, Paths} + +object TestObj { + def test2(a: String): Boolean = { + Files.exists(Paths.get(a)) //Should not get mutated! + } + + def least(a: Int, b: Int): Int = { + (a, b) match { + case (a, b) if a < b => a + case (a, b) if a > b => b + case (a, b) if a == b => 0 + } + } +} diff --git a/sbt/src/sbt-test/sbt-stryker4s/test-1/src/main/scala/example/TestObj2.scala b/sbt/src/sbt-test/sbt-stryker4s/test-1/src/main/scala/example/TestObj2.scala new file mode 100644 index 000000000..d52e088d5 --- /dev/null +++ b/sbt/src/sbt-test/sbt-stryker4s/test-1/src/main/scala/example/TestObj2.scala @@ -0,0 +1,9 @@ +package example + +object TestObj2 { + //spacing to get exact same line as in TestObj + + def str(a: String): Boolean = { + a == "blah" + } +} diff --git a/sbt/src/sbt-test/sbt-stryker4s/test-1/src/test/scala/example/TestObj1Spec.scala b/sbt/src/sbt-test/sbt-stryker4s/test-1/src/test/scala/example/TestObj1Spec.scala new file mode 100644 index 000000000..4a61b27d2 --- /dev/null +++ b/sbt/src/sbt-test/sbt-stryker4s/test-1/src/test/scala/example/TestObj1Spec.scala @@ -0,0 +1,19 @@ +package example + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class Spec extends AnyFlatSpec with Matchers { + it should "return the lesser" in { + TestObj.least(1, 2) shouldBe 1 + TestObj.least(2, 1) shouldBe 1 + } + + it should "return 0 if equal" in { + TestObj.least(0, 0) shouldBe 0 + } + + it should "return false if a file does not exists" in { + TestObj.test2("/home/blah/fake") shouldBe false + } +} \ No newline at end of file diff --git a/sbt/src/sbt-test/sbt-stryker4s/test-1/src/test/scala/example/TestObj2Spec.scala b/sbt/src/sbt-test/sbt-stryker4s/test-1/src/test/scala/example/TestObj2Spec.scala new file mode 100644 index 000000000..44de3c0c1 --- /dev/null +++ b/sbt/src/sbt-test/sbt-stryker4s/test-1/src/test/scala/example/TestObj2Spec.scala @@ -0,0 +1,11 @@ +package example + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class Spec2 extends AnyFlatSpec with Matchers { + it should "check the string" in { + TestObj2.str("hi") shouldBe false + TestObj2.str("blah") shouldBe true + } +} \ No newline at end of file From 568dfee262950559a1828ae0650ec4b9d0f20ae1 Mon Sep 17 00:00:00 2001 From: Martin Welgemoed Date: Fri, 1 Oct 2021 11:41:04 +0200 Subject: [PATCH 19/26] Fixed scalafmt in test project --- .../test-1/src/main/scala/example/TestObj1.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sbt/src/sbt-test/sbt-stryker4s/test-1/src/main/scala/example/TestObj1.scala b/sbt/src/sbt-test/sbt-stryker4s/test-1/src/main/scala/example/TestObj1.scala index 1740192e8..3ab69346a 100644 --- a/sbt/src/sbt-test/sbt-stryker4s/test-1/src/main/scala/example/TestObj1.scala +++ b/sbt/src/sbt-test/sbt-stryker4s/test-1/src/main/scala/example/TestObj1.scala @@ -9,8 +9,8 @@ object TestObj { def least(a: Int, b: Int): Int = { (a, b) match { - case (a, b) if a < b => a - case (a, b) if a > b => b + case (a, b) if a < b => a + case (a, b) if a > b => b case (a, b) if a == b => 0 } } From 5af8a1012f6c2dd79d997179677e07c36caed3bc Mon Sep 17 00:00:00 2001 From: Martin Welgemoed Date: Fri, 1 Oct 2021 11:44:24 +0200 Subject: [PATCH 20/26] Fixed Maven project using old CompilerError class --- .../src/main/scala/stryker4s/maven/Stryker4sMavenRunner.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maven/src/main/scala/stryker4s/maven/Stryker4sMavenRunner.scala b/maven/src/main/scala/stryker4s/maven/Stryker4sMavenRunner.scala index 70bab01fd..fb06309de 100644 --- a/maven/src/main/scala/stryker4s/maven/Stryker4sMavenRunner.scala +++ b/maven/src/main/scala/stryker4s/maven/Stryker4sMavenRunner.scala @@ -8,7 +8,7 @@ import org.apache.maven.shared.invoker.Invoker import stryker4s.config.Config import stryker4s.log.Logger import stryker4s.maven.runner.MavenTestRunner -import stryker4s.model.CompileError +import stryker4s.model.CompilerErrMsg import stryker4s.mutants.applymutants.ActiveMutationContext.{envVar, ActiveMutationContext} import stryker4s.run.Stryker4sRunner @@ -20,7 +20,7 @@ class Stryker4sMavenRunner(project: MavenProject, invoker: Invoker)(implicit log override def resolveTestRunners( tmpDir: Path - )(implicit config: Config): Either[NonEmptyList[CompileError], NonEmptyList[Resource[IO, MavenTestRunner]]] = { + )(implicit config: Config): Either[NonEmptyList[CompilerErrMsg], NonEmptyList[Resource[IO, MavenTestRunner]]] = { val goals = List("test") val properties = new Properties(project.getProperties) From 322a84bc1cfd756ede86180e2d5df5029fcb8a20 Mon Sep 17 00:00:00 2001 From: Martin Welgemoed Date: Fri, 1 Oct 2021 12:22:59 +0200 Subject: [PATCH 21/26] Cleanup and better logging for mutant rollbacks --- .../scala/stryker4s/mutants/Mutator.scala | 4 ++++ .../stryker4s/mutants/RollbackHandler.scala | 21 ++++++++++++++----- .../stryker4s/sbt/Stryker4sSbtRunner.scala | 5 ++--- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/core/src/main/scala/stryker4s/mutants/Mutator.scala b/core/src/main/scala/stryker4s/mutants/Mutator.scala index 91aaf13f1..09115ec98 100644 --- a/core/src/main/scala/stryker4s/mutants/Mutator.scala +++ b/core/src/main/scala/stryker4s/mutants/Mutator.scala @@ -24,6 +24,10 @@ class Mutator( private val rollbackHandler: RollbackHandler = new RollbackHandler(matchBuilder) def mutate(files: Stream[IO, Path], compileErrors: Seq[CompilerErrMsg] = Seq.empty): IO[Seq[MutatedFile]] = { + if (compileErrors.nonEmpty) { + log.debug("Trying to remove mutants that gave these errors:") + compileErrors.foreach(err => log.debug(s"\t$err")) + } files .parEvalMapUnordered(config.concurrency)(p => findMutants(p).tupleLeft(p)) .map { case (file, mutationsInSource) => diff --git a/core/src/main/scala/stryker4s/mutants/RollbackHandler.scala b/core/src/main/scala/stryker4s/mutants/RollbackHandler.scala index e9542a2f0..9e54000fb 100644 --- a/core/src/main/scala/stryker4s/mutants/RollbackHandler.scala +++ b/core/src/main/scala/stryker4s/mutants/RollbackHandler.scala @@ -1,12 +1,13 @@ package stryker4s.mutants import fs2.io.file.Path +import stryker4s.log.Logger import stryker4s.model._ import stryker4s.mutants.applymutants.MatchBuilder import scala.meta.{Case, Source, _} -class RollbackHandler(matchBuilder: MatchBuilder) { +class RollbackHandler(matchBuilder: MatchBuilder)(implicit log: Logger) { def rollbackNonCompilingMutants( file: Path, mutationsInSource: MutationsInSource, @@ -21,11 +22,21 @@ class RollbackHandler(matchBuilder: MatchBuilder) { val originalFile = mutateFile(file, mutationsInSource) val errorsInThisFile = compileErrors.filter(err => file.toString.endsWith(err.path)) - val nonCompilingIds = errorsToIds(errorsInThisFile, originalFile.mutatedSource, mutationsInSource.mutants) - val (nonCompilingMutants, compilingMutants) = - mutationsInSource.mutants.partition(mut => nonCompilingIds.contains(mut.id)) + if (errorsInThisFile.isEmpty) { + log.debug(s"No compiler errors in $file") + originalFile + } else { + log.debug(s"Found ${errorsInThisFile.mkString(" ")} in $file") - mutateFile(file, mutationsInSource.copy(mutants = compilingMutants)).copy(nonCompilingMutants = nonCompilingMutants) + val nonCompilingIds = errorsToIds(errorsInThisFile, originalFile.mutatedSource, mutationsInSource.mutants) + log.debug(s"Removed mutant id[s] ${nonCompilingIds.mkString(";")} in $file") + + val (nonCompilingMutants, compilingMutants) = + mutationsInSource.mutants.partition(mut => nonCompilingIds.contains(mut.id)) + + mutateFile(file, mutationsInSource.copy(mutants = compilingMutants)) + .copy(nonCompilingMutants = nonCompilingMutants) + } } //Given compiler errors, return the mutants that caused it by searching for the matching case statement at that line diff --git a/sbt/src/main/scala/stryker4s/sbt/Stryker4sSbtRunner.scala b/sbt/src/main/scala/stryker4s/sbt/Stryker4sSbtRunner.scala index 4cdaa8adc..898ebd16d 100644 --- a/sbt/src/main/scala/stryker4s/sbt/Stryker4sSbtRunner.scala +++ b/sbt/src/main/scala/stryker4s/sbt/Stryker4sSbtRunner.scala @@ -79,13 +79,12 @@ class Stryker4sSbtRunner( def extractTaskValue[T](task: TaskKey[T], name: String) = { - val ret = Project.runTask(task, newState) match { + Project.runTask(task, newState) match { case Some((_, Value(result))) => result case other => log.debug(s"Expected $name but got $other") - throw new TestSetupException(name) + throw TestSetupException(name) } - ret } //SBT returns any errors as a Incomplete case class, which can contain other Incomplete instances From 7e1784f6322fdfda091fed5bf7bca453ce4ef71e Mon Sep 17 00:00:00 2001 From: Martin Welgemoed Date: Fri, 1 Oct 2021 15:23:23 +0200 Subject: [PATCH 22/26] No longer comparing files as strings --- core/src/main/scala/stryker4s/mutants/RollbackHandler.scala | 2 +- sbt/src/main/scala/stryker4s/sbt/Stryker4sSbtRunner.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala/stryker4s/mutants/RollbackHandler.scala b/core/src/main/scala/stryker4s/mutants/RollbackHandler.scala index 9e54000fb..e84ba3b93 100644 --- a/core/src/main/scala/stryker4s/mutants/RollbackHandler.scala +++ b/core/src/main/scala/stryker4s/mutants/RollbackHandler.scala @@ -21,7 +21,7 @@ class RollbackHandler(matchBuilder: MatchBuilder)(implicit log: Logger) { //This is not very performant, but you only pay the cost if there actually is a compiler error val originalFile = mutateFile(file, mutationsInSource) - val errorsInThisFile = compileErrors.filter(err => file.toString.endsWith(err.path)) + val errorsInThisFile = compileErrors.filter(err => file.endsWith(err.path)) if (errorsInThisFile.isEmpty) { log.debug(s"No compiler errors in $file") originalFile diff --git a/sbt/src/main/scala/stryker4s/sbt/Stryker4sSbtRunner.scala b/sbt/src/main/scala/stryker4s/sbt/Stryker4sSbtRunner.scala index 898ebd16d..91680d387 100644 --- a/sbt/src/main/scala/stryker4s/sbt/Stryker4sSbtRunner.scala +++ b/sbt/src/main/scala/stryker4s/sbt/Stryker4sSbtRunner.scala @@ -105,7 +105,7 @@ class Stryker4sSbtRunner( exception.problems.flatMap { e => for { path <- e.position().sourceFile().asScala - pathStr = path.toPath.toAbsolutePath.toString.replace(tmpDir.toString, "") + pathStr = tmpDir.toNioPath.relativize(path.toPath.toAbsolutePath).toString line <- e.position().line().asScala } yield CompilerErrMsg(e.message(), pathStr, line) }.toSeq From 45d8cef1e8f4ae0d6b02b0cd8be1f927089a6ed0 Mon Sep 17 00:00:00 2001 From: Martin Welgemoed Date: Fri, 1 Oct 2021 16:06:13 +0200 Subject: [PATCH 23/26] Scalafmt --- .../test-1/src/test/scala/example/TestObj1Spec.scala | 2 +- .../test-1/src/test/scala/example/TestObj2Spec.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sbt/src/sbt-test/sbt-stryker4s/test-1/src/test/scala/example/TestObj1Spec.scala b/sbt/src/sbt-test/sbt-stryker4s/test-1/src/test/scala/example/TestObj1Spec.scala index 4a61b27d2..86c83683f 100644 --- a/sbt/src/sbt-test/sbt-stryker4s/test-1/src/test/scala/example/TestObj1Spec.scala +++ b/sbt/src/sbt-test/sbt-stryker4s/test-1/src/test/scala/example/TestObj1Spec.scala @@ -16,4 +16,4 @@ class Spec extends AnyFlatSpec with Matchers { it should "return false if a file does not exists" in { TestObj.test2("/home/blah/fake") shouldBe false } -} \ No newline at end of file +} diff --git a/sbt/src/sbt-test/sbt-stryker4s/test-1/src/test/scala/example/TestObj2Spec.scala b/sbt/src/sbt-test/sbt-stryker4s/test-1/src/test/scala/example/TestObj2Spec.scala index 44de3c0c1..597857063 100644 --- a/sbt/src/sbt-test/sbt-stryker4s/test-1/src/test/scala/example/TestObj2Spec.scala +++ b/sbt/src/sbt-test/sbt-stryker4s/test-1/src/test/scala/example/TestObj2Spec.scala @@ -8,4 +8,4 @@ class Spec2 extends AnyFlatSpec with Matchers { TestObj2.str("hi") shouldBe false TestObj2.str("blah") shouldBe true } -} \ No newline at end of file +} From 537dd498e5448f5db75f6796d12b896d8a683607 Mon Sep 17 00:00:00 2001 From: Martin Welgemoed Date: Fri, 1 Oct 2021 17:02:25 +0200 Subject: [PATCH 24/26] Using case statements after the StatementTransformer to search for mutants --- .../scala/stryker4s/mutants/Mutator.scala | 82 +++++++++++++++++-- .../stryker4s/mutants/RollbackHandler.scala | 70 ---------------- 2 files changed, 73 insertions(+), 79 deletions(-) delete mode 100644 core/src/main/scala/stryker4s/mutants/RollbackHandler.scala diff --git a/core/src/main/scala/stryker4s/mutants/Mutator.scala b/core/src/main/scala/stryker4s/mutants/Mutator.scala index 09115ec98..bb23415b1 100644 --- a/core/src/main/scala/stryker4s/mutants/Mutator.scala +++ b/core/src/main/scala/stryker4s/mutants/Mutator.scala @@ -11,6 +11,8 @@ import stryker4s.model._ import stryker4s.mutants.applymutants.{MatchBuilder, StatementTransformer} import stryker4s.mutants.findmutants.MutantFinder +import scala.meta._ + class Mutator( mutantFinder: MutantFinder, transformer: StatementTransformer, @@ -20,9 +22,6 @@ class Mutator( log: Logger ) { - //Logic for dealing with compiler errors and removing non-compiling mutants from files - private val rollbackHandler: RollbackHandler = new RollbackHandler(matchBuilder) - def mutate(files: Stream[IO, Path], compileErrors: Seq[CompilerErrMsg] = Seq.empty): IO[Seq[MutatedFile]] = { if (compileErrors.nonEmpty) { log.debug("Trying to remove mutants that gave these errors:") @@ -31,10 +30,7 @@ class Mutator( files .parEvalMapUnordered(config.concurrency)(p => findMutants(p).tupleLeft(p)) .map { case (file, mutationsInSource) => - if (compileErrors.isEmpty) - mutateFile(file, mutationsInSource) - else - rollbackHandler.rollbackNonCompilingMutants(file, mutationsInSource, mutateFile, compileErrors) + mutateFile(file, mutationsInSource, compileErrors) } .filterNot(mutatedFile => mutatedFile.mutants.isEmpty && mutatedFile.excludedMutants == 0) .compile @@ -44,18 +40,58 @@ class Mutator( private def mutateFile( file: Path, - mutationsInSource: MutationsInSource + mutationsInSource: MutationsInSource, + compileErrors: Seq[CompilerErrMsg] ): MutatedFile = { val transformed = transformStatements(mutationsInSource) val builtTree = buildMatches(transformed) - MutatedFile( + val mutatedFile = MutatedFile( fileOrigin = file, tree = builtTree, mutants = mutationsInSource.mutants, nonCompilingMutants = Seq.empty, excludedMutants = mutationsInSource.excluded ) + + if (compileErrors.isEmpty) + mutatedFile + else { + //If there are any compiler errors (i.e. we're currently retrying the mutation with the bad ones rolled back) + //Then we take the original tree built that didn't compile + //And then we search inside it to translate the compile errors to mutants + //Finally we rebuild it from scratch without those mutants + //This is not very performant, but you only pay the cost if there actually is a compiler error + val errorsInThisFile = compileErrors.filter(err => file.endsWith(err.path)) + if (errorsInThisFile.isEmpty) { + log.debug(s"No compiler errors in $file") + mutatedFile + } else { + log.debug(s"Found ${errorsInThisFile.mkString(" ")} in $file") + + val nonCompilingIds = errorsToIds( + errorsInThisFile, + mutatedFile.mutatedSource, + transformed.transformedStatements.flatMap(_.mutantStatements) + ) + log.debug(s"Removed mutant id[s] ${nonCompilingIds.mkString(";")} in $file") + + val (nonCompilingMutants, compilingMutants) = + mutationsInSource.mutants.partition(mut => nonCompilingIds.contains(mut.id)) + + val mutationsInSourceWithoutErrors = mutationsInSource.copy(mutants = compilingMutants) + val transformedWithoutErrors = transformStatements(mutationsInSourceWithoutErrors) + val builtTreeWithoutErrors = buildMatches(transformedWithoutErrors) + + MutatedFile( + fileOrigin = file, + tree = builtTreeWithoutErrors, + mutants = compilingMutants, + nonCompilingMutants = nonCompilingMutants, + excludedMutants = mutationsInSource.excluded + ) + } + } } /** Step 1: Find mutants in the found files @@ -95,4 +131,32 @@ class Mutator( } else IO.unit } } + + //Given compiler errors, return the mutants that caused it by searching for the matching case statement at that line + private def errorsToIds( + compileErrors: Seq[CompilerErrMsg], + mutatedFile: String, + mutants: Seq[Mutant] + ): Seq[MutantId] = { + val statementToMutIdMap = mutants.map { mutant => + matchBuilder.mutantToCase(mutant).structure -> mutant.id + }.toMap + + val lineToMutantId: Map[Int, MutantId] = mutatedFile + //Parsing the mutated tree again as a string is the only way to get the position info of the mutated statements + .parse[Source] + .getOrElse(throw new RuntimeException(s"Failed to parse $mutatedFile to remove non-compiling mutants")) + .collect { + case node: Case if statementToMutIdMap.contains(node.structure) => + val mutId = statementToMutIdMap(node.structure) + //+1 because scalameta uses zero-indexed line numbers + (node.pos.startLine to node.pos.endLine).map(i => i + 1 -> mutId) + } + .flatten + .toMap + + compileErrors.flatMap { err => + lineToMutantId.get(err.line) + } + } } diff --git a/core/src/main/scala/stryker4s/mutants/RollbackHandler.scala b/core/src/main/scala/stryker4s/mutants/RollbackHandler.scala deleted file mode 100644 index e84ba3b93..000000000 --- a/core/src/main/scala/stryker4s/mutants/RollbackHandler.scala +++ /dev/null @@ -1,70 +0,0 @@ -package stryker4s.mutants - -import fs2.io.file.Path -import stryker4s.log.Logger -import stryker4s.model._ -import stryker4s.mutants.applymutants.MatchBuilder - -import scala.meta.{Case, Source, _} - -class RollbackHandler(matchBuilder: MatchBuilder)(implicit log: Logger) { - def rollbackNonCompilingMutants( - file: Path, - mutationsInSource: MutationsInSource, - mutateFile: (Path, MutationsInSource) => MutatedFile, - compileErrors: Seq[CompilerErrMsg] - ): MutatedFile = { - //If there are any compiler errors (i.e. we're currently retrying the mutation with the bad ones rolled back) - //Then we take the original tree built that didn't compile - //And then we search inside it to translate the compile errors to mutants - //Finally we rebuild it from scratch without those mutants - //This is not very performant, but you only pay the cost if there actually is a compiler error - val originalFile = mutateFile(file, mutationsInSource) - - val errorsInThisFile = compileErrors.filter(err => file.endsWith(err.path)) - if (errorsInThisFile.isEmpty) { - log.debug(s"No compiler errors in $file") - originalFile - } else { - log.debug(s"Found ${errorsInThisFile.mkString(" ")} in $file") - - val nonCompilingIds = errorsToIds(errorsInThisFile, originalFile.mutatedSource, mutationsInSource.mutants) - log.debug(s"Removed mutant id[s] ${nonCompilingIds.mkString(";")} in $file") - - val (nonCompilingMutants, compilingMutants) = - mutationsInSource.mutants.partition(mut => nonCompilingIds.contains(mut.id)) - - mutateFile(file, mutationsInSource.copy(mutants = compilingMutants)) - .copy(nonCompilingMutants = nonCompilingMutants) - } - } - - //Given compiler errors, return the mutants that caused it by searching for the matching case statement at that line - private def errorsToIds( - compileErrors: Seq[CompilerErrMsg], - mutatedFile: String, - mutants: Seq[Mutant] - ): Seq[MutantId] = { - val statementToMutIdMap = mutants.map { mutant => - matchBuilder.mutantToCase(mutant).structure -> mutant.id - }.toMap - - val lineToMutantId: Map[Int, MutantId] = mutatedFile - //Parsing the mutated tree again as a string is the only way to get the position info of the mutated statements - .parse[Source] - .getOrElse(throw new RuntimeException(s"Failed to parse $mutatedFile to remove non-compiling mutants")) - .collect { - case node: Case if statementToMutIdMap.contains(node.structure) => - val mutId = statementToMutIdMap(node.structure) - //+1 because scalameta uses zero-indexed line numbers - (node.pos.startLine to node.pos.endLine).map(i => i + 1 -> mutId) - } - .flatten - .toMap - - compileErrors.flatMap { err => - lineToMutantId.get(err.line) - } - } - -} From 5439ac1e1b5b8b295303c4a2ca88097a6abed0cf Mon Sep 17 00:00:00 2001 From: Martin Welgemoed Date: Fri, 1 Oct 2021 17:12:05 +0200 Subject: [PATCH 25/26] Fixed name in test --- core/src/test/scala/stryker4s/mutants/RollbackTest.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/test/scala/stryker4s/mutants/RollbackTest.scala b/core/src/test/scala/stryker4s/mutants/RollbackTest.scala index 40aa0e35c..118df2df6 100644 --- a/core/src/test/scala/stryker4s/mutants/RollbackTest.scala +++ b/core/src/test/scala/stryker4s/mutants/RollbackTest.scala @@ -12,7 +12,7 @@ import stryker4s.testutil.stubs.TestFileResolver import scala.meta._ class RollbackTest extends Stryker4sIOSuite with LogMatchers { - describe("RollbackHandler") { + describe("Mutator") { it("should remove a non-compiling mutant") { implicit val conf: Config = Config.default.copy( baseDir = FileUtil.getResource("rollbackTest"), From 0a9fd4d784643eef4af19c4c2c708f25e9211cd9 Mon Sep 17 00:00:00 2001 From: Hugo van Rijswijk Date: Sat, 16 Oct 2021 16:13:08 +0200 Subject: [PATCH 26/26] small changes --- .../main/scala/stryker4s/mutants/Mutator.scala | 3 +-- .../scala/stryker4s/run/MutantRunnerTest.scala | 2 +- .../scala/stryker4s/sbt/Stryker4sSbtRunner.scala | 2 +- .../test-1/src/main/scala/example/TestObj1.scala | 11 ++++------- .../src/test/scala/example/TestObj1Spec.scala | 16 +++++++++------- .../sbt-test/sbt-stryker4s/test-1/stryker4s.conf | 8 ++++---- 6 files changed, 20 insertions(+), 22 deletions(-) diff --git a/core/src/main/scala/stryker4s/mutants/Mutator.scala b/core/src/main/scala/stryker4s/mutants/Mutator.scala index bb23415b1..317f24dc7 100644 --- a/core/src/main/scala/stryker4s/mutants/Mutator.scala +++ b/core/src/main/scala/stryker4s/mutants/Mutator.scala @@ -24,8 +24,7 @@ class Mutator( def mutate(files: Stream[IO, Path], compileErrors: Seq[CompilerErrMsg] = Seq.empty): IO[Seq[MutatedFile]] = { if (compileErrors.nonEmpty) { - log.debug("Trying to remove mutants that gave these errors:") - compileErrors.foreach(err => log.debug(s"\t$err")) + log.debug("Trying to remove mutants that gave these errors:\n\t" + compileErrors.mkString("\n\t")) } files .parEvalMapUnordered(config.concurrency)(p => findMutants(p).tupleLeft(p)) diff --git a/core/src/test/scala/stryker4s/run/MutantRunnerTest.scala b/core/src/test/scala/stryker4s/run/MutantRunnerTest.scala index be503e7f5..f07b1cda3 100644 --- a/core/src/test/scala/stryker4s/run/MutantRunnerTest.scala +++ b/core/src/test/scala/stryker4s/run/MutantRunnerTest.scala @@ -32,7 +32,7 @@ class MutantRunnerTest extends Stryker4sIOSuite with MockitoIOSuite with LogMatc val mutants = Seq(mutant, secondMutant, thirdMutant) val mutatedFile = MutatedFile(file, q"def foo = 4", mutants, Seq.empty, 0) - sut(_ => IO(List(mutatedFile))).asserting { result => + sut(_ => IO.pure(List(mutatedFile))).asserting { result => val captor = ArgCaptor[FinishedRunEvent] verify(reporterMock, times(1)).onRunFinished(captor.capture) val runReport = captor.value.report.files.loneElement diff --git a/sbt/src/main/scala/stryker4s/sbt/Stryker4sSbtRunner.scala b/sbt/src/main/scala/stryker4s/sbt/Stryker4sSbtRunner.scala index 91680d387..2ed9c730a 100644 --- a/sbt/src/main/scala/stryker4s/sbt/Stryker4sSbtRunner.scala +++ b/sbt/src/main/scala/stryker4s/sbt/Stryker4sSbtRunner.scala @@ -105,7 +105,7 @@ class Stryker4sSbtRunner( exception.problems.flatMap { e => for { path <- e.position().sourceFile().asScala - pathStr = tmpDir.toNioPath.relativize(path.toPath.toAbsolutePath).toString + pathStr = tmpDir.relativize(Path(path.absolutePath)).toString line <- e.position().line().asScala } yield CompilerErrMsg(e.message(), pathStr, line) }.toSeq diff --git a/sbt/src/sbt-test/sbt-stryker4s/test-1/src/main/scala/example/TestObj1.scala b/sbt/src/sbt-test/sbt-stryker4s/test-1/src/main/scala/example/TestObj1.scala index 3ab69346a..8cc638cc0 100644 --- a/sbt/src/sbt-test/sbt-stryker4s/test-1/src/main/scala/example/TestObj1.scala +++ b/sbt/src/sbt-test/sbt-stryker4s/test-1/src/main/scala/example/TestObj1.scala @@ -3,15 +3,12 @@ package example import java.nio.file.{Files, Paths} object TestObj { + + def mutatesOkay(s: String) = s.exists(_ == 'a') + def test2(a: String): Boolean = { Files.exists(Paths.get(a)) //Should not get mutated! } - def least(a: Int, b: Int): Int = { - (a, b) match { - case (a, b) if a < b => a - case (a, b) if a > b => b - case (a, b) if a == b => 0 - } - } + def alsoMutatesOkay(s: String) = s.exists(_ == 'b') } diff --git a/sbt/src/sbt-test/sbt-stryker4s/test-1/src/test/scala/example/TestObj1Spec.scala b/sbt/src/sbt-test/sbt-stryker4s/test-1/src/test/scala/example/TestObj1Spec.scala index 86c83683f..b2df64ee2 100644 --- a/sbt/src/sbt-test/sbt-stryker4s/test-1/src/test/scala/example/TestObj1Spec.scala +++ b/sbt/src/sbt-test/sbt-stryker4s/test-1/src/test/scala/example/TestObj1Spec.scala @@ -4,16 +4,18 @@ import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers class Spec extends AnyFlatSpec with Matchers { - it should "return the lesser" in { - TestObj.least(1, 2) shouldBe 1 - TestObj.least(2, 1) shouldBe 1 - } - - it should "return 0 if equal" in { - TestObj.least(0, 0) shouldBe 0 + it should "check for a's in mutatesOkay" in { + TestObj.mutatesOkay(" a ") shouldBe true + TestObj.mutatesOkay(" b ") shouldBe false } it should "return false if a file does not exists" in { TestObj.test2("/home/blah/fake") shouldBe false } + + it should "check for b's in alsoMutatesOkay" in { + TestObj.alsoMutatesOkay(" b ") shouldBe true + TestObj.alsoMutatesOkay(" a ") shouldBe false + } + } diff --git a/sbt/src/sbt-test/sbt-stryker4s/test-1/stryker4s.conf b/sbt/src/sbt-test/sbt-stryker4s/test-1/stryker4s.conf index 003a54bb1..314049447 100644 --- a/sbt/src/sbt-test/sbt-stryker4s/test-1/stryker4s.conf +++ b/sbt/src/sbt-test/sbt-stryker4s/test-1/stryker4s.conf @@ -2,10 +2,10 @@ stryker4s { files: [ "*", "!global" ] reporters: ["console", "json", "html"] thresholds: { - # Should be 66,66%. Break if lower than that - high: 66 - low: 65 - break: 64 + # Should be 88%. Something is broken if it's lower than that + high: 90 + low: 89 + break: 88 } test-filter: ["!*IgnoreMeTest"] }