| // 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.base.Preconditions; |
| import com.google.common.collect.ImmutableList; |
| 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.events.ExtendedEventHandler.Postable; |
| import com.google.devtools.build.lib.skyframe.serialization.VisibleForSerialization; |
| import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec; |
| import com.google.devtools.build.lib.util.Pair; |
| import com.google.devtools.build.skyframe.SkyFunctionException.Transience; |
| import com.google.devtools.build.skyframe.SkyframeLookupResult.QueryDepCallback; |
| import com.google.errorprone.annotations.CanIgnoreReturnValue; |
| import java.util.HashMap; |
| import java.util.LinkedHashMap; |
| import java.util.LinkedHashSet; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.concurrent.atomic.AtomicReference; |
| 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; |
| |
| /** If true, uses the {@link SkyframeLookupResult#queryDep} interface to retrieve values. */ |
| private boolean useQueryDep = false; |
| |
| private final Map<SkyFunctionName, SkyFunction> functionMap = new HashMap<>(); |
| |
| private final Map<SkyKey, TestFunction> values = new HashMap<>(); |
| private final Set<SkyKey> modifiedValues = new LinkedHashSet<>(); |
| |
| public GraphTester() { |
| functionMap.put(NODE_TYPE, new DelegatingFunction()); |
| functionMap.put(FOR_TESTING_NONHERMETIC, new DelegatingFunction()); |
| } |
| |
| public void setUseQueryDep(boolean useQueryDep) { |
| this.useQueryDep = useQueryDep; |
| } |
| |
| 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)); |
| } |
| if (builder.errorEvent != null) { |
| env.getListener().handle(Event.error(builder.errorEvent)); |
| } |
| if (builder.postable != null) { |
| env.getListener().post(builder.postable); |
| } |
| Map<SkyKey, SkyValue> deps = new LinkedHashMap<>(); |
| boolean oneMissing = false; |
| for (Pair<SkyKey, SkyValue> dep : builder.deps) { |
| SkyValue value = useQueryDep ? getValueUsingQueryDep(dep, env) : getValue(dep, env); |
| 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) { |
| TestFunction builder = values.get(skyKey); |
| if (builder.builder != null) { |
| return builder.builder.extractTag(skyKey); |
| } |
| return builder.tag; |
| } |
| }; |
| } |
| |
| private static SkyValue getValue(Pair<SkyKey, SkyValue> dep, SkyFunction.Environment env) |
| throws InterruptedException { |
| 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; |
| } |
| } |
| return value; |
| } |
| |
| private static SkyValue getValueUsingQueryDep( |
| Pair<SkyKey, SkyValue> dep, SkyFunction.Environment env) throws InterruptedException { |
| SkyValue value; |
| var lookupResult = env.getValuesAndExceptions(ImmutableList.of(dep.first)); |
| if (dep.second == null) { |
| var valueRef = new AtomicReference<SkyValue>(); |
| var gotValue = |
| lookupResult.queryDep( |
| dep.first, |
| (k, v) -> { |
| assertThat(k).isEqualTo(dep.first); |
| valueRef.set(v); |
| }); |
| if ((value = valueRef.get()) != null) { |
| assertThat(gotValue).isTrue(); |
| } else { |
| assertThat(gotValue).isFalse(); |
| } |
| } else { |
| var valueRef = new AtomicReference<SkyValue>(); |
| var exceptionRef = new AtomicReference<SomeErrorException>(); |
| var gotValue = |
| lookupResult.queryDep( |
| dep.first, |
| new QueryDepCallback() { |
| @Override |
| public void acceptValue(SkyKey key, SkyValue value) { |
| assertThat(key).isEqualTo(dep.first); |
| valueRef.set(value); |
| } |
| |
| @Override |
| public boolean tryHandleException(SkyKey key, Exception e) { |
| assertThat(key).isEqualTo(dep.first); |
| if (e instanceof SomeErrorException someErrorException) { |
| exceptionRef.set(someErrorException); |
| return true; |
| } |
| return false; |
| } |
| }); |
| if ((value = valueRef.get()) != null) { |
| assertThat(gotValue).isTrue(); |
| } else if (exceptionRef.get() != null) { |
| value = dep.second; |
| assertThat(gotValue).isTrue(); |
| } else { |
| assertThat(gotValue).isFalse(); |
| } |
| } |
| return value; |
| } |
| |
| public static SkyKey skyKey(String key) { |
| return Key.create(key); |
| } |
| |
| public static NonHermeticKey nonHermeticKey(String key) { |
| return NonHermeticKey.create(key); |
| } |
| |
| public static SkipBatchPrefetchKey skipBatchPrefetchKey(String key) { |
| return SkipBatchPrefetchKey.create(key); |
| } |
| |
| /** A value in the testing graph that is constructed in the tester. */ |
| public static 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 errorEvent; |
| private Postable postable; |
| |
| private String tag; |
| |
| @CanIgnoreReturnValue |
| public TestFunction addDependency(String name) { |
| return addDependency(skyKey(name)); |
| } |
| |
| @CanIgnoreReturnValue |
| public TestFunction addDependency(SkyKey key) { |
| deps.add(Pair.of(key, null)); |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| public TestFunction removeDependency(String name) { |
| return removeDependency(skyKey(name)); |
| } |
| |
| @CanIgnoreReturnValue |
| public TestFunction removeDependency(SkyKey key) { |
| deps.remove(Pair.<SkyKey, SkyValue>of(key, null)); |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| public TestFunction addErrorDependency(SkyKey key, SkyValue altValue) { |
| deps.add(Pair.of(key, altValue)); |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| public TestFunction setConstantValue(SkyValue value) { |
| Preconditions.checkState(this.computer == null); |
| this.value = value; |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| public TestFunction unsetConstantValue() { |
| this.value = null; |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| public TestFunction setComputedValue(ValueComputer computer) { |
| Preconditions.checkState(this.value == null); |
| this.computer = computer; |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| public TestFunction unsetComputedValue() { |
| this.computer = null; |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| 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); |
| Preconditions.checkState(errorEvent == null); |
| Preconditions.checkState(tag == null); |
| this.builder = builder; |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| public TestFunction setBuilderUnconditionally(SkyFunction builder) { |
| this.builder = builder; |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| public TestFunction setHasTransientError(boolean hasError) { |
| this.hasTransientError = hasError; |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| public TestFunction setHasError(boolean hasError) { |
| // TODO(bazel-team): switch to an enum for hasError. |
| this.hasError = hasError; |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| public TestFunction setWarning(String warning) { |
| this.warning = warning; |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| public TestFunction setProgress(String info) { |
| this.progress = info; |
| return this; |
| } |
| |
| /** |
| * Sets an error message to emit as an {@link Event}. Does not imply that the function throws an |
| * error. |
| */ |
| @CanIgnoreReturnValue |
| public TestFunction setErrorEvent(String error) { |
| this.errorEvent = error; |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| public TestFunction setTag(String tag) { |
| Preconditions.checkState(builder == null); |
| this.tag = tag; |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| public TestFunction setPostable(Postable postable) { |
| this.postable = postable; |
| return this; |
| } |
| } |
| |
| public static ImmutableList<SkyKey> toSkyKeys(String... names) { |
| return toSkyKeys(/* useSkipBatchPrefetchKey= */ false, names); |
| } |
| |
| public static ImmutableList<SkyKey> toSkyKeys(boolean useSkipBatchPrefetchKey, String... names) { |
| ImmutableList.Builder<SkyKey> result = ImmutableList.builder(); |
| for (String element : names) { |
| result.add( |
| useSkipBatchPrefetchKey ? SkipBatchPrefetchKey.create(element) : Key.create(element)); |
| } |
| return result.build(); |
| } |
| |
| 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, SkyFunction> getSkyFunctionMap() { |
| return ImmutableMap.copyOf(functionMap); |
| } |
| |
| public void putDelegateFunction(SkyFunctionName functionName) { |
| putSkyFunction(functionName, new DelegatingFunction()); |
| } |
| |
| public void putSkyFunction(SkyFunctionName functionName, SkyFunction function) { |
| functionMap.put(functionName, function); |
| } |
| |
| /** 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 (this == o) { |
| return true; |
| } |
| 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: " + value; |
| } |
| |
| 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 = (deps, env) -> Iterables.getOnlyElement(deps.values()); |
| |
| public static final ValueComputer CONCATENATE = |
| (deps, env) -> { |
| StringBuilder result = new StringBuilder(); |
| for (SkyValue value : deps.values()) { |
| result.append(((StringValue) value).value); |
| } |
| return new StringValue(result.toString()); |
| }; |
| |
| public static ValueComputer formatter(SkyKey key, String format) { |
| return (deps, env) -> |
| StringValue.of(String.format(format, StringValue.from(deps.get(key)).getValue())); |
| } |
| |
| @VisibleForSerialization |
| @AutoCodec |
| static class Key extends AbstractSkyKey<String> { |
| private static final SkyKeyInterner<Key> interner = SkyKey.newInterner(); |
| |
| private Key(String arg) { |
| super(arg); |
| } |
| |
| private static Key create(String arg) { |
| return interner.intern(new Key(arg)); |
| } |
| |
| @VisibleForSerialization |
| @AutoCodec.Interner |
| static Key intern(Key key) { |
| return interner.intern(key); |
| } |
| |
| @Override |
| public SkyFunctionName functionName() { |
| return SkyFunctionName.FOR_TESTING; |
| } |
| |
| @Override |
| public SkyKeyInterner<Key> getSkyKeyInterner() { |
| return interner; |
| } |
| } |
| |
| @VisibleForSerialization |
| @AutoCodec |
| static class NonHermeticKey extends AbstractSkyKey<String> { |
| private static final SkyKeyInterner<NonHermeticKey> interner = SkyKey.newInterner(); |
| |
| private NonHermeticKey(String arg) { |
| super(arg); |
| } |
| |
| private static NonHermeticKey create(String arg) { |
| return interner.intern(new NonHermeticKey(arg)); |
| } |
| |
| @VisibleForSerialization |
| @AutoCodec.Interner |
| static NonHermeticKey intern(NonHermeticKey nonHermeticKey) { |
| return interner.intern(nonHermeticKey); |
| } |
| |
| @Override |
| public SkyFunctionName functionName() { |
| return FOR_TESTING_NONHERMETIC; |
| } |
| |
| @Override |
| public SkyKeyInterner<NonHermeticKey> getSkyKeyInterner() { |
| return interner; |
| } |
| } |
| |
| private static final SkyFunctionName FOR_TESTING_NONHERMETIC = |
| SkyFunctionName.createNonHermetic("FOR_TESTING_NONHERMETIC"); |
| |
| // TODO: b/324948927 - Remove this class along with `SkyKey#skipBatchPrefetch()` method. |
| @VisibleForSerialization |
| @AutoCodec |
| static final class SkipBatchPrefetchKey extends AbstractSkyKey<String> implements SkyKey { |
| private static final SkyKeyInterner<SkipBatchPrefetchKey> interner = SkyKey.newInterner(); |
| |
| SkipBatchPrefetchKey(String arg) { |
| super(arg); |
| } |
| |
| private static SkipBatchPrefetchKey create(String arg) { |
| return interner.intern(new SkipBatchPrefetchKey(arg)); |
| } |
| |
| @VisibleForSerialization |
| @AutoCodec.Interner |
| static SkipBatchPrefetchKey intern(SkipBatchPrefetchKey key) { |
| return interner.intern(key); |
| } |
| |
| @Override |
| public boolean skipsBatchPrefetch() { |
| return true; |
| } |
| |
| @Override |
| public SkyFunctionName functionName() { |
| return SkyFunctionName.FOR_TESTING; |
| } |
| |
| @Override |
| public SkyKeyInterner<SkipBatchPrefetchKey> getSkyKeyInterner() { |
| return interner; |
| } |
| } |
| } |