diff --git a/build.sbt b/build.sbt index a85e32ec4..cf627c2c5 100644 --- a/build.sbt +++ b/build.sbt @@ -33,7 +33,7 @@ lazy val riddl = (project in file(".")).enablePlugins(ScoverageSbtPlugin) testkit, prettify, hugo, - `git-check`, + onchange, doc, riddlc, plugin @@ -122,10 +122,10 @@ lazy val hugo: Project = project.in(file("hugo")).configure(C.withCoverage(0)) .dependsOn(language % "compile->compile", commands, testkit % "test->compile") .dependsOn(utils) -lazy val GitCheck = config("git-check") -lazy val `git-check`: Project = project.in(file("git-check")) +lazy val OnChange = config("onchange") +lazy val onchange: Project = project.in(file("onchange")) .configure(C.withCoverage(0)).configure(C.mavenPublish).settings( - name := "riddl-git-check", + name := "riddl-onchange", Compile / unmanagedResourceDirectories += { baseDirectory.value / "resources" }, @@ -139,7 +139,7 @@ lazy val scaladocSiteProjects = List( (commands, Commands), (testkit, TestKit), (prettify, Prettify), - (`git-check`, GitCheck), + (onchange, OnChange), (hugo, HugoTrans), (riddlc, Riddlc) ) @@ -186,7 +186,7 @@ lazy val riddlc: Project = project.in(file("riddlc")) commands, language, hugo, - `git-check`, + onchange, testkit % "test->compile" ).settings( name := "riddlc", diff --git a/commands/src/main/scala/com/reactific/riddl/commands/ParseCommand.scala b/commands/src/main/scala/com/reactific/riddl/commands/ParseCommand.scala index c2995a495..3b8a1ae81 100644 --- a/commands/src/main/scala/com/reactific/riddl/commands/ParseCommand.scala +++ b/commands/src/main/scala/com/reactific/riddl/commands/ParseCommand.scala @@ -13,9 +13,13 @@ import com.reactific.riddl.utils.Logger import java.nio.file.Path +object ParseCommand { + val cmdName = "parse" +} + /** A Command for Parsing RIDDL input */ -class ParseCommand extends InputFileCommandPlugin("parse") { +class ParseCommand extends InputFileCommandPlugin(ParseCommand.cmdName) { import InputFileCommandPlugin.Options override def run( options: Options, diff --git a/git-check/src/test/scala/com/reactific/riddl/translator/git/GitCheckTranslatorTest.scala b/git-check/src/test/scala/com/reactific/riddl/translator/git/GitCheckTranslatorTest.scala deleted file mode 100644 index bc4e3d9ef..000000000 --- a/git-check/src/test/scala/com/reactific/riddl/translator/git/GitCheckTranslatorTest.scala +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2019 Ossum, Inc. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.reactific.riddl.translator.git -import com.reactific.riddl.testkit.RunCommandOnExamplesTest -import com.reactific.riddl.translator.hugo_git_check.GitCheckCommand - -import java.nio.file.Path - -class GitCheckTranslatorTest - extends RunCommandOnExamplesTest[GitCheckCommand.Options, GitCheckCommand]( - "git-check" - ) { - - val output: String = "hugo-git-check/target/test" - - def makeTranslatorOptions(fileName: String): GitCheckCommand.Options = { - val gitCloneDir = Path.of(".").toAbsolutePath.getParent - val relativeDir = Path.of(".").resolve(fileName).getParent - GitCheckCommand.Options(Some(gitCloneDir), Some(relativeDir)) - } - - "HugoGitCheck" should { "run stuff when git changes" in { runTests() } } -} diff --git a/git-check/src/main/resources/META-INFo/services/com.reactific.riddl.commands.CommandPlugin b/onchange/src/main/resources/META-INFo/services/com.reactific.riddl.commands.CommandPlugin similarity index 100% rename from git-check/src/main/resources/META-INFo/services/com.reactific.riddl.commands.CommandPlugin rename to onchange/src/main/resources/META-INFo/services/com.reactific.riddl.commands.CommandPlugin diff --git a/onchange/src/main/scala/com/reactific/riddl/translator/onchange/DotWritingProgressMonitor.scala b/onchange/src/main/scala/com/reactific/riddl/translator/onchange/DotWritingProgressMonitor.scala new file mode 100644 index 000000000..bd6b2c112 --- /dev/null +++ b/onchange/src/main/scala/com/reactific/riddl/translator/onchange/DotWritingProgressMonitor.scala @@ -0,0 +1,40 @@ +/* + * Copyright 2019 Ossum, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.reactific.riddl.translator.onchange + +import com.reactific.riddl.language.CommonOptions +import com.reactific.riddl.utils.Logger +import org.eclipse.jgit.lib.ProgressMonitor + +import java.io.PrintStream + +case class DotWritingProgressMonitor(out: PrintStream, log: Logger, + options: CommonOptions) + extends ProgressMonitor { + override def start(totalTasks: Int): Unit = { + if (options.verbose) { log.info(s"Starting Fetch with $totalTasks tasks.") } + else { out.print("\n.") } + } + + override def beginTask(title: String, totalWork: Int): Unit = { + if (options.verbose) { + log.info(s"Starting Task '$title', $totalWork remaining.") + } else { out.print(".") } + } + + override def update(completed: Int): Unit = { + if (options.verbose) { log.info(s"$completed tasks completed.") } + else { out.print(".") } + } + + override def endTask(): Unit = { + if (options.verbose) { log.info(s"Task completed.") } + else { out.println(".") } + } + + override def isCancelled: Boolean = false +} diff --git a/git-check/src/main/scala/com/reactific/riddl/translator/hugo_git_check/GitCheck.scala b/onchange/src/main/scala/com/reactific/riddl/translator/onchange/OnChange.scala similarity index 77% rename from git-check/src/main/scala/com/reactific/riddl/translator/hugo_git_check/GitCheck.scala rename to onchange/src/main/scala/com/reactific/riddl/translator/onchange/OnChange.scala index 0ffcc60f0..0b03ec267 100644 --- a/git-check/src/main/scala/com/reactific/riddl/translator/hugo_git_check/GitCheck.scala +++ b/onchange/src/main/scala/com/reactific/riddl/translator/onchange/OnChange.scala @@ -4,14 +4,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -package com.reactific.riddl.translator.hugo_git_check +package com.reactific.riddl.translator.onchange import com.reactific.riddl.language.Messages.Messages import com.reactific.riddl.language.* import com.reactific.riddl.utils.Logger import org.eclipse.jgit.api.* import org.eclipse.jgit.api.errors.GitAPIException -import org.eclipse.jgit.lib.ProgressMonitor import org.eclipse.jgit.merge.MergeStrategy import org.eclipse.jgit.storage.file.FileRepositoryBuilder import org.eclipse.jgit.submodule.SubmoduleWalk @@ -24,9 +23,9 @@ import java.time.Instant import scala.collection.mutable.ArrayBuffer import scala.jdk.CollectionConverters.* -object GitCheck { +object OnChange { - private def creds(options: GitCheckCommand.Options) = + private def creds(options: OnChangeCommand.Options) = new UsernamePasswordCredentialsProvider( options.userName, options.accessToken @@ -36,12 +35,12 @@ object GitCheck { root: AST.RootContainer, log: Logger, commonOptions: CommonOptions, - options: GitCheckCommand.Options + options: OnChangeCommand.Options )(doit: ( AST.RootContainer, Logger, CommonOptions, - GitCheckCommand.Options + OnChangeCommand.Options ) => Either[Messages, Unit] ): Either[Messages, Unit] = { require( @@ -81,7 +80,7 @@ object GitCheck { def gitHasChanges( log: Logger, commonOptions: CommonOptions, - options: GitCheckCommand.Options, + options: OnChangeCommand.Options, git: Git, minTime: FileTime ): Boolean = { @@ -93,9 +92,9 @@ object GitCheck { val relativized = top.relativize(relativeDir) if (relativized.getNameCount > 1) relativized.toString else "." } else { "." } - val status = git.status() - .setProgressMonitor(DotWritingProgressMonitor(log, commonOptions)) - .setIgnoreSubmodules(SubmoduleWalk.IgnoreSubmoduleMode.ALL) + val status = git.status().setProgressMonitor( + DotWritingProgressMonitor(System.out, log, commonOptions) + ).setIgnoreSubmodules(SubmoduleWalk.IgnoreSubmoduleMode.ALL) .addPath(subPath).call() val potentiallyChangedFiles = @@ -113,7 +112,7 @@ object GitCheck { def pullCommits( log: Logger, commonOptions: CommonOptions, - options: GitCheckCommand.Options, + options: OnChangeCommand.Options, git: Git ): Boolean = { try { @@ -133,8 +132,8 @@ object GitCheck { } def prepareOptions( - options: GitCheckCommand.Options - ): GitCheckCommand.Options = { + options: OnChangeCommand.Options + ): OnChangeCommand.Options = { require(options.inputFile.isEmpty, "inputFile not used by this command") options } @@ -175,30 +174,4 @@ object GitCheck { } } - case class DotWritingProgressMonitor(log: Logger, options: CommonOptions) - extends ProgressMonitor { - override def start(totalTasks: Int): Unit = { - if (options.verbose) { - log.info(s"Starting Fetch with $totalTasks tasks.") - } else { System.out.print("\n.") } - } - - override def beginTask(title: String, totalWork: Int): Unit = { - if (options.verbose) { - log.info(s"Starting Task '$title', $totalWork remaining.") - } else { System.out.print(".") } - } - - override def update(completed: Int): Unit = { - if (options.verbose) { log.info(s"$completed tasks completed.") } - else { System.out.print(".") } - } - - override def endTask(): Unit = { - if (options.verbose) { log.info(s"Task completed.") } - else { System.out.println(".") } - } - - override def isCancelled: Boolean = false - } } diff --git a/git-check/src/main/scala/com/reactific/riddl/translator/hugo_git_check/GitCheckCommand.scala b/onchange/src/main/scala/com/reactific/riddl/translator/onchange/OnChangeCommand.scala similarity index 86% rename from git-check/src/main/scala/com/reactific/riddl/translator/hugo_git_check/GitCheckCommand.scala rename to onchange/src/main/scala/com/reactific/riddl/translator/onchange/OnChangeCommand.scala index 3473fc562..134e0bc9e 100644 --- a/git-check/src/main/scala/com/reactific/riddl/translator/hugo_git_check/GitCheckCommand.scala +++ b/onchange/src/main/scala/com/reactific/riddl/translator/onchange/OnChangeCommand.scala @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -package com.reactific.riddl.translator.hugo_git_check +package com.reactific.riddl.translator.onchange + import com.reactific.riddl.commands.CommandOptions.optional import com.reactific.riddl.commands.CommandOptions import com.reactific.riddl.commands.CommandPlugin @@ -19,28 +20,30 @@ import scopt.OParser import java.io.File import java.nio.file.Path -object GitCheckCommand { +object OnChangeCommand { + final val cmdName: String = "onchange" case class Options( gitCloneDir: Option[Path] = None, relativeDir: Option[Path] = None, userName: String = "", accessToken: String = "") extends CommandOptions { - def command: String = "hugo-git-check" + def command: String = cmdName def inputFile: Option[Path] = None } } /** HugoGitCheck Command */ -class GitCheckCommand - extends CommandPlugin[GitCheckCommand.Options]("hugo-git-check") { - import GitCheckCommand.Options +class OnChangeCommand + extends CommandPlugin[OnChangeCommand.Options](OnChangeCommand.cmdName) + { + import OnChangeCommand.Options override def getOptions: (OParser[Unit, Options], Options) = { val builder = OParser.builder[Options] import builder.* OParser.sequence( - cmd("git-check").children( + cmd("onchange").children( opt[File]("git-clone-dir").required() .action((f, opts) => opts.copy(gitCloneDir = Some(f.toPath))) .text("""Provides the top directory of a git repo clone that @@ -59,7 +62,7 @@ class GitCheckCommand ) -> Options() } - implicit val hugoGitCheckReader: ConfigReader[Options] = { + implicit val onChangeReader: ConfigReader[Options] = { (cur: ConfigCursor) => { for { @@ -73,7 +76,7 @@ class GitCheckCommand accessTokenRes <- objCur.atKey("access-token") accessTokenStr <- accessTokenRes.asString } yield { - GitCheckCommand.Options( + OnChangeCommand.Options( gitCloneDir = Some(gitCloneDir.toPath), userName = userNameStr, accessToken = accessTokenStr @@ -82,7 +85,7 @@ class GitCheckCommand } } - override def getConfigReader: ConfigReader[Options] = hugoGitCheckReader + override def getConfigReader: ConfigReader[Options] = onChangeReader /** Execute the command given the options. Error should be returned as * Left(messages) and not directly logged. The log is for verbose or debug diff --git a/onchange/src/test/scala/com/reactific/riddl/translator/onchange/DotWritingProgressMonitorTest.scala b/onchange/src/test/scala/com/reactific/riddl/translator/onchange/DotWritingProgressMonitorTest.scala new file mode 100644 index 000000000..14596b578 --- /dev/null +++ b/onchange/src/test/scala/com/reactific/riddl/translator/onchange/DotWritingProgressMonitorTest.scala @@ -0,0 +1,72 @@ +/* + * Copyright 2019 Ossum, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.reactific.riddl.translator.onchange +import com.reactific.riddl.language.CommonOptions +import com.reactific.riddl.utils.StringBuildingPrintStream +import com.reactific.riddl.utils.StringLogger +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class DotWritingProgressMonitorTest extends AnyWordSpec with Matchers { + def runTest(verbose: Boolean): (String, String) = { + val log = StringLogger(1024) + val capture = StringBuildingPrintStream() + val monitor = + DotWritingProgressMonitor(capture, log, CommonOptions(verbose = verbose)) + monitor.start(3) + def runTask(name: String, work: Int): Unit = { + monitor.beginTask(name, work) + for (i <- 1 to work) { monitor.update(i) } + monitor.endTask() + } + runTask("One", 5) + runTask("Two", 5) + runTask("Three", 5) + (capture.mkString(), log.toString()) + + } + "DotWritingProgressMonitor" should { + "product correct output for a set of tasks" in { + val (capture, log) = runTest(true) + capture mustBe empty + log must be("""|[info] Starting Fetch with 3 tasks. + |[info] Starting Task 'One', 5 remaining. + |[info] 1 tasks completed. + |[info] 2 tasks completed. + |[info] 3 tasks completed. + |[info] 4 tasks completed. + |[info] 5 tasks completed. + |[info] Task completed. + |[info] Starting Task 'Two', 5 remaining. + |[info] 1 tasks completed. + |[info] 2 tasks completed. + |[info] 3 tasks completed. + |[info] 4 tasks completed. + |[info] 5 tasks completed. + |[info] Task completed. + |[info] Starting Task 'Three', 5 remaining. + |[info] 1 tasks completed. + |[info] 2 tasks completed. + |[info] 3 tasks completed. + |[info] 4 tasks completed. + |[info] 5 tasks completed. + |[info] Task completed. + |""".stripMargin) + } + "produce correct output in non-verbose mode" in { + val (capture, log) = runTest(false) + log mustBe empty + capture must be(""" + |........ + |....... + |....... + |""".stripMargin) + + } + } + +} diff --git a/onchange/src/test/scala/com/reactific/riddl/translator/onchange/OnChangeTranslatorTest.scala b/onchange/src/test/scala/com/reactific/riddl/translator/onchange/OnChangeTranslatorTest.scala new file mode 100644 index 000000000..da45c7826 --- /dev/null +++ b/onchange/src/test/scala/com/reactific/riddl/translator/onchange/OnChangeTranslatorTest.scala @@ -0,0 +1,63 @@ +/* + * Copyright 2019 Ossum, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.reactific.riddl.translator.onchange + +import com.reactific.riddl.commands.CommandOptions +import com.reactific.riddl.commands.CommandPlugin +import com.reactific.riddl.commands.InputFileCommandPlugin +import com.reactific.riddl.commands.ParseCommand +import com.reactific.riddl.language.Messages.Messages +import com.reactific.riddl.testkit.RunCommandOnExamplesTest +import org.scalatest.Assertion + +import java.nio.file.Path +import scala.annotation.unused + +class OnChangeTranslatorTest + extends RunCommandOnExamplesTest[ + InputFileCommandPlugin.Options, + ParseCommand + ](ParseCommand.cmdName) { + + val output: String = s"${OnChangeCommand.cmdName}/target/test" + + def makeTranslatorOptions(fileName: String): OnChangeCommand.Options = { + val gitCloneDir = Path.of(".").toAbsolutePath.getParent + val relativeDir = Path.of(".").resolve(fileName).getParent + OnChangeCommand.Options(Some(gitCloneDir), Some(relativeDir)) + } + + val root = "onchange/src/test/input/" + + "OnChangeCommand" should { + "handle simple case" in { + runTest(root + "simple") + } + "handle harder case" in { + runTest(root + "harder") + } + } + + override def onSuccess( + commandName: String, + @unused caseName: String, + @unused configFile: Path, + @unused command: CommandPlugin[CommandOptions], + @unused tempDir: Path + ): Assertion = { + info(s"Case $caseName at $configFile succeeded") + succeed + } + + override def onFailure( + @unused commandName: String, + @unused caseName: String, + @unused configFile: Path, + @unused messages: Messages, + @unused tempDir: Path + ): Assertion = { fail(messages.format) } +} diff --git a/project/Helpers.scala b/project/Helpers.scala index f8afae6d7..849becba9 100644 --- a/project/Helpers.scala +++ b/project/Helpers.scala @@ -23,6 +23,7 @@ object V { val scalacheck = "1.17.0" val scalatest = "3.2.14" val scopt = "4.1.0" + val slf4j = "2.0.3" } object Dep { @@ -37,9 +38,14 @@ object Dep { val scalatest = "org.scalatest" %% "scalatest" % V.scalatest val scalacheck = "org.scalacheck" %% "scalacheck" % V.scalacheck val scopt = "com.github.scopt" %% "scopt" % V.scopt + val slf4j = "org.slf4j" % "slf4j-nop" % V.slf4j - val testing: Seq[ModuleID] = - Seq(scalactic % "test", scalatest % "test", scalacheck % "test") + val testing: Seq[ModuleID] = Seq( + scalactic % "test", + scalatest % "test", + scalacheck % "test", + slf4j % "test" + ) val testKitDeps: Seq[ModuleID] = Seq(scalactic, scalatest, scalacheck) } diff --git a/riddlc/src/test/scala/com/reactific/riddl/RunHugoOnExamplesTest.scala b/riddlc/src/test/scala/com/reactific/riddl/RunHugoOnExamplesTest.scala index 98aa8c76d..144ca514f 100644 --- a/riddlc/src/test/scala/com/reactific/riddl/RunHugoOnExamplesTest.scala +++ b/riddlc/src/test/scala/com/reactific/riddl/RunHugoOnExamplesTest.scala @@ -24,7 +24,9 @@ class RunHugoOnExamplesTest override val outDir: Path = Path.of("riddlc/target/test/hugo-examples") - "Run Hugo On Examples" should { "work " in { runTests() } } + override def validate(name: String): Boolean = name == "ReactiveBBQ" + + "Run Hugo On Examples" should { "should work" in { runTests() } } override def onSuccess( @unused commandName: String, diff --git a/testkit/src/main/scala/com/reactific/riddl/testkit/RunCommandOnExamplesTest.scala b/testkit/src/main/scala/com/reactific/riddl/testkit/RunCommandOnExamplesTest.scala index bd1e2cf7c..9b35817ba 100644 --- a/testkit/src/main/scala/com/reactific/riddl/testkit/RunCommandOnExamplesTest.scala +++ b/testkit/src/main/scala/com/reactific/riddl/testkit/RunCommandOnExamplesTest.scala @@ -31,7 +31,21 @@ import java.nio.file.Path import scala.annotation.unused import scala.jdk.CollectionConverters.IteratorHasAsScala -/** Test Setup for running a command on the examples */ +/** Test Setup for running a command on the riddl-examples repos. + * + * This testkit helper allows you to create a test that runs a command on all + * the examples in the riddl-examples repo. It will download the riddl-examples + * repo, unzip it, and run the command on each example. The command is run in a + * temporary directory, and the output is compared to the expected output in + * the example. + * + * @tparam OPT + * The class for the Options of the command + * @tparam CMD + * The class for the Command + * @param commandName + * The name of the command to run. + */ abstract class RunCommandOnExamplesTest[ OPT <: CommandOptions, CMD <: CommandPlugin[OPT] @@ -71,7 +85,9 @@ abstract class RunCommandOnExamplesTest[ def validate(@unused name: String): Boolean = true - def forEachConfigFile[T](f: (String, Path) => T): Seq[Either[Messages, T]] = { + def forEachConfigFile[T]( + f: (String, Path) => T + ): Seq[Either[(String, Messages), T]] = { val configs = FileUtils .iterateFiles(srcDir.toFile, Array[String](suffix), true).asScala.toSeq for { @@ -79,12 +95,17 @@ abstract class RunCommandOnExamplesTest[ name = config.getName.dropRight(suffix.length + 1) } yield { if (validate(name)) { - val commands = CommandPlugin.loadCandidateCommands(config.toPath) - if (commands.contains(commandName)) { Right(f(name, config.toPath)) } - else { Left(errors(s"Command $commandName not found in $config")) } + CommandPlugin.loadCandidateCommands(config.toPath) match { + case Right(commands) => + if (commands.contains(commandName)) { + Right(f(name, config.toPath)) + } else { + Left(name -> errors(s"Command $commandName not found in $config")) + } + case Left(messages) => Left(name -> messages) + } } else { - info(s"Skipping $name") - Left(warnings(s"Command $commandName skipped for $name")) + Left(name -> warnings(s"Command $commandName skipped for $name")) } } } @@ -128,7 +149,7 @@ abstract class RunCommandOnExamplesTest[ /** Call this from your test suite subclass to run all the examples found. */ def runTests(): Unit = { - forEachConfigFile { case (name, path) => + val results = forEachConfigFile { case (name, path) => val outputDir = outDir.resolve(name) val result = CommandPlugin.runCommandNamed( @@ -139,8 +160,20 @@ abstract class RunCommandOnExamplesTest[ outputDirOverride = Some(outputDir) ) result match { - case Right(cmd) => onSuccess(commandName, name, path, cmd, outputDir) - case Left(messages) => fail(messages.format) + case Right(command) => + onSuccess(commandName, name, path, command, outputDir) -> name + case Left(messages) => + onFailure(commandName, name, path, messages, outputDir) -> name + } + } + for { result <- results } { + result match { + case Right(_) => // do nothing + case Left((name, messages)) => + val errors = messages.justErrors + if (errors.nonEmpty) { + fail(s"Test case $name failed:\n${errors.format}") + } else { info(messages.format) } } } } @@ -159,8 +192,11 @@ abstract class RunCommandOnExamplesTest[ outputDirOverride = Some(outputDir) ) result match { - case Right(cmd) => onSuccess(commandName, name, path, cmd, outputDir) - case Left(messages) => fail(messages.format) + case Right(command) => + onSuccess(commandName, name, path, command, outputDir) + case Left(messages) => + onFailure(commandName, name, path, messages, outputDir) + } } } @@ -181,4 +217,13 @@ abstract class RunCommandOnExamplesTest[ @unused command: CommandPlugin[CommandOptions], @unused tempDir: Path ): Assertion = { succeed } + + def onFailure( + @unused commandName: String, + @unused caseName: String, + @unused configFile: Path, + @unused messages: Messages, + @unused tempDir: Path + ): Assertion = { fail(messages.format) } + }