Update from Google.
--
MOE_MIGRATED_REVID=85702957
diff --git a/src/test/java/com/google/devtools/build/skyframe/GraphTester.java b/src/test/java/com/google/devtools/build/skyframe/GraphTester.java
new file mode 100644
index 0000000..6095bb8
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/skyframe/GraphTester.java
@@ -0,0 +1,340 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.skyframe;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * A helper class to create graphs and run skyframe tests over these graphs.
+ *
+ * <p>There are two types of values, computing values, which may not be set to a constant value,
+ * and leaf values, which must be set to a constant value and may not have any dependencies.
+ *
+ * <p>Note that the value builder looks into the test values created here to determine how to
+ * behave. However, skyframe will only re-evaluate the value and call the value builder if any of
+ * its dependencies has changed. That means in order to change the set of dependencies of a value,
+ * you need to also change one of its previous dependencies to force re-evaluation. Changing a
+ * computing value does not mark it as modified.
+ */
+public class GraphTester {
+
+ // TODO(bazel-team): Split this for computing and non-computing values?
+ public static final SkyFunctionName NODE_TYPE = new SkyFunctionName("Type", false);
+
+ private final Map<SkyKey, TestFunction> values = new HashMap<>();
+ private final Set<SkyKey> modifiedValues = new LinkedHashSet<>();
+
+ public TestFunction getOrCreate(String name) {
+ return getOrCreate(skyKey(name));
+ }
+
+ public TestFunction getOrCreate(SkyKey key) {
+ return getOrCreate(key, false);
+ }
+
+ public TestFunction getOrCreate(SkyKey key, boolean markAsModified) {
+ TestFunction result = values.get(key);
+ if (result == null) {
+ result = new TestFunction();
+ values.put(key, result);
+ } else if (markAsModified) {
+ modifiedValues.add(key);
+ }
+ return result;
+ }
+
+ public TestFunction set(String key, SkyValue value) {
+ return set(skyKey(key), value);
+ }
+
+ public TestFunction set(SkyKey key, SkyValue value) {
+ return getOrCreate(key, true).setConstantValue(value);
+ }
+
+ public Collection<SkyKey> getModifiedValues() {
+ return modifiedValues;
+ }
+
+ public SkyFunction getFunction() {
+ return new SkyFunction() {
+ @Override
+ public SkyValue compute(SkyKey key, Environment env)
+ throws SkyFunctionException, InterruptedException {
+ TestFunction builder = values.get(key);
+ Preconditions.checkState(builder != null, "No TestFunction for " + key);
+ if (builder.builder != null) {
+ return builder.builder.compute(key, env);
+ }
+ if (builder.warning != null) {
+ env.getListener().handle(Event.warn(builder.warning));
+ }
+ if (builder.progress != null) {
+ env.getListener().handle(Event.progress(builder.progress));
+ }
+ Map<SkyKey, SkyValue> deps = new LinkedHashMap<>();
+ boolean oneMissing = false;
+ for (Pair<SkyKey, SkyValue> dep : builder.deps) {
+ SkyValue value;
+ if (dep.second == null) {
+ value = env.getValue(dep.first);
+ } else {
+ try {
+ value = env.getValueOrThrow(dep.first, SomeErrorException.class);
+ } catch (SomeErrorException e) {
+ value = dep.second;
+ }
+ }
+ if (value == null) {
+ oneMissing = true;
+ } else {
+ deps.put(dep.first, value);
+ }
+ Preconditions.checkState(oneMissing == env.valuesMissing());
+ }
+ if (env.valuesMissing()) {
+ return null;
+ }
+
+ if (builder.hasTransientError) {
+ throw new GenericFunctionException(new SomeErrorException(key.toString()),
+ Transience.TRANSIENT);
+ }
+ if (builder.hasError) {
+ throw new GenericFunctionException(new SomeErrorException(key.toString()),
+ Transience.PERSISTENT);
+ }
+
+ if (builder.value != null) {
+ return builder.value;
+ }
+
+ if (Thread.currentThread().isInterrupted()) {
+ throw new InterruptedException(key.toString());
+ }
+
+ return builder.computer.compute(deps, env);
+ }
+
+ @Nullable
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return values.get(skyKey).tag;
+ }
+ };
+ }
+
+ public static SkyKey skyKey(String key) {
+ return new SkyKey(NODE_TYPE, key);
+ }
+
+ /**
+ * A value in the testing graph that is constructed in the tester.
+ */
+ public class TestFunction {
+ // TODO(bazel-team): We could use a multiset here to simulate multi-pass dependency discovery.
+ private final Set<Pair<SkyKey, SkyValue>> deps = new LinkedHashSet<>();
+ private SkyValue value;
+ private ValueComputer computer;
+ private SkyFunction builder = null;
+
+ private boolean hasTransientError;
+ private boolean hasError;
+
+ private String warning;
+ private String progress;
+
+ private String tag;
+
+ public TestFunction addDependency(String name) {
+ return addDependency(skyKey(name));
+ }
+
+ public TestFunction addDependency(SkyKey key) {
+ deps.add(Pair.<SkyKey, SkyValue>of(key, null));
+ return this;
+ }
+
+ public TestFunction removeDependency(String name) {
+ return removeDependency(skyKey(name));
+ }
+
+ public TestFunction removeDependency(SkyKey key) {
+ deps.remove(Pair.<SkyKey, SkyValue>of(key, null));
+ return this;
+ }
+
+ public TestFunction addErrorDependency(String name, SkyValue altValue) {
+ return addErrorDependency(skyKey(name), altValue);
+ }
+
+ public TestFunction addErrorDependency(SkyKey key, SkyValue altValue) {
+ deps.add(Pair.of(key, altValue));
+ return this;
+ }
+
+ public TestFunction setConstantValue(SkyValue value) {
+ Preconditions.checkState(this.computer == null);
+ this.value = value;
+ return this;
+ }
+
+ public TestFunction setComputedValue(ValueComputer computer) {
+ Preconditions.checkState(this.value == null);
+ this.computer = computer;
+ return this;
+ }
+
+ public TestFunction setBuilder(SkyFunction builder) {
+ Preconditions.checkState(this.value == null);
+ Preconditions.checkState(this.computer == null);
+ Preconditions.checkState(deps.isEmpty());
+ Preconditions.checkState(!hasTransientError);
+ Preconditions.checkState(!hasError);
+ Preconditions.checkState(warning == null);
+ Preconditions.checkState(progress == null);
+ this.builder = builder;
+ return this;
+ }
+
+ public TestFunction setHasTransientError(boolean hasError) {
+ this.hasTransientError = hasError;
+ return this;
+ }
+
+ public TestFunction setHasError(boolean hasError) {
+ // TODO(bazel-team): switch to an enum for hasError.
+ this.hasError = hasError;
+ return this;
+ }
+
+ public TestFunction setWarning(String warning) {
+ this.warning = warning;
+ return this;
+ }
+
+ public TestFunction setProgress(String info) {
+ this.progress = info;
+ return this;
+ }
+
+ public TestFunction setTag(String tag) {
+ this.tag = tag;
+ return this;
+ }
+
+ }
+
+ public static SkyKey[] toSkyKeys(String... names) {
+ SkyKey[] result = new SkyKey[names.length];
+ for (int i = 0; i < names.length; i++) {
+ result[i] = new SkyKey(GraphTester.NODE_TYPE, names[i]);
+ }
+ return result;
+ }
+
+ public static SkyKey toSkyKey(String name) {
+ return toSkyKeys(name)[0];
+ }
+
+ private class DelegatingFunction implements SkyFunction {
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException,
+ InterruptedException {
+ return getFunction().compute(skyKey, env);
+ }
+
+ @Nullable
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return getFunction().extractTag(skyKey);
+ }
+ }
+
+ public DelegatingFunction createDelegatingFunction() {
+ return new DelegatingFunction();
+ }
+
+ /**
+ * Simple value class that stores strings.
+ */
+ public static class StringValue implements SkyValue {
+ private final String value;
+
+ public StringValue(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof StringValue)) {
+ return false;
+ }
+ return value.equals(((StringValue) o).value);
+ }
+
+ @Override
+ public int hashCode() {
+ return value.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return "StringValue: " + getValue();
+ }
+ }
+
+ /**
+ * A callback interface to provide the value computation.
+ */
+ public interface ValueComputer {
+ /** This is called when all the declared dependencies exist. It may request new dependencies. */
+ SkyValue compute(Map<SkyKey, SkyValue> deps, SkyFunction.Environment env)
+ throws InterruptedException;
+ }
+
+ public static final ValueComputer COPY = new ValueComputer() {
+ @Override
+ public SkyValue compute(Map<SkyKey, SkyValue> deps, SkyFunction.Environment env) {
+ return Iterables.getOnlyElement(deps.values());
+ }
+ };
+
+ public static final ValueComputer CONCATENATE = new ValueComputer() {
+ @Override
+ public SkyValue compute(Map<SkyKey, SkyValue> deps, SkyFunction.Environment env) {
+ StringBuilder result = new StringBuilder();
+ for (SkyValue value : deps.values()) {
+ result.append(((StringValue) value).value);
+ }
+ return new StringValue(result.toString());
+ }
+ };
+}