diff --git a/blinky-cli/src/main/scala/blinky/run/Instruction.scala b/blinky-cli/src/main/scala/blinky/run/Instruction.scala index 32e00b32..274dba5b 100644 --- a/blinky-cli/src/main/scala/blinky/run/Instruction.scala +++ b/blinky-cli/src/main/scala/blinky/run/Instruction.scala @@ -76,6 +76,12 @@ object Instruction { next: Option[Boolean] => Instruction[A] ) extends Instruction[A] + final case class GrepFiles[+A]( + basePath: Path, + fileName: String, + next: Seq[String] => Instruction[A] + ) extends Instruction[A] + def succeed[A](value: => A): Return[A] = Return(() => value) @@ -161,4 +167,7 @@ object Instruction { ): Timeout[Option[Boolean]] = Timeout(runFunction, millis, succeed(_: Option[Boolean])) + def grepFiles(basePath: Path, fileName: String): GrepFiles[Seq[String]] = + GrepFiles(basePath, fileName, succeed(_: Seq[String])) + } diff --git a/blinky-cli/src/main/scala/blinky/run/Interpreter.scala b/blinky-cli/src/main/scala/blinky/run/Interpreter.scala index 766daed1..1d3a8daf 100644 --- a/blinky-cli/src/main/scala/blinky/run/Interpreter.scala +++ b/blinky-cli/src/main/scala/blinky/run/Interpreter.scala @@ -87,6 +87,9 @@ object Interpreter { case CopyRelativeFiles(filesToCopy, fromPath, toPath, next) => val result = externalCalls.copyRelativeFiles(filesToCopy, fromPath, toPath) interpreterNext(next(result)) + case GrepFiles(basePath, fileName, next) => + val result = externalCalls.grepFiles(basePath, fileName) + interpreterNext(next(result)) case timeout @ Timeout(_, _, _) => Left(timeout) } diff --git a/blinky-cli/src/main/scala/blinky/run/Run.scala b/blinky-cli/src/main/scala/blinky/run/Run.scala index 000b2932..66ab9c08 100644 --- a/blinky-cli/src/main/scala/blinky/run/Run.scala +++ b/blinky-cli/src/main/scala/blinky/run/Run.scala @@ -3,7 +3,7 @@ package blinky.run import ammonite.ops.{Path, RelPath} import blinky.BuildInfo import blinky.run.Instruction._ -import blinky.run.config.{MutationsConfigValidated, SimpleBlinkyConfig} +import blinky.run.config.{FileFilter, MutationsConfigValidated, SimpleBlinkyConfig} import blinky.run.modules.CliModule import blinky.v0.BlinkyConfig import zio.{ExitCode, RIO} @@ -71,66 +71,59 @@ object Run { .split(System.lineSeparator()) .toSeq .map(file => cloneProjectBaseFolder / RelPath(file)) - .filter(file => file.ext == "scala" || file.ext == "sbt") + .filter(_.ext == "scala") .map(_.toString) + processResult <- + processFilesToMutate(projectRealPath, config.filesToMutate) + result <- - if (base.isEmpty) - succeed(Right(base)) - else - for { - copyResult <- copyFilesToTempFolder( - originalProjectRoot, - originalProjectPath, - projectRealPath - ) - result <- copyResult match { - case Left(result) => - succeed(Left(result)) - case Right(_) => - // This part is just an optimization of 'base' - val configFileOrFolderToMutate: Path = - Try(Path(config.filesToMutate)) - .getOrElse( - projectRealPath / RelPath(config.filesToMutate) - ) - - val configFileOrFolderToMutateStr = - configFileOrFolderToMutate.toString - - IsFile( - configFileOrFolderToMutate, - if (_) - if (base.contains(configFileOrFolderToMutateStr)) - succeed(Seq(configFileOrFolderToMutateStr)) - else - succeed(Seq.empty[String]) - else - succeed( - base.filter( - _.startsWith(configFileOrFolderToMutateStr) - ) - ) - ).map(Right(_)) - } - } yield result + processResult match { + case Left(value) => + succeed(Left(value)) + case Right(filesToMutateStr) => + if (base.isEmpty) + succeed(Right((filesToMutateStr, base))) + else + for { + copyResult <- copyFilesToTempFolder( + originalProjectRoot, + originalProjectPath, + projectRealPath + ) + result <- optimiseFilesToMutate( + base, + copyResult, + projectRealPath, + config.filesToMutate + ) + } yield result + } } yield result } else - copyFilesToTempFolder( - originalProjectRoot, - originalProjectPath, - projectRealPath - ).map(_ => Right(Seq("all"))) + for { + _ <- copyFilesToTempFolder( + originalProjectRoot, + originalProjectPath, + projectRealPath + ) + processResult <- processFilesToMutate( + projectRealPath, + config.filesToMutate + ) + } yield processResult.map { filesToMutateStr => + (filesToMutateStr, Seq("all")) + } } runResult <- filesToMutateEither match { case Left(exitCode) => succeed(exitCode) - case Right(Seq()) => + case Right((_, Seq())) => ConsoleReporter.filesToMutateIsEmpty .map(_ => ExitCode.success) - case Right(filesToMutate) => + case Right((filesToMutateStr, filesToMutateSeq)) => for { coursier <- Setup.setupCoursier(projectRealPath) _ <- Setup.sbtCompileWithSemanticDB(projectRealPath) @@ -139,7 +132,7 @@ object Run { // Setup BlinkyConfig object blinkyConf: BlinkyConfig = BlinkyConfig( mutantsOutputFile = (projectRealPath / "blinky.mutants").toString, - filesToMutate = filesToMutate, + filesToMutate = filesToMutateSeq, specificMutants = config.options.mutant, enabledMutators = config.mutators.enabled, disabledMutators = config.mutators.disabled @@ -180,7 +173,7 @@ object Run { s"--exclude=${config.filesToExclude}" else "", s"--tool-classpath=$toolPath", - s"--files=${config.filesToMutate}", + s"--files=$filesToMutateStr", s"--config=$scalafixConfFile", "--auto-classpath=target" ).filter(_.nonEmpty) @@ -198,6 +191,77 @@ object Run { } } yield inst + private def filterFiles( + files: Seq[String], + fileName: String + ): Instruction[Either[ExitCode, String]] = { + val filesFiltered = files.collect { case file if file.endsWith(fileName) => file } + filesFiltered match { + case List(singleFile) => + succeed(Right(singleFile)) + case Nil => + printLine(s"--filesToMutate '$fileName' does not exist.") + .map(_ => Left(ExitCode.failure)) + case _ => + printLine(s"--filesToMutate is ambiguous.").map(_ => Left(ExitCode.failure)) + } + } + + def processFilesToMutate( + projectRealPath: Path, + filesToMutate: FileFilter + ): Instruction[Either[ExitCode, String]] = + filesToMutate match { + case FileFilter.SingleFileOrFolder(fileOrFolder) => + succeed(Right(fileOrFolder.toString)) + case FileFilter.FileName(fileName) => + grepFiles( + projectRealPath, + fileName + ).flatMap(filterFiles(_, fileName)) + } + + def optimiseFilesToMutate( + base: Seq[String], + copyResult: Either[ExitCode, Unit], + projectRealPath: Path, + filesToMutate: FileFilter + ): Instruction[Either[ExitCode, (String, Seq[String])]] = + copyResult match { + case Left(result) => + succeed(Left(result)) + case Right(_) => // This part is just an optimization of 'base' + val fileToMutateInst: Instruction[Either[ExitCode, Path]] = + filesToMutate match { + case FileFilter.SingleFileOrFolder(fileOrFolder) => + succeed(Right(projectRealPath / fileOrFolder)) + case FileFilter.FileName(fileName) => + filterFiles(base, fileName).map(_.map(Path(_))) + } + + for { + fileToMutateResult <- fileToMutateInst + result <- + fileToMutateResult match { + case Left(exitCode) => + succeed(Left(exitCode)) + case Right(configFileOrFolderToMutate) => + val configFileOrFolderToMutateStr = + configFileOrFolderToMutate.toString + IsFile( + configFileOrFolderToMutate, + if (_) + if (base.contains(configFileOrFolderToMutateStr)) + succeed(Seq(configFileOrFolderToMutateStr)) + else + succeed(Seq.empty[String]) + else + succeed(base.filter(_.startsWith(configFileOrFolderToMutateStr))) + ).map(baseFiltered => Right((configFileOrFolderToMutateStr, baseFiltered))) + } + } yield result + } + def copyFilesToTempFolder( originalProjectRoot: Path, originalProjectPath: Path, diff --git a/blinky-cli/src/main/scala/blinky/run/config/FileFilter.scala b/blinky-cli/src/main/scala/blinky/run/config/FileFilter.scala new file mode 100644 index 00000000..b4a1a543 --- /dev/null +++ b/blinky-cli/src/main/scala/blinky/run/config/FileFilter.scala @@ -0,0 +1,13 @@ +package blinky.run.config + +import ammonite.ops.RelPath + +sealed trait FileFilter + +object FileFilter { + + case class SingleFileOrFolder(fileOrFolder: RelPath) extends FileFilter + + case class FileName(fileName: String) extends FileFilter + +} diff --git a/blinky-cli/src/main/scala/blinky/run/config/MutationsConfigValidated.scala b/blinky-cli/src/main/scala/blinky/run/config/MutationsConfigValidated.scala index 40622699..2e8f20f4 100644 --- a/blinky-cli/src/main/scala/blinky/run/config/MutationsConfigValidated.scala +++ b/blinky-cli/src/main/scala/blinky/run/config/MutationsConfigValidated.scala @@ -1,12 +1,12 @@ package blinky.run.config +import ammonite.ops.RelPath import better.files.File - -import scala.util.Try +import blinky.run.config.FileFilter.{FileName, SingleFileOrFolder} case class MutationsConfigValidated( projectPath: File, - filesToMutate: String, + filesToMutate: FileFilter, filesToExclude: String, mutators: SimpleBlinkyConfig, options: OptionsConfig @@ -21,21 +21,23 @@ object MutationsConfigValidated { else if (!projectPath.exists) Left(s"--projectPath '$projectPath' does not exist.") else { - val filesToMutateEither: Either[String, String] = { - val filesToMutate: File = - Try(File(config.filesToMutate)) - .filter(_.exists) - .getOrElse(projectPath / config.filesToMutate) + val filesToMutate: FileFilter = { + val filesToMutate: File = projectPath / config.filesToMutate if (filesToMutate.exists) - Right(config.filesToMutate) - else if (filesToMutate.extension.isEmpty && File(filesToMutate.toString + ".scala").exists) - Right(config.filesToMutate + ".scala") - else - Left(s"--filesToMutate '${config.filesToMutate}' does not exist.") + SingleFileOrFolder(RelPath(config.filesToMutate)) + else if (filesToMutate.extension.isEmpty) { + if (File(filesToMutate.toString + ".scala").exists) + SingleFileOrFolder(RelPath(config.filesToMutate + ".scala")) + else { + FileName(config.filesToMutate + ".scala") + } + } else { + FileName(config.filesToMutate) + } } - filesToMutateEither.map(filesToMutate => + Right( MutationsConfigValidated( projectPath, filesToMutate, diff --git a/blinky-cli/src/main/scala/blinky/run/external/AmmoniteExternalCalls.scala b/blinky-cli/src/main/scala/blinky/run/external/AmmoniteExternalCalls.scala index c07cebcf..c904200d 100644 --- a/blinky-cli/src/main/scala/blinky/run/external/AmmoniteExternalCalls.scala +++ b/blinky-cli/src/main/scala/blinky/run/external/AmmoniteExternalCalls.scala @@ -67,4 +67,12 @@ object AmmoniteExternalCalls extends ExternalCalls { } ).toEither + def grepFiles( + basePath: Path, + fileName: String + ): Seq[String] = + Try( + ls.rec(basePath).map(_.toString) + ).getOrElse(Seq.empty) + } diff --git a/blinky-cli/src/main/scala/blinky/run/external/ExternalCalls.scala b/blinky-cli/src/main/scala/blinky/run/external/ExternalCalls.scala index 9bec8b31..18fa42b2 100644 --- a/blinky-cli/src/main/scala/blinky/run/external/ExternalCalls.scala +++ b/blinky-cli/src/main/scala/blinky/run/external/ExternalCalls.scala @@ -36,4 +36,9 @@ trait ExternalCalls { toPath: Path ): Either[Throwable, Unit] + def grepFiles( + basePath: Path, + fileName: String + ): Seq[String] + } diff --git a/blinky-cli/src/main/scala/blinky/run/package.scala b/blinky-cli/src/main/scala/blinky/run/package.scala index e499b84c..46e49036 100644 --- a/blinky-cli/src/main/scala/blinky/run/package.scala +++ b/blinky-cli/src/main/scala/blinky/run/package.scala @@ -52,6 +52,8 @@ package object run { ) case Timeout(runFunction, millis, next) => Timeout(runFunction, millis, next(_: Option[Boolean]).flatMap(f)) + case GrepFiles(basePath, fileName, next) => + GrepFiles(basePath, fileName, next(_: Seq[String]).flatMap(f)) } } diff --git a/blinky-cli/src/test/scala/blinky/cli/CliTest.scala b/blinky-cli/src/test/scala/blinky/cli/CliTest.scala index f6bfbbaa..2f634fdc 100644 --- a/blinky-cli/src/test/scala/blinky/cli/CliTest.scala +++ b/blinky-cli/src/test/scala/blinky/cli/CliTest.scala @@ -3,9 +3,11 @@ package blinky.cli import better.files.File import blinky.BuildInfo.version import blinky.TestSpec +import blinky.run.config.FileFilter.{FileName, SingleFileOrFolder} import blinky.run.config.{MutationsConfigValidated, OptionsConfig, SimpleBlinkyConfig} import blinky.run.modules.{CliModule, ParserModule, TestModules} import blinky.v0.{MutantRange, Mutators} +import os.RelPath import scopt.DefaultOEffectSetup import zio.test.Assertion._ import zio.test._ @@ -81,7 +83,7 @@ object CliTest extends TestSpec { Right( MutationsConfigValidated( projectPath = File(getFilePath("some-project")), - filesToMutate = "src/main/scala", + filesToMutate = SingleFileOrFolder(RelPath("src/main/scala")), filesToExclude = "", mutators = SimpleBlinkyConfig( enabled = Mutators.all, @@ -145,7 +147,9 @@ object CliTest extends TestSpec { } yield assert(parser.getOut)(equalTo("")) && assert(parser.getErr)(equalTo("")) && assert(config.map(_.projectPath))(equalSome(File(getFilePath("some-project")))) && - assert(config.map(_.filesToMutate))(equalSome("src/main/scala/Example.scala")) && + assert(config.map(_.filesToMutate))( + equalSome(SingleFileOrFolder(RelPath("src/main/scala/Example.scala"))) + ) && assert(config.map(_.options.compileCommand))(equalSome("example1")) && assert(config.map(_.options.testCommand))(equalSome("example1")) }, @@ -160,39 +164,39 @@ object CliTest extends TestSpec { } yield assert(parser.getOut)(equalTo("")) && assert(parser.getErr)(equalTo("")) && assert(config.map(_.projectPath))(equalSome(File(getFilePath("some-project")))) && - assert(config.map(_.filesToMutate))(equalSome("src/main/scala/Example.scala")) && + assert(config.map(_.filesToMutate))( + equalSome(SingleFileOrFolder(RelPath("src/main/scala/Example.scala"))) + ) && assert(config.map(_.options.compileCommand))(equalSome("example1")) && assert(config.map(_.options.testCommand))(equalSome("example1")) }, testM( - "blinky wrongPath1.conf returns an error when the 'filesToMutate' param is invalid" + "blinky wrongPath1.conf returns a fileName object" ) { val (zioResult, parser) = parse(getFilePath("wrongPath1.conf"))() for { result <- zioResult + config = result.toOption } yield assert(parser.getOut)(equalTo("")) && assert(parser.getErr)(equalTo("")) && - assert(result)(equalTo { - Left( - "--filesToMutate 'src/main/scala/UnknownFile.scala' does not exist." - ) - }) + assert(config.map(_.filesToMutate))( + equalSome(FileName("src/main/scala/UnknownFile.scala")) + ) }, testM( - "blinky wrongPath2.conf returns an error when the 'filesToMutate' param is invalid" + "blinky wrongPath2.conf returns a fileName object" ) { val (zioResult, parser) = parse(getFilePath("wrongPath2.conf"))() for { result <- zioResult + config = result.toOption } yield assert(parser.getOut)(equalTo("")) && assert(parser.getErr)(equalTo("")) && - assert(result)(equalTo { - Left( - "--filesToMutate 'src/main/scala/UnknownFile' does not exist." - ) - }) + assert(config.map(_.filesToMutate))( + equalSome(FileName("src/main/scala/UnknownFile.scala")) + ) }, testM("blinky returns an error if there is no default .blinky.conf file") { val pwdFolder = File(".") @@ -275,10 +279,10 @@ object CliTest extends TestSpec { config = result.toOption } yield assert(parser.getOut)(equalTo("")) && assert(parser.getErr)(equalTo("")) && - assert(config.map(_.projectPath))( - equalSome(File(getFilePath("some-project"))) + assert(config.map(_.projectPath))(equalSome(File(getFilePath("some-project")))) && + assert(config.map(_.filesToMutate))( + equalSome(SingleFileOrFolder(RelPath("src/main/scala/Main.scala"))) ) && - assert(config.map(_.filesToMutate))(equalSome("src/main/scala/Main.scala")) && assert(config.map(_.filesToExclude))(equalSome("src/main/scala/Utils.scala")) && assert(config.map(_.options))(equalSome { OptionsConfig( @@ -313,7 +317,7 @@ object CliTest extends TestSpec { assert(parser.getErr)(equalTo("")) && assert(result)(equalTo { Left( - s"""--projectPath '${pwd / "non-existent" / "project-path"}' does not exist.""".stripMargin + s"""--projectPath '${pwd / "non-existent" / "project-path"}' does not exist.""" ) }) } @@ -328,7 +332,7 @@ object CliTest extends TestSpec { assert(parser.getErr)(equalTo("")) && assert(result)(equalTo { Left( - s"""mutationMinimum value is invalid. It should be a number between 0 and 100.""".stripMargin + "mutationMinimum value is invalid. It should be a number between 0 and 100." ) }) }, @@ -341,7 +345,7 @@ object CliTest extends TestSpec { assert(parser.getErr)(equalTo("")) && assert(result)(equalTo { Left( - s"""mutationMinimum value is invalid. It should be a number between 0 and 100.""".stripMargin + "mutationMinimum value is invalid. It should be a number between 0 and 100." ) }) }, diff --git a/ci-tests/examples/example9/.blinky.conf b/ci-tests/examples/example9/.blinky.conf new file mode 100644 index 00000000..d1de6eba --- /dev/null +++ b/ci-tests/examples/example9/.blinky.conf @@ -0,0 +1,9 @@ +projectPath = "." +projectName = "example9" +filesToMutate = "Example" +options = { + maxRunningTime = 1 minute + + failOnMinimum = true + mutationMinimum = 75 +} diff --git a/ci-tests/examples/example9/build.sbt b/ci-tests/examples/example9/build.sbt new file mode 100644 index 00000000..bbfe3ad0 --- /dev/null +++ b/ci-tests/examples/example9/build.sbt @@ -0,0 +1,2 @@ +scalaVersion := "2.12.11" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.6" % Test diff --git a/ci-tests/examples/example9/src/main/scala/Example.scala b/ci-tests/examples/example9/src/main/scala/Example.scala new file mode 100644 index 00000000..a4db41a0 --- /dev/null +++ b/ci-tests/examples/example9/src/main/scala/Example.scala @@ -0,0 +1,11 @@ +object Example { + def calc(nOpt: Option[Int]): String = + nOpt + .map { n => + if (n > 5) + "big" + else + "small" + } + .getOrElse("") +} diff --git a/ci-tests/examples/example9/src/test/scala/ExampleTest.scala b/ci-tests/examples/example9/src/test/scala/ExampleTest.scala new file mode 100644 index 00000000..9167f5ab --- /dev/null +++ b/ci-tests/examples/example9/src/test/scala/ExampleTest.scala @@ -0,0 +1,18 @@ +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class ExampleTest extends AnyWordSpec with Matchers { + "Example" must { + "return big for the number 6" in { + Example.calc(Some(6)) mustEqual "big" + } + + "return small for the number 4" in { + Example.calc(Some(4)) mustEqual "small" + } + + "return an answer when the input is None" in { + assert(Example.calc(None).nonEmpty) + } + } +}