diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 93867950..6ebecf67 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,2 +1,6 @@ # Format the codebase for the first time b049841d5707d5bd87be516d8cda7be2a7585eae + +# Reformatted test code +e437d63c40713e0645a4895bbe5a0fc565cd56db + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3b87dddf..ae478a5e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,21 +11,25 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest, macos-latest] + os: [ubuntu-latest, windows-latest] java-version: [8, 17] + include: + - os: macos-latest + java-version: 17 + - os: macos-latest + java-version: 11 runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: ${{ matrix.java-version }} - - name: Fetch millw launcher (Windows) run: curl -Lo mill.bat "https://raw.githubusercontent.com/lefou/millw/main/millw.bat" if: matrix.os == 'windows-latest' @@ -38,11 +42,11 @@ jobs: check-bin-compat: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: 8 @@ -52,16 +56,16 @@ jobs: check-formatting: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: 8 + java-version: 17 - - run: ./mill -i -k __.checkFormat + - run: ./mill -i mill.scalalib.scalafmt.ScalafmtModule/checkFormatAll __.sources publish-sonatype: if: github.repository == 'com-lihaoyi/os-lib' && contains(github.ref, 'refs/tags/') @@ -76,8 +80,8 @@ jobs: LC_MESSAGES: "en_US.UTF-8" LC_ALL: "en_US.UTF-8" steps: - - uses: actions/checkout@v3 - - uses: actions/setup-java@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: 8 diff --git a/.github/workflows/dependency-graph.yml b/.github/workflows/dependency-graph.yml index 0d9c84e3..35915888 100644 --- a/.github/workflows/dependency-graph.yml +++ b/.github/workflows/dependency-graph.yml @@ -10,13 +10,13 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: coursier/cache-action@v6 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '17' diff --git a/.gitignore b/.gitignore index 5771059b..c0b4b082 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ target/ .project .cache .sbtserver +.scala-build/ +.bsp/ project/.sbtserver tags nohup.out diff --git a/.mill-version b/.mill-version index 51114462..26696041 100644 --- a/.mill-version +++ b/.mill-version @@ -1 +1 @@ -0.10.12 \ No newline at end of file +0.11.7-29-f2e220 \ No newline at end of file diff --git a/.scalafmt.conf b/.scalafmt.conf index c8f8c35c..1b0ea88e 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = "3.5.8" +version = "3.8.1" align.preset = none align.openParenCallSite = false diff --git a/Readme.adoc b/Readme.adoc index 869cac74..d99fe42a 100644 --- a/Readme.adoc +++ b/Readme.adoc @@ -1,8 +1,5 @@ = OS-Lib -:version: 0.9.1 -:toc-placement: preamble -:toclevels: 3 -:toc: +:version: 0.10.0 :link-geny: https://github.com/com-lihaoyi/geny :link-oslib: https://github.com/com-lihaoyi/os-lib :link-oslib-gitter: https://gitter.im/lihaoyi/os-lib @@ -14,6 +11,7 @@ image:{link-oslib}/actions/workflows/build.yml/badge.svg[Build Status,link={link-oslib}/actions] image:https://badges.gitter.im/Join%20Chat.svg[Gitter Chat,link={link-oslib-gitter}] image:https://img.shields.io/badge/patreon-sponsor-ff69b4.svg[Patreon,link=https://www.patreon.com/lihaoyi] +image:https://javadoc.io/badge2/com.lihaoyi/os-lib_3/scaladoc.svg[API Docs (Scala 3),link=https://javadoc.io/doc/com.lihaoyi/os-lib_3] [source,scala] ---- @@ -86,6 +84,8 @@ ivy"com.lihaoyi::os-lib:{version}" "com.lihaoyi" %% "os-lib" % "{version}" ---- +https://javadoc.io/doc/com.lihaoyi/os-lib_3[API Documentation (Scala 3)] + == Cookbook Most operation in OS-Lib take place on <>s, which are @@ -1128,7 +1128,7 @@ os.readLink(wd / "misc" / "broken-abs-symlink") ==> os.root / "doesnt" / "exist" ---- Note that symbolic links can be either absolute ``os.Path``s or relative -``os.RelPath``s, represented by `os.FilePath`. You can also use `os.readLink.all` +``os.RelPath``s, represented by `os.FilePath`. You can also use `os.readLink.absolute` to automatically resolve relative symbolic links to their absolute destination: [source,scala] @@ -1483,7 +1483,7 @@ the subprocess via `os.SubProcess#stdin`, and if used on its stdout it lets the parent process read from the subprocess via `os.SubProcess#stdout` and `os.SubProcess#stderr`. * `os.Inherit`: inherits the stream from the parent process. This lets the -subprocess read directly from the paren process's standard input or write +subprocess read directly from the parent process's standard input or write directly to the parent process's standard output or error * `os.Path`: connects the subprocess's stream to the given filesystem path, reading its standard input from a file or writing its standard @@ -1691,6 +1691,48 @@ val sha = os.proc("shasum", "-a", "256").spawn(stdin = gzip.stdout) sha.stdout.trim ==> "acc142175fa520a1cb2be5b97cbbe9bea092e8bba3fe2e95afa645615908229e -" ---- +== Spawning Pipelines of Subprocesses + +After constructing a subprocess with `os.proc`, you can use the `pipeTo` method +to pipe its output to another subprocess: + +[source,scala] +---- +val wc = os.proc("ls", "-l") + .pipeTo(os.proc("wc", "-l")) + .call() + .out.text() +---- + +This is equivalent to the shell command `ls -l | wc -l`. You can chain together +as many subprocesses as you like. Note that by using this API you can utilize +the broken pipe behaviour of Unix systems. For example, you can take 10 first elements +of output from the `yes` command, and after the `head` command terminates, the `yes` +command will be terminated as well: + +[source,scala] +---- +val yes10 = os.proc("yes") + .pipeTo(os.proc("head", "-n", "10")) + .call() + .out.text() +---- + +This feature is implemented inside the library and will terminate any process reading the +stdin of other process in pipeline on every IO error. This behavior can be disabled via the +`handleBrokenPipe` flag on `call` and `spawn` methods. Note that Windows does not support +broken pipe behaviour, so a command like`yes` would run forever. `handleBrokenPipe` is set +to false by default on Windows. + +Both `call` and `spawn` correspond in their behavior to their counterparts in the `os.proc`, +but `spawn` returns the `os.ProcessPipeline` instance instead. It offers the same +`API` as `SubProcess`, but will operate on the set of processes instead of a single one. + +`Pipefail` is enabled by default, so if any of the processes in the pipeline fails, the whole +pipeline will have a non-zero exit code. This behavior can be disabled via the `pipefail` flag +on `call` and `spawn` methods. Note that the pipefail does not kill the processes in the pipeline, +it just sets the exit code of the pipeline to the exit code of the failed process. + === Watching for Changes ==== `os.watch.watch` @@ -1752,8 +1794,6 @@ paths changed: /Users/lihaoyi/Github/Ammonite/out/i am,/Users/lihaoyi/Github/Amm paths changed: /Users/lihaoyi/Github/Ammonite/out/version/log,/Users/lihaoyi/Github/Ammonite/out/version/meta.json,/Users/lihaoyi/Github/Ammonite/out/version ---- -`watch` currently only supports Linux and Mac-OSX, and not Windows. - == Data Types === `os.Path` @@ -2059,6 +2099,38 @@ Python, ...) do. Even in cases where it's uncertain, e.g. you're taking user input as a String, you have to either handle both possibilities with BasePath or explicitly choose to convert relative paths to absolute using some base. +==== Roots and filesystems + +If you are using a system that supports different roots of paths, e.g. Windows, +you can use the argument of `os.root` to specify which root you want to use. +If not specified, the default root will be used (usually, C on Windows, / on Unix). + +[source,scala] +---- +val root = os.root('C:\') / "Users" / "me" +assert(root == os.Path("C:\Users\me")) +---- + +Additionally, custom filesystems can be specified by passing a `FileSystem` to +`os.root`. This allows you to use OS-Lib with non-standard filesystems, such as +jar filesystems or in-memory filesystems. + +[source,scala] +---- +val uri = new URI("jar", Paths.get("foo.jar").toURI().toString, null); +val env = new HashMap[String, String](); +env.put("create", "true"); +val fs = FileSystems.newFileSystem(uri, env); +val path = os.root("/", fs) / "dir" +---- + +Note that the jar file system operations suchs as writing to a file are supported +only on JVM 11+. Depending on the filesystem, some operations may not be supported - +for example, running an `os.proc` with pwd in a jar file won't work. You may also +meet limitations imposed by the implementations - in jar file system, the files are +created only after the file system is closed. Until that, the ones created in your +program are kept in memory. + ==== `os.ResourcePath` In addition to manipulating paths on the filesystem, you can also manipulate @@ -2186,6 +2258,31 @@ string, int or set representations of the `os.PermSet` via: == Changelog +[#0-10-0] +=== 0.10.0 + +* Support for Scala-Native 0.5.0 +* Dropped support for Scala 2.11.x +* Minimum version of Scala 3 increased to 3.3.1 + + +[#0-9-3] +=== 0.9.3 - 2024-01-01 + +* Fix `os.watch` on Windows (#236) +* Fix propagateEnv = false to not propagate env (#238) +* Make os.home a def (#239) + +=== 0.9.2 - 2023-11-05 + +* Added new convenience API to create pipes between processes with `.pipeTo` +* Fixed issue with leading `..` / `os.up` in path segments created from a `Seq` +* Fixed Windows-specific issues with relative paths with leading (back)slashes +* Removed some internal use of deprecated API +* ScalaDoc now maps some external references to their online sites +* Dependency updates: sourcecode 0.3.1 +* Tooling updates: acyclic 0.3.9, Mill 0.11.5, mill-mima 0.0.24, mill-vcs-version 0.4.0, scalafmt 3.7.15 + === 0.9.1 - 2023-03-07 * Refined return types when constructing paths with `/` and get rid of long `ThisType#ThisType` cascades. diff --git a/build.sc b/build.sc index 7d578847..8dc78dea 100644 --- a/build.sc +++ b/build.sc @@ -1,19 +1,11 @@ // plugins -import $ivy.`de.tototec::de.tobiasroeser.mill.vcs.version::0.3.0` -import $ivy.`com.github.lolgab::mill-mima::0.0.13` +import $ivy.`de.tototec::de.tobiasroeser.mill.vcs.version::0.4.0` +import $ivy.`com.github.lolgab::mill-mima::0.1.0` // imports -import mill._ -import mill.define.{Task, Target} -import mill.scalalib._ -import mill.scalalib.scalafmt._ -import mill.scalanativelib._ -import mill.scalalib.publish._ +import mill._, scalalib._, scalanativelib._, publish._ import mill.scalalib.api.ZincWorkerUtil -// avoid name collisions -import _root_.{os => oslib} -import com.github.lolgab.mill.mima.Mima - +import com.github.lolgab.mill.mima._ import de.tobiasroeser.mill.vcs.version.VcsVersion val communityBuildDottyVersion = sys.props.get("dottyVersion").toList @@ -21,113 +13,62 @@ val communityBuildDottyVersion = sys.props.get("dottyVersion").toList val scala213Version = "2.13.10" val scalaVersions = Seq( - "3.1.3", + "3.3.1", "2.12.17", - scala213Version, - "2.11.12" + scala213Version ) ++ communityBuildDottyVersion -val scalaNativeVersions = scalaVersions.map((_, "0.4.5")) - -val backwardCompatibleVersions: Seq[String] = Seq("0.9.0", "0.9.1") - object Deps { - val acyclic = ivy"com.lihaoyi:::acyclic:0.3.6" - val jna = ivy"net.java.dev.jna:jna:5.13.0" - val geny = ivy"com.lihaoyi::geny::1.0.0" + val acyclic = ivy"com.lihaoyi:::acyclic:0.3.12" + val jna = ivy"net.java.dev.jna:jna:5.14.0" + val geny = ivy"com.lihaoyi::geny::1.1.0" val scalaCollectionCompat = ivy"org.scala-lang.modules::scala-collection-compat::2.9.0" - val sourcecode = ivy"com.lihaoyi::sourcecode::0.3.0" - val utest = ivy"com.lihaoyi::utest::0.8.1" def scalaLibrary(version: String) = ivy"org.scala-lang:scala-library:${version}" -} - -object os extends Module { - - object jvm extends Cross[OsJvmModule](scalaVersions: _*) - class OsJvmModule(val crossScalaVersion: String) extends OsModule with MiMaChecks { - def platformSegment = "jvm" - object test extends Tests with OsLibTestModule { - def platformSegment = "jvm" - } - } - - object native extends Cross[OsNativeModule](scalaNativeVersions: _*) - class OsNativeModule( - val crossScalaVersion: String, - crossScalaNativeVersion: String - ) extends OsModule - with ScalaNativeModule { - def platformSegment = "native" - override def millSourcePath = super.millSourcePath / oslib.up - def scalaNativeVersion = crossScalaNativeVersion - object test extends Tests with OsLibTestModule { - def platformSegment = "native" - override def nativeLinkStubs = true - } - } - - object watch extends Module { - - object jvm extends Cross[WatchJvmModule](scalaVersions: _*) - class WatchJvmModule(val crossScalaVersion: String) extends WatchModule { - def platformSegment = "jvm" - override def moduleDeps = super.moduleDeps :+ os.jvm() - override def ivyDeps = Agg( - Deps.jna - ) - object test extends Tests with OsLibTestModule { - def platformSegment = "jvm" - override def moduleDeps = super.moduleDeps :+ os.jvm().test - } - } - - /* - object native extends Cross[WatchNativeModule](scalaNativeVersions:_*) - class WatchNativeModule(val crossScalaVersion: String, crossScalaNativeVersion: String) extends WatchModule with ScalaNativeModule { - def platformSegment = "native" - def millSourcePath = super.millSourcePath / ammonite.ops.up - def scalaNativeVersion = crossScalaNativeVersion - def moduleDeps = super.moduleDeps :+ os.native() - object test extends Tests with OsLibTestModule { - def platformSegment = "native" - def moduleDeps = super.moduleDeps :+ os.native().test - def nativeLinkStubs = true - } - } - */ - } + val sourcecode = ivy"com.lihaoyi::sourcecode::0.4.1" + val utest = ivy"com.lihaoyi::utest::0.8.3" } trait AcyclicModule extends ScalaModule { def acyclicDep: T[Agg[Dep]] = T { - if (!ZincWorkerUtil.isScala3(scalaVersion())) Agg(Deps.acyclic) - else Agg.empty[Dep] + Agg.from(Option.when(!ZincWorkerUtil.isScala3(scalaVersion()))(Deps.acyclic)) } def acyclicOptions: T[Seq[String]] = T { - if (!ZincWorkerUtil.isScala3(scalaVersion())) Seq("-P:acyclic:force") - else Seq.empty - } - override def compileIvyDeps = acyclicDep - override def scalacPluginIvyDeps = acyclicDep - override def scalacOptions = T { - super.scalacOptions() ++ acyclicOptions() + Option.when(!ZincWorkerUtil.isScala3(scalaVersion()))("-P:acyclic:force").toSeq } + def compileIvyDeps = acyclicDep + def scalacPluginIvyDeps = acyclicDep + def scalacOptions = super.scalacOptions() ++ acyclicOptions() } trait SafeDeps extends ScalaModule { - override def mapDependencies: Task[coursier.Dependency => coursier.Dependency] = T.task { + def mapDependencies: Task[coursier.Dependency => coursier.Dependency] = T.task { val sd = Deps.scalaLibrary(scala213Version) super.mapDependencies().andThen { d => // enforce up-to-date Scala 2.13.x version - if (d.module == sd.dep.module && d.version.startsWith("2.13.")) { - sd.dep - } else d + if (d.module == sd.dep.module && d.version.startsWith("2.13.")) sd.dep + else d } } } -trait OsLibModule extends CrossScalaModule with PublishModule with AcyclicModule with SafeDeps - with ScalafmtModule { +trait MiMaChecks extends Mima { + def mimaPreviousVersions = Seq("0.9.0", "0.9.1", "0.9.2", "0.9.3", "0.10.0") + override def mimaBinaryIssueFilters: T[Seq[ProblemFilter]] = Seq( + ProblemFilter.exclude[ReversedMissingMethodProblem]("os.PathConvertible.isCustomFs") + ) +} + +object testJarWriter extends JavaModule +object testJarReader extends JavaModule +object testJarExit extends JavaModule + +trait OsLibModule + extends CrossScalaModule + with PublishModule + with AcyclicModule + with SafeDeps + with PlatformScalaModule { outer => + def publishVersion = VcsVersion.vcsState().format() def pomSettings = PomSettings( description = artifactName(), @@ -155,25 +96,21 @@ trait OsLibModule extends CrossScalaModule with PublishModule with AcyclicModule } } -trait OsLibTestModule extends ScalaModule with TestModule.Utest with SafeDeps { - override def ivyDeps = Agg( - Deps.utest, - Deps.sourcecode - ) - def platformSegment: String - override def sources = T.sources( - millSourcePath / "src", - millSourcePath / s"src-$platformSegment" - ) + trait OsLibTestModule extends ScalaModule with TestModule.Utest with SafeDeps { + def ivyDeps = Agg(Deps.utest, Deps.sourcecode) - // we check the textual output of system commands and expect it in english - override def forkEnv: Target[Map[String, String]] = T { - super.forkEnv() ++ Map("LC_ALL" -> "C") + // we check the textual output of system commands and expect it in english + def forkEnv = super.forkEnv() ++ Map( + "LC_ALL" -> "C", + "TEST_JAR_WRITER_ASSEMBLY" -> testJarWriter.assembly().path.toString, + "TEST_JAR_READER_ASSEMBLY" -> testJarReader.assembly().path.toString, + "TEST_JAR_EXIT_ASSEMBLY" -> testJarExit.assembly().path.toString, + "TEST_SUBPROCESS_ENV" -> "value" + ) } } trait OsModule extends OsLibModule { - override def artifactName = "os-lib" override def ivyDeps = T { val scalaV = scalaVersion() if (scalaV.startsWith("2.11") || scalaV.startsWith("2.12")) { @@ -181,23 +118,53 @@ trait OsModule extends OsLibModule { Agg(Deps.geny, Deps.scalaCollectionCompat) } else Agg(Deps.geny) } -} -trait WatchModule extends OsLibModule { - override def artifactName = "os-lib-watch" -} + def artifactName = "os-lib" -trait MiMaChecks extends Mima { - override def mimaPreviousVersions = backwardCompatibleVersions - override def mimaPreviousArtifacts: Target[Agg[Dep]] = T { - val versions = mimaPreviousVersions().distinct - val info = artifactMetadata() - if (versions.isEmpty) - T.log.error("No binary compatible versions configured!") - Agg.from( - versions.map(version => - ivy"${info.group}:${info.id}:${version}" + val scalaDocExternalMappings = Seq( + ".*scala.*::scaladoc3::https://scala-lang.org/api/3.x/", + ".*java.*::javadoc::https://docs.oracle.com/javase/8/docs/api/", + s".*geny.*::scaladoc3::https://javadoc.io/doc/com.lihaoyi/geny_3/${Deps.geny.dep.version}/" + ).mkString(",") + + def conditionalScalaDocOptions: T[Seq[String]] = T { + if (ZincWorkerUtil.isDottyOrScala3(scalaVersion())) + Seq( + s"-external-mappings:${scalaDocExternalMappings}" ) - ) + else Seq() + } + + def scalaDocOptions = super.scalaDocOptions() ++ conditionalScalaDocOptions() + +} + +object os extends Module { + + object jvm extends Cross[OsJvmModule](scalaVersions) + trait OsJvmModule extends OsModule with MiMaChecks { + object test extends ScalaTests with OsLibTestModule + object nohometest extends ScalaTests with OsLibTestModule + } + + object native extends Cross[OsNativeModule](scalaVersions) + trait OsNativeModule extends OsModule with ScalaNativeModule { + def scalaNativeVersion = "0.5.0" + object test extends ScalaNativeTests with OsLibTestModule { + def nativeLinkStubs = true + } + object nohometest extends ScalaNativeTests with OsLibTestModule + } + + object watch extends Module { + object jvm extends Cross[WatchJvmModule](scalaVersions) + trait WatchJvmModule extends OsLibModule { + def artifactName = "os-lib-watch" + def moduleDeps = super.moduleDeps ++ Seq(os.jvm()) + def ivyDeps = Agg(Deps.jna) + object test extends ScalaTests with OsLibTestModule { + def moduleDeps = super.moduleDeps ++ Seq(os.jvm().test) + } + } } } diff --git a/mill b/mill index e616548f..cb1ee32f 100755 --- a/mill +++ b/mill @@ -3,13 +3,18 @@ # This is a wrapper script, that automatically download mill from GitHub release pages # You can give the required mill version with MILL_VERSION env variable # If no version is given, it falls back to the value of DEFAULT_MILL_VERSION -DEFAULT_MILL_VERSION=0.10.5 set -e +if [ -z "${DEFAULT_MILL_VERSION}" ] ; then + DEFAULT_MILL_VERSION=0.11.0 +fi + if [ -z "$MILL_VERSION" ] ; then if [ -f ".mill-version" ] ; then MILL_VERSION="$(head -n 1 .mill-version 2> /dev/null)" + elif [ -f ".config/mill-version" ] ; then + MILL_VERSION="$(head -n 1 .config/mill-version 2> /dev/null)" elif [ -f "mill" ] && [ "$0" != "mill" ] ; then MILL_VERSION=$(grep -F "DEFAULT_MILL_VERSION=" "mill" | head -n 1 | cut -d= -f2) else @@ -35,7 +40,7 @@ if [ ! -s "$MILL_EXEC_PATH" ] ; then fi DOWNLOAD_FILE=$MILL_EXEC_PATH-tmp-download MILL_VERSION_TAG=$(echo $MILL_VERSION | sed -E 's/([^-]+)(-M[0-9]+)?(-.*)?/\1\2/') - MILL_DOWNLOAD_URL="https://github.com/lihaoyi/mill/releases/download/${MILL_VERSION_TAG}/$MILL_VERSION${ASSEMBLY}" + MILL_DOWNLOAD_URL="https://repo1.maven.org/maven2/com/lihaoyi/mill-dist/$MILL_VERSION/mill-dist-$MILL_VERSION.jar" curl --fail -L -o "$DOWNLOAD_FILE" "$MILL_DOWNLOAD_URL" chmod +x "$DOWNLOAD_FILE" mv "$DOWNLOAD_FILE" "$MILL_EXEC_PATH" @@ -43,7 +48,18 @@ if [ ! -s "$MILL_EXEC_PATH" ] ; then unset MILL_DOWNLOAD_URL fi +if [ -z "$MILL_MAIN_CLI" ] ; then + MILL_MAIN_CLI="${0}" +fi + +MILL_FIRST_ARG="" +if [ "$1" = "--bsp" ] || [ "$1" = "-i" ] || [ "$1" = "--interactive" ] || [ "$1" = "--no-server" ] || [ "$1" = "--repl" ] || [ "$1" = "--help" ] ; then + # Need to preserve the first position of those listed options + MILL_FIRST_ARG=$1 + shift +fi + unset MILL_DOWNLOAD_PATH unset MILL_VERSION -exec $MILL_EXEC_PATH "$@" +exec $MILL_EXEC_PATH $MILL_FIRST_ARG -D "mill.main.cli=${MILL_MAIN_CLI}" "$@" diff --git a/os/nohometest/src/NoHomeTests.scala b/os/nohometest/src/NoHomeTests.scala new file mode 100644 index 00000000..d91c976c --- /dev/null +++ b/os/nohometest/src/NoHomeTests.scala @@ -0,0 +1,25 @@ +package test.os + +import utest._ + +object NoHomeTests extends TestSuite { + private lazy val isWindows = sys.props("os.name").toLowerCase().contains("windows") + + val tests = Tests { + test("pwd when home is not available") { + System.setProperty("user.home", "?") + val homeException = intercept[IllegalArgumentException] { os.home } + .getMessage() + + val expectedException = + if (isWindows) + "Illegal char at index 0: ?" + else + "requirement failed: ? is not an absolute path" + + assert(homeException == expectedException) + os.pwd + () + } + } +} diff --git a/os/src-jvm/ProcessOps.scala b/os/src-jvm/ProcessOps.scala index a221ce80..1ba98468 100644 --- a/os/src-jvm/ProcessOps.scala +++ b/os/src-jvm/ProcessOps.scala @@ -1,8 +1,14 @@ package os import java.util.concurrent.{ArrayBlockingQueue, Semaphore, TimeUnit} - +import collection.JavaConverters._ import scala.annotation.tailrec +import java.lang.ProcessBuilder.Redirect +import os.SubProcess.InputStream +import java.io.IOException +import java.util.concurrent.LinkedBlockingQueue +import ProcessOps._ +import scala.util.Try /** * Convenience APIs around [[java.lang.Process]] and [[java.lang.ProcessBuilder]]: @@ -21,13 +27,14 @@ import scala.annotation.tailrec * the standard stdin/stdout/stderr streams, using whatever protocol you * want */ + case class proc(command: Shellable*) { def commandChunks: Seq[String] = command.flatMap(_.value) /** * Invokes the given subprocess like a function, passing in input and returning a * [[CommandResult]]. You can then call `result.exitCode` to see how it exited, or - * `result.out.bytes` or `result.err.string` to access the aggregated stdout and + * `result.out.bytes` or `result.err.text()` to access the aggregated stdout and * stderr of the subprocess in a number of convenient ways. If a non-zero exit code * is returned, this throws a [[os.SubprocessException]] containing the * [[CommandResult]], unless you pass in `check = false`. @@ -79,7 +86,6 @@ case class proc(command: Shellable*) { mergeErrIntoOut, propagateEnv ) - import collection.JavaConverters._ sub.join(timeout) @@ -94,9 +100,6 @@ case class proc(command: Shellable*) { * and starts a subprocess, and returns it as a `java.lang.Process` for you to * interact with however you like. * - * To implement pipes, you can spawn a process, take it's stdout, and pass it - * as the stdin of a second spawned process. - * * Note that if you provide `ProcessOutput` callbacks to `stdout`/`stderr`, * the calls to those callbacks take place on newly spawned threads that * execute in parallel with the main thread. Thus make sure any data @@ -111,28 +114,13 @@ case class proc(command: Shellable*) { mergeErrIntoOut: Boolean = false, propagateEnv: Boolean = true ): SubProcess = { - val builder = new java.lang.ProcessBuilder() - - val baseEnv = - if (propagateEnv) sys.env - else Map() - for ((k, v) <- baseEnv ++ Option(env).getOrElse(Map())) { - if (v != null) builder.environment().put(k, v) - else builder.environment().remove(k) - } - - builder.directory(Option(cwd).getOrElse(os.pwd).toIO) + val builder = + buildProcess(commandChunks, cwd, env, stdin, stdout, stderr, mergeErrIntoOut, propagateEnv) val cmdChunks = commandChunks val commandStr = cmdChunks.mkString(" ") lazy val proc: SubProcess = new SubProcess( - builder - .command(cmdChunks: _*) - .redirectInput(stdin.redirectFrom) - .redirectOutput(stdout.redirectTo) - .redirectError(stderr.redirectTo) - .redirectErrorStream(mergeErrIntoOut) - .start(), + builder.start(), stdin.processInput(proc.stdin).map(new Thread(_, commandStr + " stdin thread")), stdout.processOutput(proc.stdout).map(new Thread(_, commandStr + " stdout thread")), stderr.processOutput(proc.stderr).map(new Thread(_, commandStr + " stderr thread")) @@ -143,4 +131,232 @@ case class proc(command: Shellable*) { proc.errorPumperThread.foreach(_.start()) proc } + + /** + * Pipes the output of this process into the input of the [[next]] process. Returns a + * [[ProcGroup]] containing both processes, which you can then either execute or + * pipe further. + */ + def pipeTo(next: proc): ProcGroup = ProcGroup(Seq(this, next)) +} + +/** + * A group of processes that are piped together, corresponding to e.g. `ls -l | grep .scala`. + * You can create a `ProcGroup` by calling `.pipeTo` on a [[proc]] multiple times. + * Contains methods corresponding to the methods on [[proc]], but defined for pipelines + * of processes. + */ +case class ProcGroup private[os] (commands: Seq[proc]) { + assert(commands.size >= 2) + + private lazy val isWindows = sys.props("os.name").toLowerCase().contains("windows") + + /** + * Invokes the given pipeline like a function, passing in input and returning a + * [[CommandResult]]. You can then call `result.exitCode` to see how it exited, or + * `result.out.bytes` or `result.err.string` to access the aggregated stdout and + * stderr of the subprocess in a number of convenient ways. If a non-zero exit code + * is returned, this throws a [[os.SubprocessException]] containing the + * [[CommandResult]], unless you pass in `check = false`. + * + * For each process in pipeline, the output will be forwarded to the input of the next + * process. Input of the first process is set to provided [[stdin]] The output of the last + * process will be returned as the output of the pipeline. [[stderr]] is set for all processes. + * + * `call` provides a number of parameters that let you configure how the pipeline + * is run: + * + * @param cwd the working directory of the pipeline + * @param env any additional environment variables you wish to set in the pipeline + * @param stdin any data you wish to pass to the pipelines's standard input (to the first process) + * @param stdout How the pipelines's output stream is configured (the last process stdout) + * @param stderr How the process's error stream is configured (set for all processes) + * @param mergeErrIntoOut merges the pipeline's stderr stream into it's stdout. Note that then the + * stderr will be forwarded with stdout to subsequent processes in the pipeline. + * @param timeout how long to wait in milliseconds for the pipeline to complete + * @param check disable this to avoid throwing an exception if the pipeline + * fails with a non-zero exit code + * @param propagateEnv disable this to avoid passing in this parent process's + * environment variables to the pipeline + * @param pipefail if true, the pipeline's exitCode will be the exit code of the first + * failing process. If no process fails, the exit code will be 0. + * @param handleBrokenPipe if true, every [[java.io.IOException]] when redirecting output of a process + * will be caught and handled by killing the writing process. This behaviour + * is consistent with handlers of SIGPIPE signals in most programs + * supporting interruptable piping. Disabled by default on Windows. + */ + def call( + cwd: Path = null, + env: Map[String, String] = null, + stdin: ProcessInput = Pipe, + stdout: ProcessOutput = Pipe, + stderr: ProcessOutput = os.Inherit, + mergeErrIntoOut: Boolean = false, + timeout: Long = -1, + check: Boolean = true, + propagateEnv: Boolean = true, + pipefail: Boolean = true, + handleBrokenPipe: Boolean = !isWindows + ): CommandResult = { + val chunks = new java.util.concurrent.ConcurrentLinkedQueue[Either[geny.Bytes, geny.Bytes]] + + val sub = spawn( + cwd, + env, + stdin, + if (stdout ne os.Pipe) stdout + else os.ProcessOutput.ReadBytes((buf, n) => + chunks.add(Left(new geny.Bytes(java.util.Arrays.copyOf(buf, n)))) + ), + if (stderr ne os.Pipe) stderr + else os.ProcessOutput.ReadBytes((buf, n) => + chunks.add(Right(new geny.Bytes(java.util.Arrays.copyOf(buf, n)))) + ), + mergeErrIntoOut, + propagateEnv, + pipefail + ) + + sub.join(timeout) + + val chunksSeq = chunks.iterator.asScala.toIndexedSeq + val res = + CommandResult(commands.flatMap(_.commandChunks :+ "|").init, sub.exitCode(), chunksSeq) + if (res.exitCode == 0 || !check) res + else throw SubprocessException(res) + } + + /** + * The most flexible of the [[os.ProcGroup]] calls. It sets-up a pipeline of processes, + * and returns a [[ProcessPipeline]] for you to interact with however you like. + * + * Note that if you provide `ProcessOutput` callbacks to `stdout`/`stderr`, + * the calls to those callbacks take place on newly spawned threads that + * execute in parallel with the main thread. Thus make sure any data + * processing you do in those callbacks is thread safe! + * @param cwd the working directory of the pipeline + * @param env any additional environment variables you wish to set in the pipeline + * @param stdin any data you wish to pass to the pipelines's standard input (to the first process) + * @param stdout How the pipelines's output stream is configured (the last process stdout) + * @param stderr How the process's error stream is configured (set for all processes) + * @param mergeErrIntoOut merges the pipeline's stderr stream into it's stdout. Note that then the + * stderr will be forwarded with stdout to subsequent processes in the pipeline. + * @param propagateEnv disable this to avoid passing in this parent process's + * environment variables to the pipeline + * @param pipefail if true, the pipeline's exitCode will be the exit code of the first + * failing process. If no process fails, the exit code will be 0. + * @param handleBrokenPipe if true, every [[java.io.IOException]] when redirecting output of a process + * will be caught and handled by killing the writing process. This behaviour + * is consistent with handlers of SIGPIPE signals in most programs + * supporting interruptable piping. Disabled by default on Windows. + */ + def spawn( + cwd: Path = null, + env: Map[String, String] = null, + stdin: ProcessInput = Pipe, + stdout: ProcessOutput = Pipe, + stderr: ProcessOutput = os.Inherit, + mergeErrIntoOut: Boolean = false, + propagateEnv: Boolean = true, + pipefail: Boolean = true, + handleBrokenPipe: Boolean = !isWindows + ): ProcessPipeline = { + val brokenPipeQueue = new LinkedBlockingQueue[Int]() + val (_, procs) = + commands.zipWithIndex.foldLeft((Option.empty[ProcessInput], Seq.empty[SubProcess])) { + case ((None, _), (proc, _)) => + val spawned = proc.spawn(cwd, env, stdin, Pipe, stderr, mergeErrIntoOut, propagateEnv) + (Some(spawned.stdout), Seq(spawned)) + case ((Some(input), acc), (proc, index)) if index == commands.length - 1 => + val spawned = proc.spawn( + cwd, + env, + wrapWithBrokenPipeHandler(input, index - 1, brokenPipeQueue), + stdout, + stderr, + mergeErrIntoOut, + propagateEnv + ) + (None, acc :+ spawned) + case ((Some(input), acc), (proc, index)) => + val spawned = proc.spawn( + cwd, + env, + wrapWithBrokenPipeHandler(input, index - 1, brokenPipeQueue), + Pipe, + stderr, + mergeErrIntoOut, + propagateEnv + ) + (Some(spawned.stdout), acc :+ spawned) + } + val pipeline = + new ProcessPipeline(procs, pipefail, if (handleBrokenPipe) Some(brokenPipeQueue) else None) + pipeline.brokenPipeHandler.foreach(_.start()) + pipeline + } + + private def wrapWithBrokenPipeHandler( + wrapped: ProcessInput, + index: Int, + queue: LinkedBlockingQueue[Int] + ) = + new ProcessInput { + override def redirectFrom: Redirect = wrapped.redirectFrom + override def processInput(stdin: => InputStream): Option[Runnable] = + wrapped.processInput(stdin).map { runnable => + new Runnable { + def run() = { + try { + runnable.run() + } catch { + case e: IOException => + queue.put(index) + } + } + } + } + } + + /** + * Pipes the output of this pipeline into the input of the [[next]] process. + */ + def pipeTo(next: proc) = ProcGroup(commands :+ next) +} + +private[os] object ProcessOps { + def buildProcess( + command: Seq[String], + cwd: Path = null, + env: Map[String, String] = null, + stdin: ProcessInput = Pipe, + stdout: ProcessOutput = Pipe, + stderr: ProcessOutput = os.Inherit, + mergeErrIntoOut: Boolean = false, + propagateEnv: Boolean = true + ): ProcessBuilder = { + val builder = new java.lang.ProcessBuilder() + + val environment = builder.environment() + + if (!propagateEnv) { + environment.clear() + } + + if (env != null) { + for ((k, v) <- env) { + if (v != null) builder.environment().put(k, v) + else builder.environment().remove(k) + } + } + + builder.directory(Option(cwd).getOrElse(os.pwd).toIO) + + builder + .command(command: _*) + .redirectInput(stdin.redirectFrom) + .redirectOutput(stdout.redirectTo) + .redirectError(stderr.redirectTo) + .redirectErrorStream(mergeErrIntoOut) + } } diff --git a/os/src-jvm/SubProcess.scala b/os/src-jvm/SubProcess.scala index a956add2..72f8d7ec 100644 --- a/os/src-jvm/SubProcess.scala +++ b/os/src-jvm/SubProcess.scala @@ -4,6 +4,59 @@ import java.io._ import java.util.concurrent.TimeUnit import scala.language.implicitConversions +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.LinkedTransferQueue +import java.util.concurrent.LinkedBlockingQueue +import scala.annotation.tailrec + +/** + * Parent type for single processes and process pipelines. + */ +sealed trait ProcessLike extends java.lang.AutoCloseable { + + /** + * The exit code of this [[ProcessLike]]. Conventionally, 0 exit code represents a + * successful termination, and non-zero exit code indicates a failure. + * + * Throws an exception if the subprocess has not terminated + */ + def exitCode(): Int + + /** + * Returns `true` if the [[ProcessLike]] is still running and has not terminated + */ + def isAlive(): Boolean + + /** + * Attempt to destroy the [[ProcessLike]] (gently), via the underlying JVM APIs + */ + def destroy(): Unit + + /** + * Force-destroys the [[ProcessLike]], via the underlying JVM APIs + */ + def destroyForcibly(): Unit + + /** + * Alias for [[destroy]], implemented for [[java.lang.AutoCloseable]] + */ + override def close(): Unit + + /** + * Wait up to `millis` for the [[ProcessLike]] to terminate, by default waits + * indefinitely. Returns `true` if the [[ProcessLike]] has terminated by the time + * this method returns. + */ + def waitFor(timeout: Long = -1): Boolean + + /** + * Wait up to `millis` for the [[ProcessLike]] to terminate and all stdout and stderr + * from the subprocess to be handled. By default waits indefinitely; if a time + * limit is given, explicitly destroys the [[ProcessLike]] if it has not completed by + * the time the timeout has occurred + */ + def join(timeout: Long = -1): Boolean +} /** * Represents a spawn subprocess that has started and may or may not have @@ -14,7 +67,7 @@ class SubProcess( val inputPumperThread: Option[Thread], val outputPumperThread: Option[Thread], val errorPumperThread: Option[Thread] -) extends java.lang.AutoCloseable { +) extends ProcessLike { val stdin: SubProcess.InputStream = new SubProcess.InputStream(wrapped.getOutputStream) val stdout: SubProcess.OutputStream = new SubProcess.OutputStream(wrapped.getInputStream) val stderr: SubProcess.OutputStream = new SubProcess.OutputStream(wrapped.getErrorStream) @@ -169,6 +222,149 @@ object SubProcess { } } +class ProcessPipeline( + val processes: Seq[SubProcess], + pipefail: Boolean, + brokenPipeQueue: Option[LinkedBlockingQueue[Int]] // to emulate pipeline behavior in jvm < 9 +) extends ProcessLike { + pipeline => + + /** + * String representation of the pipeline. + */ + def commandString = processes.map(_.wrapped.toString).mkString(" | ") + + private[os] val brokenPipeHandler: Option[Thread] = brokenPipeQueue.map { queue => + new Thread( + new Runnable { + override def run(): Unit = { + var pipelineRunning = true + while (pipelineRunning) { + val brokenPipeIndex = queue.take() + if (brokenPipeIndex == processes.length) { // Special case signaling finished pipeline + pipelineRunning = false + } else { + processes(brokenPipeIndex).destroyForcibly() + } + } + new Thread( + new Runnable { + override def run(): Unit = { + while (!pipeline.waitFor()) {} // handle spurious wakes + queue.put(processes.length) // Signal finished pipeline + } + }, + commandString + " pipeline termination handler" + ).start() + } + }, + commandString + " broken pipe handler" + ) + } + + /** + * The exit code of this [[ProcessPipeline]]. Conventionally, 0 exit code represents a + * successful termination, and non-zero exit code indicates a failure. Throws an exception + * if the subprocess has not terminated. + * + * If pipefail is set, the exit code is the first non-zero exit code of the pipeline. If no + * process in the pipeline has a non-zero exit code, the exit code is 0. + */ + override def exitCode(): Int = { + if (pipefail) + processes.map(_.exitCode()) + .filter(_ != 0) + .headOption + .getOrElse(0) + else + processes.last.exitCode() + } + + /** + * Returns `true` if the [[ProcessPipeline]] is still running and has not terminated. + * Any process in the pipeline can be alive for the pipeline to be alive. + */ + override def isAlive(): Boolean = { + processes.exists(_.isAlive()) + } + + /** + * Attempt to destroy the [[ProcessPipeline]] (gently), via the underlying JVM APIs. + * All processes in the pipeline are destroyed. + */ + override def destroy(): Unit = { + processes.foreach(_.destroy()) + } + + /** + * Force-destroys the [[ProcessPipeline]], via the underlying JVM APIs. + * All processes in the pipeline are force-destroyed. + */ + override def destroyForcibly(): Unit = { + processes.foreach(_.destroyForcibly()) + } + + /** + * Alias for [[destroy]], implemented for [[java.lang.AutoCloseable]]. + */ + override def close(): Unit = { + processes.foreach(_.close()) + } + + /** + * Wait up to `millis` for the [[ProcessPipeline]] to terminate, by default waits + * indefinitely. Returns `true` if the [[ProcessPipeline]] has terminated by the time + * this method returns. + * + * Waits for each process one by one, while aggregating the total time waited. If + * [[timeout]] has passed before all processes have terminated, returns `false`. + */ + override def waitFor(timeout: Long = -1): Boolean = { + @tailrec + def waitForRec(startedAt: Long, processesLeft: Seq[SubProcess]): Boolean = processesLeft match { + case Nil => true + case head :: tail => + val elapsed = System.currentTimeMillis() - startedAt + val timeoutLeft = timeout - elapsed + if (timeoutLeft < 0) false + else if (head.waitFor(timeoutLeft)) waitForRec(startedAt, tail) + else false + } + + if (timeout == -1) { + processes.forall(_.waitFor()) + } else { + val timeNow = System.currentTimeMillis() + waitForRec(timeNow, processes) + } + } + + /** + * Wait up to `millis` for the [[ProcessPipeline]] to terminate all the processes + * in pipeline. By default waits indefinitely; if a time limit is given, explicitly + * destroys each process if it has not completed by the time the timeout has occurred. + */ + override def join(timeout: Long = -1): Boolean = { + @tailrec + def joinRec(startedAt: Long, processesLeft: Seq[SubProcess], result: Boolean): Boolean = + processesLeft match { + case Nil => result + case head :: tail => + val elapsed = System.currentTimeMillis() - startedAt + val timeoutLeft = Math.max(0, timeout - elapsed) + val exitedCleanly = head.join(timeoutLeft) + joinRec(startedAt, tail, result && exitedCleanly) + } + + if (timeout == -1) { + processes.forall(_.join()) + } else { + val timeNow = System.currentTimeMillis() + joinRec(timeNow, processes, true) + } + } +} + /** * Represents the configuration of a SubProcess's input stream. Can either be * [[os.Inherit]], [[os.Pipe]], [[os.Path]] or a [[os.Source]] diff --git a/os/src-jvm/package.scala b/os/src-jvm/package.scala index 1774cb37..e5053657 100644 --- a/os/src-jvm/package.scala +++ b/os/src-jvm/package.scala @@ -1,4 +1,7 @@ import scala.language.implicitConversions +import java.nio.file.FileSystem +import java.nio.file.FileSystems +import java.nio.file.Paths package object os { type Generator[+T] = geny.Generator[T] @@ -10,14 +13,27 @@ package object os { */ val root: Path = Path(java.nio.file.Paths.get(".").toAbsolutePath.getRoot) + def root(root: String, fileSystem: FileSystem = FileSystems.getDefault()): Path = { + val path = Path(fileSystem.getPath(root)) + assert(path.root == root || path.root == root.replace('/', '\\'), s"$root is not a root path") + path + } + def resource(implicit resRoot: ResourceRoot = Thread.currentThread().getContextClassLoader) = { os.ResourcePath.resource(resRoot) } + // See https://github.com/com-lihaoyi/os-lib/pull/239 + // and https://github.com/lightbend/mima/issues/794 + // why the need the inner object to preserve binary compatibility + private object _home { + lazy val value = Path(System.getProperty("user.home")) + } + /** * The user's home directory */ - val home: Path = Path(System.getProperty("user.home")) + def home: Path = _home.value /** * The current working directory for this process. diff --git a/os/src-native/package.scala b/os/src-native/package.scala index 0edf9f2d..ea021c90 100644 --- a/os/src-native/package.scala +++ b/os/src-native/package.scala @@ -1,3 +1,5 @@ +import java.nio.file.FileSystem +import java.nio.file.FileSystems package object os { type Generator[+T] = geny.Generator[T] val Generator = geny.Generator @@ -8,10 +10,23 @@ package object os { */ val root: Path = Path(java.nio.file.Paths.get(".").toAbsolutePath.getRoot) + def root(root: String, fileSystem: FileSystem = FileSystems.getDefault()): Path = { + val path = Path(fileSystem.getPath(root)) + assert(path.root == root || path.root == root.replace('/', '\\'), s"$root is not a root path") + path + } + + // See https://github.com/com-lihaoyi/os-lib/pull/239 + // and https://github.com/lightbend/mima/issues/794 + // why the need the inner object to preserve binary compatibility + private object _home { + lazy val value = Path(System.getProperty("user.home")) + } + /** * The user's home directory */ - val home: Path = Path(System.getProperty("user.home")) + def home: Path = _home.value /** * The current working directory for this process. diff --git a/os/src/Model.scala b/os/src/Model.scala index 136e0df9..2d9fb45e 100644 --- a/os/src/Model.scala +++ b/os/src/Model.scala @@ -224,6 +224,9 @@ object Shellable { implicit def RelPathShellable(s: RelPath): Shellable = Shellable(Seq(s.toString)) implicit def NumericShellable[T: Numeric](s: T): Shellable = Shellable(Seq(s.toString)) + implicit def OptionShellable[T](s: Option[T])(implicit f: T => Shellable): Shellable = + Shellable(s.toSeq.flatMap(f(_).value)) + implicit def IterableShellable[T](s: Iterable[T])(implicit f: T => Shellable): Shellable = Shellable(s.toSeq.flatMap(f(_).value)) diff --git a/os/src/Path.scala b/os/src/Path.scala index 22b4783c..4afa76eb 100644 --- a/os/src/Path.scala +++ b/os/src/Path.scala @@ -6,6 +6,7 @@ import java.nio.file.Files import collection.JavaConverters._ import scala.language.implicitConversions +import java.nio.file trait PathChunk { def segments: Seq[String] @@ -238,10 +239,12 @@ sealed trait FilePath extends BasePath { def toNIO: java.nio.file.Path def resolveFrom(base: os.Path): os.Path } + object FilePath { def apply[T: PathConvertible](f0: T) = { - val f = implicitly[PathConvertible[T]].apply(f0) - if (f.isAbsolute) Path(f0) + def f = implicitly[PathConvertible[T]].apply(f0) + // if Windows root-relative path, convert it to an absolute path + if (Path.driveRelative(f0) || f.isAbsolute) Path(f0) else { val r = RelPath(f0) if (r.ups == 0) r.asSubPath @@ -298,7 +301,7 @@ object RelPath { def apply[T: PathConvertible](f0: T): RelPath = { val f = implicitly[PathConvertible[T]].apply(f0) - require(!f.isAbsolute, s"$f is not a relative path") + require(!f.isAbsolute && !Path.driveRelative(f0), s"$f is not a relative path") val segments = BasePath.chunkify(f.normalize()) val (ups, rest) = segments.partition(_ == "..") @@ -399,13 +402,17 @@ object Path { def apply[T: PathConvertible](f: T, base: Path): Path = apply(FilePath(f), base) def apply[T: PathConvertible](f0: T): Path = { - val f = implicitly[PathConvertible[T]].apply(f0) + val pathConvertible = implicitly[PathConvertible[T]] + // drive letter prefix is empty unless running in Windows. + val f = if (!pathConvertible.isCustomFs(f0) && driveRelative(f0)) { + Paths.get(s"$driveRoot$f0") + } else { + pathConvertible.apply(f0) + } if (f.iterator.asScala.count(_.startsWith("..")) > f.getNameCount / 2) { throw PathError.AbsolutePathOutsideRoot } - - val normalized = f.normalize() - new Path(normalized) + new Path(f.normalize()) } implicit val pathOrdering: Ordering[Path] = new Ordering[Path] { @@ -437,6 +444,32 @@ object Path { } } + /** + * @return true if Windows OS and path begins with slash or backslash. + * Examples: + * driveRelative("/Users") // true in `Windows`, false elsewhere. + * driveRelative("\\Users") // true in `Windows`, false elsewhere. + * driveRelative("C:/Users") // false always + */ + def driveRelative[T: PathConvertible](f0: T): Boolean = { + if (driveRoot.isEmpty) { + false // non-Windows os + } else { + f0.toString.take(1) match { + case "\\" | "/" => true + case _ => false + } + } + } + + /** + * @return current working drive if Windows, empty string elsewhere. + * Paths.get(driveRoot) == current working directory on all platforms. + */ + lazy val driveRoot: String = Paths.get(".").toAbsolutePath.getRoot.toString match { + case "/" => "" // implies a non-Windows platform + case s => s.take(2) // Windows current working drive (e.g., "C:") + } } trait ReadablePath { @@ -453,7 +486,10 @@ class Path private[os] (val wrapped: java.nio.file.Path) def toSource: SeekableSource = new SeekableSource.ChannelSource(Files.newByteChannel(wrapped)) - require(wrapped.isAbsolute, s"$wrapped is not an absolute path") + require(wrapped.isAbsolute || Path.driveRelative(wrapped), s"$wrapped is not an absolute path") + def root = Option(wrapped.getRoot).map(_.toString).getOrElse("") + def fileSystem = wrapped.getFileSystem() + def segments: Iterator[String] = wrapped.iterator().asScala.map(_.toString) def getSegment(i: Int): String = wrapped.getName(i).toString def segmentCount = wrapped.getNameCount @@ -479,7 +515,11 @@ class Path private[os] (val wrapped: java.nio.file.Path) def endsWith(target: RelPath) = wrapped.endsWith(target.toString) def relativeTo(base: Path): RelPath = { - + if (fileSystem != base.fileSystem) { + throw new IllegalArgumentException( + s"Paths $wrapped and $base are on different filesystems" + ) + } val nioRel = base.wrapped.relativize(wrapped) val segments = nioRel.iterator().asScala.map(_.toString).toArray match { case Array("") => Internals.emptyStringArray @@ -528,6 +568,7 @@ class TempPath private[os] (wrapped: java.nio.file.Path) sealed trait PathConvertible[T] { def apply(t: T): java.nio.file.Path + def isCustomFs(t: T): Boolean = false } object PathConvertible { @@ -539,6 +580,8 @@ object PathConvertible { } implicit object NioPathConvertible extends PathConvertible[java.nio.file.Path] { def apply(t: java.nio.file.Path) = t + override def isCustomFs(t: java.nio.file.Path): Boolean = + t.getFileSystem() != java.nio.file.FileSystems.getDefault() } implicit object UriPathConvertible extends PathConvertible[URI] { def apply(uri: URI) = uri.getScheme() match { diff --git a/os/test/src-jvm/ExampleTests.scala b/os/test/src-jvm/ExampleTests.scala index 291389fa..fbb2335e 100644 --- a/os/test/src-jvm/ExampleTests.scala +++ b/os/test/src-jvm/ExampleTests.scala @@ -3,71 +3,75 @@ package test.os import java.nio.file.attribute.{GroupPrincipal, FileTime} import utest._ -object ExampleTests extends TestSuite{ +object ExampleTests extends TestSuite { val tests = Tests { - test("splash") - TestUtil.prep{wd => if (Unix()){ - // Make sure working directory exists and is empty - val wd = os.pwd/"out"/"splash" - os.remove.all(wd) - os.makeDir.all(wd) + test("splash") - TestUtil.prep { wd => + if (Unix()) { + // Make sure working directory exists and is empty + val wd = os.pwd / "out" / "splash" + os.remove.all(wd) + os.makeDir.all(wd) - os.write(wd/"file.txt", "hello") - os.read(wd/"file.txt") ==> "hello" + os.write(wd / "file.txt", "hello") + os.read(wd / "file.txt") ==> "hello" - os.copy(wd/"file.txt", wd/"copied.txt") - os.list(wd) ==> Seq(wd/"copied.txt", wd/"file.txt") + os.copy(wd / "file.txt", wd / "copied.txt") + os.list(wd) ==> Seq(wd / "copied.txt", wd / "file.txt") - val invoked = os.proc("cat", wd/"file.txt", wd/"copied.txt").call(cwd = wd) - invoked.out.trim() ==> "hellohello" - }} + val invoked = os.proc("cat", wd / "file.txt", wd / "copied.txt").call(cwd = wd) + invoked.out.trim() ==> "hellohello" + } + } - test("concatTxt") - TestUtil.prep{wd => + test("concatTxt") - TestUtil.prep { wd => // Find and concatenate all .txt files directly in the working directory os.write( - wd/"all.txt", + wd / "all.txt", os.list(wd).filter(_.ext == "txt").map(os.read) ) - os.read(wd/"all.txt") ==> + os.read(wd / "all.txt") ==> """I am cowI am cow |Hear me moo |I weigh twice as much as you |And I look good on the barbecue""".stripMargin } - test("subprocessConcat") - TestUtil.prep{wd => - val catCmd = if(scala.util.Properties.isWin) "type" else "cat" + test("subprocessConcat") - TestUtil.prep { wd => + val catCmd = if (scala.util.Properties.isWin) "type" else "cat" // Find and concatenate all .txt files directly in the working directory TestUtil.proc(catCmd, os.list(wd).filter(_.ext == "txt")) - .call(stdout = wd/"all.txt") + .call(stdout = wd / "all.txt") - os.read(wd/"all.txt") ==> + os.read(wd / "all.txt") ==> """I am cowI am cow |Hear me moo |I weigh twice as much as you |And I look good on the barbecue""".stripMargin } - test("curlToTempFile") - TestUtil.prep{wd => if (Unix()){ - // Curl to temporary file - val temp = os.temp() - os.proc("curl", "-L" , ExampleResourcess.RemoteReadme.url) - .call(stdout = temp) + test("curlToTempFile") - TestUtil.prep { wd => + if (Unix()) { + // Curl to temporary file + val temp = os.temp() + os.proc("curl", "-L", ExampleResourcess.RemoteReadme.url) + .call(stdout = temp) - os.size(temp) ==> ExampleResourcess.RemoteReadme.size + os.size(temp) ==> ExampleResourcess.RemoteReadme.size - // Curl to temporary file - val temp2 = os.temp() - val proc = os.proc("curl", "-L" , ExampleResourcess.RemoteReadme.url).spawn() + // Curl to temporary file + val temp2 = os.temp() + val proc = os.proc("curl", "-L", ExampleResourcess.RemoteReadme.url).spawn() - os.write.over(temp2, proc.stdout) - os.size(temp2) ==> ExampleResourcess.RemoteReadme.size + os.write.over(temp2, proc.stdout) + os.size(temp2) ==> ExampleResourcess.RemoteReadme.size - assert(os.size(temp) == os.size(temp2)) - }} + assert(os.size(temp) == os.size(temp2)) + } + } - test("lineCount") - TestUtil.prep{wd => + test("lineCount") - TestUtil.prep { wd => // Line-count of all .txt files recursively in wd val lineCount = os.walk(wd) .filter(_.ext == "txt") @@ -78,7 +82,7 @@ object ExampleTests extends TestSuite{ lineCount ==> 9 } - test("largestThree") - TestUtil.prep{ wd => + test("largestThree") - TestUtil.prep { wd => // Find the largest three files in the given folder tree val largestThree = os.walk(wd) .filter(os.isFile(_, followLinks = false)) @@ -96,29 +100,33 @@ object ExampleTests extends TestSuite{ ) } - test("moveOut") - TestUtil.prep{ wd => + test("moveOut") - TestUtil.prep { wd => // Move all files inside the "misc" folder out of it import os.{GlobSyntax, /} - os.list(wd/"misc").map(os.move.matching{case p/"misc"/x => p/x }) + os.list(wd / "misc").map(os.move.matching { case p / "misc" / x => p / x }) } - test("frequency") - TestUtil.prep{ wd => + test("frequency") - TestUtil.prep { wd => // Calculate the word frequency of all the text files in the folder tree def txt = os.walk(wd).filter(_.ext == "txt").map(os.read) def freq(s: Seq[String]) = s.groupBy(x => x).mapValues(_.length).toSeq val map = freq(txt.flatMap(_.split("[^a-zA-Z0-9_]"))).sortBy(-_._2) map } - test("comparison"){ + test("comparison") { - os.remove.all(os.pwd/"out"/"scratch"/"folder"/"thing"/"file") - os.write(os.pwd/"out"/"scratch"/"folder"/"thing"/"file", "Hello!", createFolders = true) + os.remove.all(os.pwd / "out" / "scratch" / "folder" / "thing" / "file") + os.write( + os.pwd / "out" / "scratch" / "folder" / "thing" / "file", + "Hello!", + createFolders = true + ) def removeAll(path: String) = { def getRecursively(f: java.io.File): Seq[java.io.File] = { f.listFiles.filter(_.isDirectory).flatMap(getRecursively) ++ f.listFiles } - getRecursively(new java.io.File(path)).foreach{f => + getRecursively(new java.io.File(path)).foreach { f => println(f) if (!f.delete()) throw new RuntimeException("Failed to delete " + f.getAbsolutePath) @@ -127,15 +135,19 @@ object ExampleTests extends TestSuite{ } removeAll("out/scratch/folder/thing") - assert(os.list(os.pwd/"out"/"scratch"/"folder").toSeq == Nil) + assert(os.list(os.pwd / "out" / "scratch" / "folder").toSeq == Nil) - os.write(os.pwd/"out"/"scratch"/"folder"/"thing"/"file", "Hello!", createFolders = true) + os.write( + os.pwd / "out" / "scratch" / "folder" / "thing" / "file", + "Hello!", + createFolders = true + ) - os.remove.all(os.pwd/"out"/"scratch"/"folder"/"thing") - assert(os.list(os.pwd/"out"/"scratch"/"folder").toSeq == Nil) + os.remove.all(os.pwd / "out" / "scratch" / "folder" / "thing") + assert(os.list(os.pwd / "out" / "scratch" / "folder").toSeq == Nil) } - test("constructingPaths"){ + test("constructingPaths") { // Get the process' Current Working Directory. As a convention // the directory that "this" code cares about (which may differ @@ -143,105 +155,106 @@ object ExampleTests extends TestSuite{ val wd = os.pwd // A path nested inside `wd` - wd/"folder"/"file" + wd / "folder" / "file" // A path starting from the root - os.root/"folder"/"file" + os.root / "folder" / "file" // A path with spaces or other special characters - wd/"My Folder"/"My File.txt" + wd / "My Folder" / "My File.txt" // Up one level from the wd - wd/os.up + wd / os.up // Up two levels from the wd - wd/os.up/os.up + wd / os.up / os.up } - test("newPath"){ + test("newPath") { - val target = os.pwd/"out"/"scratch" + val target = os.pwd / "out" / "scratch" } - test("relPaths"){ + test("relPaths") { // The path "folder/file" - val rel1 = os.rel/"folder"/"file" - val rel2 = os.rel/"folder"/"file" + val rel1 = os.rel / "folder" / "file" + val rel2 = os.rel / "folder" / "file" // The relative difference between two paths - val target = os.pwd/"out"/"scratch"/"file" - assert((target relativeTo os.pwd) == os.rel/"out"/"scratch"/"file") + val target = os.pwd / "out" / "scratch" / "file" + assert((target relativeTo os.pwd) == os.rel / "out" / "scratch" / "file") // `up`s get resolved automatically val minus = os.pwd relativeTo target - val ups = os.up/os.up/os.up + val ups = os.up / os.up / os.up assert(minus == ups) ( rel1: os.RelPath, rel2: os.RelPath ) } - test("subPaths"){ + test("subPaths") { // The path "folder/file" - val sub1 = os.sub/"folder"/"file" - val sub2 = os.sub/"folder"/"file" + val sub1 = os.sub / "folder" / "file" + val sub2 = os.sub / "folder" / "file" // The relative difference between two paths - val target = os.pwd/"out"/"scratch"/"file" - assert((target subRelativeTo os.pwd) == os.sub/"out"/"scratch"/"file") + val target = os.pwd / "out" / "scratch" / "file" + assert((target subRelativeTo os.pwd) == os.sub / "out" / "scratch" / "file") // Converting os.RelPath to os.SubPath - val rel3 = os.rel/"folder"/"file" + val rel3 = os.rel / "folder" / "file" val sub3 = rel3.asSubPath - // `up`s are not allowed in sub paths intercept[Exception](os.pwd subRelativeTo target) } - test("relSubPathEquality"){ + test("relSubPathEquality") { assert( - (os.sub/"hello"/"world") == (os.rel/"hello"/"world"), + (os.sub / "hello" / "world") == (os.rel / "hello" / "world"), os.sub == os.rel ) } - test("relPathCombine"){ - val target = os.pwd/"out"/"scratch"/"file" + test("relPathCombine") { + val target = os.pwd / "out" / "scratch" / "file" val rel = target relativeTo os.pwd - val newBase = os.root/"code"/"server" - assert(newBase/rel == os.root/"code"/"server"/"out"/"scratch"/"file") + val newBase = os.root / "code" / "server" + assert(newBase / rel == os.root / "code" / "server" / "out" / "scratch" / "file") } - test("subPathCombine"){ - val target = os.pwd/"out"/"scratch"/"file" + test("subPathCombine") { + val target = os.pwd / "out" / "scratch" / "file" val sub = target subRelativeTo os.pwd - val newBase = os.root/"code"/"server" + val newBase = os.root / "code" / "server" assert( - newBase/sub == os.root/"code"/"server"/"out"/"scratch"/"file", - sub / sub == os.sub/"out"/"scratch"/"file"/"out"/"scratch"/"file" + newBase / sub == os.root / "code" / "server" / "out" / "scratch" / "file", + sub / sub == os.sub / "out" / "scratch" / "file" / "out" / "scratch" / "file" ) } - test("pathUp"){ - val target = os.root/"out"/"scratch"/"file" - assert(target/os.up == os.root/"out"/"scratch") + test("pathUp") { + val target = os.root / "out" / "scratch" / "file" + assert(target / os.up == os.root / "out" / "scratch") } - test("relPathUp"){ - val target = os.rel/"out"/"scratch"/"file" - assert(target/os.up == os.rel/"out"/"scratch") + test("relPathUp") { + val target = os.rel / "out" / "scratch" / "file" + assert(target / os.up == os.rel / "out" / "scratch") } - test("relPathUp"){ - val target = os.sub/"out"/"scratch"/"file" - assert(target/os.up == os.sub/"out"/"scratch") + test("relPathUp") { + val target = os.sub / "out" / "scratch" / "file" + assert(target / os.up == os.sub / "out" / "scratch") } - test("canonical"){if (Unix()){ + test("canonical") { + if (Unix()) { - assert((os.root/"folder"/"file"/os.up).toString == "/folder") - // not "/folder/file/.." + assert((os.root / "folder" / "file" / os.up).toString == "/folder") + // not "/folder/file/.." - assert((os.rel/"folder"/"file"/os.up).toString == "folder") - // not "folder/file/.." - }} - test("findWc"){ + assert((os.rel / "folder" / "file" / os.up).toString == "folder") + // not "folder/file/.." + } + } + test("findWc") { - val wd = os.pwd/"os"/"test"/"resources"/"test" + val wd = os.pwd / "os" / "test" / "resources" / "test" // find . -name '*.txt' | xargs wc -l val lines = os.walk(wd) @@ -253,14 +266,14 @@ object ExampleTests extends TestSuite{ assert(lines == 9) } - test("rename"){ + test("rename") { // val d1/"omg"/x1 = wd // val d2/"omg"/x2 = wd // ls! wd |? (_.ext == "scala") | (x => mv! x ! x.pref) } - test("allSubpathsResolveCorrectly"){ + test("allSubpathsResolveCorrectly") { - for(abs <- os.walk(os.pwd)){ + for (abs <- os.walk(os.pwd)) { val rel = abs.relativeTo(os.pwd) assert(rel.ups == 0) assert(os.pwd / rel == abs) diff --git a/os/test/src-jvm/FilesystemMetadataTests.scala b/os/test/src-jvm/FilesystemMetadataTests.scala index 7e982d1d..6846ea78 100644 --- a/os/test/src-jvm/FilesystemMetadataTests.scala +++ b/os/test/src-jvm/FilesystemMetadataTests.scala @@ -8,9 +8,9 @@ object FilesystemMetadataTests extends TestSuite { // on unix it is 81 bytes, win adds 3 bytes (3 \r characters) private val multilineSizes = Set[Long](81, 84) - def tests = Tests{ - test("stat"){ - test - prep{ wd => + def tests = Tests { + test("stat") { + test - prep { wd => os.stat(wd / "File.txt").size ==> 8 assert(multilineSizes contains os.stat(wd / "Multi Line.txt").size) os.stat(wd / "folder1").fileType ==> os.FileType.Dir @@ -23,8 +23,8 @@ object FilesystemMetadataTests extends TestSuite { // } // } } - test("isFile"){ - test - prep{ wd => + test("isFile") { + test - prep { wd => os.isFile(wd / "File.txt") ==> true os.isFile(wd / "folder1") ==> false @@ -33,8 +33,8 @@ object FilesystemMetadataTests extends TestSuite { os.isFile(wd / "misc" / "file-symlink", followLinks = false) ==> false } } - test("isDir"){ - test - prep{ wd => + test("isDir") { + test - prep { wd => os.isDir(wd / "File.txt") ==> false os.isDir(wd / "folder1") ==> true @@ -43,21 +43,21 @@ object FilesystemMetadataTests extends TestSuite { os.isDir(wd / "misc" / "folder-symlink", followLinks = false) ==> false } } - test("isLink"){ - test - prep{ wd => + test("isLink") { + test - prep { wd => os.isLink(wd / "misc" / "file-symlink") ==> true os.isLink(wd / "misc" / "folder-symlink") ==> true os.isLink(wd / "folder1") ==> false } } - test("size"){ - test - prep{ wd => + test("size") { + test - prep { wd => os.size(wd / "File.txt") ==> 8 assert(multilineSizes contains os.size(wd / "Multi Line.txt")) } } - test("mtime"){ - test - prep{ wd => + test("mtime") { + test - prep { wd => os.mtime.set(wd / "File.txt", 0) os.mtime(wd / "File.txt") ==> 0 diff --git a/os/test/src-jvm/FilesystemPermissionsTests.scala b/os/test/src-jvm/FilesystemPermissionsTests.scala index fcf6d695..3b6c8439 100644 --- a/os/test/src-jvm/FilesystemPermissionsTests.scala +++ b/os/test/src-jvm/FilesystemPermissionsTests.scala @@ -4,45 +4,51 @@ import test.os.TestUtil.prep import utest._ object FilesystemPermissionsTests extends TestSuite { - def tests = Tests{ - test("perms"){ - test - prep { wd => if (Unix()){ - os.perms.set(wd / "File.txt", "rwxrwxrwx") - os.perms(wd / "File.txt").toString() ==> "rwxrwxrwx" - os.perms(wd / "File.txt").toInt() ==> Integer.parseInt("777", 8) - - os.perms.set(wd / "File.txt", Integer.parseInt("755", 8)) - os.perms(wd / "File.txt").toString() ==> "rwxr-xr-x" - - os.perms.set(wd / "File.txt", "r-xr-xr-x") - os.perms.set(wd / "File.txt", Integer.parseInt("555", 8)) - }} + def tests = Tests { + test("perms") { + test - prep { wd => + if (Unix()) { + os.perms.set(wd / "File.txt", "rwxrwxrwx") + os.perms(wd / "File.txt").toString() ==> "rwxrwxrwx" + os.perms(wd / "File.txt").toInt() ==> Integer.parseInt("777", 8) + + os.perms.set(wd / "File.txt", Integer.parseInt("755", 8)) + os.perms(wd / "File.txt").toString() ==> "rwxr-xr-x" + + os.perms.set(wd / "File.txt", "r-xr-xr-x") + os.perms.set(wd / "File.txt", Integer.parseInt("555", 8)) + } + } } - test("owner"){ - test - prep { wd => if (Unix()){ - // Only works as root :( - if(false){ - val originalOwner = os.owner(wd / "File.txt") - - os.owner.set(wd / "File.txt", "nobody") - os.owner(wd / "File.txt").getName ==> "nobody" - - os.owner.set(wd / "File.txt", originalOwner) + test("owner") { + test - prep { wd => + if (Unix()) { + // Only works as root :( + if (false) { + val originalOwner = os.owner(wd / "File.txt") + + os.owner.set(wd / "File.txt", "nobody") + os.owner(wd / "File.txt").getName ==> "nobody" + + os.owner.set(wd / "File.txt", originalOwner) + } } - }} + } } - test("group"){ - test - prep { wd => if (Unix()){ - // Only works as root :( - if (false){ - val originalOwner = os.owner(wd / "File.txt") - - os.owner.set(wd / "File.txt", "nobody") - os.owner(wd / "File.txt").getName ==> "nobody" - - os.owner.set(wd / "File.txt", originalOwner) + test("group") { + test - prep { wd => + if (Unix()) { + // Only works as root :( + if (false) { + val originalOwner = os.owner(wd / "File.txt") + + os.owner.set(wd / "File.txt", "nobody") + os.owner(wd / "File.txt").getName ==> "nobody" + + os.owner.set(wd / "File.txt", originalOwner) + } } - }} + } } } } diff --git a/os/test/src-jvm/ListingWalkingTests.scala b/os/test/src-jvm/ListingWalkingTests.scala index 5ef6d466..b9101186 100644 --- a/os/test/src-jvm/ListingWalkingTests.scala +++ b/os/test/src-jvm/ListingWalkingTests.scala @@ -4,9 +4,9 @@ import test.os.TestUtil.prep import utest._ object ListingWalkingTests extends TestSuite { - def tests = Tests{ - test("list"){ - test - prep{ wd => + def tests = Tests { + test("list") { + test - prep { wd => os.list(wd / "folder1") ==> Seq(wd / "folder1" / "one.txt") os.list(wd / "folder2") ==> Seq( wd / "folder2" / "nestedA", @@ -17,19 +17,19 @@ object ListingWalkingTests extends TestSuite { wd / "misc" / "folder-symlink" / "one.txt" ) } - test("stream"){ - test - prep{ wd => + test("stream") { + test - prep { wd => os.list.stream(wd / "folder2").count() ==> 2 // Streaming the listed files to the console - for(line <- os.list.stream(wd / "folder2")){ + for (line <- os.list.stream(wd / "folder2")) { println(line) } } } } - test("walk"){ - test - prep{ wd => + test("walk") { + test - prep { wd => os.walk(wd / "folder1") ==> Seq(wd / "folder1" / "one.txt") os.walk(wd / "folder1", includeTarget = true) ==> Seq( @@ -65,33 +65,35 @@ object ListingWalkingTests extends TestSuite { wd / "misc" / "folder-symlink" / "one.txt" ) } - test("attrs"){ - test - prep{ wd => if(Unix()){ - val filesSortedBySize = os.walk.attrs(wd / "misc", followLinks = true) - .sortBy{case (p, attrs) => attrs.size} - .collect{case (p, attrs) if attrs.isFile => p} + test("attrs") { + test - prep { wd => + if (Unix()) { + val filesSortedBySize = os.walk.attrs(wd / "misc", followLinks = true) + .sortBy { case (p, attrs) => attrs.size } + .collect { case (p, attrs) if attrs.isFile => p } - filesSortedBySize ==> Seq( - wd / "misc" / "echo", - wd / "misc" / "file-symlink", - wd / "misc" / "echo_with_wd", - wd / "misc" / "folder-symlink" / "one.txt", - wd / "misc" / "binary.png" - ) - }} + filesSortedBySize ==> Seq( + wd / "misc" / "echo", + wd / "misc" / "file-symlink", + wd / "misc" / "echo_with_wd", + wd / "misc" / "folder-symlink" / "one.txt", + wd / "misc" / "binary.png" + ) + } + } } - test("stream"){ - test - prep{ wd => + test("stream") { + test - prep { wd => os.walk.stream(wd / "folder1").count() ==> 1 os.walk.stream(wd / "folder2").count() ==> 4 os.walk.stream(wd / "folder2", skip = _.last == "nestedA").count() ==> 2 } - test("attrs"){ - test - prep{ wd => + test("attrs") { + test - prep { wd => def totalFileSizes(p: os.Path) = os.walk.stream.attrs(p) - .collect{case (p, attrs) if attrs.isFile => attrs.size} + .collect { case (p, attrs) if attrs.isFile => attrs.size } .sum totalFileSizes(wd / "folder1") ==> 22 diff --git a/os/test/src-jvm/ManipulatingFilesFoldersTests.scala b/os/test/src-jvm/ManipulatingFilesFoldersTests.scala index a64067e0..ead571eb 100644 --- a/os/test/src-jvm/ManipulatingFilesFoldersTests.scala +++ b/os/test/src-jvm/ManipulatingFilesFoldersTests.scala @@ -4,9 +4,9 @@ import test.os.TestUtil.prep import utest._ object ManipulatingFilesFoldersTests extends TestSuite { - def tests = Tests{ - test("exists"){ - test - prep{ wd => + def tests = Tests { + test("exists") { + test - prep { wd => os.exists(wd / "File.txt") ==> true os.exists(wd / "folder1") ==> true os.exists(wd / "doesnt-exist") ==> false @@ -17,8 +17,8 @@ object ManipulatingFilesFoldersTests extends TestSuite { os.exists(wd / "misc" / "broken-symlink", followLinks = false) ==> true } } - test("move"){ - test - prep{ wd => + test("move") { + test - prep { wd => os.list(wd / "folder1") ==> Seq(wd / "folder1" / "one.txt") os.move(wd / "folder1" / "one.txt", wd / "folder1" / "first.txt") os.list(wd / "folder1") ==> Seq(wd / "folder1" / "first.txt") @@ -35,8 +35,8 @@ object ManipulatingFilesFoldersTests extends TestSuite { |I weigh twice as much as you |And I look good on the barbecue""".stripMargin } - test("matching"){ - test - prep{ wd => + test("matching") { + test - prep { wd => import os.{GlobSyntax, /} os.walk(wd / "folder2").toSet ==> Set( wd / "folder2" / "nestedA", @@ -45,7 +45,7 @@ object ManipulatingFilesFoldersTests extends TestSuite { wd / "folder2" / "nestedB" / "b.txt" ) - os.walk(wd/"folder2").collect(os.move.matching{case p/g"$x.txt" => p/g"$x.data"}) + os.walk(wd / "folder2").collect(os.move.matching { case p / g"$x.txt" => p / g"$x.data" }) os.walk(wd / "folder2").toSet ==> Set( wd / "folder2" / "nestedA", @@ -55,23 +55,23 @@ object ManipulatingFilesFoldersTests extends TestSuite { ) } } - test("into"){ - test - prep{ wd => + test("into") { + test - prep { wd => os.list(wd / "folder1") ==> Seq(wd / "folder1" / "one.txt") os.move.into(wd / "File.txt", wd / "folder1") os.list(wd / "folder1") ==> Seq(wd / "folder1" / "File.txt", wd / "folder1" / "one.txt") } } - test("over"){ - test - prep{ wd => + test("over") { + test - prep { wd => os.list(wd / "folder2") ==> Seq(wd / "folder2" / "nestedA", wd / "folder2" / "nestedB") os.move.over(wd / "folder1", wd / "folder2") os.list(wd / "folder2") ==> Seq(wd / "folder2" / "one.txt") } } } - test("copy"){ - test - prep{ wd => + test("copy") { + test - prep { wd => os.list(wd / "folder1") ==> Seq(wd / "folder1" / "one.txt") os.copy(wd / "folder1" / "one.txt", wd / "folder1" / "first.txt") os.list(wd / "folder1") ==> Seq(wd / "folder1" / "first.txt", wd / "folder1" / "one.txt") @@ -92,15 +92,15 @@ object ManipulatingFilesFoldersTests extends TestSuite { |I weigh twice as much as you |And I look good on the barbecue""".stripMargin } - test("into"){ - test - prep{ wd => + test("into") { + test - prep { wd => os.list(wd / "folder1") ==> Seq(wd / "folder1" / "one.txt") os.copy.into(wd / "File.txt", wd / "folder1") os.list(wd / "folder1") ==> Seq(wd / "folder1" / "File.txt", wd / "folder1" / "one.txt") } } - test("over"){ - test - prep{ wd => + test("over") { + test - prep { wd => os.list(wd / "folder2") ==> Seq(wd / "folder2" / "nestedA", wd / "folder2" / "nestedB") os.copy.over(wd / "folder1", wd / "folder2") os.list(wd / "folder2") ==> Seq(wd / "folder2" / "one.txt") @@ -108,7 +108,7 @@ object ManipulatingFilesFoldersTests extends TestSuite { } test("symlinks") { val src = os.temp.dir(deleteOnExit = true) - + os.makeDir(src / "t0") os.write(src / "t0" / "file", "hello") os.symlink(src / "t1", os.rel / "t0") @@ -149,22 +149,22 @@ object ManipulatingFilesFoldersTests extends TestSuite { } } } - test("makeDir"){ - test - prep{ wd => + test("makeDir") { + test - prep { wd => os.exists(wd / "new_folder") ==> false os.makeDir(wd / "new_folder") os.exists(wd / "new_folder") ==> true } - test("all"){ - test - prep{ wd => + test("all") { + test - prep { wd => os.exists(wd / "new_folder") ==> false os.makeDir.all(wd / "new_folder" / "inner" / "deep") os.exists(wd / "new_folder" / "inner" / "deep") ==> true } } } - test("remove"){ - test - prep{ wd => + test("remove") { + test - prep { wd => os.exists(wd / "File.txt") ==> true os.remove(wd / "File.txt") os.exists(wd / "File.txt") ==> false @@ -175,8 +175,8 @@ object ManipulatingFilesFoldersTests extends TestSuite { os.exists(wd / "folder1" / "one.txt") ==> false os.exists(wd / "folder1") ==> false } - test("link"){ - test - prep{ wd => + test("link") { + test - prep { wd => os.remove(wd / "misc" / "file-symlink") os.exists(wd / "misc" / "file-symlink", followLinks = false) ==> false os.exists(wd / "File.txt", followLinks = false) ==> true @@ -190,15 +190,15 @@ object ManipulatingFilesFoldersTests extends TestSuite { os.exists(wd / "misc" / "broken-symlink", followLinks = false) ==> false } } - test("all"){ - test - prep{ wd => + test("all") { + test - prep { wd => os.exists(wd / "folder1" / "one.txt") ==> true os.remove.all(wd / "folder1") os.exists(wd / "folder1" / "one.txt") ==> false os.exists(wd / "folder1") ==> false } - test("link"){ - test - prep{ wd => + test("link") { + test - prep { wd => os.remove.all(wd / "misc" / "file-symlink") os.exists(wd / "misc" / "file-symlink", followLinks = false) ==> false os.exists(wd / "File.txt", followLinks = false) ==> true @@ -214,16 +214,16 @@ object ManipulatingFilesFoldersTests extends TestSuite { } } } - test("hardlink"){ - test - prep{ wd => + test("hardlink") { + test - prep { wd => os.hardlink(wd / "Linked.txt", wd / "File.txt") os.exists(wd / "Linked.txt") os.read(wd / "Linked.txt") ==> "I am cow" os.isLink(wd / "Linked.txt") ==> false } } - test("symlink"){ - test - prep{ wd => + test("symlink") { + test - prep { wd => os.symlink(wd / "Linked.txt", wd / "File.txt") os.exists(wd / "Linked.txt") os.read(wd / "Linked.txt") ==> "I am cow" @@ -235,35 +235,37 @@ object ManipulatingFilesFoldersTests extends TestSuite { os.isLink(wd / "Linked2.txt") ==> true } } - test("followLink"){ - test - prep{ wd => + test("followLink") { + test - prep { wd => os.followLink(wd / "misc" / "file-symlink") ==> Some(wd / "File.txt") os.followLink(wd / "misc" / "folder-symlink") ==> Some(wd / "folder1") os.followLink(wd / "misc" / "broken-symlink") ==> None } } - test("readLink"){ - test - prep{ wd => if(Unix()){ - os.readLink(wd / "misc" / "file-symlink") ==> os.up / "File.txt" - os.readLink(wd / "misc" / "folder-symlink") ==> os.up / "folder1" - os.readLink(wd / "misc" / "broken-symlink") ==> os.rel / "broken" - os.readLink(wd / "misc" / "broken-abs-symlink") ==> os.root / "doesnt" / "exist" + test("readLink") { + test - prep { wd => + if (Unix()) { + os.readLink(wd / "misc" / "file-symlink") ==> os.up / "File.txt" + os.readLink(wd / "misc" / "folder-symlink") ==> os.up / "folder1" + os.readLink(wd / "misc" / "broken-symlink") ==> os.rel / "broken" + os.readLink(wd / "misc" / "broken-abs-symlink") ==> os.root / "doesnt" / "exist" - os.readLink.absolute(wd / "misc" / "file-symlink") ==> wd / "File.txt" - os.readLink.absolute(wd / "misc" / "folder-symlink") ==> wd / "folder1" - os.readLink.absolute(wd / "misc" / "broken-symlink") ==> wd / "misc" / "broken" - os.readLink.absolute(wd / "misc" / "broken-abs-symlink") ==> os.root / "doesnt" / "exist" - }} + os.readLink.absolute(wd / "misc" / "file-symlink") ==> wd / "File.txt" + os.readLink.absolute(wd / "misc" / "folder-symlink") ==> wd / "folder1" + os.readLink.absolute(wd / "misc" / "broken-symlink") ==> wd / "misc" / "broken" + os.readLink.absolute(wd / "misc" / "broken-abs-symlink") ==> os.root / "doesnt" / "exist" + } + } } - test("temp"){ - test - prep{ wd => + test("temp") { + test - prep { wd => val tempOne = os.temp("default content") os.read(tempOne) ==> "default content" os.write.over(tempOne, "Hello") os.read(tempOne) ==> "Hello" } - test("dir"){ - test - prep{ wd => + test("dir") { + test - prep { wd => val tempDir = os.temp.dir() os.list(tempDir) ==> Nil os.write(tempDir / "file", "Hello") diff --git a/os/test/src-jvm/OpTestsJvmOnly.scala b/os/test/src-jvm/OpTestsJvmOnly.scala index 71313114..5952f695 100644 --- a/os/test/src-jvm/OpTestsJvmOnly.scala +++ b/os/test/src-jvm/OpTestsJvmOnly.scala @@ -3,187 +3,190 @@ package test.os import java.nio.file.NoSuchFileException import java.nio.{file => nio} - import utest._ import os.{GlobSyntax, /} import java.nio.charset.Charset -object OpTestsJvmOnly extends TestSuite{ +object OpTestsJvmOnly extends TestSuite { val tests = Tests { - val res = os.pwd/"os"/"test"/"resources"/"test" + val res = os.pwd / "os" / "test" / "resources" / "test" - test("lsR"){ + test("lsR") { os.walk(res).foreach(println) - intercept[java.nio.file.NoSuchFileException](os.walk(os.pwd/"out"/"scratch"/"nonexistent")) + intercept[java.nio.file.NoSuchFileException]( + os.walk(os.pwd / "out" / "scratch" / "nonexistent") + ) assert( - os.walk(res/"folder2"/"nestedB") == Seq(res/"folder2"/"nestedB"/"b.txt"), - os.walk(res/"folder2").toSet == Set( - res/"folder2"/"nestedA", - res/"folder2"/"nestedA"/"a.txt", - res/"folder2"/"nestedB", - res/"folder2"/"nestedB"/"b.txt" + os.walk(res / "folder2" / "nestedB") == Seq(res / "folder2" / "nestedB" / "b.txt"), + os.walk(res / "folder2").toSet == Set( + res / "folder2" / "nestedA", + res / "folder2" / "nestedA" / "a.txt", + res / "folder2" / "nestedB", + res / "folder2" / "nestedB" / "b.txt" ) ) } - test("lsRecPermissions"){ - if(Unix()){ - assert(os.walk(os.root/"var"/"run").nonEmpty) + test("lsRecPermissions") { + if (Unix()) { + assert(os.walk(os.root / "var" / "run").nonEmpty) } } - test("readResource"){ - test("positive"){ - test("absolute"){ - val contents = os.read(os.resource/"test"/"os"/"folder"/"file.txt") + test("readResource") { + test("positive") { + test("absolute") { + val contents = os.read(os.resource / "test" / "os" / "folder" / "file.txt") assert(contents.contains("file contents lols")) val cl = getClass.getClassLoader - val contents2 = os.read(os.resource(cl)/"test"/"os"/"folder"/"file.txt") + val contents2 = os.read(os.resource(cl) / "test" / "os" / "folder" / "file.txt") assert(contents2.contains("file contents lols")) } - test("relative"){ + test("relative") { val cls = classOf[_root_.test.os.Testing] - val contents = os.read(os.resource(cls)/"folder"/"file.txt") + val contents = os.read(os.resource(cls) / "folder" / "file.txt") assert(contents.contains("file contents lols")) - val contents2 = os.read(os.resource(getClass)/"folder"/"file.txt") + val contents2 = os.read(os.resource(getClass) / "folder" / "file.txt") assert(contents2.contains("file contents lols")) } } - test("negative"){ - test - intercept[os.ResourceNotFoundException]{ - os.read(os.resource/"folder"/"file.txt") + test("negative") { + test - intercept[os.ResourceNotFoundException] { + os.read(os.resource / "folder" / "file.txt") } - test - intercept[os.ResourceNotFoundException]{ - os.read(os.resource(classOf[_root_.test.os.Testing])/"test"/"os"/"folder"/"file.txt") + test - intercept[os.ResourceNotFoundException] { + os.read( + os.resource(classOf[_root_.test.os.Testing]) / "test" / "os" / "folder" / "file.txt" + ) } - test - intercept[os.ResourceNotFoundException]{ - os.read(os.resource(getClass)/"test"/"os"/"folder"/"file.txt") + test - intercept[os.ResourceNotFoundException] { + os.read(os.resource(getClass) / "test" / "os" / "folder" / "file.txt") } - test - intercept[os.ResourceNotFoundException]{ - os.read(os.resource(getClass.getClassLoader)/"folder"/"file.txt") + test - intercept[os.ResourceNotFoundException] { + os.read(os.resource(getClass.getClassLoader) / "folder" / "file.txt") } } } - test("Mutating"){ - val testFolder = os.pwd/"out"/"scratch"/"test" + test("Mutating") { + val testFolder = os.pwd / "out" / "scratch" / "test" os.remove.all(testFolder) os.makeDir.all(testFolder) - test("cp"){ - val d = testFolder/"copying" - test("basic"){ + test("cp") { + val d = testFolder / "copying" + test("basic") { assert( - !os.exists(d/"folder"), - !os.exists(d/"file") + !os.exists(d / "folder"), + !os.exists(d / "file") ) - os.makeDir.all(d/"folder") - os.write(d/"file", "omg") + os.makeDir.all(d / "folder") + os.write(d / "file", "omg") assert( - os.exists(d/"folder"), - os.exists(d/"file"), - os.read(d/"file") == "omg" + os.exists(d / "folder"), + os.exists(d / "file"), + os.read(d / "file") == "omg" ) - os.copy(d/"folder", d/"folder2") - os.copy(d/"file", d/"file2") + os.copy(d / "folder", d / "folder2") + os.copy(d / "file", d / "file2") assert( - os.exists(d/"folder"), - os.exists(d/"file"), - os.read(d/"file") == "omg", - os.exists(d/"folder2"), - os.exists(d/"file2"), - os.read(d/"file2") == "omg" + os.exists(d / "folder"), + os.exists(d / "file"), + os.read(d / "file") == "omg", + os.exists(d / "folder2"), + os.exists(d / "file2"), + os.read(d / "file2") == "omg" ) } - test("deep"){ - os.write(d/"folderA"/"folderB"/"file", "Cow", createFolders = true) - os.copy(d/"folderA", d/"folderC") - assert(os.read(d/"folderC"/"folderB"/"file") == "Cow") + test("deep") { + os.write(d / "folderA" / "folderB" / "file", "Cow", createFolders = true) + os.copy(d / "folderA", d / "folderC") + assert(os.read(d / "folderC" / "folderB" / "file") == "Cow") } - test("merging"){ - val mergeDir = d/"merge" - os.write(mergeDir/"folderA"/"folderB"/"file", "Cow", createFolders = true) - os.write(mergeDir/"folderC"/"file", "moo", createFolders = true) - os.copy(mergeDir/"folderA", mergeDir/"folderC", mergeFolders = true) - assert(os.read(mergeDir/"folderC"/"folderB"/"file") == "Cow") - assert(os.read(mergeDir/"folderC"/"file") == "moo") + test("merging") { + val mergeDir = d / "merge" + os.write(mergeDir / "folderA" / "folderB" / "file", "Cow", createFolders = true) + os.write(mergeDir / "folderC" / "file", "moo", createFolders = true) + os.copy(mergeDir / "folderA", mergeDir / "folderC", mergeFolders = true) + assert(os.read(mergeDir / "folderC" / "folderB" / "file") == "Cow") + assert(os.read(mergeDir / "folderC" / "file") == "moo") } } - test("mv"){ - test("basic"){ - val d = testFolder/"moving" - os.makeDir.all(d/"folder") - assert(os.list(d) == Seq(d/"folder")) - os.move(d/"folder", d/"folder2") - assert(os.list(d) == Seq(d/"folder2")) + test("mv") { + test("basic") { + val d = testFolder / "moving" + os.makeDir.all(d / "folder") + assert(os.list(d) == Seq(d / "folder")) + os.move(d / "folder", d / "folder2") + assert(os.list(d) == Seq(d / "folder2")) } - test("shallow"){ - val d = testFolder/"moving2" + test("shallow") { + val d = testFolder / "moving2" os.makeDir(d) - os.write(d/"A.scala", "AScala") - os.write(d/"B.scala", "BScala") - os.write(d/"A.py", "APy") - os.write(d/"B.py", "BPy") + os.write(d / "A.scala", "AScala") + os.write(d / "B.scala", "BScala") + os.write(d / "A.py", "APy") + os.write(d / "B.py", "BPy") def fileSet = os.list(d).map(_.last).toSet assert(fileSet == Set("A.scala", "B.scala", "A.py", "B.py")) - test("partialMoves"){ - os.list(d).collect(os.move.matching{case p/g"$x.scala" => p/g"$x.java"}) + test("partialMoves") { + os.list(d).collect(os.move.matching { case p / g"$x.scala" => p / g"$x.java" }) assert(fileSet == Set("A.java", "B.java", "A.py", "B.py")) - os.list(d).collect(os.move.matching{case p/g"A.$x" => p/g"C.$x"}) + os.list(d).collect(os.move.matching { case p / g"A.$x" => p / g"C.$x" }) assert(fileSet == Set("C.java", "B.java", "C.py", "B.py")) } - test("fullMoves"){ - os.list(d).map(os.move.matching{case p/g"$x.$y" => p/g"$y.$x"}) + test("fullMoves") { + os.list(d).map(os.move.matching { case p / g"$x.$y" => p / g"$y.$x" }) assert(fileSet == Set("scala.A", "scala.B", "py.A", "py.B")) - def die = os.list(d).map(os.move.matching{case p/g"A.$x" => p/g"C.$x"}) - intercept[MatchError]{ die } + def die = os.list(d).map(os.move.matching { case p / g"A.$x" => p / g"C.$x" }) + intercept[MatchError] { die } } } - test("deep"){ - val d = testFolder/"moving2" + test("deep") { + val d = testFolder / "moving2" os.makeDir(d) - os.makeDir(d/"scala") - os.write(d/"scala"/"A", "AScala") - os.write(d/"scala"/"B", "BScala") - os.makeDir(d/"py") - os.write(d/"py"/"A", "APy") - os.write(d/"py"/"B", "BPy") - test("partialMoves"){ - os.walk(d).collect(os.move.matching{case d/"py"/x => d/x }) + os.makeDir(d / "scala") + os.write(d / "scala" / "A", "AScala") + os.write(d / "scala" / "B", "BScala") + os.makeDir(d / "py") + os.write(d / "py" / "A", "APy") + os.write(d / "py" / "B", "BPy") + test("partialMoves") { + os.walk(d).collect(os.move.matching { case d / "py" / x => d / x }) assert( os.walk(d).toSet == Set( - d/"py", - d/"scala", - d/"scala"/"A", - d/"scala"/"B", - d/"A", - d/"B" + d / "py", + d / "scala", + d / "scala" / "A", + d / "scala" / "B", + d / "A", + d / "B" ) ) } - test("fullMoves"){ - def die = os.walk(d).map(os.move.matching{case d/"py"/x => d/x }) - intercept[MatchError]{ die } + test("fullMoves") { + def die = os.walk(d).map(os.move.matching { case d / "py" / x => d / x }) + intercept[MatchError] { die } - os.walk(d).filter(os.isFile).map(os.move.matching{ - case d/"py"/x => d/"scala"/"py"/x - case d/"scala"/x => d/"py"/"scala"/x + os.walk(d).filter(os.isFile).map(os.move.matching { + case d / "py" / x => d / "scala" / "py" / x + case d / "scala" / x => d / "py" / "scala" / x case d => println("NOT FOUND " + d); d }) assert( os.walk(d).toSet == Set( - d/"py", - d/"scala", - d/"py"/"scala", - d/"scala"/"py", - d/"scala"/"py"/"A", - d/"scala"/"py"/"B", - d/"py"/"scala"/"A", - d/"py"/"scala"/"B" + d / "py", + d / "scala", + d / "py" / "scala", + d / "scala" / "py", + d / "scala" / "py" / "A", + d / "scala" / "py" / "B", + d / "py" / "scala" / "A", + d / "py" / "scala" / "B" ) ) } @@ -191,84 +194,87 @@ object OpTestsJvmOnly extends TestSuite{ // ls! wd | mv* } - test("mkdirRm"){ - test("singleFolder"){ - val single = testFolder/"single" - os.makeDir.all(single/"inner") - assert(os.list(single) == Seq(single/"inner")) - os.remove(single/"inner") + test("mkdirRm") { + test("singleFolder") { + val single = testFolder / "single" + os.makeDir.all(single / "inner") + assert(os.list(single) == Seq(single / "inner")) + os.remove(single / "inner") assert(os.list(single) == Seq()) } - test("nestedFolders"){ - val nested = testFolder/"nested" - os.makeDir.all(nested/"inner"/"innerer"/"innerest") + test("nestedFolders") { + val nested = testFolder / "nested" + os.makeDir.all(nested / "inner" / "innerer" / "innerest") assert( - os.list(nested) == Seq(nested/"inner"), - os.list(nested/"inner") == Seq(nested/"inner"/"innerer"), - os.list(nested/"inner"/"innerer") == Seq(nested/"inner"/"innerer"/"innerest") + os.list(nested) == Seq(nested / "inner"), + os.list(nested / "inner") == Seq(nested / "inner" / "innerer"), + os.list(nested / "inner" / "innerer") == Seq(nested / "inner" / "innerer" / "innerest") ) - os.remove.all(nested/"inner") + os.remove.all(nested / "inner") assert(os.list(nested) == Seq()) } } - test("readWrite"){ - val d = testFolder/"readWrite" + test("readWrite") { + val d = testFolder / "readWrite" os.makeDir.all(d) - test("simple"){ - os.write(d/"file", "i am a cow") - assert(os.read(d/"file") == "i am a cow") + test("simple") { + os.write(d / "file", "i am a cow") + assert(os.read(d / "file") == "i am a cow") } - test("autoMkdir"){ - os.write(d/"folder"/"folder"/"file", "i am a cow", createFolders = true) - assert(os.read(d/"folder"/"folder"/"file") == "i am a cow") + test("autoMkdir") { + os.write(d / "folder" / "folder" / "file", "i am a cow", createFolders = true) + assert(os.read(d / "folder" / "folder" / "file") == "i am a cow") } - test("binary"){ - os.write(d/"file", Array[Byte](1, 2, 3, 4)) - assert(os.read(d/"file").toSeq == Array[Byte](1, 2, 3, 4).toSeq) + test("binary") { + os.write(d / "file", Array[Byte](1, 2, 3, 4)) + assert(os.read(d / "file").toSeq == Array[Byte](1, 2, 3, 4).toSeq) } - test("concatenating"){ - os.write(d/"concat1", Seq("a", "b", "c")) - assert(os.read(d/"concat1") == "abc") - os.write(d/"concat2", Seq(Array[Byte](1, 2), Array[Byte](3, 4))) - assert(os.read.bytes(d/"concat2").toSeq == Array[Byte](1, 2, 3, 4).toSeq) - os.write(d/"concat3", geny.Generator(Array[Byte](1, 2), Array[Byte](3, 4))) - assert(os.read.bytes(d/"concat3").toSeq == Array[Byte](1, 2, 3, 4).toSeq) + test("concatenating") { + os.write(d / "concat1", Seq("a", "b", "c")) + assert(os.read(d / "concat1") == "abc") + os.write(d / "concat2", Seq(Array[Byte](1, 2), Array[Byte](3, 4))) + assert(os.read.bytes(d / "concat2").toSeq == Array[Byte](1, 2, 3, 4).toSeq) + os.write(d / "concat3", geny.Generator(Array[Byte](1, 2), Array[Byte](3, 4))) + assert(os.read.bytes(d / "concat3").toSeq == Array[Byte](1, 2, 3, 4).toSeq) } - test("writeAppend"){ - os.write.append(d/"append.txt", "Hello") - assert(os.read(d/"append.txt") == "Hello") - os.write.append(d/"append.txt", " World") - assert(os.read(d/"append.txt") == "Hello World") + test("writeAppend") { + os.write.append(d / "append.txt", "Hello") + assert(os.read(d / "append.txt") == "Hello") + os.write.append(d / "append.txt", " World") + assert(os.read(d / "append.txt") == "Hello World") } - test("writeOver"){ - os.write.over(d/"append.txt", "Hello") - assert(os.read(d/"append.txt") == "Hello") - os.write.over(d/"append.txt", " Wor") - assert(os.read(d/"append.txt") == " Wor") + test("writeOver") { + os.write.over(d / "append.txt", "Hello") + assert(os.read(d / "append.txt") == "Hello") + os.write.over(d / "append.txt", " Wor") + assert(os.read(d / "append.txt") == " Wor") } test("charset") { - os.write.over(d/"charset.txt", "funcionó".getBytes(Charset.forName("Windows-1252"))) - assert(os.read.lines(d/"charset.txt", Charset.forName("Windows-1252")).head == "funcionó") + os.write.over(d / "charset.txt", "funcionó".getBytes(Charset.forName("Windows-1252"))) + assert(os.read.lines( + d / "charset.txt", + Charset.forName("Windows-1252") + ).head == "funcionó") } } - test("Failures"){ - val d = testFolder/"failures" + test("Failures") { + val d = testFolder / "failures" os.makeDir.all(d) - test("nonexistant"){ - test - intercept[nio.NoSuchFileException](os.list(d/"nonexistent")) - test - intercept[nio.NoSuchFileException](os.read(d/"nonexistent")) - test - intercept[nio.NoSuchFileException](os.copy(d/"nonexistent", d/"yolo")) - test - intercept[nio.NoSuchFileException](os.move(d/"nonexistent", d/"yolo")) + test("nonexistant") { + test - intercept[nio.NoSuchFileException](os.list(d / "nonexistent")) + test - intercept[nio.NoSuchFileException](os.read(d / "nonexistent")) + test - intercept[nio.NoSuchFileException](os.copy(d / "nonexistent", d / "yolo")) + test - intercept[nio.NoSuchFileException](os.move(d / "nonexistent", d / "yolo")) + } + test("collisions") { + os.makeDir.all(d / "folder") + os.write(d / "file", "lolol") + test - intercept[nio.FileAlreadyExistsException](os.move(d / "file", d / "folder")) + test - intercept[nio.FileAlreadyExistsException](os.copy(d / "file", d / "folder")) + test - intercept[nio.FileAlreadyExistsException](os.write(d / "file", "lols")) } - test("collisions"){ - os.makeDir.all(d/"folder") - os.write(d/"file", "lolol") - test - intercept[nio.FileAlreadyExistsException](os.move(d/"file", d/"folder")) - test - intercept[nio.FileAlreadyExistsException](os.copy(d/"file", d/"folder")) - test - intercept[nio.FileAlreadyExistsException](os.write(d/"file", "lols")) - } } } } diff --git a/os/test/src-jvm/PathTestsCustomFilesystem.scala b/os/test/src-jvm/PathTestsCustomFilesystem.scala new file mode 100644 index 00000000..3b05265f --- /dev/null +++ b/os/test/src-jvm/PathTestsCustomFilesystem.scala @@ -0,0 +1,249 @@ +package test.os + +import utest._ +import os._ +import java.util.HashMap +import java.nio.file.FileSystems +import java.net.URI +import java.nio.file.FileSystem +import java.nio.file.Paths + +object PathTestsCustomFilesystem extends TestSuite { + + def customFsUri(jarName: String = "foo.jar") = { + val path = java.nio.file.Paths.get(jarName); + path.toUri() + } + + def withCustomFs(f: FileSystem => Unit, fsUri: URI = customFsUri()): Unit = { + val uri = new URI("jar", fsUri.toString(), null); + val env = new HashMap[String, String](); + env.put("create", "true"); + val fs = FileSystems.newFileSystem(uri, env); + val p = os.root("/", fs) + try { + os.makeDir(p / "test") + os.makeDir(p / "test" / "dir") + f(fs) + } finally { + cleanUpFs(fs, fsUri) + } + } + + def cleanUpFs(fs: FileSystem, fsUri: URI): Unit = { + fs.close() + os.remove(Path(fsUri)) + } + + val testsCommon = Tests { // native doesnt support custom fs yet + test("customFilesystem") { + test("createPath") { + withCustomFs { fileSystem => + val p = os.root("/", fileSystem) / "test" / "dir" + assert(p.root == "/") + assert(p.fileSystem == fileSystem) + } + } + test("list") { + withCustomFs { fileSystem => + val p = os.root("/", fileSystem) / "test" + os.makeDir(p / "dir2") + os.makeDir(p / "dir3") + assert(os.list(p).size == 3) + } + } + test("removeDir") { + withCustomFs { fileSystem => + val p = os.root("/", fileSystem) / "test" / "dir" / "dir2" + os.makeDir.all(p) + assert(os.exists(p)) + os.remove.all(p) + assert(!os.exists(p)) + } + } + test("failTemp") { + withCustomFs { fileSystem => + val p = os.root("/", fileSystem) / "test" / "dir" + intercept[UnsupportedOperationException] { + os.temp.dir(dir = p) + } + } + } + test("failProcCall") { + withCustomFs { fileSystem => + val p = os.root("/", fileSystem) / "test" / "dir" + intercept[UnsupportedOperationException] { + os.proc("echo", "hello").call(cwd = p) + } + } + } + test("up") { + withCustomFs { fileSystem => + val p = os.root("/", fileSystem) / "test" / "dir" + assert((p / os.up) == os.root("/", fileSystem) / "test") + } + } + test("withRelPath") { + withCustomFs { fileSystem => + val p = os.root("/", fileSystem) / "test" / "dir" + val rel = os.rel / os.up / "file.txt" + assert((p / rel) == os.root("/", fileSystem) / "test" / "file.txt") + } + } + test("withSubPath") { + withCustomFs { fileSystem => + val p = os.root("/", fileSystem) / "test" / "dir" + val sub = os.sub / "file.txt" + assert((p / sub) == os.root("/", fileSystem) / "test" / "dir" / "file.txt") + } + } + test("differentFsCompare") { + withCustomFs { fs1 => + withCustomFs( + { fs2 => + val p1 = os.root("/", fs1) / "test" / "dir" + val p2 = os.root("/", fs2) / "test" / "dir" + assert(p1 != p2) + }, + fsUri = customFsUri("bar.jar") + ) + } + } + test("failRelativeToDifferentFs") { + withCustomFs { fs1 => + withCustomFs( + { fs2 => + val p1 = os.root("/", fs1) / "test" / "dir" + val p2 = os.root("/", fs2) / "test" / "dir" + intercept[IllegalArgumentException] { + p1.relativeTo(p2) + } + }, + fsUri = customFsUri("bar.jar") + ) + } + } + test("failSubRelativeToDifferentFs") { + withCustomFs { fs1 => + withCustomFs( + { fs2 => + val p1 = os.root("/", fs1) / "test" / "dir" + val p2 = os.root("/", fs2) / "test" / "dir" + intercept[IllegalArgumentException] { + p1.subRelativeTo(p2) + } + }, + fsUri = customFsUri("bar.jar") + ) + } + } + } + } + + val testsJava11 = Tests { + test("customFilesystem") { + test("writeAndRead") { + withCustomFs { fileSystem => + val p = root("/", fileSystem) / "test" / "dir" + os.write(p / "file.txt", "Hello") + assert(os.read(p / "file.txt") == "Hello") + } + } + test("writeOver") { + withCustomFs { fileSystem => + val p = os.root("/", fileSystem) / "test" / "dir" + os.write(p / "file.txt", "Hello World") + os.write.over(p / "file.txt", "Hello World2") + assert(os.read(p / "file.txt") == "Hello World2") + } + } + test("move") { + withCustomFs { fileSystem => + val p = os.root("/", fileSystem) / "test" / "dir" + os.write(p / "file.txt", "Hello World") + os.move(p / "file.txt", p / "file2.txt") + assert(os.read(p / "file2.txt") == "Hello World") + assert(!os.exists(p / "file.txt")) + } + } + test("copy") { + withCustomFs { fileSystem => + val p = os.root("/", fileSystem) / "test" / "dir" + os.write(p / "file.txt", "Hello World") + os.copy(p / "file.txt", p / "file2.txt") + assert(os.read(p / "file2.txt") == "Hello World") + assert(os.exists(p / "file.txt")) + } + } + test("remove") { + withCustomFs { fileSystem => + val p = os.root("/", fileSystem) / "test" / "dir" + os.write(p / "file.txt", "Hello World") + assert(os.exists(p / "file.txt")) + os.remove(p / "file.txt") + assert(!os.exists(p / "file.txt")) + } + } + test("removeAll") { + withCustomFs { fileSystem => + val p = os.root("/", fileSystem) / "test" / "dir" + os.write(p / "file.txt", "Hello World") + os.write(p / "file2.txt", "Hello World") + os.remove.all(p) + assert(!os.exists(p / "file.txt")) + assert(!os.exists(p / "file2.txt")) + } + } + test("failSymlink") { + withCustomFs { fileSystem => + val p = os.root("/", fileSystem) / "test" / "dir" + os.write(p / "file.txt", "Hello World") + intercept[UnsupportedOperationException] { + os.symlink(p / "link", p / "file.txt") + } + } + } + test("walk") { + withCustomFs { fileSystem => + val p = os.root("/", fileSystem) / "test" / "dir" + os.write(p / "file.txt", "Hello World") + os.write(p / "file2.txt", "Hello World") + os.write(p / "file3.txt", "Hello World") + os.makeDir(p / "dir2") + os.write(p / "dir2" / "file.txt", "Hello World") + assert(os.walk(p).map(_.relativeTo(p)).toSet == + Set( + RelPath("file.txt"), + RelPath("file2.txt"), + RelPath("file3.txt"), + RelPath("dir2"), + RelPath("dir2/file.txt") + )) + } + } + } + } + + val testWindows = Tests { + test("cRootPath") { + val p1 = os.root("C:\\") / "Users" + assert(p1.toString == "C:\\Users") + val p2 = os.root("C:/") / "Users" + assert(p2.toString == "C:\\Users") + } + } + + private lazy val isWindows: Boolean = { + sys.props("os.name").toLowerCase().contains("windows") + } + + private lazy val isJava11OrAbove: Boolean = { + val version = System.getProperty("java.version") + val major = version.split("\\.")(0).toInt + major >= 11 + } + + override val tests: Tests = + testsCommon ++ (if (isJava11OrAbove) testsJava11 else Tests {}) ++ (if (isWindows) testWindows + else Tests {}) +} diff --git a/os/test/src-jvm/PathTestsJvmOnly.scala b/os/test/src-jvm/PathTestsJvmOnly.scala index 353028fc..fbb23ad7 100644 --- a/os/test/src-jvm/PathTestsJvmOnly.scala +++ b/os/test/src-jvm/PathTestsJvmOnly.scala @@ -4,40 +4,44 @@ import java.nio.file.Paths import os._ import utest._ -object PathTestsJvmOnly extends TestSuite{ +import java.util.HashMap +import java.nio.file.FileSystems +import java.net.URI + +object PathTestsJvmOnly extends TestSuite { val tests = Tests { - test("construction"){ - test("symlinks"){ + test("construction") { + test("symlinks") { val names = Seq("test123", "test124", "test125", "test126") val twd = temp.dir() - test("nestedSymlinks"){ - if(Unix()) { - names.foreach(p => os.remove.all(twd/p)) - os.makeDir.all(twd/"test123") - os.symlink(twd/"test124", twd/"test123") - os.symlink(twd/"test125", twd/"test124") - os.symlink(twd/"test126", twd/"test125") - assert(followLink(twd/"test126").get == followLink(twd/"test123").get) - names.foreach(p => os.remove(twd/p)) - names.foreach(p => assert(!exists(twd/p))) + test("nestedSymlinks") { + if (Unix()) { + names.foreach(p => os.remove.all(twd / p)) + os.makeDir.all(twd / "test123") + os.symlink(twd / "test124", twd / "test123") + os.symlink(twd / "test125", twd / "test124") + os.symlink(twd / "test126", twd / "test125") + assert(followLink(twd / "test126").get == followLink(twd / "test123").get) + names.foreach(p => os.remove(twd / p)) + names.foreach(p => assert(!exists(twd / p))) } } - test("danglingSymlink"){ - if(Unix()) { - names.foreach(p => os.remove.all(twd/p)) - os.makeDir.all(twd/"test123") - os.symlink(twd/"test124", twd/"test123") - os.symlink(twd/"test125", twd/"test124") - os.symlink(twd/"test126", twd/"test125") + test("danglingSymlink") { + if (Unix()) { + names.foreach(p => os.remove.all(twd / p)) + os.makeDir.all(twd / "test123") + os.symlink(twd / "test124", twd / "test123") + os.symlink(twd / "test125", twd / "test124") + os.symlink(twd / "test126", twd / "test125") os.remove(twd / "test123") assert(followLink(twd / "test126").isEmpty) names.foreach(p => os.remove.all(twd / p)) names.foreach(p => assert(!exists(twd / p))) - names.foreach(p => os.remove.all(twd/p)) - names.foreach(p => assert(!exists(twd/p))) + names.foreach(p => os.remove.all(twd / p)) + names.foreach(p => assert(!exists(twd / p))) } } } diff --git a/os/test/src-jvm/ProcessPipelineTests.scala b/os/test/src-jvm/ProcessPipelineTests.scala new file mode 100644 index 00000000..bfeb6326 --- /dev/null +++ b/os/test/src-jvm/ProcessPipelineTests.scala @@ -0,0 +1,160 @@ +package test.os + +import java.io._ +import java.nio.charset.StandardCharsets + +import os._ +import utest._ +import TestUtil.prep +import scala.util.Try + +object ProcessPipelineTests extends TestSuite { + val scriptFolder = pwd / "os" / "test" / "resources" / "scripts" + + lazy val scalaHome = sys.env("SCALA_HOME") + + def isWindows = System.getProperty("os.name").toLowerCase().contains("windows") + + def scriptProc(name: String, args: String*): Seq[String] = + Seq( + "java", + "-jar", + name + ) ++ args.toSeq + + def writerProc(n: Int, wait: Int, debugOutput: Boolean = true): Seq[String] = + scriptProc(sys.env("TEST_JAR_WRITER_ASSEMBLY"), n.toString, wait.toString, debugOutput.toString) + def readerProc(n: Int, wait: Int, debugOutput: Boolean = true): Seq[String] = + scriptProc(sys.env("TEST_JAR_READER_ASSEMBLY"), n.toString, wait.toString, debugOutput.toString) + def exitProc(code: Int, wait: Int): Seq[String] = + scriptProc(sys.env("TEST_JAR_EXIT_ASSEMBLY"), code.toString, wait.toString) + + val commonTests = Tests { + test("pipelineCall") { + val resultLines = os.proc(writerProc(10, 10)) + .pipeTo(os.proc(readerProc(10, 10))) + .call().out.lines().toSeq + + val expectedLog = (0 until 10).map(i => s"Read: Hello $i") + assert(expectedLog.forall(resultLines.contains)) + } + + test("pipelineSpawn") { + val buffer = new collection.mutable.ArrayBuffer[String]() + val p = os.proc(writerProc(10, 10)) + .pipeTo(os.proc(readerProc(10, 10))) + .spawn(stdout = os.ProcessOutput.Readlines(s => buffer.append(s))) + + p.waitFor() + + val expectedLog = (0 until 10).map(i => s"Read: Hello $i") + assert(expectedLog.forall(buffer.contains)) + } + + test("longPipepelineSpawn") { + val buffer = new collection.mutable.ArrayBuffer[String]() + val p = os.proc(writerProc(10, 10)) + .pipeTo(os.proc(readerProc(10, 10))) + .pipeTo(os.proc(readerProc(10, 10))) + .pipeTo(os.proc(readerProc(10, 10))) + .spawn(stdout = os.ProcessOutput.Readlines(s => buffer.append(s))) + + p.waitFor() + + val expectedLog = + (0 until 10).map(i => s"Read: Read: Read: Hello $i") // each reader appends "Read:" + assert(expectedLog.forall(buffer.contains)) + } + + test("pipelineSpawnWithStdin") { + test - prep { wd => + val buffer = new collection.mutable.ArrayBuffer[String]() + val p = os.proc(readerProc(1, 10)) + .pipeTo(os.proc(readerProc(1, 10))) + .spawn( + stdin = wd / "File.txt", + stdout = os.ProcessOutput.Readlines(s => buffer.append(s)) + ) + + p.waitFor() + + assert(buffer.contains("Read: Read: I am cow")) + } + } + + test("pipelineSpawnWithStderr") { + val buffer = new collection.mutable.ArrayBuffer[String]() + val p = os.proc(writerProc(10, 10)) + .pipeTo(os.proc(readerProc(10, 10))) + .spawn(stderr = os.ProcessOutput.Readlines(s => synchronized { buffer.append(s) })) + + p.waitFor() + + val expectedLog = (0 until 10).flatMap(i => Seq(s"At: $i", s"Written $i")) + + assert(expectedLog.forall(buffer.contains)) + } + + test("pipelineWithoutPipefail") { + val p = os.proc(exitProc(0, 300)) + .pipeTo(os.proc(exitProc(213, 100))) + .pipeTo(os.proc(exitProc(0, 400))) + .spawn(pipefail = false) + + p.waitFor() + assert(p.exitCode() == 0) + } + + test("pipelineWithPipefail") { + val p = os.proc(exitProc(0, 300)) + .pipeTo(os.proc(exitProc(1, 100))) + .pipeTo(os.proc(exitProc(0, 400))) + .spawn(pipefail = true) + + p.waitFor() + assert(p.exitCode() == 1) + } + } + + val nonWindowsTests = Tests { + test("brokenPipe") { + val p = os.proc(writerProc(-1, 0, false)) + .pipeTo(os.proc(readerProc(3, 0, false))) + .spawn() + + p.waitFor(10000) + val finished = !p.isAlive() + p.destroy() + + assert(finished) + } + + test("brokenPipeNotHandled") { + val p = os.proc(writerProc(-1, 0, false)) + .pipeTo(os.proc(readerProc(3, 0, false))) + .spawn(handleBrokenPipe = false) + + p.waitFor(1000) + val alive = p.isAlive() + p.destroy() + + assert(alive) + } + + test("longBrokenPipePropagate") { + val p = os.proc(writerProc(-1, 0, false)) + .pipeTo(os.proc(readerProc(-1, 0, false))) + .pipeTo(os.proc(readerProc(-1, 0, false))) + .pipeTo(os.proc(readerProc(3, 0, false))) + .spawn() + + p.waitFor(30000) // long to avoid flaky tests + val finished = !p.isAlive() + p.destroy() + + assert(finished) + } + } + + override def tests: Tests = if (!isWindows) commonTests ++ nonWindowsTests else commonTests +} diff --git a/os/test/src-jvm/ReadingWritingTests.scala b/os/test/src-jvm/ReadingWritingTests.scala index 81385a9b..ccc90333 100644 --- a/os/test/src-jvm/ReadingWritingTests.scala +++ b/os/test/src-jvm/ReadingWritingTests.scala @@ -2,9 +2,9 @@ package test.os import utest._ import TestUtil._ object ReadingWritingTests extends TestSuite { - def tests = Tests{ - test("read"){ - test - prep{ wd => + def tests = Tests { + test("read") { + test - prep { wd => os.read(wd / "File.txt") ==> "I am cow" os.read(wd / "folder1" / "one.txt") ==> "Contents of folder one" os.read(wd / "Multi Line.txt") ==> @@ -13,8 +13,8 @@ object ReadingWritingTests extends TestSuite { |I weigh twice as much as you |And I look good on the barbecue""".stripMargin } - test("inputStream"){ - test - prep{wd => + test("inputStream") { + test - prep { wd => val is = os.read.inputStream(wd / "File.txt") // ==> "I am cow" is.read() ==> 'I' is.read() ==> ' ' @@ -28,16 +28,16 @@ object ReadingWritingTests extends TestSuite { is.close() } } - test("bytes"){ - test - prep{ wd => + test("bytes") { + test - prep { wd => os.read.bytes(wd / "File.txt") ==> "I am cow".getBytes os.read.bytes(wd / "misc" / "binary.png").length ==> 711 } } - test("chunks"){ - test - prep{ wd => + test("chunks") { + test - prep { wd => val chunks = os.read.chunks(wd / "File.txt", chunkSize = 2) - .map{case (buf, n) => buf.take(n).toSeq } // copy the buffer to save the data + .map { case (buf, n) => buf.take(n).toSeq } // copy the buffer to save the data .toSeq chunks ==> Seq( @@ -49,8 +49,8 @@ object ReadingWritingTests extends TestSuite { } } - test("lines"){ - test - prep{ wd => + test("lines") { + test - prep { wd => os.read.lines(wd / "File.txt") ==> Seq("I am cow") os.read.lines(wd / "Multi Line.txt") ==> Seq( "I am cow", @@ -59,13 +59,13 @@ object ReadingWritingTests extends TestSuite { "And I look good on the barbecue" ) } - test("stream"){ - test - prep{ wd => + test("stream") { + test - prep { wd => os.read.lines.stream(wd / "File.txt").count() ==> 1 os.read.lines.stream(wd / "Multi Line.txt").count() ==> 4 // Streaming the lines to the console - for(line <- os.read.lines.stream(wd / "Multi Line.txt")){ + for (line <- os.read.lines.stream(wd / "Multi Line.txt")) { println(line) } } @@ -73,16 +73,16 @@ object ReadingWritingTests extends TestSuite { } } - test("write"){ - test - prep{ wd => + test("write") { + test - prep { wd => os.write(wd / "New File.txt", "New File Contents") os.read(wd / "New File.txt") ==> "New File Contents" os.write(wd / "NewBinary.bin", Array[Byte](0, 1, 2, 3)) os.read.bytes(wd / "NewBinary.bin") ==> Array[Byte](0, 1, 2, 3) } - test("append"){ - test - prep{ wd => + test("append") { + test - prep { wd => os.read(wd / "File.txt") ==> "I am cow" os.write.append(wd / "File.txt", ", hear me moo") @@ -97,8 +97,8 @@ object ReadingWritingTests extends TestSuite { os.read.bytes(wd / "misc" / "binary.png").length ==> 714 } } - test("over"){ - test - prep{ wd => + test("over") { + test - prep { wd => os.read(wd / "File.txt") ==> "I am cow" os.write.over(wd / "File.txt", "You are cow") @@ -111,8 +111,8 @@ object ReadingWritingTests extends TestSuite { os.read(wd / "File.txt") ==> "We are sow" } } - test("inputStream"){ - test - prep{ wd => + test("inputStream") { + test - prep { wd => val out = os.write.outputStream(wd / "New File.txt") out.write('H') out.write('e') @@ -125,8 +125,8 @@ object ReadingWritingTests extends TestSuite { } } } - test("truncate"){ - test - prep{ wd => + test("truncate") { + test - prep { wd => os.read(wd / "File.txt") ==> "I am cow" os.truncate(wd / "File.txt", 4) @@ -135,4 +135,3 @@ object ReadingWritingTests extends TestSuite { } } } - diff --git a/os/test/src-jvm/SubprocessTests.scala b/os/test/src-jvm/SubprocessTests.scala index 108d8e66..16eefe93 100644 --- a/os/test/src-jvm/SubprocessTests.scala +++ b/os/test/src-jvm/SubprocessTests.scala @@ -8,13 +8,13 @@ import utest._ import scala.collection.mutable -object SubprocessTests extends TestSuite{ - val scriptFolder = pwd/"os"/"test"/"resources"/"test" +object SubprocessTests extends TestSuite { + val scriptFolder = pwd / "os" / "test" / "resources" / "test" val lsCmd = if (scala.util.Properties.isWin) "dir" else "ls" val tests = Tests { - test("lines"){ + test("lines") { val res = TestUtil.proc(lsCmd, scriptFolder).call() assert( res.out.lines().exists(_.contains("File.txt")), @@ -22,7 +22,7 @@ object SubprocessTests extends TestSuite{ res.out.lines().exists(_.contains("folder2")) ) } - test("string"){ + test("string") { val res = TestUtil.proc(lsCmd, scriptFolder).call() assert( res.out.text().contains("File.txt"), @@ -30,35 +30,35 @@ object SubprocessTests extends TestSuite{ res.out.text().contains("folder2") ) } - test("bytes"){ - if(Unix()){ + test("bytes") { + if (Unix()) { val res = proc(scriptFolder / "misc" / "echo", "abc").call() val listed = res.out.bytes listed ==> "abc\n".getBytes } } - test("chained"){ + test("chained") { assert( proc("git", "init").call().out.text().contains("Reinitialized existing Git repository"), proc("git", "init").call().out.text().contains("Reinitialized existing Git repository"), TestUtil.proc(lsCmd, pwd).call().out.text().contains("Readme.adoc") ) } - test("basicList"){ + test("basicList") { val files = List("Readme.adoc", "build.sc") val output = TestUtil.proc(lsCmd, files).call().out.text() assert(files.forall(output.contains)) } - test("listMixAndMatch"){ + test("listMixAndMatch") { val stuff = List("I", "am", "bovine") val result = TestUtil.proc("echo", "Hello,", stuff, "hear me roar").call() - if(Unix()) + if (Unix()) assert(result.out.text().contains("Hello, " + stuff.mkString(" ") + " hear me roar")) else // win quotes multiword args assert(result.out.text().contains("Hello, " + stuff.mkString(" ") + " \"hear me roar\"")) } - test("failures"){ - val ex = intercept[os.SubprocessException]{ + test("failures") { + val ex = intercept[os.SubprocessException] { TestUtil.proc(lsCmd, "does-not-exist").call(check = true, stderr = os.Pipe) } val res: CommandResult = ex.result @@ -69,22 +69,21 @@ object SubprocessTests extends TestSuite{ ) } - test("filebased"){ - if(Unix()){ - assert(proc(scriptFolder/"misc"/"echo", "HELLO").call().out.lines().mkString == "HELLO") + test("filebased") { + if (Unix()) { + assert(proc(scriptFolder / "misc" / "echo", "HELLO").call().out.lines().mkString == "HELLO") val res: CommandResult = - proc(root/"bin"/"bash", "-c", "echo 'Hello'$ENV_ARG").call( + proc(root / "bin" / "bash", "-c", "echo 'Hello'$ENV_ARG").call( env = Map("ENV_ARG" -> "123") ) - assert(res.out.text().trim()== "Hello123") + assert(res.out.text().trim() == "Hello123") } } - test("filebased2"){ - if(Unix()){ - val possiblePaths = Seq(root / "bin", - root / "usr" / "bin").map { pfx => pfx / "echo" } + test("filebased2") { + if (Unix()) { + val possiblePaths = Seq(root / "bin", root / "usr" / "bin").map { pfx => pfx / "echo" } val res = proc("which", "echo").call() val echoRoot = Path(res.out.text().trim()) assert(possiblePaths.contains(echoRoot)) @@ -93,7 +92,7 @@ object SubprocessTests extends TestSuite{ } } - test("charSequence"){ + test("charSequence") { val charSequence = new StringBuilder("This is a CharSequence") val cmd = Seq( "echo", @@ -103,38 +102,74 @@ object SubprocessTests extends TestSuite{ assert(res.out.text().trim() == charSequence.toString()) } - test("envArgs"){ if(Unix()){ - val res0 = proc("bash", "-c", "echo \"Hello$ENV_ARG\"").call(env = Map("ENV_ARG" -> "12")) - assert(res0.out.lines() == Seq("Hello12")) - - val res1 = proc("bash", "-c", "echo \"Hello$ENV_ARG\"").call(env = Map("ENV_ARG" -> "12")) - assert(res1.out.lines() == Seq("Hello12")) - - val res2 = proc("bash", "-c", "echo 'Hello$ENV_ARG'").call(env = Map("ENV_ARG" -> "12")) - assert(res2.out.lines() == Seq("Hello$ENV_ARG")) - - val res3 = proc("bash", "-c", "echo 'Hello'$ENV_ARG").call(env = Map("ENV_ARG" -> "123")) - assert(res3.out.lines() == Seq("Hello123")) - }} - test("multiChunk"){ + test("envArgs") { + if (Unix()) { + locally { + val res0 = proc("bash", "-c", "echo \"Hello$ENV_ARG\"").call(env = Map("ENV_ARG" -> "12")) + assert(res0.out.lines() == Seq("Hello12")) + } + + locally { + val res1 = proc("bash", "-c", "echo \"Hello$ENV_ARG\"").call(env = Map("ENV_ARG" -> "12")) + assert(res1.out.lines() == Seq("Hello12")) + } + + locally { + val res2 = proc("bash", "-c", "echo 'Hello$ENV_ARG'").call(env = Map("ENV_ARG" -> "12")) + assert(res2.out.lines() == Seq("Hello$ENV_ARG")) + } + + locally { + val res3 = proc("bash", "-c", "echo 'Hello'$ENV_ARG").call(env = Map("ENV_ARG" -> "123")) + assert(res3.out.lines() == Seq("Hello123")) + } + + locally { + // TEST_SUBPROCESS_ENV env should be set in forkEnv in build.sc + assert(sys.env.get("TEST_SUBPROCESS_ENV") == Some("value")) + val res4 = proc("bash", "-c", "echo \"$TEST_SUBPROCESS_ENV\"").call( + env = Map.empty, + propagateEnv = false + ).out.lines() + assert(res4 == Seq("")) + } + + locally { + // TEST_SUBPROCESS_ENV env should be set in forkEnv in build.sc + assert(sys.env.get("TEST_SUBPROCESS_ENV") == Some("value")) + + val res5 = proc("bash", "-c", "echo \"$TEST_SUBPROCESS_ENV\"").call( + env = Map.empty, + propagateEnv = true + ).out.lines() + assert(res5 == Seq("value")) + } + } + } + test("multiChunk") { // Make sure that in the case where multiple chunks are being read from // the subprocess in quick succession, we ensure that the output handler // callbacks are properly ordered such that the output is aggregated // correctly - test("bashC"){ if(TestUtil.isInstalled("python")) { - os.proc("python", "-c", - """import sys, time - |for i in range(5): - | for j in range(10): - | sys.stdout.write(str(j)) - | # Make sure it comes as multiple chunks, but close together! - | # Vary how close they are together to try and trigger race conditions - | time.sleep(0.00001 * i) - | sys.stdout.flush() - """.stripMargin).call().out.text() ==> - "01234567890123456789012345678901234567890123456789" - }} - test("jarTf"){ + test("bashC") { + if (TestUtil.isInstalled("python")) { + os.proc( + "python", + "-c", + """import sys, time + |for i in range(5): + | for j in range(10): + | sys.stdout.write(str(j)) + | # Make sure it comes as multiple chunks, but close together! + | # Vary how close they are together to try and trigger race conditions + | time.sleep(0.00001 * i) + | sys.stdout.flush() + """.stripMargin + ).call().out.text() ==> + "01234567890123456789012345678901234567890123456789" + } + } + test("jarTf") { // This was the original repro for the multi-chunk concurrency bugs val jarFile = os.pwd / "os" / "test" / "resources" / "misc" / "out.jar" assert(TestUtil.eqIgnoreNewlineStyle( @@ -150,21 +185,21 @@ object SubprocessTests extends TestSuite{ )) } } - test("workingDirectory"){ + test("workingDirectory") { val listed1 = TestUtil.proc(lsCmd).call(cwd = pwd) val listed2 = TestUtil.proc(lsCmd).call(cwd = pwd / up) assert(listed2 != listed1) } - test("customWorkingDir"){ + test("customWorkingDir") { val res1 = TestUtil.proc(lsCmd).call(cwd = pwd) // explicitly // or implicitly val res2 = TestUtil.proc(lsCmd).call() } - test("fileCustomWorkingDir"){ - if(Unix()){ - val output = proc(scriptFolder/"misc"/"echo_with_wd", "HELLO").call(cwd = root/"usr") + test("fileCustomWorkingDir") { + if (Unix()) { + val output = proc(scriptFolder / "misc" / "echo_with_wd", "HELLO").call(cwd = root / "usr") assert(output.out.lines() == Seq("HELLO /usr")) } } diff --git a/os/test/src-jvm/TestUtil.scala b/os/test/src-jvm/TestUtil.scala index ffcc6327..49eec2f7 100644 --- a/os/test/src-jvm/TestUtil.scala +++ b/os/test/src-jvm/TestUtil.scala @@ -10,7 +10,7 @@ object TestUtil { val NewLineRegex = "\r\n|\r|\n" def isInstalled(executable: String): Boolean = { - val getPathCmd = if(scala.util.Properties.isWin) "where" else "which" + val getPathCmd = if (scala.util.Properties.isWin) "where" else "which" os.proc(getPathCmd, executable).call(check = false).exitCode == 0 } @@ -20,15 +20,15 @@ object TestUtil { // run Unix command normally, Windows in CMD context def proc(command: os.Shellable*) = { - if(scala.util.Properties.isWin) { + if (scala.util.Properties.isWin) { val cmd = ("CMD.EXE": os.Shellable) :: ("/C": os.Shellable) :: command.toList os.proc(cmd: _*) } else os.proc(command) } - // 1. when using Git "core.autocrlf true" + // 1. when using Git "core.autocrlf true" // some tests would fail when comparing with only \n - // 2. when using Git "core.autocrlf false" + // 2. when using Git "core.autocrlf false" // some tests would fail when comparing with process outputs which produce CRLF strings /** Compares two strings, ignoring line-ending style */ def eqIgnoreNewlineStyle(str1: String, str2: String) = { @@ -37,36 +37,41 @@ object TestUtil { str1Normalized == str2Normalized } - def prep[T](f: os.Path => T)(implicit tp: TestPath, - fn: sourcecode.FullName) ={ + def prep[T](f: os.Path => T)(implicit tp: TestPath, fn: sourcecode.FullName) = { val segments = Seq("out", "scratch") ++ fn.value.split('.').drop(2) ++ tp.value val directory = Paths.get(segments.mkString("/")) if (!Files.exists(directory)) Files.createDirectories(directory.getParent) - else Files.walkFileTree(directory, new SimpleFileVisitor[Path]() { - override def visitFile(file: Path, attrs: BasicFileAttributes) = { - Files.delete(file) - FileVisitResult.CONTINUE - } + else Files.walkFileTree( + directory, + new SimpleFileVisitor[Path]() { + override def visitFile(file: Path, attrs: BasicFileAttributes) = { + Files.delete(file) + FileVisitResult.CONTINUE + } - override def postVisitDirectory(dir: Path, exc: IOException) = { - Files.delete(dir) - FileVisitResult.CONTINUE + override def postVisitDirectory(dir: Path, exc: IOException) = { + Files.delete(dir) + FileVisitResult.CONTINUE + } } - }) + ) val original = Paths.get("os", "test", "resources", "test") - Files.walkFileTree(original, new SimpleFileVisitor[Path]() { - override def preVisitDirectory(dir: Path, attrs: BasicFileAttributes) = { - Files.copy(dir, directory.resolve(original.relativize(dir)), LinkOption.NOFOLLOW_LINKS) - FileVisitResult.CONTINUE - } + Files.walkFileTree( + original, + new SimpleFileVisitor[Path]() { + override def preVisitDirectory(dir: Path, attrs: BasicFileAttributes) = { + Files.copy(dir, directory.resolve(original.relativize(dir)), LinkOption.NOFOLLOW_LINKS) + FileVisitResult.CONTINUE + } - override def visitFile(file: Path, attrs: BasicFileAttributes) = { - Files.copy(file, directory.resolve(original.relativize(file)), LinkOption.NOFOLLOW_LINKS) - FileVisitResult.CONTINUE + override def visitFile(file: Path, attrs: BasicFileAttributes) = { + Files.copy(file, directory.resolve(original.relativize(file)), LinkOption.NOFOLLOW_LINKS) + FileVisitResult.CONTINUE + } } - }) + ) f(os.Path(directory.toAbsolutePath)) } diff --git a/os/test/src/OpTests.scala b/os/test/src/OpTests.scala index 2e26ec81..212f4dc8 100644 --- a/os/test/src/OpTests.scala +++ b/os/test/src/OpTests.scala @@ -3,36 +3,35 @@ package test.os import java.nio.file.NoSuchFileException import java.nio.{file => nio} - import utest._ import os.{GlobSyntax, /} -object OpTests extends TestSuite{ +object OpTests extends TestSuite { val tests = Tests { - val res = os.pwd/"os"/"test"/"resources"/"test" + val res = os.pwd / "os" / "test" / "resources" / "test" test("ls") - assert( os.list(res).toSet == Set( - res/"folder1", - res/"folder2", - res/"misc", - res/"os", - res/"File.txt", - res/"Multi Line.txt" + res / "folder1", + res / "folder2", + res / "misc", + res / "os", + res / "File.txt", + res / "Multi Line.txt" ), - os.list(res/"folder2").toSet == Set( - res/"folder2"/"nestedA", - res/"folder2"/"nestedB" + os.list(res / "folder2").toSet == Set( + res / "folder2" / "nestedA", + res / "folder2" / "nestedB" ) ) - test("rm"){ + test("rm") { // shouldn't crash - os.remove.all(os.pwd/"out"/"scratch"/"nonexistent") + os.remove.all(os.pwd / "out" / "scratch" / "nonexistent") // shouldn't crash - os.remove(os.pwd/"out"/"scratch"/"nonexistent") ==> false + os.remove(os.pwd / "out" / "scratch" / "nonexistent") ==> false // should crash - intercept[NoSuchFileException]{ - os.remove(os.pwd/"out"/"scratch"/"nonexistent", checkExists = true) + intercept[NoSuchFileException] { + os.remove(os.pwd / "out" / "scratch" / "nonexistent", checkExists = true) } } } diff --git a/os/test/src/OsTestMain.scala b/os/test/src/OsTestMain.scala index 0a1cdb7b..ea7020c5 100644 --- a/os/test/src/OsTestMain.scala +++ b/os/test/src/OsTestMain.scala @@ -1,7 +1,5 @@ package test.os object OsTestMain { - def main(args: Array[String]): Unit = { - - } + def main(args: Array[String]): Unit = {} } diff --git a/os/test/src/PathTests.scala b/os/test/src/PathTests.scala index be630d81..98aa6ae4 100644 --- a/os/test/src/PathTests.scala +++ b/os/test/src/PathTests.scala @@ -1,76 +1,92 @@ package test.os import java.nio.file.Paths +import java.io.File import os._ +import os.Path.{driveRoot} import utest.{assert => _, _} import java.net.URI -object PathTests extends TestSuite{ +object PathTests extends TestSuite { val tests = Tests { - test("Basic"){ - val base = rel/"src"/"main"/"scala" - val subBase = sub/"src"/"main"/"scala" - test("Transformers"){ - if(Unix()){ - // os.Path to java.nio.file.Path - assert((root/"omg").wrapped == Paths.get("/omg")) + test("Basic") { + val base = rel / "src" / "main" / "scala" + val subBase = sub / "src" / "main" / "scala" + test("Transform posix paths") { + // verify posix string format of driveRelative path + assert(posix(root / "omg") == posix(Paths.get("/omg").toAbsolutePath)) - // java.nio.file.Path to os.Path - assert(root/"omg" == Path(Paths.get("/omg"))) - assert(rel/"omg" == RelPath(Paths.get("omg"))) - assert(sub/"omg" == SubPath(Paths.get("omg"))) + // verify driveRelative path + assert(sameFile((root / "omg").wrapped, Paths.get("/omg"))) - // URI to os.Path - assert(root / "omg" == Path(Paths.get("/omg").toUri())) + // driveRelative path is an absolute path + assert(posix(root / "omg") == s"$driveRoot/omg") - // We only support file schemes like above, but nothing else - val httpUri = URI.create( - "https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top" - ) - val ldapUri = URI.create( - "ldap://[2001:db8::7]/c=GB?objectClass?one" - ) - intercept[IllegalArgumentException](Path(httpUri)) - intercept[IllegalArgumentException](Path(ldapUri)) + // Paths.get(driveRoot) same file as pwd + val p1 = posix(Paths.get(driveRoot).toAbsolutePath) match { + case s if s.matches(".:.*/") => + s.stripSuffix("/") // java 8, remove spurious trailing slash + case s => + s + } + val p2 = posix(pwd.toNIO.toAbsolutePath) + System.err.printf("p1[%s]\np2[%s]\n", p1, p2) + assert(p1 == p2) + } + test("Transformers") { + // java.nio.file.Path to os.Path + assert(root / "omg" == Path(Paths.get("/omg"))) + assert(rel / "omg" == RelPath(Paths.get("omg"))) + assert(sub / "omg" == SubPath(Paths.get("omg"))) + // URI to os.Path + assert(root / "omg" == Path(Paths.get("/omg").toUri())) - // os.Path to String - assert((root/"omg").toString == "/omg") - assert((rel/"omg").toString == "omg") - assert((sub/"omg").toString == "omg") - assert((up/"omg").toString == "../omg") - assert((up/up/"omg").toString == "../../omg") + // We only support file schemes like above, but nothing else + val httpUri = URI.create( + "https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top" + ) + val ldapUri = URI.create( + "ldap://[2001:db8::7]/c=GB?objectClass?one" + ) + intercept[IllegalArgumentException](Path(httpUri)) + intercept[IllegalArgumentException](Path(ldapUri)) - // String to os.Path - assert(root/"omg" == Path("/omg")) - assert(rel/"omg" == RelPath("omg")) - assert(sub/"omg" == SubPath("omg")) - } + // os.Path to String + assert((rel / "omg").toString == "omg") + assert((sub / "omg").toString == "omg") + assert((up / "omg").toString == "../omg") + assert((up / up / "omg").toString == "../../omg") + + // String to os.Path + assert(root / "omg" == Path("/omg")) + assert(rel / "omg" == RelPath("omg")) + assert(sub / "omg" == SubPath("omg")) } - test("BasePath"){ - test("baseName"){ + test("BasePath") { + test("baseName") { assert((base / "baseName.ext").baseName == "baseName") assert((base / "baseName.v2.0.ext").baseName == "baseName.v2.0") assert((base / "baseOnly").baseName == "baseOnly") assert((base / "baseOnly.").baseName == "baseOnly") } - test("ext"){ + test("ext") { assert((base / "baseName.ext").ext == "ext") assert((base / "baseName.v2.0.ext").ext == "ext") assert((base / "baseOnly").ext == "") assert((base / "baseOnly.").ext == "") } - test("emptyExt"){ + test("emptyExt") { os.root.ext ==> "" os.rel.ext ==> "" os.sub.ext ==> "" os.up.ext ==> "" } - test("emptyLast"){ + test("emptyLast") { intercept[PathError.LastOnEmptyPath](os.root.last).getMessage ==> "empty path has no last segment" intercept[PathError.LastOnEmptyPath](os.rel.last).getMessage ==> @@ -82,195 +98,179 @@ object PathTests extends TestSuite{ } } - test("RelPath"){ - test("Constructors"){ - test("Symbol"){ - if (Unix()){ - val rel1 = base / "ammonite" - assert( - rel1.segments == Seq("src", "main", "scala", "ammonite"), - rel1.toString == "src/main/scala/ammonite" - ) - } + test("RelPath") { + test("Constructors") { + test("Symbol") { + val rel1 = base / "ammonite" + assert( + rel1.segments == Seq("src", "main", "scala", "ammonite"), + rel1.toString == "src/main/scala/ammonite" + ) } - test("String"){ - if (Unix()){ - val rel1 = base / "Path.scala" - assert( - rel1.segments == Seq("src", "main", "scala", "Path.scala"), - rel1.toString == "src/main/scala/Path.scala" - ) - } + test("String") { + val rel1 = base / "Path.scala" + assert( + rel1.segments == Seq("src", "main", "scala", "Path.scala"), + rel1.toString == "src/main/scala/Path.scala" + ) } - test("Combos"){ + test("Combos") { def check(rel1: RelPath) = assert( rel1.segments == Seq("src", "main", "scala", "sub1", "sub2"), rel1.toString == "src/main/scala/sub1/sub2" ) - test("ArrayString"){ - if (Unix()){ - val arr = Array("sub1", "sub2") - check(base / arr) - } + test("ArrayString") { + val arr = Array("sub1", "sub2") + check(base / arr) } - test("ArraySymbol"){ - if (Unix()){ - val arr = Array("sub1", "sub2") - check(base / arr) - } + test("ArraySymbol") { + val arr = Array("sub1", "sub2") + check(base / arr) } - test("SeqString"){ - if (Unix()) check(base / Seq("sub1", "sub2")) + test("SeqString") { + check(base / Seq("sub1", "sub2")) } - test("SeqSymbol"){ - if (Unix()) check(base / Seq("sub1", "sub2")) + test("SeqSymbol") { + check(base / Seq("sub1", "sub2")) } - test("SeqSeqSeqSymbol"){ - if (Unix()){ - check( - base / Seq(Seq(Seq("sub1"), Seq()), Seq(Seq("sub2")), Seq()) - ) - } + test("SeqSeqSeqSymbol") { + check( + base / Seq(Seq(Seq("sub1"), Seq()), Seq(Seq("sub2")), Seq()) + ) } } } - test("Relativize"){ + test("Relativize") { def eq[T](p: T, q: T) = assert(p == q) - test - eq(rel/"omg"/"bbq"/"wtf" relativeTo rel/"omg"/"bbq"/"wtf", rel) - test - eq(rel/"omg"/"bbq" relativeTo rel/"omg"/"bbq"/"wtf", up) - test - eq(rel/"omg"/"bbq"/"wtf" relativeTo rel/"omg"/"bbq", rel/"wtf") - test - eq(rel/"omg"/"bbq" relativeTo rel/"omg"/"bbq"/"wtf", up) - test - eq(up/"omg"/"bbq" relativeTo rel/"omg"/"bbq", up/up/up/"omg"/"bbq") - test - intercept[PathError.NoRelativePath](rel/"omg"/"bbq" relativeTo up/"omg"/"bbq") + test - eq(rel / "omg" / "bbq" / "wtf" relativeTo rel / "omg" / "bbq" / "wtf", rel) + test - eq(rel / "omg" / "bbq" relativeTo rel / "omg" / "bbq" / "wtf", up) + test - eq(rel / "omg" / "bbq" / "wtf" relativeTo rel / "omg" / "bbq", rel / "wtf") + test - eq(rel / "omg" / "bbq" relativeTo rel / "omg" / "bbq" / "wtf", up) + test - eq(up / "omg" / "bbq" relativeTo rel / "omg" / "bbq", up / up / up / "omg" / "bbq") + test - intercept[PathError.NoRelativePath]( + rel / "omg" / "bbq" relativeTo up / "omg" / "bbq" + ) } } - test("SubPath"){ - test("Constructors"){ - test("Symbol"){ - if (Unix()){ - val rel1 = subBase / "ammonite" - assert( - rel1.segments == Seq("src", "main", "scala", "ammonite"), - rel1.toString == "src/main/scala/ammonite" - ) - } + test("SubPath") { + test("Constructors") { + test("Symbol") { + val rel1 = subBase / "ammonite" + assert( + rel1.segments == Seq("src", "main", "scala", "ammonite"), + rel1.toString == "src/main/scala/ammonite" + ) } - test("String"){ - if (Unix()){ - val rel1 = subBase / "Path.scala" - assert( - rel1.segments == Seq("src", "main", "scala", "Path.scala"), - rel1.toString == "src/main/scala/Path.scala" - ) - } + test("String") { + val rel1 = subBase / "Path.scala" + assert( + rel1.segments == Seq("src", "main", "scala", "Path.scala"), + rel1.toString == "src/main/scala/Path.scala" + ) } - test("Combos"){ + test("Combos") { def check(rel1: SubPath) = assert( rel1.segments == Seq("src", "main", "scala", "sub1", "sub2"), rel1.toString == "src/main/scala/sub1/sub2" ) - test("ArrayString"){ - if (Unix()){ - val arr = Array("sub1", "sub2") - check(subBase / arr) - } + test("ArrayString") { + val arr = Array("sub1", "sub2") + check(subBase / arr) } - test("ArraySymbol"){ - if (Unix()){ - val arr = Array("sub1", "sub2") - check(subBase / arr) - } + test("ArraySymbol") { + val arr = Array("sub1", "sub2") + check(subBase / arr) } - test("SeqString"){ - if (Unix()) check(subBase / Seq("sub1", "sub2")) + test("SeqString") { + check(subBase / Seq("sub1", "sub2")) } - test("SeqSymbol"){ - if (Unix()) check(subBase / Seq("sub1", "sub2")) + test("SeqSymbol") { + check(subBase / Seq("sub1", "sub2")) } - test("SeqSeqSeqSymbol"){ - if (Unix()){ - check( - subBase / Seq(Seq(Seq("sub1"), Seq()), Seq(Seq("sub2")), Seq()) - ) - } + test("SeqSeqSeqSymbol") { + check( + subBase / Seq(Seq(Seq("sub1"), Seq()), Seq(Seq("sub2")), Seq()) + ) } } } - test("Relativize"){ + test("Relativize") { def eq[T](p: T, q: T) = assert(p == q) - test - eq(sub/"omg"/"bbq"/"wtf" relativeTo sub/"omg"/"bbq"/"wtf", rel) - test - eq(sub/"omg"/"bbq" relativeTo sub/"omg"/"bbq"/"wtf", up) - test - eq(sub/"omg"/"bbq"/"wtf" relativeTo sub/"omg"/"bbq", rel/"wtf") - test - eq(sub/"omg"/"bbq" relativeTo sub/"omg"/"bbq"/"wtf", up) + test - eq(sub / "omg" / "bbq" / "wtf" relativeTo sub / "omg" / "bbq" / "wtf", rel) + test - eq(sub / "omg" / "bbq" relativeTo sub / "omg" / "bbq" / "wtf", up) + test - eq(sub / "omg" / "bbq" / "wtf" relativeTo sub / "omg" / "bbq", rel / "wtf") + test - eq(sub / "omg" / "bbq" relativeTo sub / "omg" / "bbq" / "wtf", up) } } - test("AbsPath"){ + test("AbsPath") { val d = pwd val abs = d / base - test("Constructor"){ - if (Unix()) assert( - abs.toString.drop(d.toString.length) == "/src/main/scala", + test("Constructor") { + assert( + posix(abs).drop(d.toString.length) == "/src/main/scala", abs.toString.length > d.toString.length ) } - test("Relativize"){ + test("Relativize") { def eq[T](p: T, q: T) = assert(p == q) - test - eq(root/"omg"/"bbq"/"wtf" relativeTo root/"omg"/"bbq"/"wtf", rel) - test - eq(root/"omg"/"bbq" relativeTo root/"omg"/"bbq"/"wtf", up) - test - eq(root/"omg"/"bbq"/"wtf" relativeTo root/"omg"/"bbq", rel/"wtf") - test - eq(root/"omg"/"bbq" relativeTo root/"omg"/"bbq"/"wtf", up) - test - intercept[PathError.NoRelativePath](rel/"omg"/"bbq" relativeTo up/"omg"/"bbq") + test - eq(root / "omg" / "bbq" / "wtf" relativeTo root / "omg" / "bbq" / "wtf", rel) + test - eq(root / "omg" / "bbq" relativeTo root / "omg" / "bbq" / "wtf", up) + test - eq(root / "omg" / "bbq" / "wtf" relativeTo root / "omg" / "bbq", rel / "wtf") + test - eq(root / "omg" / "bbq" relativeTo root / "omg" / "bbq" / "wtf", up) + test - intercept[PathError.NoRelativePath]( + rel / "omg" / "bbq" relativeTo up / "omg" / "bbq" + ) } } - test("Ups"){ - test("RelativeUps"){ - val rel2 = base/up - assert(rel2 == rel/"src"/"main") - assert(base/up/up == rel/"src") - assert(base/up/up/up == rel) - assert(base/up/up/up/up == up) - assert(base/up/up/up/up/up == up/up) - assert(up/base == up/"src"/"main"/"scala") + test("Ups") { + test("RelativeUps") { + val rel2 = base / up + assert(rel2 == rel / "src" / "main") + assert(base / up / up == rel / "src") + assert(base / up / up / up == rel) + assert(base / up / up / up / up == up) + assert(base / up / up / up / up / up == up / up) + assert(up / base == up / "src" / "main" / "scala") } - test("AbsoluteUps"){ + test("AbsoluteUps") { // Keep applying `up` and verify that the path gets // shorter and shorter and eventually errors. var abs = pwd var i = abs.segmentCount - while(i > 0){ + while (i > 0) { abs /= up - i-=1 + i -= 1 assert(abs.segmentCount == i) } - intercept[PathError.AbsolutePathOutsideRoot.type]{ abs/up } + intercept[PathError.AbsolutePathOutsideRoot.type] { abs / up } } - test("RootUpBreak"){ - intercept[PathError.AbsolutePathOutsideRoot.type]{ root/up } - val x = root/"omg" - val y = x/up - intercept[PathError.AbsolutePathOutsideRoot.type]{ y / up } + test("RootUpBreak") { + intercept[PathError.AbsolutePathOutsideRoot.type] { root / up } + val x = root / "omg" + val y = x / up + intercept[PathError.AbsolutePathOutsideRoot.type] { y / up } } } - test("Comparison"){ + test("Comparison") { test("Relative") - { - assert(rel/"omg"/"wtf" == rel/"omg"/"wtf") - assert(rel/"omg"/"wtf" != rel/"omg"/"wtf"/"bbq") - assert(rel/"omg"/"wtf"/"bbq" startsWith rel/"omg"/"wtf") - assert(rel/"omg"/"wtf" startsWith rel/"omg"/"wtf") - assert(up/"omg"/"wtf" startsWith up/"omg"/"wtf") - assert(!(rel/"omg"/"wtf" startsWith rel/"omg"/"wtf"/"bbq")) - assert(!(up/"omg"/"wtf" startsWith rel/"omg"/"wtf")) - assert(!(rel/"omg"/"wtf" startsWith up/"omg"/"wtf")) + assert(rel / "omg" / "wtf" == rel / "omg" / "wtf") + assert(rel / "omg" / "wtf" != rel / "omg" / "wtf" / "bbq") + assert(rel / "omg" / "wtf" / "bbq" startsWith rel / "omg" / "wtf") + assert(rel / "omg" / "wtf" startsWith rel / "omg" / "wtf") + assert(up / "omg" / "wtf" startsWith up / "omg" / "wtf") + assert(!(rel / "omg" / "wtf" startsWith rel / "omg" / "wtf" / "bbq")) + assert(!(up / "omg" / "wtf" startsWith rel / "omg" / "wtf")) + assert(!(rel / "omg" / "wtf" startsWith up / "omg" / "wtf")) } test("Absolute") - { - assert(root/"omg"/"wtf" == root/"omg"/"wtf") - assert(root/"omg"/"wtf" != root/"omg"/"wtf"/"bbq") - assert(root/"omg"/"wtf"/"bbq" startsWith root/"omg"/"wtf") - assert(root/"omg"/"wtf" startsWith root/"omg"/"wtf") - assert(!(root/"omg"/"wtf" startsWith root/"omg"/"wtf"/"bbq")) + assert(root / "omg" / "wtf" == root / "omg" / "wtf") + assert(root / "omg" / "wtf" != root / "omg" / "wtf" / "bbq") + assert(root / "omg" / "wtf" / "bbq" startsWith root / "omg" / "wtf") + assert(root / "omg" / "wtf" startsWith root / "omg" / "wtf") + assert(!(root / "omg" / "wtf" startsWith root / "omg" / "wtf" / "bbq")) } - test("Invalid"){ + test("Invalid") { compileError("""root/"omg"/"wtf" < "omg"/"wtf"""") compileError("""root/"omg"/"wtf" > "omg"/"wtf"""") compileError(""""omg"/"wtf" < root/"omg"/"wtf"""") @@ -278,159 +278,174 @@ object PathTests extends TestSuite{ } } } - test("Errors"){ - test("InvalidChars"){ - val ex = intercept[PathError.InvalidSegment](rel/"src"/"Main/.scala") + test("Errors") { + test("InvalidChars") { + val ex = intercept[PathError.InvalidSegment](rel / "src" / "Main/.scala") val PathError.InvalidSegment("Main/.scala", msg1) = ex assert(msg1.contains("[/] is not a valid character to appear in a path segment")) - val ex2 = intercept[PathError.InvalidSegment](root/"hello"/".."/"world") + val ex2 = intercept[PathError.InvalidSegment](root / "hello" / ".." / "world") val PathError.InvalidSegment("..", msg2) = ex2 assert(msg2.contains("use the `up` segment from `os.up`")) } - test("InvalidSegments"){ - intercept[PathError.InvalidSegment]{root/ "core/src/test"} - intercept[PathError.InvalidSegment]{root/ ""} - intercept[PathError.InvalidSegment]{root/ "."} - intercept[PathError.InvalidSegment]{root/ ".."} + test("InvalidSegments") { + intercept[PathError.InvalidSegment] { root / "core/src/test" } + intercept[PathError.InvalidSegment] { root / "" } + intercept[PathError.InvalidSegment] { root / "." } + intercept[PathError.InvalidSegment] { root / ".." } } - test("EmptySegment"){ - intercept[PathError.InvalidSegment](rel/"src" / "") - intercept[PathError.InvalidSegment](rel/"src" / ".") - intercept[PathError.InvalidSegment](rel/"src" / "..") + test("EmptySegment") { + intercept[PathError.InvalidSegment](rel / "src" / "") + intercept[PathError.InvalidSegment](rel / "src" / ".") + intercept[PathError.InvalidSegment](rel / "src" / "..") } - test("CannotRelativizeAbsAndRel"){if(Unix()){ + test("CannotRelativizeAbsAndRel") { val abs = pwd - val rel = os.rel/"omg"/"wtf" + val rel = os.rel / "omg" / "wtf" compileError(""" - abs.relativeTo(rel) - """).msg.toLowerCase.contains("required: os.path") ==> true + abs.relativeTo(rel) + """).msg.toLowerCase.contains("required: os.path") ==> true compileError(""" - rel.relativeTo(abs) - """).msg.toLowerCase.contains("required: os.relpath") ==> true - }} - test("InvalidCasts"){ - if(Unix()){ - intercept[IllegalArgumentException](Path("omg/cow")) - intercept[IllegalArgumentException](RelPath("/omg/cow")) - } + rel.relativeTo(abs) + """).msg.toLowerCase.contains("required: os.relpath") ==> true + } + test("InvalidCasts") { + intercept[IllegalArgumentException](Path("omg/cow")) + intercept[IllegalArgumentException](RelPath("/omg/cow")) } - test("Pollution"){ + test("Pollution") { // Make sure we"re" not polluting too much compileError(""""omg".ext""") compileError(""" "omg".ext """) } } - test("Extractors"){ - test("paths"){ - val a/b/c/d/"omg" = pwd/"A"/"B"/"C"/"D"/"omg" - assert(a == pwd/"A") + test("Extractors") { + test("paths") { + val a / b / c / d / "omg" = pwd / "A" / "B" / "C" / "D" / "omg" + assert(a == pwd / "A") assert(b == "B") assert(c == "C") assert(d == "D") // If the paths aren"t" deep enough, it // just doesn"t" match but doesn"t" blow up - root/"omg" match { - case a3/b3/c3/d3/e3 => assert(false) + root / "omg" match { + case a3 / b3 / c3 / d3 / e3 => assert(false) case _ => } } } - test("sorting"){ + test("sorting") { test - { assert( - Seq(root/"c", root, root/"b", root/"a").sorted == - Seq(root, root/"a", root/"b", root/"c") + Seq(root / "c", root, root / "b", root / "a").sorted == + Seq(root, root / "a", root / "b", root / "c") ) } test - assert( - Seq(up/"c", up/up/"c", rel/"b"/"c", rel/"a"/"c", rel/"a"/"d").sorted == - Seq(rel/"a"/"c", rel/"a"/"d", rel/"b"/"c", up/"c", up/up/"c") + Seq(up / "c", up / up / "c", rel / "b" / "c", rel / "a" / "c", rel / "a" / "d").sorted == + Seq(rel / "a" / "c", rel / "a" / "d", rel / "b" / "c", up / "c", up / up / "c") ) test - assert( Seq(os.root / "yo", os.root / "yo").sorted == - Seq(os.root / "yo", os.root / "yo") + Seq(os.root / "yo", os.root / "yo") ) } - test("construction"){ - test("success"){ - if(Unix()){ - val relStr = "hello/cow/world/.." - val absStr = "/hello/world" + test("construction") { + test("success") { + val relStr = "hello/cow/world/.." + val absStr = "/hello/world" - val lhs = Path(absStr) - val rhs = root/"hello"/"world" - assert( - RelPath(relStr) == rel/"hello"/"cow", - // Path(...) also allows paths starting with ~, - // which is expanded to become your home directory - lhs == rhs - ) - - // You can also pass in java.io.File and java.nio.file.Path - // objects instead of Strings when constructing paths - val relIoFile = new java.io.File(relStr) - val absNioFile = java.nio.file.Paths.get(absStr) + val lhs = Path(absStr) + val rhs = root / "hello" / "world" + assert( + RelPath(relStr) == rel / "hello" / "cow", + // Path(...) also allows paths starting with ~, + // which is expanded to become your home directory + lhs == rhs + ) + // You can also pass in java.io.File and java.nio.file.Path + // objects instead of Strings when constructing paths + val relIoFile = new java.io.File(relStr) + val absNioFile = java.nio.file.Paths.get(absStr) - assert(RelPath(relIoFile) == rel/"hello"/"cow") - assert(Path(absNioFile) == root/"hello"/"world") - assert(Path(relIoFile, root/"base") == root/"base"/"hello"/"cow") - } + assert(RelPath(relIoFile) == rel / "hello" / "cow") + assert(Path(absNioFile) == root / "hello" / "world") + assert(Path(relIoFile, root / "base") == root / "base" / "hello" / "cow") } - test("basepath"){ - if(Unix()){ - val relStr = "hello/cow/world/.." - val absStr = "/hello/world" - assert( - FilePath(relStr) == rel/"hello"/"cow", - FilePath(absStr) == root/"hello"/"world" - ) - } + test("basepath") { + val relStr = "hello/cow/world/.." + val absStr = "/hello/world" + assert( + FilePath(relStr) == rel / "hello" / "cow", + FilePath(absStr) == root / "hello" / "world" + ) } - test("based"){ - if(Unix()){ - val relStr = "hello/cow/world/.." - val absStr = "/hello/world" - val basePath: FilePath = FilePath(relStr) - assert(Path(relStr, root/"base") == root/"base"/"hello"/"cow") - assert(Path(absStr, root/"base") == root/"hello"/"world") - assert(Path(basePath, root/"base") == root/"base"/"hello"/"cow") - assert(Path(".", pwd).last != "") - } + test("based") { + val relStr = "hello/cow/world/.." + val absStr = "/hello/world" + val basePath: FilePath = FilePath(relStr) + assert(Path(relStr, root / "base") == root / "base" / "hello" / "cow") + assert(Path(absStr, root / "base") == root / "hello" / "world") + assert(Path(basePath, root / "base") == root / "base" / "hello" / "cow") + assert(Path(".", pwd).last != "") } - test("failure"){ - if(Unix()){ - val relStr = "hello/.." - intercept[java.lang.IllegalArgumentException]{ - Path(relStr) - } + test("failure") { + val relStr = "hello/.." + intercept[java.lang.IllegalArgumentException] { + Path(relStr) + } - val absStr = "/hello" - intercept[java.lang.IllegalArgumentException]{ - RelPath(absStr) - } + val absStr = "/hello" + intercept[java.lang.IllegalArgumentException] { + RelPath(absStr) + } - val tooManyUpsStr = "/hello/../.." - intercept[PathError.AbsolutePathOutsideRoot.type]{ - Path(tooManyUpsStr) - } + val tooManyUpsStr = "/hello/../.." + intercept[PathError.AbsolutePathOutsideRoot.type] { + Path(tooManyUpsStr) } } } - test("issue159"){ - val result1 = os.rel / Seq(os.up, os.rel/"hello", os.rel/"world") - val result2 = os.rel / Array(os.up, os.rel/"hello", os.rel/"world") + test("issue159") { + val result1 = os.rel / Seq(os.up, os.rel / "hello", os.rel / "world") + val result2 = os.rel / Array(os.up, os.rel / "hello", os.rel / "world") val expected = os.up / "hello" / "world" assert(result1 == expected) assert(result2 == expected) } + test("custom root") { + assert(os.root == os.root(os.root.root)) + File.listRoots().foreach { root => + val path = os.root(root.toPath().toString) / "test" / "dir" + assert(path.root == root.toString) + assert(path.relativeTo(os.root(root.toPath().toString)) == rel / "test" / "dir") + } + } + test("issue201") { + val p = Path("/omg") // driveRelative path does not throw exception. + System.err.printf("p[%s]\n", posix(p)) + assert(posix(p) contains "/omg") + } + } + // compare absolute paths + def sameFile(a: java.nio.file.Path, b: java.nio.file.Path): Boolean = { + a.toAbsolutePath == b.toAbsolutePath + } + def sameFile(a: os.Path, b: java.nio.file.Path): Boolean = { + sameFile(a.wrapped, b) + } + def sameFile(a: Path, b: Path): Boolean = { + sameFile(a.wrapped, b.wrapped) } + def posix(s: String): String = s.replace('\\', '/') + def posix(p: java.nio.file.Path): String = posix(p.toString) + def posix(p: os.Path): String = posix(p.toNIO) } diff --git a/os/test/src/unix.scala b/os/test/src/unix.scala index 7183ccb8..4c78946d 100644 --- a/os/test/src/unix.scala +++ b/os/test/src/unix.scala @@ -1,13 +1,13 @@ package test.os /** - * Created by haoyi on 2/17/16. - */ + * Created by haoyi on 2/17/16. + */ object Unix { def apply(): Boolean = java.nio.file.Paths.get("").toAbsolutePath.getRoot.toString == "/" } /** - * Dummy class just used to test classloader relative/absolute resource logic - */ -class Testing \ No newline at end of file + * Dummy class just used to test classloader relative/absolute resource logic + */ +class Testing diff --git a/os/watch/src/WatchServiceWatcher.scala b/os/watch/src/WatchServiceWatcher.scala index 647f4e4b..39f22b3d 100644 --- a/os/watch/src/WatchServiceWatcher.scala +++ b/os/watch/src/WatchServiceWatcher.scala @@ -5,11 +5,11 @@ import java.io.IOException import java.nio.file.ClosedWatchServiceException import java.util.concurrent.atomic.AtomicBoolean import java.nio.file.StandardWatchEventKinds.{ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY, OVERFLOW} - -import com.sun.nio.file.SensitivityWatchEventModifier +import com.sun.nio.file.{ExtendedWatchEventModifier, SensitivityWatchEventModifier} import scala.collection.mutable import collection.JavaConverters._ +import scala.util.Properties.isWin class WatchServiceWatcher( roots: Seq[os.Path], @@ -33,12 +33,17 @@ class WatchServiceWatcher( val isDir = os.isDir(p, followLinks = false) logger("WATCH", (p, isDir)) if (isDir) { + // https://stackoverflow.com/a/6265860/4496364 + // on Windows we watch only the root directory + val modifiers: Array[WatchEvent.Modifier] = if (isWin) + Array(SensitivityWatchEventModifier.HIGH, ExtendedWatchEventModifier.FILE_TREE) + else Array(SensitivityWatchEventModifier.HIGH) currentlyWatchedPaths.put( p, p.toNIO.register( nioWatchService, Array[WatchEvent.Kind[_]](ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY, OVERFLOW), - SensitivityWatchEventModifier.HIGH + modifiers: _* ) ) newlyWatchedPaths.append(p) @@ -68,13 +73,21 @@ class WatchServiceWatcher( } def recursiveWatches() = { - while (newlyWatchedPaths.nonEmpty) { - val top = newlyWatchedPaths.remove(newlyWatchedPaths.length - 1) - val listing = - try os.list(top) - catch { case e: java.nio.file.NotDirectoryException => Nil } - for (p <- listing) watchSinglePath(p) - bufferedEvents.add(top) + // no need to recursively watch each folder on windows + // https://stackoverflow.com/a/64030685/4496364 + if (isWin) { + // noop + } else { + while (newlyWatchedPaths.nonEmpty) { + val top = newlyWatchedPaths.remove(newlyWatchedPaths.length - 1) + val listing = + try os.list(top) + catch { + case e: java.nio.file.NotDirectoryException => Nil + } + for (p <- listing) watchSinglePath(p) + bufferedEvents.add(top) + } } } diff --git a/os/watch/src/package.scala b/os/watch/src/package.scala index 009a5f1c..714fba20 100644 --- a/os/watch/src/package.scala +++ b/os/watch/src/package.scala @@ -24,8 +24,6 @@ package object watch { * changes happening within the watched roots folder, apart from the path * at which the change happened. It is up to the `onEvent` handler to query * the filesystem and figure out what happened, and what it wants to do. - * - * `watch` currently only supports Linux and Mac-OSX, and not Windows. */ def watch( roots: Seq[os.Path], @@ -33,9 +31,8 @@ package object watch { logger: (String, Any) => Unit = (_, _) => () ): AutoCloseable = { val watcher = System.getProperty("os.name") match { - case "Linux" => new os.watch.WatchServiceWatcher(roots, onEvent, logger) case "Mac OS X" => new os.watch.FSEventsWatcher(roots, onEvent, logger, 0.05) - case osName => throw new Exception(s"watch not supported on operating system: $osName") + case _ => new os.watch.WatchServiceWatcher(roots, onEvent, logger) } val thread = new Thread { diff --git a/os/watch/test/src/WatchTests.scala b/os/watch/test/src/WatchTests.scala index aed5ea77..33bf97b8 100644 --- a/os/watch/test/src/WatchTests.scala +++ b/os/watch/test/src/WatchTests.scala @@ -1,18 +1,20 @@ package test.os.watch +import scala.util.Properties.isWin +import scala.util.Random import utest._ object WatchTests extends TestSuite with TestSuite.Retries { override val utestRetryCount = - if(sys.env.get("CI").contains("true")) { - if(sys.env.get("RUNNER_OS").contains("macOS")) 10 + if (sys.env.get("CI").contains("true")) { + if (sys.env.get("RUNNER_OS").contains("macOS")) 10 else 3 } else { 0 } val tests = Tests { - test("singleFolder") - _root_.test.os.TestUtil.prep{wd => if (_root_.test.os.Unix()){ + test("singleFolder") - _root_.test.os.TestUtil.prep { wd => val changedPaths = collection.mutable.Set.empty[os.Path] _root_.os.watch.watch( Seq(wd), @@ -27,7 +29,7 @@ object WatchTests extends TestSuite with TestSuite.Retries { def checkFileManglingChanges(p: os.Path) = { checkChanges( - os.write(p, ""), + os.write(p, Random.nextString(100)), Set(p.subRelativeTo(wd)) ) @@ -56,7 +58,9 @@ object WatchTests extends TestSuite with TestSuite.Retries { action Thread.sleep(200) val changedSubPaths = changedPaths.map(_.subRelativeTo(wd)) - assert(expectedChangedPaths == changedSubPaths) + // on Windows sometimes we get more changes + if (isWin) assert(expectedChangedPaths.subsetOf(changedSubPaths)) + else assert(expectedChangedPaths == changedSubPaths) } checkFileManglingChanges(wd / "test") @@ -73,18 +77,24 @@ object WatchTests extends TestSuite with TestSuite.Retries { checkFileManglingChanges(wd / "my-new-folder" / "test") - checkChanges( - os.move(wd / "folder2", wd / "folder3"), - Set( + locally { + val expectedChanges = if (isWin) Set( + os.sub / "folder2", + os.sub / "folder3" + ) + else Set( os.sub / "folder2", os.sub / "folder3", - os.sub / "folder3" / "nestedA", os.sub / "folder3" / "nestedA" / "a.txt", os.sub / "folder3" / "nestedB", os.sub / "folder3" / "nestedB" / "b.txt" ) - ) + checkChanges( + os.move(wd / "folder2", wd / "folder3"), + expectedChanges + ) + } checkChanges( os.copy(wd / "folder3", wd / "folder4"), @@ -123,16 +133,17 @@ object WatchTests extends TestSuite with TestSuite.Retries { checkChanges( os.hardlink(wd / "newlink3", wd / "folder3" / "nestedA" / "a.txt"), - System.getProperty("os.name") match{ - case "Linux" => Set(os.sub / "newlink3") + System.getProperty("os.name") match { case "Mac OS X" => Set( os.sub / "newlink3", os.sub / "folder3" / "nestedA", os.sub / "folder3" / "nestedA" / "a.txt" ) + case _ => Set(os.sub / "newlink3") } ) - }} + + } } } diff --git a/testJarExit/src/TestJarExit.java b/testJarExit/src/TestJarExit.java new file mode 100644 index 00000000..01a98918 --- /dev/null +++ b/testJarExit/src/TestJarExit.java @@ -0,0 +1,11 @@ +import java.util.Scanner; + +public class TestJarExit { + public static void main(String[] args) throws InterruptedException { + int exitCode = Integer.parseInt(args[0]); + int exitSleep = Integer.parseInt(args[1]); + System.err.println("Exiting with code: " + exitCode); + Thread.sleep(exitSleep); + System.exit(exitCode); + } +} diff --git a/testJarReader/src/TestJarReader.java b/testJarReader/src/TestJarReader.java new file mode 100644 index 00000000..66c7af00 --- /dev/null +++ b/testJarReader/src/TestJarReader.java @@ -0,0 +1,24 @@ +import java.util.Scanner; + +public class TestJarReader { + public static void main(String[] args) throws InterruptedException { + Scanner scanner = new Scanner(System.in); + int readN = Integer.parseInt(args[0]); + int readSleep = Integer.parseInt(args[1]); + boolean debugOutput = Boolean.parseBoolean(args[2]); + int i = 0; + while(readN == -1 || i < readN) { + if(debugOutput) { + System.err.println("At: " + i); + } + String read = scanner.nextLine(); + System.out.println("Read: " + read); + Thread.sleep(readSleep); + i++; + } + scanner.close(); + if(debugOutput) { + System.err.println("Exiting reader"); + } + } +} diff --git a/testJarWriter/src/TestJarWriter.java b/testJarWriter/src/TestJarWriter.java new file mode 100644 index 00000000..05390932 --- /dev/null +++ b/testJarWriter/src/TestJarWriter.java @@ -0,0 +1,24 @@ +import java.util.Scanner; +import java.lang.InterruptedException; + +public class TestJarWriter { + public static void main(String[] args) throws InterruptedException { + Scanner scanner = new Scanner(System.in); + int writeN = Integer.parseInt(args[0]); + int writeSleep = Integer.parseInt(args[1]); + boolean debugOutput = Boolean.parseBoolean(args[2]); + int i = 0; + while(writeN == -1 || i < writeN) { + System.out.println("Hello " + i); + if(debugOutput) { + System.err.println("Written " + i); + } + Thread.sleep(writeSleep); + i++; + } + scanner.close(); + if(debugOutput) { + System.err.println("Exiting writer"); + } + } +}