blob: e5c6ff81592fe29159971a72764acd91f532aa1d [file] [log] [blame]
// 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;
}
}
}