// Copyright 2014 The Bazel Authors. 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 static com.google.common.truth.Truth.assertThat; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; 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.lib.util.Preconditions; import com.google.devtools.build.skyframe.SkyFunction.Environment; import com.google.devtools.build.skyframe.SkyFunctionException.Transience; 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 { public static final SkyFunctionName NODE_TYPE = SkyFunctionName.FOR_TESTING; private final ImmutableMap<SkyFunctionName, ? extends SkyFunction> functionMap = ImmutableMap.of(GraphTester.NODE_TYPE, new DelegatingFunction()); 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 ImmutableSet<SkyKey> getModifiedValues() { return ImmutableSet.copyOf(modifiedValues); } public void clearModifiedValues() { modifiedValues.clear(); } 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(), "%s %s %s", dep, value, 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 LegacySkyKey.create(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 unsetComputedValue() { this.computer = null; 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] = LegacySkyKey.create(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 ImmutableMap<SkyFunctionName, ? extends SkyFunction> getSkyFunctionMap() { return functionMap; } /** * Simple value class that stores strings. */ public static class StringValue implements SkyValue { protected 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(); } public static StringValue of(String string) { return new StringValue(string); } public static StringValue from(SkyValue skyValue) { assertThat(skyValue).isInstanceOf(StringValue.class); return (StringValue) skyValue; } } /** A StringValue that is also a NotComparableSkyValue. */ public static class NotComparableStringValue extends StringValue implements NotComparableSkyValue { public NotComparableStringValue(String value) { super(value); } @Override public boolean equals(Object o) { throw new UnsupportedOperationException(value + " is incomparable - what are you doing?"); } @Override public int hashCode() { throw new UnsupportedOperationException(value + " is incomparable - what are you doing?"); } } /** * 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()); } }; public static ValueComputer formatter(final SkyKey key, final String format) { return new ValueComputer() { @Override public SkyValue compute(Map<SkyKey, SkyValue> deps, Environment env) throws InterruptedException { return StringValue.of(String.format(format, StringValue.from(deps.get(key)).getValue())); } }; } }