Skip to content

Commit

Permalink
Fix per-test coverage analysis (#1004)
Browse files Browse the repository at this point in the history
  • Loading branch information
hugo-vrijswijk authored Nov 1, 2021
1 parent 0afb265 commit 1fb400b
Show file tree
Hide file tree
Showing 15 changed files with 86 additions and 96 deletions.
16 changes: 8 additions & 8 deletions api/src/main/protobuf/stryker4s/api/testprocess.proto
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ message Request {

message StartTestRun {
int32 mutation = 1;
repeated Fingerprint fingerprints = 2;
repeated string test_names = 3;
}
message StartInitialTestRun {}

Expand Down Expand Up @@ -87,19 +87,19 @@ message ErrorDuringTestRun {
}
message CoverageTestRunResult {
bool is_successful = 1;
CoverageTestRunMap coverage_test_run_map = 2;
CoverageTestNameMap coverage_test_name_map = 3;
}

/** Convert to a flat structure to save space of duplicate Fingerprint items when serializing.
/** Convert to a flat structure to save space of duplicate test name items when serializing.
* Without it, the size grows exponentially with the number of tests and mutants
*/
message CoverageTestRunMap {
map<int32, Fingerprint> fingerprint_ids = 1;
map<int32, Fingerprints> fingerprints = 2;
message CoverageTestNameMap {
map<int32, string> test_name_ids = 1;
map<int32, TestNames> test_names = 2;
}

message Fingerprints {
repeated int32 fingerprint = 1;
message TestNames {
repeated int32 test_name = 1;
}

message Fingerprint {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package stryker4s.api.testprocess

case class CoverageReport(report: Map[Int, Seq[Fingerprint]]) extends AnyVal
case class CoverageReport(report: Map[Int, Seq[String]]) extends AnyVal

object CoverageReport {
def apply(testRunMap: CoverageTestRunMap): CoverageReport = {
val map = testRunMap.fingerprints.map { case (id, Fingerprints(fingerPrintIds)) =>
id -> fingerPrintIds.map(testRunMap.fingerprintIds)
def apply(testRunMap: CoverageTestNameMap): CoverageReport = {
val map = testRunMap.testNames.map { case (id, TestNames(testNameIds)) =>
id -> testNameIds.map(testRunMap.testNameIds)
}
CoverageReport(map)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import stryker4s.run.process.{Command, ProcessRunner}

import scala.concurrent.TimeoutException
import scala.util.{Failure, Success}
import stryker4s.api.testprocess.Fingerprint

class ProcessTestRunner(command: Command, processRunner: ProcessRunner, tmpDir: Path)(implicit config: Config)
extends TestRunner {
Expand All @@ -20,7 +19,7 @@ class ProcessTestRunner(command: Command, processRunner: ProcessRunner, tmpDir:
}
}

def runMutant(mutant: Mutant, fingerprints: Seq[Fingerprint]): IO[MutantRunResult] = {
def runMutant(mutant: Mutant, testNames: Seq[String]): IO[MutantRunResult] = {
val id = mutant.id.globalId
processRunner(command, tmpDir, ("ACTIVE_MUTATION", id.toString)).map {
case Success(0) => Survived(mutant)
Expand Down
3 changes: 1 addition & 2 deletions core/src/main/scala/stryker4s/run/MutantRunner.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import stryker4s.report.{FinishedRunEvent, MutantTestedEvent, Reporter}
import java.nio
import scala.collection.immutable.SortedMap
import scala.concurrent.duration._
import stryker4s.api.testprocess.Fingerprint

class MutantRunner(
createTestRunnerPool: Path => Either[NonEmptyList[CompilerErrMsg], Resource[IO, TestRunnerPool]],
Expand Down Expand Up @@ -228,7 +227,7 @@ class MutantRunner(

case class CoverageExclusions(
hasCoverage: Boolean,
coveredMutants: Map[Int, Seq[Fingerprint]],
coveredMutants: Map[Int, Seq[String]],
staticMutants: Seq[Int]
)

Expand Down
21 changes: 10 additions & 11 deletions core/src/main/scala/stryker4s/run/TestRunner.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,10 @@ import stryker4s.model.{Error, InitialTestRunResult, Mutant, MutantRunResult, Ti

import java.util.concurrent.TimeUnit
import scala.concurrent.duration._
import stryker4s.api.testprocess.Fingerprint

trait TestRunner {
def initialTestRun(): IO[InitialTestRunResult]
def runMutant(mutant: Mutant, fingerprints: Seq[Fingerprint]): IO[MutantRunResult]
def runMutant(mutant: Mutant, testNames: Seq[String]): IO[MutantRunResult]
}

/** Wrapping testrunners to add functionality to existing testrunners
Expand All @@ -27,12 +26,12 @@ object TestRunner {
inner.selfRecreatingResource { (testRunnerRef, releaseAndSwap) =>
IO {
new TestRunner {
override def runMutant(mutant: Mutant, fingerprints: Seq[Fingerprint]): IO[MutantRunResult] =
override def runMutant(mutant: Mutant, testNames: Seq[String]): IO[MutantRunResult] =
for {
runner <- testRunnerRef.get
time <- timeout.get
result <- runner
.runMutant(mutant, fingerprints)
.runMutant(mutant, testNames)
.timeoutTo(
time,
IO(log.debug(s"Mutant ${mutant.id} timed out over ${time.toHumanReadable}")) *>
Expand Down Expand Up @@ -69,15 +68,15 @@ object TestRunner {
IO {
new TestRunner {

override def runMutant(mutant: Mutant, fingerprints: Seq[Fingerprint]): IO[MutantRunResult] =
retryRunMutation(mutant, fingerprints)
override def runMutant(mutant: Mutant, testNames: Seq[String]): IO[MutantRunResult] =
retryRunMutation(mutant, testNames)

def retryRunMutation(
mutant: Mutant,
fingerprints: Seq[Fingerprint],
testNames: Seq[String],
retriesLeft: Long = 2
): IO[MutantRunResult] = {
testRunnerRef.get.flatMap(_.runMutant(mutant, fingerprints)).attempt.flatMap {
testRunnerRef.get.flatMap(_.runMutant(mutant, testNames)).attempt.flatMap {
// On error, get a new testRunner and set it
case Left(_) =>
IO(
Expand All @@ -87,7 +86,7 @@ object TestRunner {
) *>
// Release old resource and make a new one, then retry the mutation
releaseAndSwap *>
(if (retriesLeft > 0) retryRunMutation(mutant, fingerprints, retriesLeft - 1)
(if (retriesLeft > 0) retryRunMutation(mutant, testNames, retriesLeft - 1)
else IO.pure(Error(mutant)))

case Right(value) => IO.pure(value)
Expand All @@ -105,7 +104,7 @@ object TestRunner {
inner.selfRecreatingResource { (testRunnerRef, releaseAndSwap) =>
Ref[IO].of(0).map { usesRef =>
new TestRunner {
def runMutant(mutant: Mutant, fingerprints: Seq[Fingerprint]): IO[MutantRunResult] = for {
def runMutant(mutant: Mutant, testNames: Seq[String]): IO[MutantRunResult] = for {
uses <- usesRef.getAndUpdate(_ + 1)
_ <-
// If the limit has been reached, create a new testrunner
Expand All @@ -115,7 +114,7 @@ object TestRunner {
usesRef.set(1)
else IO.unit
runner <- testRunnerRef.get
result <- runner.runMutant(mutant, fingerprints)
result <- runner.runMutant(mutant, testNames)
} yield result

override def initialTestRun(): IO[InitialTestRunResult] = for {
Expand Down
26 changes: 13 additions & 13 deletions core/src/test/scala/stryker4s/run/TestRunnerTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package stryker4s.run

import cats.effect.{Deferred, IO, Ref, Resource}
import cats.syntax.all._
import stryker4s.api.testprocess.{CoverageReport, Fingerprint}
import stryker4s.api.testprocess.CoverageReport
import stryker4s.config.Config
import stryker4s.model._
import stryker4s.scalatest.LogMatchers
Expand All @@ -13,7 +13,7 @@ import scala.concurrent.duration._

class TestRunnerTest extends Stryker4sIOSuite with LogMatchers with TestData {

val fingerprints = Seq.empty[Fingerprint]
val coverageTestNames = Seq.empty[String]
describe("timeoutRunner") {
implicit val config = Config.default

Expand All @@ -32,7 +32,7 @@ class TestRunnerTest extends Stryker4sIOSuite with LogMatchers with TestData {
val reportedTimeout = 2.seconds
val op = for {
timeout <- Deferred[IO, FiniteDuration]
emptyReport = CoverageReport(Map.empty[Int, Seq[Fingerprint]])
emptyReport = CoverageReport(Map.empty[Int, Seq[String]])
innerTR = initialTestRunner(InitialTestRunCoverageReport(true, emptyReport, emptyReport, reportedTimeout))
sut = TestRunner.timeoutRunner(timeout, innerTR)
_ <- sut.use(_.initialTestRun())
Expand Down Expand Up @@ -67,7 +67,7 @@ class TestRunnerTest extends Stryker4sIOSuite with LogMatchers with TestData {
_ <- timeout.complete(1.millisecond)
// TestRunner is slower than timeout
sut = TestRunner.timeoutRunner(timeout, timeoutRunner(100.milliseconds, mutant))
result <- sut.use(_.runMutant(mutant, fingerprints))
result <- sut.use(_.runMutant(mutant, coverageTestNames))
} yield result

op.asserting { result =>
Expand All @@ -84,7 +84,7 @@ class TestRunnerTest extends Stryker4sIOSuite with LogMatchers with TestData {
log <- Ref[IO].of(List.empty[String])
innerTR = recreateLoggingTestRunner(log, timeoutRunner(100.milliseconds, createMutant))
sut = TestRunner.timeoutRunner(timeout, innerTR)
_ <- sut.use(_.runMutant(createMutant, fingerprints))
_ <- sut.use(_.runMutant(createMutant, coverageTestNames))
} yield log

op.flatMap(_.get).asserting { log =>
Expand All @@ -100,7 +100,7 @@ class TestRunnerTest extends Stryker4sIOSuite with LogMatchers with TestData {
_ <- timeout.complete(100.millis)
// TestRunner is slower than timeout
sut = TestRunner.timeoutRunner(timeout, timeoutRunner(1.milliseconds, mutant))
result <- sut.use(_.runMutant(mutant, fingerprints))
result <- sut.use(_.runMutant(mutant, coverageTestNames))
} yield result

op.asserting { result =>
Expand All @@ -122,7 +122,7 @@ class TestRunnerTest extends Stryker4sIOSuite with LogMatchers with TestData {
Resource.pure(new TestRunnerStub(Seq(() => fail("first attempt should be retried"), () => secondResult)))
)
sut = TestRunner.retryRunner(innerTR)
result <- sut.use(_.runMutant(mutant, fingerprints))
result <- sut.use(_.runMutant(mutant, coverageTestNames))
logResults <- log.get
} yield (result, logResults)

Expand All @@ -141,7 +141,7 @@ class TestRunnerTest extends Stryker4sIOSuite with LogMatchers with TestData {
sut = TestRunner.retryRunner(
recreateLoggingTestRunner(log, Resource.pure(new TestRunnerStub(stubResults)))
)
result <- sut.use(_.runMutant(mutant, fingerprints))
result <- sut.use(_.runMutant(mutant, coverageTestNames))
logResults <- log.get
} yield (result, logResults)

Expand All @@ -167,7 +167,7 @@ class TestRunnerTest extends Stryker4sIOSuite with LogMatchers with TestData {
sut = TestRunner.retryRunner(
recreateLoggingTestRunner(log, Resource.pure(new TestRunnerStub(stubResults)))
)
result <- sut.use(_.runMutant(mutant, fingerprints))
result <- sut.use(_.runMutant(mutant, coverageTestNames))
logResults <- log.get
} yield (result, logResults)

Expand All @@ -191,7 +191,7 @@ class TestRunnerTest extends Stryker4sIOSuite with LogMatchers with TestData {
innerTR = recreateLoggingTestRunner(log, Resource.pure(new TestRunnerStub(expectedResults.map(() => _))))
sut = TestRunner.maxReuseTestRunner(reuse, innerTR)
result <- sut.use { tr =>
mutants.traverse(tr.runMutant(_, fingerprints))
mutants.traverse(tr.runMutant(_, coverageTestNames))
}
logResults <- log.get
} yield (result, logResults)
Expand All @@ -212,7 +212,7 @@ class TestRunnerTest extends Stryker4sIOSuite with LogMatchers with TestData {
innerTR = recreateLoggingTestRunner(log, Resource.pure(new TestRunnerStub(expectedResults.map(() => _))))
sut = TestRunner.maxReuseTestRunner(reuse, innerTR)
result <- sut.use { tr =>
mutants.traverse(tr.runMutant(_, fingerprints))
mutants.traverse(tr.runMutant(_, coverageTestNames))
}
logResults <- log.get
} yield (result, logResults)
Expand All @@ -228,13 +228,13 @@ class TestRunnerTest extends Stryker4sIOSuite with LogMatchers with TestData {
def initialTestRunner(result: InitialTestRunResult = NoCoverageInitialTestRun(true)): Resource[IO, TestRunner] =
Resource.pure(new TestRunner {
def initialTestRun(): IO[InitialTestRunResult] = IO.pure(result)
def runMutant(mutant: Mutant, fingerprints: Seq[Fingerprint]): IO[MutantRunResult] = ???
def runMutant(mutant: Mutant, testNames: Seq[String]): IO[MutantRunResult] = ???
})

def timeoutRunner(sleep: FiniteDuration, result: Mutant): Resource[IO, TestRunner] =
Resource.pure(new TestRunner {
def initialTestRun(): IO[InitialTestRunResult] = ???
def runMutant(mutant: Mutant, fingerprints: Seq[Fingerprint]): IO[MutantRunResult] =
def runMutant(mutant: Mutant, testNames: Seq[String]): IO[MutantRunResult] =
IO.sleep(sleep).as(Killed(result))
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,13 @@ import stryker4s.model._
import stryker4s.run.{ResourcePool, TestRunner, TestRunnerPool}

import scala.meta._
import stryker4s.api.testprocess.Fingerprint

class TestRunnerStub(results: Seq[() => MutantRunResult]) extends TestRunner {
private val stream = Iterator.from(0)

def initialTestRun(): IO[InitialTestRunResult] = IO.pure(NoCoverageInitialTestRun(true))

def runMutant(mutant: Mutant, fingerprints: Seq[Fingerprint]): IO[MutantRunResult] = {
def runMutant(mutant: Mutant, testNames: Seq[String]): IO[MutantRunResult] = {
// Ensure runMutant can always continue
IO(results.applyOrElse(stream.next(), (_: Int) => results.head)())
.recover { case _: ArrayIndexOutOfBoundsException => results.head() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package stryker4s.maven.runner
import cats.effect.IO
import org.apache.maven.project.MavenProject
import org.apache.maven.shared.invoker.{DefaultInvocationRequest, InvocationRequest, Invoker}
import stryker4s.api.testprocess._
import stryker4s.log.Logger
import stryker4s.model._
import stryker4s.run.TestRunner
Expand All @@ -21,7 +20,7 @@ class MavenTestRunner(project: MavenProject, invoker: Invoker, val properties: P
IO(invoker.execute(request)).map(_.getExitCode() == 0).map(NoCoverageInitialTestRun(_))
}

def runMutant(mutant: Mutant, fingerprints: Seq[Fingerprint]): IO[MutantRunResult] = {
def runMutant(mutant: Mutant, testNames: Seq[String]): IO[MutantRunResult] = {
val request = createRequestWithMutation(mutant)

IO(invoker.execute(request)).map { result =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import org.apache.maven.project.MavenProject
import org.apache.maven.shared.invoker.{InvocationRequest, InvocationResult, Invoker}
import org.mockito.captor.ArgCaptor
import org.mockito.scalatest.MockitoSugar
import stryker4s.api.testprocess._
import stryker4s.config.Config
import stryker4s.extension.mutationtype.LesserThan
import stryker4s.model.{Killed, Mutant, MutantId, NoCoverageInitialTestRun, Survived}
Expand All @@ -21,7 +20,7 @@ class MavenTestRunnerTest extends Stryker4sSuite with MockitoSugar {
implicit val config: Config = Config.default

val tmpDir = Path("/home/user/tmpDir")
val fingerprints = Seq.empty[Fingerprint]
val coverageTestNames = Seq.empty[String]
def properties = new ju.Properties()
def goals = Seq("test")

Expand Down Expand Up @@ -83,7 +82,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(MutantId(1), q">", q"<", LesserThan), fingerprints).unsafeRunSync()
val result = sut.runMutant(Mutant(MutantId(1), q">", q"<", LesserThan), coverageTestNames).unsafeRunSync()

result shouldBe a[Killed]
}
Expand All @@ -95,7 +94,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(MutantId(1), q">", q"<", LesserThan), fingerprints).unsafeRunSync()
val result = sut.runMutant(Mutant(MutantId(1), q">", q"<", LesserThan), coverageTestNames).unsafeRunSync()

result shouldBe a[Survived]
}
Expand All @@ -111,7 +110,7 @@ class MavenTestRunnerTest extends Stryker4sSuite with MockitoSugar {

val sut = new MavenTestRunner(project, invokerMock, project.getProperties(), goals)

sut.runMutant(Mutant(MutantId(1), q">", q"<", LesserThan), fingerprints).unsafeRunSync()
sut.runMutant(Mutant(MutantId(1), q">", q"<", LesserThan), coverageTestNames).unsafeRunSync()

verify(invokerMock).execute(captor)
val invokedRequest = captor.value
Expand All @@ -134,7 +133,7 @@ class MavenTestRunnerTest extends Stryker4sSuite with MockitoSugar {
mavenProject.getActiveProfiles.add(profile)
val sut = new MavenTestRunner(mavenProject, invokerMock, properties, goals)

sut.runMutant(Mutant(MutantId(1), q">", q"<", LesserThan), fingerprints).unsafeRunSync()
sut.runMutant(Mutant(MutantId(1), q">", q"<", LesserThan), coverageTestNames).unsafeRunSync()

verify(invokerMock).execute(captor)
val invokedRequest = captor.value
Expand Down
Loading

0 comments on commit 1fb400b

Please sign in to comment.