diff options
-rw-r--r-- | core/src/main/scala/forge/define/Task.scala | 17 | ||||
-rw-r--r-- | core/src/test/scala/forge/EvaluationTests.scala | 132 | ||||
-rw-r--r-- | core/src/test/scala/forge/JavaCompileJarTests.scala | 3 | ||||
-rw-r--r-- | core/src/test/scala/forge/TestUtil.scala | 4 | ||||
-rw-r--r-- | scalaplugin/src/main/scala/forge/scalaplugin/Subproject.scala | 4 |
5 files changed, 116 insertions, 44 deletions
diff --git a/core/src/main/scala/forge/define/Task.scala b/core/src/main/scala/forge/define/Task.scala index a78fcaa7..298815d5 100644 --- a/core/src/main/scala/forge/define/Task.scala +++ b/core/src/main/scala/forge/define/Task.scala @@ -44,19 +44,24 @@ object Task extends Applicative.Applyer[Task, Task, Args]{ val inputs = Nil def evaluate(args: Args) = t0 } + + def apply[T](t: T): Target[T] = macro targetCachedImpl[T] def apply[T](t: Task[T]): Target[T] = macro Cacher.impl0[Task, T] - def cmd[T](t: T): Command[T] = macro targetCommandImpl[T] + def command[T](t: T): Command[T] = macro targetCommandImpl[T] + def command[T](t: Task[T]): Command[T] = new Command(t) + + def task[T](t: T): Task[T] = macro Applicative.impl[Task, T, Args] + def task[T](t: Task[T]): Task[T] = t + + def targetCommandImpl[T: c.WeakTypeTag](c: Context)(t: c.Expr[T]): c.Expr[Command[T]] = { import c.universe._ + c.Expr[Command[T]]( - q"new forge.define.Command(${Applicative.impl[Task, T, Args](c)(t).tree})" + q"new ${weakTypeOf[Command[T]]}(${Applicative.impl[Task, T, Args](c)(t).tree})" ) } - def task[T](t: T): Task[T] = macro Applicative.impl[Task, T, Args] - - def apply[T](t: T): Target[T] = macro targetCachedImpl[T] - def targetCachedImpl[T: c.WeakTypeTag](c: Context)(t: c.Expr[T]): c.Expr[Target[T]] = { c.Expr[Target[T]]( forge.define.Cacher.wrapCached(c)( diff --git a/core/src/test/scala/forge/EvaluationTests.scala b/core/src/test/scala/forge/EvaluationTests.scala index 72b7deff..5fe29446 100644 --- a/core/src/test/scala/forge/EvaluationTests.scala +++ b/core/src/test/scala/forge/EvaluationTests.scala @@ -1,7 +1,9 @@ package forge -import forge.define.Task +import forge.TestUtil.{Test, test} +import forge.define.{Target, Task} +import forge.define.Task.Cacher import forge.discover.Discovered import forge.eval.Evaluator import forge.util.OSet @@ -9,43 +11,45 @@ import utest._ import utest.framework.TestPath object EvaluationTests extends TestSuite{ + class Checker[T: Discovered](base: T)(implicit tp: TestPath) { + val workspace = ammonite.ops.pwd / 'target / 'workspace / tp.value + ammonite.ops.rm(ammonite.ops.Path(workspace, ammonite.ops.pwd)) + // Make sure data is persisted even if we re-create the evaluator each time + def evaluator = new Evaluator(workspace, Discovered.mapping(base)) + + def apply(target: Task[_], expValue: Any, + expEvaled: OSet[Task[_]], + extraEvaled: Int = 0, + secondRun: Boolean = true) = { + + val Evaluator.Results(returnedValues, returnedEvaluated) = evaluator.evaluate(OSet(target)) + + val (matchingReturnedEvaled, extra) = returnedEvaluated.items.partition(expEvaled.contains) + + assert( + returnedValues == Seq(expValue), + matchingReturnedEvaled.toSet == expEvaled.toSet, + extra.length == extraEvaled + ) + + // Second time the value is already cached, so no evaluation needed + if (secondRun){ + val Evaluator.Results(returnedValues2, returnedEvaluated2) = evaluator.evaluate(OSet(target)) + val expecteSecondRunEvaluated = OSet() + assert( + returnedValues2 == returnedValues, + returnedEvaluated2 == expecteSecondRunEvaluated + ) + } + } + } + val tests = Tests{ val graphs = new TestGraphs() import graphs._ 'evaluateSingle - { - class Checker[T: Discovered](base: T)(implicit tp: TestPath) { - val workspace = ammonite.ops.pwd / 'target / 'workspace / tp.value - ammonite.ops.rm(ammonite.ops.Path(workspace, ammonite.ops.pwd)) - // Make sure data is persisted even if we re-create the evaluator each time - def evaluator = new Evaluator( - workspace, - - Discovered.mapping(base) - ) - def apply(target: Task[_], expValue: Any, - expEvaled: OSet[Task[_]], - extraEvaled: Int = 0) = { - - val Evaluator.Results(returnedValues, returnedEvaluated) = evaluator.evaluate(OSet(target)) - - val (matchingReturnedEvaled, extra) = returnedEvaluated.items.partition(expEvaled.contains) - - assert( - returnedValues == Seq(expValue), - matchingReturnedEvaled.toSet == expEvaled.toSet, - extra.length == extraEvaled - ) - - // Second time the value is already cached, so no evaluation needed - val Evaluator.Results(returnedValues2, returnedEvaluated2) = evaluator.evaluate(OSet(target)) - assert( - returnedValues2 == returnedValues, - returnedEvaluated2 == OSet() - ) - } - } 'singleton - { import singleton._ @@ -142,6 +146,70 @@ object EvaluationTests extends TestSuite{ } } + 'evaluateMixed - { + 'tasksAreUncached - { + // ___ left ___ + // / \ + // up middle -- down + // / + // right + object taskDiamond extends Cacher{ + var leftCount = 0 + var rightCount = 0 + var middleCount = 0 + def up = T{ test() } + def left = T.task{ leftCount += 1; up() + 1 } + def middle = T.task{ middleCount += 1; 100 } + def right = T{ rightCount += 1; 10000 } + def down = T{ left() + middle() + right() } + } + + import taskDiamond._ + + // Ensure task objects themselves are not cached, and recomputed each time + assert( + up eq up, + left ne left, + middle ne middle, + right eq right, + down eq down + ) + + // During the first evaluation, they get computed normally like any + // cached target + val check = new Checker(taskDiamond) + assert(leftCount == 0, rightCount == 0) + check(down, expValue = 10101, expEvaled = OSet(up, right, down), extraEvaled = 8) + assert(leftCount == 1, middleCount == 1, rightCount == 1) + + // If the upstream `up` doesn't change, the entire block of tasks + // doesn't need to recompute + check(down, expValue = 10101, expEvaled = OSet()) + assert(leftCount == 1, middleCount == 1, rightCount == 1) + + // But if `up` changes, the entire block of downstream tasks needs to + // recompute together, including `middle` which doesn't depend on `up`, + // because tasks have no cached value that can be used. `right`, which + // is a cached Target, does not recompute + up.inputs(0).asInstanceOf[Test].counter += 1 + check(down, expValue = 10102, expEvaled = OSet(up, down), extraEvaled = 6) + assert(leftCount == 2, middleCount == 2, rightCount == 1) + + // Running the tasks themselves results in them being recomputed every + // single time, even if nothing changes + check(left, expValue = 2, expEvaled = OSet(), extraEvaled = 1, secondRun = false) + assert(leftCount == 3, middleCount == 2, rightCount == 1) + check(left, expValue = 2, expEvaled = OSet(), extraEvaled = 1, secondRun = false) + assert(leftCount == 4, middleCount == 2, rightCount == 1) + + check(middle, expValue = 100, expEvaled = OSet(), extraEvaled = 2, secondRun = false) + assert(leftCount == 4, middleCount == 3, rightCount == 1) + check(middle, expValue = 100, expEvaled = OSet(), extraEvaled = 2, secondRun = false) + assert(leftCount == 4, middleCount == 4, rightCount == 1) + } + + + } } } diff --git a/core/src/test/scala/forge/JavaCompileJarTests.scala b/core/src/test/scala/forge/JavaCompileJarTests.scala index fcd81588..1c9bf9b8 100644 --- a/core/src/test/scala/forge/JavaCompileJarTests.scala +++ b/core/src/test/scala/forge/JavaCompileJarTests.scala @@ -44,8 +44,7 @@ object JavaCompileJarTests extends TestSuite{ def classFiles = T{ compileAll(Task.ctx().dest, allSources()) } def jar = T{ jarUp(resourceRoot, classFiles) } - @forge.discover.Router.main - def run(mainClsName: String): Task[CommandResult] = T.cmd{ + def run(mainClsName: String) = T.command{ %%('java, "-cp", classFiles().path, mainClsName) } } diff --git a/core/src/test/scala/forge/TestUtil.scala b/core/src/test/scala/forge/TestUtil.scala index 91dc3da9..1ae60e6d 100644 --- a/core/src/test/scala/forge/TestUtil.scala +++ b/core/src/test/scala/forge/TestUtil.scala @@ -16,10 +16,10 @@ object TestUtil { * controlled externally, so you can construct arbitrary dataflow graphs and * test how changes propagate. */ - class Test(val inputs: Seq[Task[Int]], + class Test(override val inputs: Seq[Task[Int]], val pure: Boolean) extends Task[Int]{ var counter = 0 - def evaluate(args: Args) = { + override def evaluate(args: Args) = { counter + args.args.map(_.asInstanceOf[Int]).sum } diff --git a/scalaplugin/src/main/scala/forge/scalaplugin/Subproject.scala b/scalaplugin/src/main/scala/forge/scalaplugin/Subproject.scala index 0ec22614..a94dac6b 100644 --- a/scalaplugin/src/main/scala/forge/scalaplugin/Subproject.scala +++ b/scalaplugin/src/main/scala/forge/scalaplugin/Subproject.scala @@ -183,12 +183,12 @@ abstract class Subproject extends Cacher{ def classpath = T{ Seq(resources(), compiled()) } def jar = T{ modules.Jvm.jarUp(resources, compiled) } - def run(mainClass: String) = T.cmd{ + def run(mainClass: String) = T.command{ import ammonite.ops._, ImplicitWd._ %('java, "-cp", (runDepClasspath().map(_.path) :+ compiled()).mkString(":"), mainClass) } - def console() = T.cmd{ + def console() = T.command{ import ammonite.ops._, ImplicitWd._ %('java, "-cp", |