diff --git a/sbt/src/main/scala/sbt/Path.scala b/sbt/src/main/scala/sbt/Path.scala index c62fe4929..69c6a6ce3 100644 --- a/sbt/src/main/scala/sbt/Path.scala +++ b/sbt/src/main/scala/sbt/Path.scala @@ -293,17 +293,30 @@ sealed abstract class PathFinder extends NotNull addTo(pathSet) wrap.Wrappers.readOnly(pathSet) } + /** Only keeps paths for which `f` returns true. It is non-strict, so it is not evaluated until the returned finder is evaluated.*/ final def filter(f: Path => Boolean): PathFinder = Path.lazyPathFinder(get.filter(f)) + /* Non-strict flatMap: no evaluation occurs until the returned finder is evaluated.*/ final def flatMap(f: Path => PathFinder): PathFinder = Path.lazyPathFinder(get.flatMap(p => f(p).get)) + /** Evaluates this finder and converts the results to an `Array` of `URL`s..*/ final def getURLs: Array[URL] = Path.getURLs(get) + /** Evaluates this finder and converts the results to a `Set` of `File`s.*/ final def getFiles: immutable.Set[File] = Path.getFiles(get) + /** Evaluates this finder and converts the results to a `Set` of absolute path strings.*/ final def getPaths: immutable.Set[String] = strictMap(_.absolutePath) + /** Evaluates this finder and converts the results to a `Set` of relative path strings.*/ final def getRelativePaths: immutable.Set[String] = strictMap(_.relativePath) final def strictMap[T](f: Path => T): immutable.Set[T] = Path.mapSet(get)(f) private[sbt] def addTo(pathSet: Set[Path]) + /** Create a PathFinder from this one where each path has a unique name. + * A single path is arbitrarily selected from the set of paths with the same name.*/ + def distinct: PathFinder = Path.lazyPathFinder((Map() ++ get.map(p => (p.asFile.getName, p))) .values.toList ) + + /** Constructs a string by evaluating this finder, converting the resulting Paths to absolute path strings, and joining them with the platform path separator.*/ final def absString = Path.makeString(get) + /** Constructs a string by evaluating this finder, converting the resulting Paths to relative path strings, and joining them with the platform path separator.*/ final def relativeString = Path.makeRelativeString(get) + /** Constructs a debugging string for this finder by evaluating it and separating paths by newlines.*/ override def toString = get.mkString("\n ", "\n ","") } private class BasePathFinder(base: PathFinder) extends PathFinder diff --git a/sbt/src/test/scala/sbt/PathSpecification.scala b/sbt/src/test/scala/sbt/PathSpecification.scala index a65e2d98f..d51cb94f7 100644 --- a/sbt/src/test/scala/sbt/PathSpecification.scala +++ b/sbt/src/test/scala/sbt/PathSpecification.scala @@ -12,46 +12,59 @@ object PathSpecification extends Properties("Path") { val log = new ConsoleLogger log.setLevel(Level.Warn) + + // certain operations require a real underlying file. We'd like to create them in a managed temporary directory so that junk isn't left over from the test. + // The arguments to several properties are functions that construct a Path or PathFinder given a base directory. + type ToPath = ProjectDirectory => Path + type ToFinder = ProjectDirectory => PathFinder implicit val pathComponent: Arbitrary[String] = Arbitrary(for(id <- Gen.identifier) yield trim(id)) // TODO: make a more specific Arbitrary - implicit val projectDirectory: Arbitrary[ProjectDirectory] = Arbitrary(Gen.value(new ProjectDirectory(new File(".")))) - implicit val arbPath: Arbitrary[Path] = Arbitrary(genPath) + implicit val arbComponents: Arbitrary[List[String]] = Arbitrary(componentList) + implicit val arbDup: Arbitrary[(String, Int)] = Arbitrary.arbTuple2(pathComponent, Arbitrary(Gen.choose(0, MaxDuplicates))) + implicit val arbPath: Arbitrary[ToPath] = Arbitrary(genPath) + implicit val arbDirs: Arbitrary[ToFinder] = Arbitrary(directories) - specify("Project directory relative path empty", (projectPath: ProjectDirectory) => projectPath.relativePath.isEmpty) - specify("construction", (dir: ProjectDirectory, components: List[String]) => - pathForComponents(dir, components).asFile == fileForComponents(dir.asFile, components) ) - specify("Relative path", (dir: ProjectDirectory, a: List[String], b: List[String]) => - pathForComponents(pathForComponents(dir, a) ##, b).relativePath == pathString(b) ) - specify("Proper URL conversion", (path: Path) => path.asURL == path.asFile.toURI.toURL) - specify("Path equality", (dir: ProjectDirectory, components: List[String]) => - pathForComponents(dir, components) == pathForComponents(dir, components)) - specify("Base path equality", (dir: ProjectDirectory, a: List[String], b: List[String]) => - pathForComponents(pathForComponents(dir, a) ##, b) == pathForComponents(pathForComponents(dir, a) ##, b) ) - specify("hashCode", (path: Path) => path.hashCode == path.asFile.hashCode) - - // the relativize tests are a bit of a mess because of a few things: - // 1) relativization requires directories to exist - // 2) there is an IOException thrown in touch for paths that are too long (probably should limit the size of the Lists) - // These problems are addressed by the helper method createFileAndDo - - specify("relativize fail", (dir: ProjectDirectory, a: List[String], b: List[String]) => - { - (!a.contains("") && !b.contains("")) ==> - { - createFileAndDo(a, b) - { dir => - { - val shouldFail = (a == b) || !(b startsWith a) // will be true most of the time - val didFail = Path.relativize(pathForComponents(dir, a), pathForComponents(dir, b)).isEmpty - shouldFail == didFail - } + property("Project directory relative path empty") = secure { inTemp { dir => dir.relativePath.isEmpty } } + property("construction") = forAll { (components: List[String]) => + inTemp { dir => + pathForComponents(dir, components).asFile == fileForComponents(dir.asFile, components) + } + } + property("Relative path") = forAll { (a: List[String], b: List[String]) => + inTemp { dir => + pathForComponents(pathForComponents(dir, a) ##, b).relativePath == pathString(b) } + } + property("Proper URL conversion") = forAll { (tp: ToPath) => + withPath(tp) { path => path.asURL == path.asFile.toURI.toURL } + } + property("Path equality") = forAll { (components: List[String]) => + inTemp { dir => pathForComponents(dir, components) == pathForComponents(dir, components) } + } + property("Base path equality") = forAll { (a: List[String], b: List[String]) => + inTemp { dir => + pathForComponents(pathForComponents(dir, a) ##, b) == pathForComponents(pathForComponents(dir, a) ##, b) + } + } + + property("hashCode") = forAll { (tp: ToPath) => + withPath(tp) { path => + path.hashCode == path.asFile.hashCode + } + } + + property("relativize fail") = forAll { (a: List[String], b: List[String]) => + createFileAndDo(a, b) + { dir => + { + val shouldFail = (a == b) || !(b startsWith a) // will be true most of the time + val didFail = Path.relativize(pathForComponents(dir, a), pathForComponents(dir, b)).isEmpty + shouldFail == didFail } } - }) - specify("relativize", (a: List[String], b: List[String]) => - { - (!b.isEmpty && !a.contains("") && !b.contains("")) ==> + } + property("relativize") = forAll {(a: List[String], b: List[String]) => + (!b.isEmpty) ==> { createFileAndDo(a, b) { dir => @@ -62,9 +75,48 @@ object PathSpecification extends Properties("Path") } } } - }) - specify("fromString", (dir: ProjectDirectory, a: List[String]) => - pathForComponents(dir, a) == Path.fromString(dir, pathString(a))) + } + property("fromString") = forAll { (a: List[String]) => + inTemp { dir => + pathForComponents(dir, a) == Path.fromString(dir, pathString(a)) + } + } + + property("distinct") = forAll { (baseDirs: ToFinder, distinctNames: List[String], dupNames: List[(String, Int)]) => try { + inTemp { dir => + val bases = repeat(baseDirs(dir).get) + val reallyDistinct: Set[String] = Set() ++ distinctNames -- dupNames.map(_._1) + val dupList = dupNames.flatMap { case (name, repeat) => if(reallyDistinct(name)) Nil else List.make(repeat, name) } + + def create(names: List[String]): PathFinder = + { + val paths = (bases zip names ).map { case (a, b) => a / b }.filter(!_.exists) + paths.foreach { f => xsbt.FileUtilities.touch(f asFile) } + Path.lazyPathFinder(paths) + } + def names(s: scala.collection.Set[Path]) = s.map(_.name) + + val distinctPaths = create(reallyDistinct.toList) + val dupPaths = create(dupList) + + val all = distinctPaths +++ dupPaths + val distinct = all.distinct.get + + val allNames = Set() ++ names(all.get) + + (Set() ++ names(distinct)) == allNames && // verify nothing lost + distinct.size == allNames.size // verify duplicates removed + } } catch { case e => e.printStackTrace; throw e} + } + + private def repeat[T](s: Iterable[T]): List[T] = + List.make(100, ()).flatMap(_ => s) // should be an infinite Stream, but Stream isn't very lazy + + + private def withPath[T](tp: ToPath)(f: Path => T): T = + inTemp { f compose tp } + private def withPaths[T](ta: ToPath, tb: ToPath)(f: (Path, Path) => T): T = + inTemp { dir => f(ta(dir), tb(dir)) } private def createFileAndDo(a: List[String], b: List[String])(f: Path => Boolean) = { @@ -83,27 +135,61 @@ object PathSpecification extends Properties("Path") case Right(opt) => opt.isDefined ==> opt.get } } + private def inTemp[T](f: ProjectDirectory => T): T = + xsbt.FileUtilities.withTemporaryDirectory { dir => f(new ProjectDirectory(dir)) } private def pathString(components: List[String]): String = components.mkString(File.separator) private def pathForComponents(base: Path, components: List[String]): Path = - components.foldLeft(base)((path, component) => path / component) + components.filter(!_.isEmpty).foldLeft(base)((path, component) => path / component) private def fileForComponents(base: File, components: List[String]): File = components.foldLeft(base)((file, component) => new File(file, component)) - private def genPath: Gen[Path] = - for(projectPath <- arbitrary[ProjectDirectory]; - a <- arbitrary[List[String]]; - b <- arbitrary[Option[List[String]]]) - yield - { - val base = pathForComponents(projectPath, trim(a)) - b match - { - case None => base - case Some(relative) => pathForComponents(base ##, trim(relative)) + + private def paths(implicit d: Gen[ToPath], s: Gen[String]): Gen[ToPath] = + for(dir <- d; name <- s) yield { + (projectPath: ProjectDirectory) => + val f = dir(projectPath) / name + xsbt.FileUtilities.touch(f asFile) + f + } + + private def directories: Gen[ToFinder] = + for(dirs <- directoryList) yield { + (projectPath: ProjectDirectory) => Path.lazyPathFinder { dirs.map(_(projectPath)) } + } + private def directoryList: Gen[List[ToPath]] = genList(MaxDirectoryCount)(directory) + private def directory: Gen[ToPath] = + for(p <- genPath) yield { + (projectPath: ProjectDirectory) => { + val f = p(projectPath) + xsbt.FileUtilities.createDirectory(f asFile) + f } } + + private implicit lazy val genPath: Gen[ToPath] = + for(a <- arbitrary[List[String]]; + b <- arbitrary[Option[List[String]]]) + yield + (projectPath: ProjectDirectory) => + { + val base = pathForComponents(projectPath, a) + b match + { + case None => base + case Some(relative) => pathForComponents(base ##, relative) + } + } + private implicit lazy val componentList: Gen[List[String]] = genList[String](MaxComponentCount)(pathComponent.arbitrary) + + private def genList[A](maxSize: Int)(implicit genA: Gen[A]) = + for(size <- Gen.choose(0, maxSize); a <- Gen.listOfN(size, genA)) yield a + private def trim(components: List[String]): List[String] = components.take(MaxComponentCount) private def trim(component: String): String = component.substring(0, Math.min(component.length, MaxFilenameLength)) - val MaxFilenameLength = 20 - val MaxComponentCount = 6 + + final val MaxFilenameLength = 20 + final val MaxComponentCount = 6 + final val MaxDirectoryCount = 10 + final val MaxFilesCount = 100 + final val MaxDuplicates = 10 } \ No newline at end of file diff --git a/util/io/FileUtilities.scala b/util/io/FileUtilities.scala index 06034befd..7b9c583a0 100644 --- a/util/io/FileUtilities.scala +++ b/util/io/FileUtilities.scala @@ -74,10 +74,8 @@ object FileUtilities { createDirectory(file.getParentFile) val created = translate("Could not create file " + file) { file.createNewFile() } - if(created) + if(created || file.isDirectory) () - else if(file.isDirectory) - error("File exists and is a directory.") else if(!file.setLastModified(System.currentTimeMillis)) error("Could not update last modified time for file " + file) }