From bc1b04e33b6e40a89009d71964a471b0159d9666 Mon Sep 17 00:00:00 2001 From: reidspencer Date: Sat, 12 Nov 2022 14:13:54 -0500 Subject: [PATCH 1/8] Get coverage reporting working Signed-off-by: reidspencer --- .github/workflows/scala.yml | 10 +++++++--- README.md | 5 +++-- build.sbt | 19 ++++++++++--------- project/Helpers.scala | 8 ++++---- project/plugins.sbt | 9 +++++++-- 5 files changed, 31 insertions(+), 20 deletions(-) diff --git a/.github/workflows/scala.yml b/.github/workflows/scala.yml index 68b57da09..5ec8a618a 100644 --- a/.github/workflows/scala.yml +++ b/.github/workflows/scala.yml @@ -1,4 +1,4 @@ -name: Scala CI +name: Scala Build on: push: @@ -24,10 +24,14 @@ jobs: extended: true - name: Coursier Caching uses: coursier/cache-action@v6 - - name: Build, Run Tests, Generate Packages + - name: Build, Run Test, Coverage run: | which hugo - sbt -v clean Test/compile test + sbt -v clean coverage Test/compile test coverageAggregate coveralls + env: + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + COVERALLS_FLAG_NAME: Scala ${{ matrix.scala }} + - name: Cleanup Before Caching shell: bash run: | diff --git a/README.md b/README.md index e8b9f83e0..0819df3aa 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ -![Code Build Status](https://github.com/reactific/riddl/actions/workflows/scala.yml/badge.svg) -![Documentation Build Status](https://github.com/reactific/riddl/actions/workflows/gh-pages.yml/badge.svg) +[![Code Build Status](https://github.com/reactific/riddl/actions/workflows/scala.yml/badge.svg)](https://github.com/reactific/riddl/actions/workflows/scala.yml/badge.svg) +[![Documentation Build Status](https://github.com/reactific/riddl/actions/workflows/gh-pages.yml/badge.svg)](https://github.com/reactific/riddl/actions/workflows/gh-pages.yml/badge.svg) +[![Coverage](https://coveralls.io/repos/github/reactific/riddl/badge.svg?branch=coverage)](https://coveralls.io/github/reactific/riddl?branch=coverage) [![CLA assistant](https://cla-assistant.io/readme/badge/reactific/riddl)](https://cla-assistant.io/reactific/riddl) # RIDDL diff --git a/build.sbt b/build.sbt index f9045dd1c..a85e32ec4 100644 --- a/build.sbt +++ b/build.sbt @@ -1,5 +1,5 @@ import com.jsuereth.sbtpgp.PgpKeys.pgpSigner - +import org.scoverage.coveralls.Imports.CoverallsKeys._ import sbtbuildinfo.BuildInfoOption.ToJson import sbtbuildinfo.BuildInfoOption.ToMap import sbtbuildinfo.BuildInfoOption.BuildTime @@ -20,7 +20,7 @@ Global / onChangedBuildSource := ReloadOnSourceChanges // IT IS HANDLED BY: sbt-dynver ThisBuild / dynverSeparator := "-" -lazy val riddl = (project in file(".")).disablePlugins(ScoverageSbtPlugin) +lazy val riddl = (project in file(".")).enablePlugins(ScoverageSbtPlugin) .enablePlugins(AutomateHeaderPlugin).configure(C.withInfo).settings( publish := {}, publishLocal := {}, @@ -41,7 +41,7 @@ lazy val riddl = (project in file(".")).disablePlugins(ScoverageSbtPlugin) lazy val Utils = config("utils") lazy val utils = project.in(file("utils")).configure(C.mavenPublish) - .configure(C.withCoverage()).enablePlugins(BuildInfoPlugin).settings( + .configure(C.withCoverage(0)).enablePlugins(BuildInfoPlugin).settings( name := "riddl-utils", coverageExcludedPackages := "", libraryDependencies ++= Seq(Dep.compress, Dep.lang3) ++ Dep.testing, @@ -80,7 +80,7 @@ lazy val utils = project.in(file("utils")).configure(C.mavenPublish) ) val Language = config("language") -lazy val language = project.in(file("language")).configure(C.withCoverage()) +lazy val language = project.in(file("language")).configure(C.withCoverage(0)) .configure(C.mavenPublish).settings( name := "riddl-language", coverageExcludedPackages := @@ -91,7 +91,7 @@ lazy val language = project.in(file("language")).configure(C.withCoverage()) val Commands = config("commands") -lazy val commands = project.in(file("commands")).configure(C.withCoverage()) +lazy val commands = project.in(file("commands")).configure(C.withCoverage(0)) .configure(C.mavenPublish).settings( name := "riddl-commands", libraryDependencies ++= Seq(Dep.scopt, Dep.pureconfig) ++ Dep.testing @@ -104,13 +104,13 @@ lazy val testkit = project.in(file("testkit")).configure(C.mavenPublish) .dependsOn(commands % "compile->compile;test->test") val Prettify = config("prettify") -lazy val prettify = project.in(file("prettify")).configure(C.withCoverage()) +lazy val prettify = project.in(file("prettify")).configure(C.withCoverage(0)) .configure(C.mavenPublish) .settings(name := "riddl-prettify", libraryDependencies ++= Dep.testing) .dependsOn(commands, testkit % "test->compile").dependsOn(utils) val HugoTrans = config("hugo") -lazy val hugo: Project = project.in(file("hugo")).configure(C.withCoverage()) +lazy val hugo: Project = project.in(file("hugo")).configure(C.withCoverage(0)) .configure(C.mavenPublish).settings( name := "riddl-hugo", Compile / unmanagedResourceDirectories += { @@ -124,7 +124,7 @@ lazy val hugo: Project = project.in(file("hugo")).configure(C.withCoverage()) lazy val GitCheck = config("git-check") lazy val `git-check`: Project = project.in(file("git-check")) - .configure(C.withCoverage()).configure(C.mavenPublish).settings( + .configure(C.withCoverage(0)).configure(C.mavenPublish).settings( name := "riddl-git-check", Compile / unmanagedResourceDirectories += { baseDirectory.value / "resources" @@ -181,7 +181,7 @@ val Riddlc = config("riddlc") lazy val riddlc: Project = project.in(file("riddlc")) .enablePlugins(JavaAppPackaging, UniversalDeployPlugin) .enablePlugins(MiniDependencyTreePlugin, GraalVMNativeImagePlugin) - .configure(C.mavenPublish).configure(C.withCoverage()).dependsOn( + .configure(C.mavenPublish).configure(C.withCoverage(0)).dependsOn( utils % "compile->compile;test->test", commands, language, @@ -190,6 +190,7 @@ lazy val riddlc: Project = project.in(file("riddlc")) testkit % "test->compile" ).settings( name := "riddlc", + coverallsTokenFile := Some("/home/reid/.coveralls.yml"), mainClass := Option("com.reactific.riddl.RIDDLC"), graalVMNativeImageOptions ++= Seq( "--verbose", diff --git a/project/Helpers.scala b/project/Helpers.scala index 503bd8edd..f8afae6d7 100644 --- a/project/Helpers.scala +++ b/project/Helpers.scala @@ -58,7 +58,7 @@ object C { "Apache-2.0", new URL("https://www.apache.org/licenses/LICENSE-2.0.txt") ), - ThisBuild / versionScheme := Option("semver-spec"), + ThisBuild / versionScheme := Option("early-semver"), ThisBuild / dynverVTagPrefix := false, // NEVER SET THIS: version := "0.1" // IT IS HANDLED BY: sbt-dynver @@ -117,7 +117,8 @@ object C { coverageMinimumStmtPerPackage := percent, coverageMinimumBranchPerPackage := percent, coverageMinimumStmtPerFile := percent, - coverageMinimumBranchPerFile := percent + coverageMinimumBranchPerFile := percent, + coverageExcludedPackages := "" ) } @@ -184,8 +185,7 @@ object C { Some("releases" at nexus + "service/local/staging/deploy/maven2") } }, - publishMavenStyle := true, - versionScheme := Some("early-semver") + publishMavenStyle := true ) } } diff --git a/project/plugins.sbt b/project/plugins.sbt index 2c9afacf5..4b0d918a2 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -6,7 +6,8 @@ addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.8.0") addSbtPlugin("com.dwijnand" % "sbt-dynver" % "4.1.1") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") addSbtPlugin("com.github.sbt" % "sbt-git" % "2.0.0") -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.9.3") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.6") +addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.3.2") // Documentation generation plugins // addSbtPlugin("com.typesafe.sbt" % "sbt-ghpages" % "0.6.3") @@ -14,10 +15,14 @@ addSbtPlugin("com.typesafe.sbt" % "sbt-site" % "1.4.1") addSbtPlugin("com.github.sbt" % "sbt-unidoc" % "0.5.0") addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "1.0.0") -addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.9.9") +addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.9.11") addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.2.0") // This enables sbt-bloop to create bloop config files for Metals editors // Uncomment locally if you use metals, otherwise don't slow down other // people's builds by leaving it commented in the repo. // addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.6") + +ThisBuild / libraryDependencySchemes += + "org.scala-lang.modules" %% "scala-xml" % "always" + From 86e47a0b9fef779dbeb6dc033aa0625f29278b5a Mon Sep 17 00:00:00 2001 From: reidspencer Date: Sat, 12 Nov 2022 14:21:42 -0500 Subject: [PATCH 2/8] Try coveralls with GITHUB_TOKEN Signed-off-by: reidspencer --- .github/workflows/scala.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scala.yml b/.github/workflows/scala.yml index 5ec8a618a..511889413 100644 --- a/.github/workflows/scala.yml +++ b/.github/workflows/scala.yml @@ -29,7 +29,7 @@ jobs: which hugo sbt -v clean coverage Test/compile test coverageAggregate coveralls env: - COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} COVERALLS_FLAG_NAME: Scala ${{ matrix.scala }} - name: Cleanup Before Caching From 3738879a9e6a8c7eb3cf6b2fd3e15187c20a9241 Mon Sep 17 00:00:00 2001 From: reidspencer Date: Sat, 12 Nov 2022 18:24:23 -0500 Subject: [PATCH 3/8] Start increasing test coverage * Add DotWritingProgressMonitor Test * Rename git-check as "onchange" * Fix RunCommandOnExamplesTest to actually run commands on examples * Make RunCommandOnExamplesTest print out what its skipping * Attempt to fix SLF4J warnings Signed-off-by: reidspencer --- build.sbt | 12 ++-- .../riddl/commands/ParseCommand.scala | 6 +- .../git/GitCheckTranslatorTest.scala | 27 ------- ...com.reactific.riddl.commands.CommandPlugin | 0 .../onchange/DotWritingProgressMonitor.scala | 40 +++++++++++ .../riddl/translator/onchange/OnChange.scala | 51 ++++--------- .../translator/onchange/OnChangeCommand.scala | 23 +++--- .../DotWritingProgressMonitorTest.scala | 72 +++++++++++++++++++ .../onchange/OnChangeTranslatorTest.scala | 63 ++++++++++++++++ project/Helpers.scala | 10 ++- .../riddl/RunHugoOnExamplesTest.scala | 4 +- .../testkit/RunCommandOnExamplesTest.scala | 69 ++++++++++++++---- 12 files changed, 279 insertions(+), 98 deletions(-) delete mode 100644 git-check/src/test/scala/com/reactific/riddl/translator/git/GitCheckTranslatorTest.scala rename {git-check => onchange}/src/main/resources/META-INFo/services/com.reactific.riddl.commands.CommandPlugin (100%) create mode 100644 onchange/src/main/scala/com/reactific/riddl/translator/onchange/DotWritingProgressMonitor.scala rename git-check/src/main/scala/com/reactific/riddl/translator/hugo_git_check/GitCheck.scala => onchange/src/main/scala/com/reactific/riddl/translator/onchange/OnChange.scala (77%) rename git-check/src/main/scala/com/reactific/riddl/translator/hugo_git_check/GitCheckCommand.scala => onchange/src/main/scala/com/reactific/riddl/translator/onchange/OnChangeCommand.scala (86%) create mode 100644 onchange/src/test/scala/com/reactific/riddl/translator/onchange/DotWritingProgressMonitorTest.scala create mode 100644 onchange/src/test/scala/com/reactific/riddl/translator/onchange/OnChangeTranslatorTest.scala 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) } + } From 67ce7c8a9e63e8cb2bc27d0619b80d4293a18d7c Mon Sep 17 00:00:00 2001 From: reidspencer Date: Sat, 12 Nov 2022 22:21:07 -0500 Subject: [PATCH 4/8] Extract FileWatcher and test case Signed-off-by: reidspencer --- .../riddl/translator/onchange/OnChange.scala | 46 +++-------- .../reactific/riddl/utils/FileWatcher.scala | 80 +++++++++++++++++++ .../riddl/utils/FileWatcherTest.scala | 41 ++++++++++ 3 files changed, 132 insertions(+), 35 deletions(-) create mode 100644 utils/src/main/scala/com/reactific/riddl/utils/FileWatcher.scala create mode 100644 utils/src/test/scala/com/reactific/riddl/utils/FileWatcherTest.scala 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 index 0b03ec267..f75f25739 100644 --- a/onchange/src/main/scala/com/reactific/riddl/translator/onchange/OnChange.scala +++ b/onchange/src/main/scala/com/reactific/riddl/translator/onchange/OnChange.scala @@ -20,7 +20,6 @@ import java.nio.file.attribute.FileTime import java.nio.file.Files import java.nio.file.Path import java.time.Instant -import scala.collection.mutable.ArrayBuffer import scala.jdk.CollectionConverters.* object OnChange { @@ -138,40 +137,17 @@ object OnChange { options } - def runHugo(source: Path, log: Logger): Boolean = { - import scala.sys.process._ - val srcDir = source.toFile - require(srcDir.isDirectory, "Source directory is not a directory!") - val lineBuffer: ArrayBuffer[String] = ArrayBuffer[String]() - var hadErrorOutput: Boolean = false - var hadWarningOutput: Boolean = false - - def fout(line: String): Unit = { - lineBuffer.append(line) - if (!hadWarningOutput && line.contains("WARN")) hadWarningOutput = true - } - - def ferr(line: String): Unit = { - lineBuffer.append(line); hadErrorOutput = true - } - - val logger = ProcessLogger(fout, ferr) - val proc = Process("hugo", cwd = Option(srcDir)) - proc.!(logger) match { - case 0 => - if (hadErrorOutput) { - log.error("hugo wrote to stderr:\n " + lineBuffer.mkString("\n ")) - false - } else if (hadWarningOutput) { - log.warn("hugo issued warnings:\n " + lineBuffer.mkString("\n ")) - true - } else { true } - case rc: Int => - log.error( - s"hugo run failed with rc=$rc:\n " + lineBuffer.mkString("\n ") - ) - false - } + def dirHasChangedSince( + dir: Path, + minTime: FileTime + ): Boolean = { + val potentiallyChangedFiles = dir.toFile.listFiles().map(_.toPath) + val maybeModified = for { + fName <- potentiallyChangedFiles + timestamp = Files.getLastModifiedTime(fName) + isModified = timestamp.compareTo(minTime) > 0 + } yield { isModified } + maybeModified.exists(x => x) } } diff --git a/utils/src/main/scala/com/reactific/riddl/utils/FileWatcher.scala b/utils/src/main/scala/com/reactific/riddl/utils/FileWatcher.scala new file mode 100644 index 000000000..80f593534 --- /dev/null +++ b/utils/src/main/scala/com/reactific/riddl/utils/FileWatcher.scala @@ -0,0 +1,80 @@ +/* + * Copyright 2019 Ossum, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.reactific.riddl.utils + +import java.io.IOException +import java.nio.file.* +import java.nio.file.StandardWatchEventKinds.* +import java.time.Instant +import java.time.temporal.ChronoUnit +import scala.jdk.CollectionConverters.CollectionHasAsScala + +object FileWatcher { + + @throws[IOException] + private def registerRecursively( + root: Path, + watchService: WatchService + ): Unit = { + import java.nio.file.FileVisitResult + import java.nio.file.Files + import java.nio.file.SimpleFileVisitor + import java.nio.file.attribute.BasicFileAttributes + val sfv = new SimpleFileVisitor[Path]() { + override def preVisitDirectory( + dir: Path, + attrs: BasicFileAttributes + ): FileVisitResult = { + dir.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY) + FileVisitResult.CONTINUE + } + } + Files.walkFileTree(root, sfv) + } + + def watchForChanges( + path: Path, + periodInSeconds: Int, + intervalInMillis: Int + )(onEvents: Seq[WatchEvent[?]] => Boolean + )(notOnEvents: => Boolean + ): Boolean = { + val deadline = Instant.now().plus(periodInSeconds, ChronoUnit.SECONDS) + .toEpochMilli + val watchService: WatchService = FileSystems.getDefault.newWatchService() + try { + registerRecursively(path, watchService) + var saveKey: WatchKey = null + do { + watchService.take() match { + case key: WatchKey if key != null => + saveKey = key + val events = key.pollEvents().asScala.toSeq + events match { + case x: Seq[WatchEvent[?]] if x.isEmpty => + if (notOnEvents) { + key.reset() + Thread.sleep(intervalInMillis) + } else { + // they want to stop + key.cancel() + } + case events => + if (onEvents(events)) { + // reset the key for the next trip around + key.reset() + } else { + // they want to stop + key.cancel() + } + } + } + } while (saveKey.isValid && Instant.now().toEpochMilli < deadline) + System.currentTimeMillis() < deadline + } finally { watchService.close() } + } +} diff --git a/utils/src/test/scala/com/reactific/riddl/utils/FileWatcherTest.scala b/utils/src/test/scala/com/reactific/riddl/utils/FileWatcherTest.scala new file mode 100644 index 000000000..10cc95d22 --- /dev/null +++ b/utils/src/test/scala/com/reactific/riddl/utils/FileWatcherTest.scala @@ -0,0 +1,41 @@ +package com.reactific.riddl.utils + +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.WatchEvent +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Await +import scala.concurrent.Future +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 + def onEvents(events: Seq[WatchEvent[?]]): Boolean = { + info(s"Event: ${events.mkString(",")}") + false + } + def notOnEvents: Boolean = { + info("No events") + true + } + // watch for changes + val f = Future[Boolean] { + FileWatcher.watchForChanges(dir, 2, 10)(onEvents)(notOnEvents) + } + Thread.sleep(1000) + val changeFile = dir.resolve("change.file") + if (Files.exists(changeFile)) { Files.delete(changeFile) } + changeFile.toFile.createNewFile() + Files.delete(changeFile) + info(s"Future completed: ${f.isCompleted}") + val result = Await.result(f, Duration(1, "seconds")) + result must be(true) + } + } +} From be12831e25b3f1096f2514435d3c40ec2d728d74 Mon Sep 17 00:00:00 2001 From: reidspencer Date: Sun, 13 Nov 2022 08:41:07 -0500 Subject: [PATCH 5/8] Fix FileWatcher test case Signed-off-by: reidspencer --- .../reactific/riddl/utils/FileWatcherTest.scala | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) 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 10cc95d22..0e3a89de2 100644 --- a/utils/src/test/scala/com/reactific/riddl/utils/FileWatcherTest.scala +++ b/utils/src/test/scala/com/reactific/riddl/utils/FileWatcherTest.scala @@ -17,24 +17,27 @@ class FileWatcherTest extends AnyWordSpec with Matchers { val dir = Path.of(".").resolve("onchange").resolve("target") .toAbsolutePath def onEvents(events: Seq[WatchEvent[?]]): Boolean = { - info(s"Event: ${events.mkString(",")}") + events.foreach { ev => info(s"Event: ${ev.kind()}: ${ev.count()}") } false } def notOnEvents: Boolean = { info("No events") true } + // Resolve the file to + val changeFile = dir.resolve("change.file") + // Make sure it doesn't exist + if (Files.exists(changeFile)) { Files.delete(changeFile) } // watch for changes val f = Future[Boolean] { FileWatcher.watchForChanges(dir, 2, 10)(onEvents)(notOnEvents) } - Thread.sleep(1000) - val changeFile = dir.resolve("change.file") - if (Files.exists(changeFile)) { Files.delete(changeFile) } - changeFile.toFile.createNewFile() + Thread.sleep(900) + Files.createFile(changeFile) + require(Files.exists(changeFile), "File should exist") + Thread.sleep(100) Files.delete(changeFile) - info(s"Future completed: ${f.isCompleted}") - val result = Await.result(f, Duration(1, "seconds")) + val result = Await.result(f, Duration(3, "seconds")) result must be(true) } } From 79b43e9dd4e0d924da611a5bdaea5734ce2580f7 Mon Sep 17 00:00:00 2001 From: reidspencer Date: Sun, 13 Nov 2022 09:16:30 -0500 Subject: [PATCH 6/8] Move OnChangeCommand to commands module * Along with DotWritingProgressMonitor, OnCommand * Fix build.sbt to no build "onchange" any more Signed-off-by: reidspencer --- build.sbt | 24 +- ...com.reactific.riddl.commands.CommandPlugin | 1 + .../commands}/DotWritingProgressMonitor.scala | 2 +- .../riddl/commands/OnChangeCommand.scala | 242 ++++++++++++++++++ .../riddl/commands/RepeatCommand.scala | 7 +- .../DotWritingProgressMonitorTest.scala | 6 +- ...com.reactific.riddl.commands.CommandPlugin | 1 - .../riddl/translator/onchange/OnChange.scala | 140 ---------- .../translator/onchange/OnChangeCommand.scala | 108 -------- .../onchange/OnChangeTranslatorTest.scala | 63 ----- .../riddl/utils/FileWatcherTest.scala | 4 +- 11 files changed, 259 insertions(+), 339 deletions(-) rename {onchange/src/main/scala/com/reactific/riddl/translator/onchange => commands/src/main/scala/com/reactific/riddl/commands}/DotWritingProgressMonitor.scala (95%) create mode 100644 commands/src/main/scala/com/reactific/riddl/commands/OnChangeCommand.scala rename {onchange/src/test/scala/com/reactific/riddl/translator/onchange => commands/src/test/scala/com/reactific/riddl/commands}/DotWritingProgressMonitorTest.scala (94%) delete mode 100644 onchange/src/main/resources/META-INFo/services/com.reactific.riddl.commands.CommandPlugin delete mode 100644 onchange/src/main/scala/com/reactific/riddl/translator/onchange/OnChangeCommand.scala delete mode 100644 onchange/src/test/scala/com/reactific/riddl/translator/onchange/OnChangeTranslatorTest.scala diff --git a/build.sbt b/build.sbt index cf627c2c5..724b4572f 100644 --- a/build.sbt +++ b/build.sbt @@ -33,7 +33,6 @@ lazy val riddl = (project in file(".")).enablePlugins(ScoverageSbtPlugin) testkit, prettify, hugo, - onchange, doc, riddlc, plugin @@ -91,15 +90,17 @@ lazy val language = project.in(file("language")).configure(C.withCoverage(0)) val Commands = config("commands") -lazy val commands = project.in(file("commands")).configure(C.withCoverage(0)) - .configure(C.mavenPublish).settings( +lazy val commands: Project = project.in(file("commands")) + .configure(C.withCoverage(0)).configure(C.mavenPublish).settings( name := "riddl-commands", - libraryDependencies ++= Seq(Dep.scopt, Dep.pureconfig) ++ Dep.testing + libraryDependencies ++= Seq(Dep.scopt, Dep.pureconfig, Dep.jgit) ++ + Dep.testing ).dependsOn(utils % "compile->compile;test->test", language) val TestKit = config("testkit") -lazy val testkit = project.in(file("testkit")).configure(C.mavenPublish) +lazy val testkit: Project = project.in(file("testkit")) + .configure(C.mavenPublish) .settings(name := "riddl-testkit", libraryDependencies ++= Dep.testKitDeps) .dependsOn(commands % "compile->compile;test->test") @@ -122,24 +123,12 @@ lazy val hugo: Project = project.in(file("hugo")).configure(C.withCoverage(0)) .dependsOn(language % "compile->compile", commands, testkit % "test->compile") .dependsOn(utils) -lazy val OnChange = config("onchange") -lazy val onchange: Project = project.in(file("onchange")) - .configure(C.withCoverage(0)).configure(C.mavenPublish).settings( - name := "riddl-onchange", - Compile / unmanagedResourceDirectories += { - baseDirectory.value / "resources" - }, - Test / parallelExecution := false, - libraryDependencies ++= Seq(Dep.pureconfig, Dep.jgit) ++ Dep.testing - ).dependsOn(commands, testkit % "test->compile") - lazy val scaladocSiteProjects = List( (utils, Utils), (language, Language), (commands, Commands), (testkit, TestKit), (prettify, Prettify), - (onchange, OnChange), (hugo, HugoTrans), (riddlc, Riddlc) ) @@ -186,7 +175,6 @@ lazy val riddlc: Project = project.in(file("riddlc")) commands, language, hugo, - onchange, testkit % "test->compile" ).settings( name := "riddlc", diff --git a/commands/src/main/resources/META-INF/services/com.reactific.riddl.commands.CommandPlugin b/commands/src/main/resources/META-INF/services/com.reactific.riddl.commands.CommandPlugin index 64b3c3235..ca98aaae2 100644 --- a/commands/src/main/resources/META-INF/services/com.reactific.riddl.commands.CommandPlugin +++ b/commands/src/main/resources/META-INF/services/com.reactific.riddl.commands.CommandPlugin @@ -4,3 +4,4 @@ com.reactific.riddl.commands.ParseCommand com.reactific.riddl.commands.RepeatCommand com.reactific.riddl.commands.StatsCommand com.reactific.riddl.commands.ValidateCommand +com.reactific.riddl.commands.OnChangeCommand diff --git a/onchange/src/main/scala/com/reactific/riddl/translator/onchange/DotWritingProgressMonitor.scala b/commands/src/main/scala/com/reactific/riddl/commands/DotWritingProgressMonitor.scala similarity index 95% rename from onchange/src/main/scala/com/reactific/riddl/translator/onchange/DotWritingProgressMonitor.scala rename to commands/src/main/scala/com/reactific/riddl/commands/DotWritingProgressMonitor.scala index bd6b2c112..57e20e972 100644 --- a/onchange/src/main/scala/com/reactific/riddl/translator/onchange/DotWritingProgressMonitor.scala +++ b/commands/src/main/scala/com/reactific/riddl/commands/DotWritingProgressMonitor.scala @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package com.reactific.riddl.translator.onchange +package com.reactific.riddl.commands import com.reactific.riddl.language.CommonOptions import com.reactific.riddl.utils.Logger diff --git a/commands/src/main/scala/com/reactific/riddl/commands/OnChangeCommand.scala b/commands/src/main/scala/com/reactific/riddl/commands/OnChangeCommand.scala new file mode 100644 index 000000000..1850e6a8e --- /dev/null +++ b/commands/src/main/scala/com/reactific/riddl/commands/OnChangeCommand.scala @@ -0,0 +1,242 @@ +/* + * Copyright 2019 Ossum, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.reactific.riddl.commands + +import com.reactific.riddl.commands.CommandOptions.optional +import com.reactific.riddl.language.CommonOptions +import com.reactific.riddl.language.Messages.Messages +import com.reactific.riddl.language.Messages.errors +import com.reactific.riddl.utils.Logger +import pureconfig.ConfigCursor +import pureconfig.ConfigReader +import scopt.OParser + +import java.io.File +import java.nio.file.Path +import com.reactific.riddl.language.* +import org.eclipse.jgit.api.* +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 java.nio.file.attribute.FileTime +import java.nio.file.Files +import java.time.Instant +import scala.jdk.CollectionConverters.* + +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 = cmdName + def inputFile: Option[Path] = None + } +} + +/** HugoGitCheck Command */ +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("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 + |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)) + ).text( + """This command checks the directory for new commits + |and does a `git pull" command there if it finds some; otherwise + |it does nothing. If commits were pulled from the repository, then + |the configured command is run""".stripMargin + ) + ) -> Options() + } + + implicit val onChangeReader: ConfigReader[Options] = { (cur: ConfigCursor) => + { + for { + objCur <- cur.asObjectCursor + gitCloneDir <- 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 + } yield { + OnChangeCommand.Options( + gitCloneDir = Some(gitCloneDir.toPath), + userName = userNameStr, + accessToken = accessTokenStr + ) + } + } + } + + 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 + * output + * @param options + * The command specific options + * @param commonOptions + * The options common to all commands + * @param log + * A logger for logging errors, warnings, and info + * @return + * Either a set of Messages on error or a Unit on success + */ + override def run( + options: Options, + commonOptions: CommonOptions, + log: Logger, + 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, + commonOptions: CommonOptions, + options: OnChangeCommand.Options + )(doit: ( + AST.RootContainer, + Logger, + CommonOptions, + OnChangeCommand.Options + ) => Either[Messages, Unit] + ): Either[Messages, Unit] = { + require( + options.gitCloneDir.nonEmpty, + s"Option 'gitCloneDir' must have a value." + ) + val gitCloneDir = options.gitCloneDir.get + require(Files.isDirectory(gitCloneDir), s"$gitCloneDir is not a directory.") + val builder = new FileRepositoryBuilder + val repository = + builder.setGitDir(gitCloneDir.resolve(".git").toFile) + .build // scan up the file system tree + val git = new Git(repository) + + val when = getTimeStamp(gitCloneDir) + val opts = prepareOptions(options) + + if (gitHasChanges(log, commonOptions, opts, git, when)) { + pullCommits(log, commonOptions, opts, git) + doit(root, log, commonOptions, opts) + } else { Right(()) } + } + + private final val timeStampFileName: String = ".riddl-timestamp" + def getTimeStamp(dir: Path): FileTime = { + val filePath = dir.resolve(timeStampFileName) + if (Files.notExists(filePath)) { + Files.createFile(filePath) + FileTime.from(Instant.MIN) + } else { + val when = Files.getLastModifiedTime(filePath) + Files.setLastModifiedTime(filePath, FileTime.from(Instant.now())) + when + } + } + + def gitHasChanges( + log: Logger, + commonOptions: CommonOptions, + options: OnChangeCommand.Options, + git: Git, + minTime: FileTime + ): Boolean = { + val repo = git.getRepository + val top = repo.getDirectory.getParentFile.toPath.toAbsolutePath + val subPath = + if (options.relativeDir.nonEmpty) { + val relativeDir = options.relativeDir.get.toAbsolutePath + val relativized = top.relativize(relativeDir) + if (relativized.getNameCount > 1) relativized.toString else "." + } else { "." } + val status = git.status().setProgressMonitor( + DotWritingProgressMonitor(System.out, log, commonOptions) + ).setIgnoreSubmodules(SubmoduleWalk.IgnoreSubmoduleMode.ALL) + .addPath(subPath).call() + + val potentiallyChangedFiles = + (status.getAdded.asScala ++ status.getChanged.asScala ++ + status.getModified.asScala).toSet[String] + + val maybeModified = for { + fName <- potentiallyChangedFiles + timestamp = Files.getLastModifiedTime(Path.of(fName)) + isModified = timestamp.compareTo(minTime) > 0 + } yield { isModified } + maybeModified.exists(x => x) + } + + def pullCommits( + log: Logger, + commonOptions: CommonOptions, + options: OnChangeCommand.Options, + git: Git + ): Boolean = { + try { + if (commonOptions.verbose) { + log.info("Pulling latest changes from remote") + } + val pullCommand = git.pull + pullCommand.setCredentialsProvider(creds(options)) + .setFastForward(MergeCommand.FastForwardMode.FF_ONLY) + .setStrategy(MergeStrategy.THEIRS) + pullCommand.call.isSuccessful + } catch { + case e: GitAPIException => + log.severe("Error when pulling latest changes:", e) + false + } + } + + def prepareOptions( + options: OnChangeCommand.Options + ): OnChangeCommand.Options = { + require(options.inputFile.isEmpty, "inputFile not used by this command") + options + } + + def dirHasChangedSince( + dir: Path, + minTime: FileTime + ): Boolean = { + val potentiallyChangedFiles = dir.toFile.listFiles().map(_.toPath) + val maybeModified = for { + fName <- potentiallyChangedFiles + timestamp = Files.getLastModifiedTime(fName) + isModified = timestamp.compareTo(minTime) > 0 + } yield { isModified } + maybeModified.exists(x => x) + } + +} diff --git a/commands/src/main/scala/com/reactific/riddl/commands/RepeatCommand.scala b/commands/src/main/scala/com/reactific/riddl/commands/RepeatCommand.scala index 8b07b0afc..64f920d2e 100644 --- a/commands/src/main/scala/com/reactific/riddl/commands/RepeatCommand.scala +++ b/commands/src/main/scala/com/reactific/riddl/commands/RepeatCommand.scala @@ -27,6 +27,7 @@ import scala.util.Success object RepeatCommand { + final val cmdName = "repeat" val defaultMaxLoops: Int = 1024 case class Options( @@ -36,11 +37,11 @@ object RepeatCommand { maxCycles: Int = defaultMaxLoops, interactive: Boolean = false) extends CommandOptions { - def command: String = "repeat" + def command: String = cmdName } } -class RepeatCommand extends CommandPlugin[RepeatCommand.Options]("repeat") { +class RepeatCommand extends CommandPlugin[RepeatCommand.Options](RepeatCommand.cmdName) { import RepeatCommand.Options /** Provide an scopt OParser for the commands options type, OPT @@ -50,7 +51,7 @@ class RepeatCommand extends CommandPlugin[RepeatCommand.Options]("repeat") { */ override def getOptions: (OParser[Unit, Options], Options) = { import builder.* - cmd("repeat") + cmd(RepeatCommand.cmdName) .text("""This command supports the edit-build-check cycle. It doesn't end |until has completed or EOF is reached on standard |input. During that time, the selected subcommands are repeated. diff --git a/onchange/src/test/scala/com/reactific/riddl/translator/onchange/DotWritingProgressMonitorTest.scala b/commands/src/test/scala/com/reactific/riddl/commands/DotWritingProgressMonitorTest.scala similarity index 94% rename from onchange/src/test/scala/com/reactific/riddl/translator/onchange/DotWritingProgressMonitorTest.scala rename to commands/src/test/scala/com/reactific/riddl/commands/DotWritingProgressMonitorTest.scala index 14596b578..45228b9d4 100644 --- a/onchange/src/test/scala/com/reactific/riddl/translator/onchange/DotWritingProgressMonitorTest.scala +++ b/commands/src/test/scala/com/reactific/riddl/commands/DotWritingProgressMonitorTest.scala @@ -4,10 +4,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -package com.reactific.riddl.translator.onchange +package com.reactific.riddl.commands + import com.reactific.riddl.language.CommonOptions -import com.reactific.riddl.utils.StringBuildingPrintStream -import com.reactific.riddl.utils.StringLogger +import com.reactific.riddl.utils.{StringBuildingPrintStream, StringLogger} import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec diff --git a/onchange/src/main/resources/META-INFo/services/com.reactific.riddl.commands.CommandPlugin b/onchange/src/main/resources/META-INFo/services/com.reactific.riddl.commands.CommandPlugin deleted file mode 100644 index f589a683f..000000000 --- a/onchange/src/main/resources/META-INFo/services/com.reactific.riddl.commands.CommandPlugin +++ /dev/null @@ -1 +0,0 @@ -com.reactific.riddl.hugo_git_check.GitCheckCommand 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 index f75f25739..6b2194b13 100644 --- a/onchange/src/main/scala/com/reactific/riddl/translator/onchange/OnChange.scala +++ b/onchange/src/main/scala/com/reactific/riddl/translator/onchange/OnChange.scala @@ -6,148 +6,8 @@ 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.merge.MergeStrategy -import org.eclipse.jgit.storage.file.FileRepositoryBuilder -import org.eclipse.jgit.submodule.SubmoduleWalk -import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider - -import java.nio.file.attribute.FileTime -import java.nio.file.Files -import java.nio.file.Path -import java.time.Instant -import scala.jdk.CollectionConverters.* object OnChange { - private def creds(options: OnChangeCommand.Options) = - new UsernamePasswordCredentialsProvider( - options.userName, - options.accessToken - ) - - def runWhenGitChanges( - root: AST.RootContainer, - log: Logger, - commonOptions: CommonOptions, - options: OnChangeCommand.Options - )(doit: ( - AST.RootContainer, - Logger, - CommonOptions, - OnChangeCommand.Options - ) => Either[Messages, Unit] - ): Either[Messages, Unit] = { - require( - options.gitCloneDir.nonEmpty, - s"Option 'gitCloneDir' must have a value." - ) - val gitCloneDir = options.gitCloneDir.get - require(Files.isDirectory(gitCloneDir), s"$gitCloneDir is not a directory.") - val builder = new FileRepositoryBuilder - val repository = - builder.setGitDir(gitCloneDir.resolve(".git").toFile) - .build // scan up the file system tree - val git = new Git(repository) - - val when = getTimeStamp(gitCloneDir) - val opts = prepareOptions(options) - - if (gitHasChanges(log, commonOptions, opts, git, when)) { - pullCommits(log, commonOptions, opts, git) - doit(root, log, commonOptions, opts) - } else { Right(()) } - } - - private final val timeStampFileName: String = ".riddl-timestamp" - def getTimeStamp(dir: Path): FileTime = { - val filePath = dir.resolve(timeStampFileName) - if (Files.notExists(filePath)) { - Files.createFile(filePath) - FileTime.from(Instant.MIN) - } else { - val when = Files.getLastModifiedTime(filePath) - Files.setLastModifiedTime(filePath, FileTime.from(Instant.now())) - when - } - } - - def gitHasChanges( - log: Logger, - commonOptions: CommonOptions, - options: OnChangeCommand.Options, - git: Git, - minTime: FileTime - ): Boolean = { - val repo = git.getRepository - val top = repo.getDirectory.getParentFile.toPath.toAbsolutePath - val subPath = - if (options.relativeDir.nonEmpty) { - val relativeDir = options.relativeDir.get.toAbsolutePath - val relativized = top.relativize(relativeDir) - if (relativized.getNameCount > 1) relativized.toString else "." - } else { "." } - val status = git.status().setProgressMonitor( - DotWritingProgressMonitor(System.out, log, commonOptions) - ).setIgnoreSubmodules(SubmoduleWalk.IgnoreSubmoduleMode.ALL) - .addPath(subPath).call() - - val potentiallyChangedFiles = - (status.getAdded.asScala ++ status.getChanged.asScala ++ - status.getModified.asScala).toSet[String] - - val maybeModified = for { - fName <- potentiallyChangedFiles - timestamp = Files.getLastModifiedTime(Path.of(fName)) - isModified = timestamp.compareTo(minTime) > 0 - } yield { isModified } - maybeModified.exists(x => x) - } - - def pullCommits( - log: Logger, - commonOptions: CommonOptions, - options: OnChangeCommand.Options, - git: Git - ): Boolean = { - try { - if (commonOptions.verbose) { - log.info("Pulling latest changes from remote") - } - val pullCommand = git.pull - pullCommand.setCredentialsProvider(creds(options)) - .setFastForward(MergeCommand.FastForwardMode.FF_ONLY) - .setStrategy(MergeStrategy.THEIRS) - pullCommand.call.isSuccessful - } catch { - case e: GitAPIException => - log.severe("Error when pulling latest changes:", e) - false - } - } - - def prepareOptions( - options: OnChangeCommand.Options - ): OnChangeCommand.Options = { - require(options.inputFile.isEmpty, "inputFile not used by this command") - options - } - - def dirHasChangedSince( - dir: Path, - minTime: FileTime - ): Boolean = { - val potentiallyChangedFiles = dir.toFile.listFiles().map(_.toPath) - val maybeModified = for { - fName <- potentiallyChangedFiles - timestamp = Files.getLastModifiedTime(fName) - isModified = timestamp.compareTo(minTime) > 0 - } yield { isModified } - maybeModified.exists(x => x) - } } diff --git a/onchange/src/main/scala/com/reactific/riddl/translator/onchange/OnChangeCommand.scala b/onchange/src/main/scala/com/reactific/riddl/translator/onchange/OnChangeCommand.scala deleted file mode 100644 index 134e0bc9e..000000000 --- a/onchange/src/main/scala/com/reactific/riddl/translator/onchange/OnChangeCommand.scala +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright 2019 Ossum, Inc. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -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 -import com.reactific.riddl.language.CommonOptions -import com.reactific.riddl.language.Messages.Messages -import com.reactific.riddl.language.Messages.errors -import com.reactific.riddl.utils.Logger -import pureconfig.ConfigCursor -import pureconfig.ConfigReader -import scopt.OParser - -import java.io.File -import java.nio.file.Path - -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 = cmdName - def inputFile: Option[Path] = None - } -} - -/** HugoGitCheck Command */ -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("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 - |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)) - ).text( - """This command checks the directory for new commits - |and does a `git pull" command there if it finds some; otherwise - |it does nothing. If commits were pulled from the repository, then - |the configured command is run""".stripMargin - ) - ) -> Options() - } - - implicit val onChangeReader: ConfigReader[Options] = { - (cur: ConfigCursor) => - { - for { - objCur <- cur.asObjectCursor - gitCloneDir <- - 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 - } yield { - OnChangeCommand.Options( - gitCloneDir = Some(gitCloneDir.toPath), - userName = userNameStr, - accessToken = accessTokenStr - ) - } - } - } - - 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 - * output - * @param options - * The command specific options - * @param commonOptions - * The options common to all commands - * @param log - * A logger for logging errors, warnings, and info - * @return - * Either a set of Messages on error or a Unit on success - */ - override def run( - options: Options, - commonOptions: CommonOptions, - log: Logger, - outputDirOverride: Option[Path] - ): Either[Messages, Unit] = { Left(errors("Not Implemented")) } -} 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 deleted file mode 100644 index da45c7826..000000000 --- a/onchange/src/test/scala/com/reactific/riddl/translator/onchange/OnChangeTranslatorTest.scala +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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/utils/src/test/scala/com/reactific/riddl/utils/FileWatcherTest.scala b/utils/src/test/scala/com/reactific/riddl/utils/FileWatcherTest.scala index 0e3a89de2..e070e4d97 100644 --- a/utils/src/test/scala/com/reactific/riddl/utils/FileWatcherTest.scala +++ b/utils/src/test/scala/com/reactific/riddl/utils/FileWatcherTest.scala @@ -32,10 +32,10 @@ class FileWatcherTest extends AnyWordSpec with Matchers { val f = Future[Boolean] { FileWatcher.watchForChanges(dir, 2, 10)(onEvents)(notOnEvents) } - Thread.sleep(900) + Thread.sleep(1000) Files.createFile(changeFile) require(Files.exists(changeFile), "File should exist") - Thread.sleep(100) + Thread.sleep(200) Files.delete(changeFile) val result = Await.result(f, Duration(3, "seconds")) result must be(true) From 9535cf6f48d007040d09d89aff06b46d28916c7d Mon Sep 17 00:00:00 2001 From: reidspencer Date: Sun, 13 Nov 2022 10:35:10 -0500 Subject: [PATCH 7/8] Locate test cases closer to code they test * Add CommandsTest in commands and test the commands it defines * Add OnChangeCommandTest to commands * Test HugoCommand in hugo * Remove onchange module * Fewer assumptions about timing in FileWatcherTest Signed-off-by: reidspencer --- .../riddl/commands/OnChangeCommand.scala | 100 ++++++++++-------- .../riddl/commands/CommandsTest.scala | 78 ++++++-------- .../riddl/commands/OnChangeCommandTest.scala | 14 +++ .../translator/hugo/HugoCommandTest.scala | 41 +++++++ .../riddl/translator/onchange/OnChange.scala | 13 --- .../reactific/riddl/RiddlcCommandsTest.scala | 37 +++++++ .../riddl/utils/FileWatcherTest.scala | 6 +- 7 files changed, 187 insertions(+), 102 deletions(-) rename riddlc/src/test/scala/com/reactific/riddl/RiddlCommandsTest.scala => commands/src/test/scala/com/reactific/riddl/commands/CommandsTest.scala (66%) create mode 100644 commands/src/test/scala/com/reactific/riddl/commands/OnChangeCommandTest.scala create mode 100644 hugo/src/test/scala/com/reactific/riddl/translator/hugo/HugoCommandTest.scala delete mode 100644 onchange/src/main/scala/com/reactific/riddl/translator/onchange/OnChange.scala create mode 100644 riddlc/src/test/scala/com/reactific/riddl/RiddlcCommandsTest.scala 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) From 8a55d31cbeb90617aed9058bf927240e31a612bb Mon Sep 17 00:00:00 2001 From: reidspencer Date: Sun, 13 Nov 2022 11:05:58 -0500 Subject: [PATCH 8/8] Add Badges, Fix SonarCloud Bug Signed-off-by: reidspencer --- README.md | 7 +++++++ .../main/resources/hugo/layouts/shortcodes/collapse.html | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0819df3aa..1d2e05d86 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,13 @@ [![Coverage](https://coveralls.io/repos/github/reactific/riddl/badge.svg?branch=coverage)](https://coveralls.io/github/reactific/riddl?branch=coverage) [![CLA assistant](https://cla-assistant.io/readme/badge/reactific/riddl)](https://cla-assistant.io/reactific/riddl) +[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=reactific_riddl&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=reactific_riddl) +[![Technical Debt](https://sonarcloud.io/api/project_badges/measure?project=reactific_riddl&metric=sqale_index)](https://sonarcloud.io/summary/new_code?id=reactific_riddl) +[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=reactific_riddl&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=reactific_riddl) +[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=reactific_riddl&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=reactific_riddl) +[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=reactific_riddl&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=reactific_riddl) +[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=reactific_riddl&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=reactific_riddl) + # RIDDL ## Full Documentation diff --git a/hugo/src/main/resources/hugo/layouts/shortcodes/collapse.html b/hugo/src/main/resources/hugo/layouts/shortcodes/collapse.html index b51198240..ea484163a 100644 --- a/hugo/src/main/resources/hugo/layouts/shortcodes/collapse.html +++ b/hugo/src/main/resources/hugo/layouts/shortcodes/collapse.html @@ -2,7 +2,7 @@ \ No newline at end of file