diff --git a/commands/src/main/scala/com/reactific/riddl/commands/OnChangeCommand.scala b/commands/src/main/scala/com/reactific/riddl/commands/OnChangeCommand.scala index 1850e6a8e..308c4b105 100644 --- a/commands/src/main/scala/com/reactific/riddl/commands/OnChangeCommand.scala +++ b/commands/src/main/scala/com/reactific/riddl/commands/OnChangeCommand.scala @@ -1,9 +1,3 @@ -/* - * Copyright 2019 Ossum, Inc. - * - * SPDX-License-Identifier: Apache-2.0 - */ - package com.reactific.riddl.commands import com.reactific.riddl.commands.CommandOptions.optional @@ -23,27 +17,31 @@ import org.eclipse.jgit.api.errors.GitAPIException import org.eclipse.jgit.merge.MergeStrategy import org.eclipse.jgit.storage.file.FileRepositoryBuilder import org.eclipse.jgit.submodule.SubmoduleWalk -import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider +import pureconfig.error.CannotParse import java.nio.file.attribute.FileTime import java.nio.file.Files import java.time.Instant +import scala.concurrent.duration.Duration +import scala.concurrent.duration.DurationInt +import scala.concurrent.duration.FiniteDuration import scala.jdk.CollectionConverters.* object OnChangeCommand { final val cmdName: String = "onchange" + final val defaultMaxLoops = 1024 case class Options( - gitCloneDir: Option[Path] = None, - relativeDir: Option[Path] = None, - userName: String = "", - accessToken: String = "") + inputFile: Option[Path] = None, + watchDirectory: Option[Path] = None, + targetCommand: String = ParseCommand.cmdName, + refreshRate: FiniteDuration = 10.seconds, + maxCycles: Int = defaultMaxLoops, + interactive: Boolean = false) extends CommandOptions { def command: String = cmdName - def inputFile: Option[Path] = None } } -/** HugoGitCheck Command */ class OnChangeCommand extends CommandPlugin[OnChangeCommand.Options](OnChangeCommand.cmdName) { import OnChangeCommand.Options @@ -54,14 +52,28 @@ class OnChangeCommand OParser.sequence( cmd("onchange").children( opt[File]("git-clone-dir").required() - .action((f, opts) => opts.copy(gitCloneDir = Some(f.toPath))) + .action((f, opts) => opts.copy(watchDirectory = Some(f.toPath))) .text("""Provides the top directory of a git repo clone that |contains the to be processed.""".stripMargin), - opt[String]("user-name").optional() - .action((n, opts) => opts.copy(userName = n)) - .text("Name of the git user for pulling from remote"), - opt[String]("access-token").optional() - .action((t, opts) => opts.copy(accessToken = t)) + arg[String]("target-command").required().action { (cmd, opt) => + opt.copy(targetCommand = cmd) + }.text("The name of the command to select from the configuration file"), + arg[FiniteDuration]("refresh-rate").optional().validate { + case r if r.toMillis < 1000 => + Left(" is too fast, minimum is 1 seconds") + case r if r.toDays > 1 => + Left(" is too slow, maximum is 1 day") + case _ => Right(()) + }.action((r, c) => c.copy(refreshRate = r)) + .text("""Specifies the rate at which the is checked + |for updates so the process to regenerate the hugo site is + |started""".stripMargin), + arg[Int]("max-cycles").optional().validate { + case x if x < 1 => Left(" can't be less than 1") + case x if x > 1024 * 1024 => Left(" is too big") + case _ => Right(()) + }.action((m, c) => c.copy(maxCycles = m)) + .text("""Limit the number of check cycles that will be repeated.""") ).text( """This command checks the directory for new commits |and does a `git pull" command there if it finds some; otherwise @@ -75,18 +87,30 @@ class OnChangeCommand { for { objCur <- cur.asObjectCursor - gitCloneDir <- optional[File](objCur, "git-clone-dir", new File(".")) { + watchDir <- optional[File](objCur, "git-clone-dir", new File(".")) { cc => cc.asString.map(s => new File(s)) } - userNameRes <- objCur.atKey("user-name") - userNameStr <- userNameRes.asString - accessTokenRes <- objCur.atKey("access-token") - accessTokenStr <- accessTokenRes.asString + targetCommand <- optional(objCur, "target-command", "")(_.asString) + refreshRate <- optional(objCur, "refresh-rate", "10s")(_.asString) + .flatMap { rr => + val dur = Duration.create(rr) + if (dur.isFinite) { Right(dur.asInstanceOf[FiniteDuration]) } + else { + ConfigReader.Result.fail[FiniteDuration](CannotParse( + s"'refresh-rate' must be a finite duration, not $rr", + None + )) + } + } + maxCycles <- optional(objCur, "max-cycles", 100)(_.asInt) + interactive <- optional(objCur, "interactive", true)(_.asBoolean) } yield { OnChangeCommand.Options( - gitCloneDir = Some(gitCloneDir.toPath), - userName = userNameStr, - accessToken = accessTokenStr + watchDirectory = Some(watchDir.toPath), + targetCommand = targetCommand, + refreshRate = refreshRate, + maxCycles = maxCycles, + interactive = interactive ) } } @@ -113,12 +137,6 @@ class OnChangeCommand outputDirOverride: Option[Path] ): Either[Messages, Unit] = { Left(errors("Not Implemented")) } - private def creds(options: OnChangeCommand.Options) = - new UsernamePasswordCredentialsProvider( - options.userName, - options.accessToken - ) - def runWhenGitChanges( root: AST.RootContainer, log: Logger, @@ -132,10 +150,10 @@ class OnChangeCommand ) => Either[Messages, Unit] ): Either[Messages, Unit] = { require( - options.gitCloneDir.nonEmpty, - s"Option 'gitCloneDir' must have a value." + options.watchDirectory.nonEmpty, + s"Option 'watchDirectory' must have a value." ) - val gitCloneDir = options.gitCloneDir.get + val gitCloneDir = options.watchDirectory.get require(Files.isDirectory(gitCloneDir), s"$gitCloneDir is not a directory.") val builder = new FileRepositoryBuilder val repository = @@ -147,7 +165,7 @@ class OnChangeCommand val opts = prepareOptions(options) if (gitHasChanges(log, commonOptions, opts, git, when)) { - pullCommits(log, commonOptions, opts, git) + pullCommits(log, commonOptions, git) doit(root, log, commonOptions, opts) } else { Right(()) } } @@ -175,8 +193,8 @@ class OnChangeCommand val repo = git.getRepository val top = repo.getDirectory.getParentFile.toPath.toAbsolutePath val subPath = - if (options.relativeDir.nonEmpty) { - val relativeDir = options.relativeDir.get.toAbsolutePath + if (options.watchDirectory.nonEmpty) { + val relativeDir = options.watchDirectory.get.toAbsolutePath val relativized = top.relativize(relativeDir) if (relativized.getNameCount > 1) relativized.toString else "." } else { "." } @@ -200,7 +218,6 @@ class OnChangeCommand def pullCommits( log: Logger, commonOptions: CommonOptions, - options: OnChangeCommand.Options, git: Git ): Boolean = { try { @@ -208,8 +225,7 @@ class OnChangeCommand log.info("Pulling latest changes from remote") } val pullCommand = git.pull - pullCommand.setCredentialsProvider(creds(options)) - .setFastForward(MergeCommand.FastForwardMode.FF_ONLY) + pullCommand.setFastForward(MergeCommand.FastForwardMode.FF_ONLY) .setStrategy(MergeStrategy.THEIRS) pullCommand.call.isSuccessful } catch { diff --git a/riddlc/src/test/scala/com/reactific/riddl/RiddlCommandsTest.scala b/commands/src/test/scala/com/reactific/riddl/commands/CommandsTest.scala similarity index 66% rename from riddlc/src/test/scala/com/reactific/riddl/RiddlCommandsTest.scala rename to commands/src/test/scala/com/reactific/riddl/commands/CommandsTest.scala index 48eb6289b..9d94a56c3 100644 --- a/riddlc/src/test/scala/com/reactific/riddl/RiddlCommandsTest.scala +++ b/commands/src/test/scala/com/reactific/riddl/commands/CommandsTest.scala @@ -4,13 +4,20 @@ * SPDX-License-Identifier: Apache-2.0 */ -package com.reactific.riddl +package com.reactific.riddl.commands -import com.reactific.riddl.commands.CommandPlugin -import com.reactific.riddl.testkit.RunCommandSpecBase import org.scalatest.Assertion +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec -class RiddlCommandsTest extends RunCommandSpecBase { +class CommandsTest extends AnyWordSpec with Matchers { + + def runCommand( + args: Array[String] = Array.empty[String] + ): Assertion = { + val rc = CommandPlugin.runMain(args) + rc mustBe 0 + } val inputFile = "testkit/src/test/input/rbbq.riddl" val hugoConfig = "testkit/src/test/input/hugo.conf" @@ -18,81 +25,64 @@ class RiddlCommandsTest extends RunCommandSpecBase { val outputDir: String => String = (name: String) => s"riddlc/target/test/$name" - "Riddlc Commands" should { - "generate info" in { runCommand(Array("info")) } - "provide help" in { runCommand(Array("--quiet", "help")) } - "print version" in { runCommand(Array("--quiet", "version")) } - "handle parse" in { - val args = Array("--quiet", "parse", inputFile) - runCommand(args) - } - "handle validate" in { + "Commands" should { + "handle dump" in { val args = Array( "--quiet", "--suppress-missing-warnings", "--suppress-style-warnings", - "validate", + "dump", inputFile ) runCommand(args) } - "handle dump" in { + "handle from" in { val args = Array( "--quiet", "--suppress-missing-warnings", "--suppress-style-warnings", - "dump", - inputFile + "from", + validateConfig, + "validate" ) runCommand(args) } - "handle hugo" in { + "handle parse" in { + val args = Array("--quiet", "parse", inputFile) + runCommand(args) + } + "handle repeat" in { val args = Array( "--quiet", "--suppress-missing-warnings", "--suppress-style-warnings", - "hugo", - inputFile, - "-o", - outputDir("hugo") + "repeat", + validateConfig, + "validate", + "1s", + "2" ) runCommand(args) } - "handle hugo from config" in { + "handle stats" in { val args = Array( - "--verbose", + "--quiet", "--suppress-missing-warnings", "--suppress-style-warnings", - "from", - hugoConfig, - "hugo" + "stats", + inputFile ) runCommand(args) - // runHugo(path) - // val root = Path.of(output).resolve(path) - // val img = root.resolve("static/images/RBBQ.png") - // Files.exists(img) mustBe true } - - "repeat validation of the ReactiveBBQ example" in { + "handle validate" in { val args = Array( "--quiet", "--suppress-missing-warnings", "--suppress-style-warnings", - "repeat", - validateConfig, "validate", - "1s", - "2" + inputFile ) runCommand(args) } } - - def runCommand( - args: Array[String] = Array.empty[String] - ): Assertion = { - val rc = CommandPlugin.runMain(args) - rc mustBe 0 - } } diff --git a/commands/src/test/scala/com/reactific/riddl/commands/OnChangeCommandTest.scala b/commands/src/test/scala/com/reactific/riddl/commands/OnChangeCommandTest.scala new file mode 100644 index 000000000..5dc53e581 --- /dev/null +++ b/commands/src/test/scala/com/reactific/riddl/commands/OnChangeCommandTest.scala @@ -0,0 +1,14 @@ +/* + * Copyright 2022 Ossum, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.reactific.riddl.commands + +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class OnChangeCommandTest extends AnyWordSpec with Matchers { + "OnChangeCommand" should { "have a test" in { true must be(true) } } +} diff --git a/hugo/src/test/scala/com/reactific/riddl/translator/hugo/HugoCommandTest.scala b/hugo/src/test/scala/com/reactific/riddl/translator/hugo/HugoCommandTest.scala new file mode 100644 index 000000000..f65cb8b75 --- /dev/null +++ b/hugo/src/test/scala/com/reactific/riddl/translator/hugo/HugoCommandTest.scala @@ -0,0 +1,41 @@ +package com.reactific.riddl.translator.hugo +import com.reactific.riddl.testkit.RunCommandSpecBase + +class HugoCommandTest extends RunCommandSpecBase { + + val inputFile = "testkit/src/test/input/rbbq.riddl" + val hugoConfig = "testkit/src/test/input/hugo.conf" + val validateConfig = "testkit/src/test/input/validate.conf" + val outputDir: String => String = + (name: String) => s"riddlc/target/test/$name" + + "HugoCommand" should { + "handle hugo" in { + val args = Seq( + "--quiet", + "--suppress-missing-warnings", + "--suppress-style-warnings", + "hugo", + inputFile, + "-o", + outputDir("hugo") + ) + runWith(args) + } + "handle hugo from config" in { + val args = Seq( + "--verbose", + "--suppress-missing-warnings", + "--suppress-style-warnings", + "from", + hugoConfig, + "hugo" + ) + runWith(args) + // runHugo(path) + // val root = Path.of(output).resolve(path) + // val img = root.resolve("static/images/RBBQ.png") + // Files.exists(img) mustBe true + } + } +} diff --git a/onchange/src/main/scala/com/reactific/riddl/translator/onchange/OnChange.scala b/onchange/src/main/scala/com/reactific/riddl/translator/onchange/OnChange.scala deleted file mode 100644 index 6b2194b13..000000000 --- a/onchange/src/main/scala/com/reactific/riddl/translator/onchange/OnChange.scala +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright 2019 Ossum, Inc. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.reactific.riddl.translator.onchange - - -object OnChange { - - -} diff --git a/riddlc/src/test/scala/com/reactific/riddl/RiddlcCommandsTest.scala b/riddlc/src/test/scala/com/reactific/riddl/RiddlcCommandsTest.scala new file mode 100644 index 000000000..07aa5acd3 --- /dev/null +++ b/riddlc/src/test/scala/com/reactific/riddl/RiddlcCommandsTest.scala @@ -0,0 +1,37 @@ +/* + * Copyright 2019 Ossum, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.reactific.riddl + +import com.reactific.riddl.testkit.RunCommandSpecBase + +class RiddlcCommandsTest extends RunCommandSpecBase { + + val inputFile = "testkit/src/test/input/rbbq.riddl" + val hugoConfig = "testkit/src/test/input/hugo.conf" + val validateConfig = "testkit/src/test/input/validate.conf" + val outputDir: String => String = + (name: String) => s"riddlc/target/test/$name" + + "Riddlc Commands" should { + "tell about riddl" in { + val args = Seq("about") + runWith(args) + } + "provide help" in { + val args = Seq("--quiet", "help") + runWith(args) + } + "generate info" in { + val args = Seq("info") + runWith(args) + } + "print version" in { + val args = Seq("version") + runWith(args) + } + } +} diff --git a/utils/src/test/scala/com/reactific/riddl/utils/FileWatcherTest.scala b/utils/src/test/scala/com/reactific/riddl/utils/FileWatcherTest.scala index e070e4d97..4ebe6789d 100644 --- a/utils/src/test/scala/com/reactific/riddl/utils/FileWatcherTest.scala +++ b/utils/src/test/scala/com/reactific/riddl/utils/FileWatcherTest.scala @@ -14,8 +14,7 @@ import scala.concurrent.duration.Duration class FileWatcherTest extends AnyWordSpec with Matchers { "FileWatcher" should { "notice changes in a directory" in { - val dir = Path.of(".").resolve("onchange").resolve("target") - .toAbsolutePath + val dir = Path.of("utils").resolve("target").toAbsolutePath def onEvents(events: Seq[WatchEvent[?]]): Boolean = { events.foreach { ev => info(s"Event: ${ev.kind()}: ${ev.count()}") } false @@ -32,8 +31,9 @@ class FileWatcherTest extends AnyWordSpec with Matchers { val f = Future[Boolean] { FileWatcher.watchForChanges(dir, 2, 10)(onEvents)(notOnEvents) } - Thread.sleep(1000) + Thread.sleep(800) Files.createFile(changeFile) + Thread.sleep(100) require(Files.exists(changeFile), "File should exist") Thread.sleep(200) Files.delete(changeFile)