From 0172d118afa8f0ef43a88ffa028388ac6a552798 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Tue, 9 Jul 2019 13:46:59 -0700 Subject: [PATCH] Add parser for file size At the suggestion of @eed3si9n, instead of specifying the file cache size in bytes, we now specify it in a formatted string. For example, instead of specifying 128 megabytes in bytes (134217728), we can specify it with the string "128M". --- .../sbt/internal/util/complete/Parsers.scala | 6 ++ .../internal/util/complete/SizeParser.scala | 56 +++++++++++++++++ .../util/complete/SizeParserSpec.scala | 63 +++++++++++++++++++ main/src/main/scala/sbt/Defaults.scala | 1 + main/src/main/scala/sbt/Keys.scala | 2 +- main/src/main/scala/sbt/Main.scala | 8 ++- .../src/main/scala/sbt/internal/SysProp.scala | 5 +- 7 files changed, 137 insertions(+), 4 deletions(-) create mode 100644 internal/util-complete/src/main/scala/sbt/internal/util/complete/SizeParser.scala create mode 100644 internal/util-complete/src/test/scala/sbt/internal/util/complete/SizeParserSpec.scala diff --git a/internal/util-complete/src/main/scala/sbt/internal/util/complete/Parsers.scala b/internal/util-complete/src/main/scala/sbt/internal/util/complete/Parsers.scala index 037abccc8..3560ef2f6 100644 --- a/internal/util-complete/src/main/scala/sbt/internal/util/complete/Parsers.scala +++ b/internal/util-complete/src/main/scala/sbt/internal/util/complete/Parsers.scala @@ -219,6 +219,12 @@ trait Parsers { (DQuoteChar ~> (NotDQuoteBackslashClass | EscapeSequence).+.string <~ DQuoteChar | (DQuoteChar ~ DQuoteChar) ^^^ "") + /** + * Parses a size unit string. For example, `128K` parsers to `128L * 1024`, and `1.25g` parses + * to `1024L * 1024 * 1024 * 5 / 4`. + */ + lazy val Size: Parser[Long] = SizeParser.value + /** * Parses a brace enclosed string and, if each opening brace is matched with a closing brace, * it returns the entire string including the braces. diff --git a/internal/util-complete/src/main/scala/sbt/internal/util/complete/SizeParser.scala b/internal/util-complete/src/main/scala/sbt/internal/util/complete/SizeParser.scala new file mode 100644 index 000000000..7e4bef15e --- /dev/null +++ b/internal/util-complete/src/main/scala/sbt/internal/util/complete/SizeParser.scala @@ -0,0 +1,56 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util.complete + +import sbt.internal.util.complete.DefaultParsers._ + +private[sbt] object SizeParser { + def apply(s: String): Option[Long] = Parser.parse(s, value).toOption + private sealed trait SizeUnit + private case object Bytes extends SizeUnit + private case object KiloBytes extends SizeUnit + private case object MegaBytes extends SizeUnit + private case object GigaBytes extends SizeUnit + private def parseDouble(s: String): Parser[Either[Double, Long]] = + try Parser.success(Left(java.lang.Double.valueOf(s))) + catch { case _: NumberFormatException => Parser.failure(s"Couldn't parse $s as double.") } + private def parseLong(s: String): Parser[Either[Double, Long]] = + try Parser.success(Right(java.lang.Long.valueOf(s))) + catch { case _: NumberFormatException => Parser.failure(s"Couldn't parse $s as double.") } + private[this] val digit = charClass(_.isDigit, "digit") + private[this] val numberParser: Parser[Either[Double, Long]] = + (digit.+ ~ ('.'.examples() ~> digit.+).?).flatMap { + case (leading, Some(decimalPart)) => + parseDouble(s"${leading.mkString}.${decimalPart.mkString}") + case (leading, _) => parseLong(leading.mkString) + } + private[this] val unitParser: Parser[SizeUnit] = + token("b" | "B" | "g" | "G" | "k" | "K" | "m" | "M").map { + case "b" | "B" => Bytes + case "g" | "G" => GigaBytes + case "k" | "K" => KiloBytes + case "m" | "M" => MegaBytes + } + private[this] def multiply(left: Either[Double, Long], right: Long): Long = left match { + case Left(d) => (d * right).toLong + case Right(l) => l * right + } + private[sbt] val value: Parser[Long] = + ((numberParser <~ SpaceClass + .examples(" ", "b", "B", "g", "G", "k", "K", "m", "M") + .*) ~ unitParser.?) + .map { + case (number, unit) => + unit match { + case None | Some(Bytes) => multiply(number, right = 1) + case Some(KiloBytes) => multiply(number, right = 1024) + case Some(MegaBytes) => multiply(number, right = 1024 * 1024) + case Some(GigaBytes) => multiply(number, right = 1024 * 1024 * 1024) + } + } +} diff --git a/internal/util-complete/src/test/scala/sbt/internal/util/complete/SizeParserSpec.scala b/internal/util-complete/src/test/scala/sbt/internal/util/complete/SizeParserSpec.scala new file mode 100644 index 000000000..521f2d10a --- /dev/null +++ b/internal/util-complete/src/test/scala/sbt/internal/util/complete/SizeParserSpec.scala @@ -0,0 +1,63 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util.complete + +import org.scalatest.FlatSpec + +class SizeParserSpec extends FlatSpec { + "SizeParser" should "handle raw bytes" in { + assert(Parser.parse(str = "123456", SizeParser.value) == Right(123456L)) + } + it should "handle bytes" in { + assert(Parser.parse(str = "123456b", SizeParser.value) == Right(123456L)) + assert(Parser.parse(str = "123456B", SizeParser.value) == Right(123456L)) + assert(Parser.parse(str = "123456 b", SizeParser.value) == Right(123456L)) + assert(Parser.parse(str = "123456 B", SizeParser.value) == Right(123456L)) + } + it should "handle kilobytes" in { + assert(Parser.parse(str = "123456k", SizeParser.value) == Right(123456L * 1024)) + assert(Parser.parse(str = "123456K", SizeParser.value) == Right(123456L * 1024)) + assert(Parser.parse(str = "123456 K", SizeParser.value) == Right(123456L * 1024)) + assert(Parser.parse(str = "123456 K", SizeParser.value) == Right(123456L * 1024)) + } + it should "handle megabytes" in { + assert(Parser.parse(str = "123456m", SizeParser.value) == Right(123456L * 1024 * 1024)) + assert(Parser.parse(str = "123456M", SizeParser.value) == Right(123456L * 1024 * 1024)) + assert(Parser.parse(str = "123456 M", SizeParser.value) == Right(123456L * 1024 * 1024)) + assert(Parser.parse(str = "123456 M", SizeParser.value) == Right(123456L * 1024 * 1024)) + } + it should "handle gigabytes" in { + assert(Parser.parse(str = "123456g", SizeParser.value) == Right(123456L * 1024 * 1024 * 1024)) + assert(Parser.parse(str = "123456G", SizeParser.value) == Right(123456L * 1024 * 1024 * 1024)) + assert(Parser.parse(str = "123456 G", SizeParser.value) == Right(123456L * 1024 * 1024 * 1024)) + assert(Parser.parse(str = "123456 G", SizeParser.value) == Right(123456L * 1024 * 1024 * 1024)) + } + it should "handle doubles" in { + assert(Parser.parse(str = "1.25g", SizeParser.value) == Right(5L * 1024 * 1024 * 1024 / 4)) + assert(Parser.parse(str = "1.25 g", SizeParser.value) == Right(5L * 1024 * 1024 * 1024 / 4)) + assert(Parser.parse(str = "1.25 g", SizeParser.value) == Right(5L * 1024 * 1024 * 1024 / 4)) + assert(Parser.parse(str = "1.25 G", SizeParser.value) == Right(5L * 1024 * 1024 * 1024 / 4)) + } + private val expectedCompletions = Set("", "b", "B", "g", "G", "k", "K", "m", "M", " ") + it should "have completions for long" in { + val completions = Parser.completions(SizeParser.value, "123", level = 0).get.map(_.display) + assert(completions == expectedCompletions) + } + it should "have completions for long with spaces" in { + val completions = Parser.completions(SizeParser.value, "123", level = 0).get.map(_.display) + assert(completions == expectedCompletions) + } + it should "have completions for double " in { + val completions = Parser.completions(SizeParser.value, "1.25", level = 0).get.map(_.display) + assert(completions == expectedCompletions) + } + it should "have completions for double with spaces" in { + val completions = Parser.completions(SizeParser.value, "1.25 ", level = 0).get.map(_.display) + assert(completions == expectedCompletions + "") + } +} diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 56a2aa864..3d3ea1dc3 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -258,6 +258,7 @@ object Defaults extends BuildCommon { checkBuildSources / Continuous.dynamicInputs := None, checkBuildSources / fileInputs := CheckBuildSources.buildSourceFileInputs.value, checkBuildSources := CheckBuildSources.needReloadImpl.value, + fileCacheSize := "128M", trapExit :== true, connectInput :== false, cancelable :== true, diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 7a1a62369..22fde35ec 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -489,7 +489,7 @@ object Keys { val globalPluginUpdate = taskKey[UpdateReport]("A hook to get the UpdateReport of the global plugin.").withRank(DTask) private[sbt] val taskCancelStrategy = settingKey[State => TaskCancellationStrategy]("Experimental task cancellation handler.").withRank(DTask) private[sbt] val cacheStoreFactory = AttributeKey[CacheStoreFactoryFactory]("cache-store-factory") - val fileCacheSize = settingKey[Long]("The approximate maximum size in bytes of the cache used to store previous task results.") + val fileCacheSize = settingKey[String]("The approximate maximum size in bytes of the cache used to store previous task results. For example, it could be set to \"256M\" to make the maximum size 256 megabytes.") // Experimental in sbt 0.13.2 to enable grabbing semantic compile failures. private[sbt] val compilerReporter = taskKey[xsbti.Reporter]("Experimental hook to listen (or send) compilation failure messages.").withRank(DTask) diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index 3f8111c5c..c0221798b 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -23,7 +23,7 @@ import sbt.internal._ import sbt.internal.inc.ScalaInstance import sbt.internal.util.Types.{ const, idFun } import sbt.internal.util._ -import sbt.internal.util.complete.Parser +import sbt.internal.util.complete.{ SizeParser, Parser } import sbt.io._ import sbt.io.syntax._ import sbt.util.{ Level, Logger, Show } @@ -848,7 +848,11 @@ object BuiltinCommands { } private val addCacheStoreFactoryFactory: State => State = (s: State) => { - val size = Project.extract(s).getOpt(Keys.fileCacheSize).getOrElse(SysProp.fileCacheSize) + val size = Project + .extract(s) + .getOpt(Keys.fileCacheSize) + .flatMap(SizeParser(_)) + .getOrElse(SysProp.fileCacheSize) s.get(Keys.cacheStoreFactory).foreach(_.close()) s.put(Keys.cacheStoreFactory, InMemoryCacheStore.factory(size)) } diff --git a/main/src/main/scala/sbt/internal/SysProp.scala b/main/src/main/scala/sbt/internal/SysProp.scala index 394248410..81bb987f1 100644 --- a/main/src/main/scala/sbt/internal/SysProp.scala +++ b/main/src/main/scala/sbt/internal/SysProp.scala @@ -9,8 +9,10 @@ package sbt package internal import java.util.Locale + import scala.util.control.NonFatal import sbt.internal.util.ConsoleAppender +import sbt.internal.util.complete.SizeParser // See also BuildPaths.scala // See also LineReader.scala @@ -84,7 +86,8 @@ object SysProp { def closeClassLoaders: Boolean = getOrTrue("sbt.classloader.close") - def fileCacheSize: Long = long("sbt.file.cache.size", 128 * 1024 * 1024) + def fileCacheSize: Long = + SizeParser(System.getProperty("sbt.file.cache.size", "128M")).getOrElse(128 * 1024 * 1024) def supershell: Boolean = color && getOrTrue("sbt.supershell") def supershellSleep: Long = long("sbt.supershell.sleep", 100L)