From 2ac75fbd27be039e9a7181cae0d46354fcd107a9 Mon Sep 17 00:00:00 2001 From: Jason Pickens Date: Thu, 12 Jan 2023 00:17:53 +1300 Subject: [PATCH 1/4] Reproduce background watch bug --- .../sbt-mdoc/run-in-background/build.sbt | 85 +++++++++++++++++++ .../sbt-mdoc/run-in-background/docs/readme.md | 3 + .../project/build.properties | 1 + .../run-in-background/project/plugins.sbt | 1 + .../src/main/scala/example/Example.scala | 5 ++ .../sbt-test/sbt-mdoc/run-in-background/test | 3 + 6 files changed, 98 insertions(+) create mode 100644 mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/build.sbt create mode 100644 mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/docs/readme.md create mode 100644 mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/project/build.properties create mode 100644 mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/project/plugins.sbt create mode 100644 mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/src/main/scala/example/Example.scala create mode 100644 mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/test diff --git a/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/build.sbt b/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/build.sbt new file mode 100644 index 000000000..79f4877b7 --- /dev/null +++ b/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/build.sbt @@ -0,0 +1,85 @@ +import java.nio.charset.StandardCharsets +import java.util.concurrent.LinkedBlockingQueue +import scala.concurrent.duration._ +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future +import scala.sys.process._ +import MdocPlugin._ + +ThisBuild / scalaVersion := "2.12.17" + +enablePlugins(MdocPlugin) + +InputKey[Unit]("mdocBg") := Def.inputTaskDyn { + validateSettings.value + val parsed = sbt.complete.DefaultParsers.spaceDelimited("").parsed + val args = (mdocExtraArguments.value ++ parsed).mkString(" ") + (Compile / bgRunMain).toTask(s" mdoc.SbtMain $args") +}.evaluated + +TaskKey[Unit]("check") := { + val commands = new LinkedBlockingQueue[String]() + def sendInput(output: java.io.OutputStream): Unit = { + val newLine = "\n".getBytes(StandardCharsets.UTF_8) + try { + while (true) { + val command = commands.take() + output.write(command.getBytes(StandardCharsets.UTF_8)) + output.write(newLine) + output.flush() + } + } catch { + case _: InterruptedException => // Ignore + } finally { + output.close() + } + } + + val output = new StringBuilder() + def processOut(out: String): Unit = { + if (out.endsWith("[info] started sbt server")) { + commands.put("mdocBg --watch") + } else if (out.endsWith("Waiting for file changes (press enter to interrupt)")) { + // Wait for the input eating to start. + Thread.sleep(3000) + commands.put("show version") + } else if (out.endsWith("[info] 0.1.0-SNAPSHOT")) { + commands.put("") + commands.put("exit") + } + println(s"[TEST] $out") + output.append(out) + output.append('\n') + } + + val error = new StringBuilder() + def processError(err: String): Unit = { + println(s"[TEST ERROR] $err") + error.append(err) + output.append('\n') + } + + // TODO: Do we need the -Xmx setting and any other future options? + val command = Seq( + "sbt", + s"-Dplugin.version=${sys.props("plugin.version")}", + "--no-colors", + "--supershell=never" + ) + val logger = ProcessLogger(processOut, processError) + val basicIO = BasicIO(withIn = false, logger) + val io = new ProcessIO(sendInput, basicIO.processOutput, basicIO.processError) + val p = command.run(io) + val deadline = Deadline.now + 30.seconds + Future { + while (p.isAlive()) { + if (deadline.isOverdue()) { + p.destroy() + } + } + } + p.exitValue() + val code = p.exitValue() + + assert(code == 0, s"Expected exit code 0 but got $code") +} diff --git a/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/docs/readme.md b/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/docs/readme.md new file mode 100644 index 000000000..7f627b9da --- /dev/null +++ b/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/docs/readme.md @@ -0,0 +1,3 @@ +```scala mdoc +println(example.Example.greeting) +``` diff --git a/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/project/build.properties b/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/project/build.properties new file mode 100644 index 000000000..f344c1483 --- /dev/null +++ b/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.8.2 diff --git a/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/project/plugins.sbt b/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/project/plugins.sbt new file mode 100644 index 000000000..36ec193de --- /dev/null +++ b/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("org.scalameta" % "sbt-mdoc" % sys.props("plugin.version")) diff --git a/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/src/main/scala/example/Example.scala b/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/src/main/scala/example/Example.scala new file mode 100644 index 000000000..9b8ea3a7f --- /dev/null +++ b/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/src/main/scala/example/Example.scala @@ -0,0 +1,5 @@ +package example + +object Example { + def greeting = "Hello world!" +} diff --git a/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/test b/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/test new file mode 100644 index 000000000..31fd898f8 --- /dev/null +++ b/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/test @@ -0,0 +1,3 @@ +# Not sure why we need to do this. If we don't it fails with java.nio.file.NoSuchFileException. +$ mkdir target/mdoc +> check From e82a649c9ad8b7d01ad6c0f409e98bb06d04ce50 Mon Sep 17 00:00:00 2001 From: Jason Pickens Date: Thu, 12 Jan 2023 08:53:23 +1300 Subject: [PATCH 2/4] Add background setting --- .../scala/mdoc/internal/cli/Settings.scala | 3 +++ docs/installation.md | 8 ++++++++ .../sbt-mdoc/run-in-background/build.sbt | 2 +- .../scala/mdoc/internal/cli/MainOps.scala | 5 ++--- .../mdoc/internal/io/MdocFileListener.scala | 19 ++++++++++++------- 5 files changed, 26 insertions(+), 11 deletions(-) diff --git a/cli/src/main/scala/mdoc/internal/cli/Settings.scala b/cli/src/main/scala/mdoc/internal/cli/Settings.scala index d18faefdc..db51b1c70 100644 --- a/cli/src/main/scala/mdoc/internal/cli/Settings.scala +++ b/cli/src/main/scala/mdoc/internal/cli/Settings.scala @@ -53,6 +53,9 @@ case class Settings( @Description("Start a file watcher and incrementally re-generate the site on file save.") @ExtraName("w") watch: Boolean = false, + @Description("Sets the file watcher to run in the background and not ask for user input in order to stop.") + @ExtraName("b") + background: Boolean = false, @Description( "Instead of generating a new site, report an error if generating the site would produce a diff " + "against an existing site. Useful for asserting in CI that a site is up-to-date." diff --git a/docs/installation.md b/docs/installation.md index f493fde8f..b645d3e01 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -105,6 +105,14 @@ performance. > docs/mdoc --watch ``` +If running mdoc as a background job in sbt then you should enable watch mode +with the `--background` argument so that it is non-interactive and you can +still issue commands to sbt. + +```scala +> docs/bgRunMain mdoc.SbtMain --watch --background +``` + See [`--help`](#help) to learn more how to use the command-line interface. ```scala diff --git a/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/build.sbt b/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/build.sbt index 79f4877b7..df8d6429e 100644 --- a/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/build.sbt +++ b/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/build.sbt @@ -38,7 +38,7 @@ TaskKey[Unit]("check") := { val output = new StringBuilder() def processOut(out: String): Unit = { if (out.endsWith("[info] started sbt server")) { - commands.put("mdocBg --watch") + commands.put("mdocBg --watch --background") } else if (out.endsWith("Waiting for file changes (press enter to interrupt)")) { // Wait for the input eating to start. Thread.sleep(3000) diff --git a/mdoc/src/main/scala/mdoc/internal/cli/MainOps.scala b/mdoc/src/main/scala/mdoc/internal/cli/MainOps.scala index 13abf51e6..1964b0afe 100644 --- a/mdoc/src/main/scala/mdoc/internal/cli/MainOps.scala +++ b/mdoc/src/main/scala/mdoc/internal/cli/MainOps.scala @@ -1,6 +1,5 @@ package mdoc.internal.cli -import com.vladsch.flexmark.parser.Parser import io.methvin.watcher.DirectoryChangeEvent import io.methvin.watcher.hashing.FileHash import io.methvin.watcher.hashing.FileHasher @@ -13,7 +12,6 @@ import mdoc.internal.livereload.UndertowLiveReload import mdoc.internal.markdown.DocumentLinks import mdoc.internal.markdown.LinkHygiene import mdoc.internal.markdown.Markdown -import mdoc.internal.markdown.DeadLinkInfo import mdoc.internal.pos.DiffUtils import metaconfig.Configured @@ -225,7 +223,8 @@ final class MainOps( def runFileWatcher(): Unit = { val executor = Executors.newFixedThreadPool(1) - val watcher = MdocFileListener.create(settings.in, executor, System.in)(handleWatchEvent) + val in = if (settings.background) None else Some(System.in) + val watcher = MdocFileListener.create(settings.in, executor, in)(handleWatchEvent) watcher.watchUntilInterrupted() this.livereload.foreach(_.stop()) } diff --git a/mdoc/src/main/scala/mdoc/internal/io/MdocFileListener.scala b/mdoc/src/main/scala/mdoc/internal/io/MdocFileListener.scala index c8dcd92d7..50aa131c5 100644 --- a/mdoc/src/main/scala/mdoc/internal/io/MdocFileListener.scala +++ b/mdoc/src/main/scala/mdoc/internal/io/MdocFileListener.scala @@ -7,21 +7,19 @@ import io.methvin.watcher.DirectoryWatcher import java.io.InputStream import java.nio.file.Files import java.util.Scanner -import java.util.concurrent.Executor import java.util.concurrent.ExecutorService -import org.slf4j.Logger import org.slf4j.helpers.NOPLogger import scala.meta.io.AbsolutePath import mdoc.internal.pos.PositionSyntax._ final class MdocFileListener( executor: ExecutorService, - in: InputStream, - runAction: DirectoryChangeEvent => Unit + in: Option[InputStream], + runAction: DirectoryChangeEvent => Unit, ) extends DirectoryChangeListener { private var myIsWatching: Boolean = true private var watcher: DirectoryWatcher = _ - private def blockUntilEnterKey(): Unit = { + private def blockUntilEnterKey(in: InputStream): Unit = { try { new Scanner(in).nextLine() println("Shutting down...") @@ -29,9 +27,16 @@ final class MdocFileListener( case _: NoSuchElementException => } } + private def blockUntilInterrupted(): Unit = { + try { + this.wait() + } catch { + case _: InterruptedException => + } + } def watchUntilInterrupted(): Unit = { watcher.watchAsync(executor) - blockUntilEnterKey() + in.fold(blockUntilInterrupted())(blockUntilEnterKey) executor.shutdown() myIsWatching = false watcher.close() @@ -51,7 +56,7 @@ final class MdocFileListener( } object MdocFileListener { - def create(inputs: List[AbsolutePath], executor: ExecutorService, in: InputStream)( + def create(inputs: List[AbsolutePath], executor: ExecutorService, in: Option[InputStream])( runAction: DirectoryChangeEvent => Unit ): MdocFileListener = { val listener = new MdocFileListener(executor, in, runAction) From 50d0f6931ccef7a233a1dd5026fbefe20c8fe9e2 Mon Sep 17 00:00:00 2001 From: Jason Pickens Date: Thu, 12 Jan 2023 12:27:48 +1300 Subject: [PATCH 3/4] Add mdocBgStart and mdocBgStop sbt tasks --- docs/installation.md | 7 +- mdoc-sbt/src/main/scala/mdoc/MdocPlugin.scala | 67 +++++++++++++++- .../src/sbt-test/sbt-mdoc/bg-tasks/build.sbt | 14 ++++ .../sbt-test/sbt-mdoc/bg-tasks/docs/readme.md | 3 + .../sbt-mdoc/bg-tasks/project/SbtTest.scala | 77 +++++++++++++++++++ .../bg-tasks/project/TestCommand.scala | 22 ++++++ .../bg-tasks/project/build.properties | 1 + .../sbt-mdoc/bg-tasks/project/plugins.sbt | 1 + .../src/main/scala/example/Example.scala | 5 ++ mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/test | 3 + .../sbt-mdoc/run-in-background/build.sbt | 72 +---------------- .../run-in-background/project/SbtTest.scala | 77 +++++++++++++++++++ .../project/TestCommand.scala | 22 ++++++ .../scala/mdoc/internal/cli/MainOps.scala | 15 ++-- 14 files changed, 307 insertions(+), 79 deletions(-) create mode 100644 mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/build.sbt create mode 100644 mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/docs/readme.md create mode 100644 mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/project/SbtTest.scala create mode 100644 mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/project/TestCommand.scala create mode 100644 mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/project/build.properties create mode 100644 mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/project/plugins.sbt create mode 100644 mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/src/main/scala/example/Example.scala create mode 100644 mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/test create mode 100644 mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/project/SbtTest.scala create mode 100644 mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/project/TestCommand.scala diff --git a/docs/installation.md b/docs/installation.md index b645d3e01..1363bc3bb 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -105,12 +105,11 @@ performance. > docs/mdoc --watch ``` -If running mdoc as a background job in sbt then you should enable watch mode -with the `--background` argument so that it is non-interactive and you can -still issue commands to sbt. +You can run mdoc in the background so that you can issue other commands to sbt. ```scala -> docs/bgRunMain mdoc.SbtMain --watch --background +> docs/mdocBgStart +> docs/mdocBgStop ``` See [`--help`](#help) to learn more how to use the command-line interface. diff --git a/mdoc-sbt/src/main/scala/mdoc/MdocPlugin.scala b/mdoc-sbt/src/main/scala/mdoc/MdocPlugin.scala index c92eb02c7..0f4514ac6 100644 --- a/mdoc-sbt/src/main/scala/mdoc/MdocPlugin.scala +++ b/mdoc-sbt/src/main/scala/mdoc/MdocPlugin.scala @@ -2,7 +2,8 @@ package mdoc import java.io.File import sbt.Keys._ -import sbt._ +import sbt.{taskKey, _} + import scala.collection.mutable.ListBuffer object MdocPlugin extends AutoPlugin { @@ -47,6 +48,15 @@ object MdocPlugin extends AutoPlugin { "If false, do not add mdoc as a library dependency this project. " + "Default value is true." ) + val mdocBgStart = + inputKey[JobHandle]( + "Run mdoc in the background. " + + "By default it runs with arguments --watch and --background (via `mdocBgStart/mdocExtraArguments`)." + ) + val mdocBgStop = + taskKey[Unit]( + "Stops mdoc that is running in the background." + ) } val mdocInternalVariables = settingKey[List[(String, String)]]( @@ -71,19 +81,23 @@ object MdocPlugin extends AutoPlugin { "if not provided, the classpath will be formed by resolving the worker dependency" ) + // The macro is overzealous and prevents us using this. + private val showKey = Def.showFullKey.show _ + override def projectSettings: Seq[Def.Setting[_]] = List( mdocIn := baseDirectory.in(ThisBuild).value / "docs", mdocOut := target.in(Compile).value / "mdoc", mdocVariables := Map.empty, mdocExtraArguments := Nil, + mdocExtraArguments.in(mdocBgStart) := Vector("--watch", "--background"), mdocJS := None, mdocJSLibraries := Nil, mdocJSWorkerClasspath := None, mdocAutoDependency := true, mdocInternalVariables := Nil, mdoc := Def.inputTaskDyn { - validateSettings.value + val _ = validateSettings.value val parsed = sbt.complete.DefaultParsers.spaceDelimited("").parsed val args = Iterator( mdocExtraArguments.value, @@ -93,6 +107,55 @@ object MdocPlugin extends AutoPlugin { runMain.in(Compile).toTask(s" mdoc.SbtMain $args") } }.evaluated, + // Workaround for https://github.com/sbt/sbt/issues/3572. + mdocBgStart := InputTask + .createDyn[Seq[String], JobHandle] { + InputTask.initParserAsInput(Def.setting { + sbt.complete.DefaultParsers.spaceDelimited("") + }) + } { + Def.task { parsed => + Def.taskDyn { + val _ = validateSettings.value + val args = Iterator( + mdocExtraArguments.in(mdocBgStart).value, + parsed + ).flatten.mkString(" ") + val service = bgJobService.value + val spawningTask = resolvedScoped.value + val s = state.value + service.jobs.find(_.spawningTask == spawningTask) match { + case Some(jobHandle) => + Def.task { + s.log.info(s"mdoc is already running in the background") + jobHandle + } + case None => + // Use this rather than bgRunMain so that the spawningTask is set correctly. + Defaults.bgRunMainTask( + exportedProductJars.in(Compile), + fullClasspathAsJars.in(Compile), + bgCopyClasspath.in(Compile, bgRunMain), + runner.in(run) + ).toTask(s" mdoc.SbtMain $args") + } + } + } + }.evaluated, + mdocBgStop := Def.task { + val service = bgJobService.value + val spawningTask = resolvedScoped.value.copy(key = mdocBgStart.key) + val s = state.value + s.log.debug(s"looking for background job that was spawned by ${showKey(spawningTask)}") + service.jobs.find(_.spawningTask == spawningTask) match { + case Some(jobHandle) => + s.log.info(s"stopping mdoc") + service.stop(jobHandle) + service.waitFor(jobHandle) + case None => + s.log.info(s"mdoc is not running in the background") + } + }.value, dependencyOverrides ++= List( "org.scala-lang" %% "scala3-library" % scalaVersion.value, "org.scala-lang" %% "scala3-compiler" % scalaVersion.value, diff --git a/mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/build.sbt b/mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/build.sbt new file mode 100644 index 000000000..b69cd8b72 --- /dev/null +++ b/mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/build.sbt @@ -0,0 +1,14 @@ +ThisBuild / scalaVersion := "2.12.17" + +enablePlugins(MdocPlugin) + +TaskKey[Unit]("check") := { + SbtTest.test( + TestCommand("mdocBgStart", "Waiting for file changes (press enter to interrupt)"), + TestCommand("show version", "[info] 0.1.0-SNAPSHOT"), + TestCommand("mdocBgStart", "mdoc is already running in the background"), + TestCommand("mdocBgStop", "stopping mdoc"), + TestCommand("mdocBgStop", "mdoc is not running in the background"), + TestCommand("exit") + ) +} diff --git a/mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/docs/readme.md b/mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/docs/readme.md new file mode 100644 index 000000000..7f627b9da --- /dev/null +++ b/mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/docs/readme.md @@ -0,0 +1,3 @@ +```scala mdoc +println(example.Example.greeting) +``` diff --git a/mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/project/SbtTest.scala b/mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/project/SbtTest.scala new file mode 100644 index 000000000..b9ded4d79 --- /dev/null +++ b/mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/project/SbtTest.scala @@ -0,0 +1,77 @@ +import java.nio.charset.StandardCharsets +import java.util.concurrent.LinkedBlockingQueue +import scala.collection.mutable +import scala.concurrent.duration._ +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future +import scala.sys.process._ + +object SbtTest { + + def test(commands: TestCommand*) = { + val commandsToSend = new LinkedBlockingQueue[String]() + def sendInput(output: java.io.OutputStream): Unit = { + val newLine = "\n".getBytes(StandardCharsets.UTF_8) + try { + while (true) { + val command = commandsToSend.take() + output.write(command.getBytes(StandardCharsets.UTF_8)) + output.write(newLine) + output.flush() + } + } catch { + case _: InterruptedException => // Ignore + } finally { + output.close() + } + } + + val commandQueue: mutable.Queue[TestCommand] = mutable.Queue(commands: _*) + var expectedOutput: Option[String] = Some("[info] started sbt server") + def processOut(out: String): Unit = { + if (expectedOutput.forall(out.endsWith)) { + if (commandQueue.nonEmpty) { + val command = commandQueue.dequeue() + Thread.sleep(command.delay.toMillis) + commandsToSend.put(command.command) + expectedOutput = command.expectedOutput + } + } + println(s"[SbtTest] $out") + } + + val error = new StringBuilder() + def processError(err: String): Unit = { + println(s"[SbtTest error] $err") + error.append(err) + } + + // TODO: Do we need the -Xmx setting and any other future options? + val command = Seq( + "sbt", + s"-Dplugin.version=${sys.props("plugin.version")}", + "--no-colors", + "--supershell=never" + ) + val logger = ProcessLogger(processOut, processError) + val basicIO = BasicIO(withIn = false, logger) + val io = new ProcessIO(sendInput, basicIO.processOutput, basicIO.processError) + val p = command.run(io) + + val deadline = 30.seconds.fromNow + Future { + while (p.isAlive()) { + if (deadline.isOverdue()) { + p.destroy() + } + } + } + + val code = p.exitValue() + + expectedOutput.foreach { expected => + throw new AssertionError(s"Expected to find output: $expected") + } + assert(code == 0, s"Expected exit code 0 but got $code") + } +} diff --git a/mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/project/TestCommand.scala b/mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/project/TestCommand.scala new file mode 100644 index 000000000..8d0333680 --- /dev/null +++ b/mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/project/TestCommand.scala @@ -0,0 +1,22 @@ +import scala.concurrent.duration._ + +/** + * @param command the command to send + * @param expectedOutput expected output of the command + * @param delay time to wait before sending the command + */ +final case class TestCommand(command: String, expectedOutput: Option[String], delay: FiniteDuration) + +object TestCommand { + def apply(command: String, expectedOutput: String, delay: FiniteDuration): TestCommand = + TestCommand(command, Some(expectedOutput), delay) + + def apply(command: String, expectedOutput: String): TestCommand = + TestCommand(command, Some(expectedOutput), Duration.Zero) + + def apply(command: String, delay: FiniteDuration): TestCommand = + TestCommand(command, None, delay) + + def apply(command: String): TestCommand = + TestCommand(command, None, Duration.Zero) +} diff --git a/mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/project/build.properties b/mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/project/build.properties new file mode 100644 index 000000000..f344c1483 --- /dev/null +++ b/mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.8.2 diff --git a/mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/project/plugins.sbt b/mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/project/plugins.sbt new file mode 100644 index 000000000..36ec193de --- /dev/null +++ b/mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("org.scalameta" % "sbt-mdoc" % sys.props("plugin.version")) diff --git a/mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/src/main/scala/example/Example.scala b/mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/src/main/scala/example/Example.scala new file mode 100644 index 000000000..9b8ea3a7f --- /dev/null +++ b/mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/src/main/scala/example/Example.scala @@ -0,0 +1,5 @@ +package example + +object Example { + def greeting = "Hello world!" +} diff --git a/mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/test b/mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/test new file mode 100644 index 000000000..31fd898f8 --- /dev/null +++ b/mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/test @@ -0,0 +1,3 @@ +# Not sure why we need to do this. If we don't it fails with java.nio.file.NoSuchFileException. +$ mkdir target/mdoc +> check diff --git a/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/build.sbt b/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/build.sbt index df8d6429e..9645f4976 100644 --- a/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/build.sbt +++ b/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/build.sbt @@ -1,9 +1,4 @@ -import java.nio.charset.StandardCharsets -import java.util.concurrent.LinkedBlockingQueue import scala.concurrent.duration._ -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future -import scala.sys.process._ import MdocPlugin._ ThisBuild / scalaVersion := "2.12.17" @@ -18,68 +13,9 @@ InputKey[Unit]("mdocBg") := Def.inputTaskDyn { }.evaluated TaskKey[Unit]("check") := { - val commands = new LinkedBlockingQueue[String]() - def sendInput(output: java.io.OutputStream): Unit = { - val newLine = "\n".getBytes(StandardCharsets.UTF_8) - try { - while (true) { - val command = commands.take() - output.write(command.getBytes(StandardCharsets.UTF_8)) - output.write(newLine) - output.flush() - } - } catch { - case _: InterruptedException => // Ignore - } finally { - output.close() - } - } - - val output = new StringBuilder() - def processOut(out: String): Unit = { - if (out.endsWith("[info] started sbt server")) { - commands.put("mdocBg --watch --background") - } else if (out.endsWith("Waiting for file changes (press enter to interrupt)")) { - // Wait for the input eating to start. - Thread.sleep(3000) - commands.put("show version") - } else if (out.endsWith("[info] 0.1.0-SNAPSHOT")) { - commands.put("") - commands.put("exit") - } - println(s"[TEST] $out") - output.append(out) - output.append('\n') - } - - val error = new StringBuilder() - def processError(err: String): Unit = { - println(s"[TEST ERROR] $err") - error.append(err) - output.append('\n') - } - - // TODO: Do we need the -Xmx setting and any other future options? - val command = Seq( - "sbt", - s"-Dplugin.version=${sys.props("plugin.version")}", - "--no-colors", - "--supershell=never" + SbtTest.test( + TestCommand("mdocBg --watch --background", "Waiting for file changes (press enter to interrupt)"), + TestCommand("show version", "[info] 0.1.0-SNAPSHOT", 3.seconds), + TestCommand("exit") ) - val logger = ProcessLogger(processOut, processError) - val basicIO = BasicIO(withIn = false, logger) - val io = new ProcessIO(sendInput, basicIO.processOutput, basicIO.processError) - val p = command.run(io) - val deadline = Deadline.now + 30.seconds - Future { - while (p.isAlive()) { - if (deadline.isOverdue()) { - p.destroy() - } - } - } - p.exitValue() - val code = p.exitValue() - - assert(code == 0, s"Expected exit code 0 but got $code") } diff --git a/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/project/SbtTest.scala b/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/project/SbtTest.scala new file mode 100644 index 000000000..b9ded4d79 --- /dev/null +++ b/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/project/SbtTest.scala @@ -0,0 +1,77 @@ +import java.nio.charset.StandardCharsets +import java.util.concurrent.LinkedBlockingQueue +import scala.collection.mutable +import scala.concurrent.duration._ +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future +import scala.sys.process._ + +object SbtTest { + + def test(commands: TestCommand*) = { + val commandsToSend = new LinkedBlockingQueue[String]() + def sendInput(output: java.io.OutputStream): Unit = { + val newLine = "\n".getBytes(StandardCharsets.UTF_8) + try { + while (true) { + val command = commandsToSend.take() + output.write(command.getBytes(StandardCharsets.UTF_8)) + output.write(newLine) + output.flush() + } + } catch { + case _: InterruptedException => // Ignore + } finally { + output.close() + } + } + + val commandQueue: mutable.Queue[TestCommand] = mutable.Queue(commands: _*) + var expectedOutput: Option[String] = Some("[info] started sbt server") + def processOut(out: String): Unit = { + if (expectedOutput.forall(out.endsWith)) { + if (commandQueue.nonEmpty) { + val command = commandQueue.dequeue() + Thread.sleep(command.delay.toMillis) + commandsToSend.put(command.command) + expectedOutput = command.expectedOutput + } + } + println(s"[SbtTest] $out") + } + + val error = new StringBuilder() + def processError(err: String): Unit = { + println(s"[SbtTest error] $err") + error.append(err) + } + + // TODO: Do we need the -Xmx setting and any other future options? + val command = Seq( + "sbt", + s"-Dplugin.version=${sys.props("plugin.version")}", + "--no-colors", + "--supershell=never" + ) + val logger = ProcessLogger(processOut, processError) + val basicIO = BasicIO(withIn = false, logger) + val io = new ProcessIO(sendInput, basicIO.processOutput, basicIO.processError) + val p = command.run(io) + + val deadline = 30.seconds.fromNow + Future { + while (p.isAlive()) { + if (deadline.isOverdue()) { + p.destroy() + } + } + } + + val code = p.exitValue() + + expectedOutput.foreach { expected => + throw new AssertionError(s"Expected to find output: $expected") + } + assert(code == 0, s"Expected exit code 0 but got $code") + } +} diff --git a/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/project/TestCommand.scala b/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/project/TestCommand.scala new file mode 100644 index 000000000..8d0333680 --- /dev/null +++ b/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/project/TestCommand.scala @@ -0,0 +1,22 @@ +import scala.concurrent.duration._ + +/** + * @param command the command to send + * @param expectedOutput expected output of the command + * @param delay time to wait before sending the command + */ +final case class TestCommand(command: String, expectedOutput: Option[String], delay: FiniteDuration) + +object TestCommand { + def apply(command: String, expectedOutput: String, delay: FiniteDuration): TestCommand = + TestCommand(command, Some(expectedOutput), delay) + + def apply(command: String, expectedOutput: String): TestCommand = + TestCommand(command, Some(expectedOutput), Duration.Zero) + + def apply(command: String, delay: FiniteDuration): TestCommand = + TestCommand(command, None, delay) + + def apply(command: String): TestCommand = + TestCommand(command, None, Duration.Zero) +} diff --git a/mdoc/src/main/scala/mdoc/internal/cli/MainOps.scala b/mdoc/src/main/scala/mdoc/internal/cli/MainOps.scala index 1964b0afe..66b3ed675 100644 --- a/mdoc/src/main/scala/mdoc/internal/cli/MainOps.scala +++ b/mdoc/src/main/scala/mdoc/internal/cli/MainOps.scala @@ -282,11 +282,16 @@ object MainOps { ctx.reporter.setDebugEnabled(true) } val runner = new MainOps(ctx) - val exit = runner.run() - if (exit.isSuccess) { - 0 - } else { - 1 // error + try { + val exit = runner.run() + if (exit.isSuccess) { + 0 + } else { + 1 // error + } + } catch { + case _: InterruptedException if ctx.settings.background => + 0 // It is expected that we are interrupted when running in the background. } } } From 7b14686f608cdf504d78d9ee2b92bf67f1b1f7a3 Mon Sep 17 00:00:00 2001 From: Jason Pickens Date: Sun, 15 Jan 2023 20:29:49 +1300 Subject: [PATCH 4/4] Run scalafmt --- .../main/scala/mdoc/internal/cli/Settings.scala | 4 +++- mdoc-sbt/src/main/scala/mdoc/MdocPlugin.scala | 17 ++++++++++------- .../sbt-mdoc/bg-tasks/project/TestCommand.scala | 10 ++++++---- .../sbt-mdoc/run-in-background/build.sbt | 5 ++++- .../run-in-background/project/TestCommand.scala | 10 ++++++---- .../mdoc/internal/io/MdocFileListener.scala | 2 +- 6 files changed, 30 insertions(+), 18 deletions(-) diff --git a/cli/src/main/scala/mdoc/internal/cli/Settings.scala b/cli/src/main/scala/mdoc/internal/cli/Settings.scala index db51b1c70..ac066c27e 100644 --- a/cli/src/main/scala/mdoc/internal/cli/Settings.scala +++ b/cli/src/main/scala/mdoc/internal/cli/Settings.scala @@ -53,7 +53,9 @@ case class Settings( @Description("Start a file watcher and incrementally re-generate the site on file save.") @ExtraName("w") watch: Boolean = false, - @Description("Sets the file watcher to run in the background and not ask for user input in order to stop.") + @Description( + "Sets the file watcher to run in the background and not ask for user input in order to stop." + ) @ExtraName("b") background: Boolean = false, @Description( diff --git a/mdoc-sbt/src/main/scala/mdoc/MdocPlugin.scala b/mdoc-sbt/src/main/scala/mdoc/MdocPlugin.scala index 0f4514ac6..cb94ec656 100644 --- a/mdoc-sbt/src/main/scala/mdoc/MdocPlugin.scala +++ b/mdoc-sbt/src/main/scala/mdoc/MdocPlugin.scala @@ -132,16 +132,19 @@ object MdocPlugin extends AutoPlugin { } case None => // Use this rather than bgRunMain so that the spawningTask is set correctly. - Defaults.bgRunMainTask( - exportedProductJars.in(Compile), - fullClasspathAsJars.in(Compile), - bgCopyClasspath.in(Compile, bgRunMain), - runner.in(run) - ).toTask(s" mdoc.SbtMain $args") + Defaults + .bgRunMainTask( + exportedProductJars.in(Compile), + fullClasspathAsJars.in(Compile), + bgCopyClasspath.in(Compile, bgRunMain), + runner.in(run) + ) + .toTask(s" mdoc.SbtMain $args") } } } - }.evaluated, + } + .evaluated, mdocBgStop := Def.task { val service = bgJobService.value val spawningTask = resolvedScoped.value.copy(key = mdocBgStart.key) diff --git a/mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/project/TestCommand.scala b/mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/project/TestCommand.scala index 8d0333680..abdfa787f 100644 --- a/mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/project/TestCommand.scala +++ b/mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/project/TestCommand.scala @@ -1,9 +1,11 @@ import scala.concurrent.duration._ -/** - * @param command the command to send - * @param expectedOutput expected output of the command - * @param delay time to wait before sending the command +/** @param command + * the command to send + * @param expectedOutput + * expected output of the command + * @param delay + * time to wait before sending the command */ final case class TestCommand(command: String, expectedOutput: Option[String], delay: FiniteDuration) diff --git a/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/build.sbt b/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/build.sbt index 9645f4976..5263029df 100644 --- a/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/build.sbt +++ b/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/build.sbt @@ -14,7 +14,10 @@ InputKey[Unit]("mdocBg") := Def.inputTaskDyn { TaskKey[Unit]("check") := { SbtTest.test( - TestCommand("mdocBg --watch --background", "Waiting for file changes (press enter to interrupt)"), + TestCommand( + "mdocBg --watch --background", + "Waiting for file changes (press enter to interrupt)" + ), TestCommand("show version", "[info] 0.1.0-SNAPSHOT", 3.seconds), TestCommand("exit") ) diff --git a/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/project/TestCommand.scala b/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/project/TestCommand.scala index 8d0333680..abdfa787f 100644 --- a/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/project/TestCommand.scala +++ b/mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/project/TestCommand.scala @@ -1,9 +1,11 @@ import scala.concurrent.duration._ -/** - * @param command the command to send - * @param expectedOutput expected output of the command - * @param delay time to wait before sending the command +/** @param command + * the command to send + * @param expectedOutput + * expected output of the command + * @param delay + * time to wait before sending the command */ final case class TestCommand(command: String, expectedOutput: Option[String], delay: FiniteDuration) diff --git a/mdoc/src/main/scala/mdoc/internal/io/MdocFileListener.scala b/mdoc/src/main/scala/mdoc/internal/io/MdocFileListener.scala index 50aa131c5..c01338c71 100644 --- a/mdoc/src/main/scala/mdoc/internal/io/MdocFileListener.scala +++ b/mdoc/src/main/scala/mdoc/internal/io/MdocFileListener.scala @@ -15,7 +15,7 @@ import mdoc.internal.pos.PositionSyntax._ final class MdocFileListener( executor: ExecutorService, in: Option[InputStream], - runAction: DirectoryChangeEvent => Unit, + runAction: DirectoryChangeEvent => Unit ) extends DirectoryChangeListener { private var myIsWatching: Boolean = true private var watcher: DirectoryWatcher = _