blob: c3a412c67a31b4b99fcde2b1fb81e3b4be30e7bd [file] [log] [blame]
/*
* Copyright 2012-present Facebook, Inc.
*
* 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.facebook.buck.rules;
import static com.facebook.buck.event.TestEventConfigerator.configureTestEvent;
import static org.easymock.EasyMock.capture;
import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expect;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import com.facebook.buck.event.BuckEvent;
import com.facebook.buck.event.BuckEventBus;
import com.facebook.buck.event.BuckEventBusFactory;
import com.facebook.buck.event.FakeBuckEventListener;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.BuildTargetFactory;
import com.facebook.buck.model.BuildTargetPattern;
import com.facebook.buck.step.AbstractExecutionStep;
import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.step.Step;
import com.facebook.buck.step.StepFailedException;
import com.facebook.buck.step.StepRunner;
import com.facebook.buck.testutil.RuleMap;
import com.facebook.buck.util.concurrent.MoreFutures;
import com.facebook.buck.util.ProjectFilesystem;
import com.google.common.base.Functions;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.hash.Hashing;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import org.easymock.Capture;
import org.easymock.EasyMockSupport;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ExecutionException;
/**
* Ensuring that build rule caching works correctly in Buck is imperative for both its performance
* and correctness.
*/
public class AbstractCachingBuildRuleTest extends EasyMockSupport {
private static final BuildTarget buildTarget = BuildTargetFactory.newInstance(
"//src/com/facebook/orca", "orca");
/**
* Tests what should happen when a rule is built for the first time: it should have no cached
* RuleKey, nor should it have any artifact in the ArtifactCache. The sequence of events should be
* as follows:
* <ol>
* <li>The rule invokes the {@link BuildRule#build(BuildContext)} method of each of its deps.
* <li>The rule computes its own {@link RuleKey}.
* <li>It compares its {@link RuleKey} to the one on disk, if present.
* <li>Because the rule has no {@link RuleKey} on disk, the rule tries to build itself.
* <li>First, it checks the artifact cache, but there is a cache miss.
* <li>The rule generates its build steps and executes them.
* <li>Upon executing its steps successfully, it should write its {@link RuleKey} to disk.
* <li>It should persist its output to the ArtifactCache.
* </ol>
*/
@Test
@SuppressWarnings({"deprecation", "PMD.UseAssertTrueInsteadOfAssertEquals"})
public void testBuildRuleWithoutSuccessFileOrCachedArtifact()
throws IOException, InterruptedException, ExecutionException, StepFailedException {
// Create a dep for the build rule.
BuildRule dep = createMock(BuildRule.class);
expect(dep.isVisibleTo(buildTarget)).andReturn(true);
// The EventBus should be updated with events indicating how the rule was built.
BuckEventBus buckEventBus = BuckEventBusFactory.newInstance();
FakeBuckEventListener listener = new FakeBuckEventListener();
buckEventBus.register(listener);
// Create an ArtifactCache whose expectations will be set later.
ArtifactCache mockArtifactCache = createMock(ArtifactCache.class);
ArtifactCache artifactCache = new LoggingArtifactCacheDecorator(buckEventBus)
.decorate(mockArtifactCache);
// Replay the mocks to instantiate the AbstractCachingBuildRule.
replayAll();
String pathToOutputFile = "some_file";
File outputFile = new File(pathToOutputFile);
List<Step> buildSteps = Lists.newArrayList();
AbstractCachingBuildRule cachingRule = createRule(
ImmutableSet.of(dep),
ImmutableList.<InputRule>of(FakeInputRule.createWithRuleKey("/dev/null",
new RuleKey("ae8c0f860a0ecad94ecede79b69460434eddbfbc"))),
buildSteps,
/* ruleKeyOnDisk */ Optional.<RuleKey>absent(),
pathToOutputFile);
verifyAll();
resetAll();
String expectedRuleKeyHash = Hashing.sha1().newHasher()
.putBytes(RuleKey.Builder.buckVersionUID.getBytes())
.putByte(RuleKey.Builder.SEPARATOR)
.putByte(RuleKey.Builder.SEPARATOR)
.putBytes("name".getBytes())
.putByte(RuleKey.Builder.SEPARATOR)
.putBytes(cachingRule.getFullyQualifiedName().getBytes())
.putByte(RuleKey.Builder.SEPARATOR)
.putByte(RuleKey.Builder.SEPARATOR)
.putBytes("buck.type".getBytes())
.putByte(RuleKey.Builder.SEPARATOR)
.putBytes("java_library".getBytes())
.putByte(RuleKey.Builder.SEPARATOR)
.putByte(RuleKey.Builder.SEPARATOR)
.putBytes("deps".getBytes())
.putByte(RuleKey.Builder.SEPARATOR)
.putBytes("19d2558a6bd3a34fb3f95412de9da27ed32fe208".getBytes())
.putByte(RuleKey.Builder.SEPARATOR)
.putByte(RuleKey.Builder.SEPARATOR)
.putByte(RuleKey.Builder.SEPARATOR)
.putBytes("buck.inputs".getBytes())
.putByte(RuleKey.Builder.SEPARATOR)
.putBytes("ae8c0f860a0ecad94ecede79b69460434eddbfbc".getBytes())
.putByte(RuleKey.Builder.SEPARATOR)
.putByte(RuleKey.Builder.SEPARATOR)
.hash()
.toString();
// The BuildContext that will be used by the rule's build() method.
BuildContext context = createMock(BuildContext.class);
expect(context.getExecutor()).andReturn(MoreExecutors.sameThreadExecutor());
expect(context.getEventBus()).andReturn(buckEventBus).anyTimes();
context.logBuildInfo("[BUILDING %s]", "//src/com/facebook/orca:orca");
StepRunner stepRunner = createMock(StepRunner.class);
expect(context.getStepRunner()).andReturn(stepRunner);
ProjectFilesystem projectFilesystem = createMock(ProjectFilesystem.class);
expect(context.getProjectFilesystem()).andReturn(projectFilesystem);
String pathToSuccessFile = cachingRule.getPathToSuccessFile();
projectFilesystem.createParentDirs(pathToSuccessFile);
Capture<Iterable<String>> linesCapture = new Capture<Iterable<String>>();
projectFilesystem.writeLinesToPath(capture(linesCapture), eq(pathToSuccessFile));
expect(projectFilesystem.getFileForRelativePath(pathToOutputFile)).andReturn(outputFile).times(2);
// There will initially be a cache miss, later followed by a cache store.
RuleKey expectedRuleKey = new RuleKey(expectedRuleKeyHash);
expect(mockArtifactCache.fetch(expectedRuleKey, outputFile)).andReturn(false);
mockArtifactCache.store(expectedRuleKey, outputFile);
expect(context.getArtifactCache()).andReturn(artifactCache).times(2);
// The dependent rule will be built immediately with a distinct rule key.
expect(dep.build(context)).andReturn(
Futures.immediateFuture(new BuildRuleSuccess(dep, BuildRuleSuccess.Type.BUILT_LOCALLY)));
expect(dep.getRuleKey()).andReturn(new RuleKey("19d2558a6bd3a34fb3f95412de9da27ed32fe208"));
// Add a build step so we can verify that the steps are executed.
Step buildStep = createMock(Step.class);
buildSteps.add(buildStep);
stepRunner.runStepForBuildTarget(buildStep, buildTarget);
// Attempting to build the rule should force a rebuild due to a cache miss.
replayAll();
BuildRuleSuccess result = cachingRule.build(context).get();
assertEquals(BuildRuleSuccess.Type.BUILT_LOCALLY, result.getType());
verifyAll();
// Verify that the correct value was written to the .success file.
String firstLineInSuccessFile = Iterables.getFirst(linesCapture.getValue(),
/* defaultValue */ null);
assertEquals(expectedRuleKeyHash, firstLineInSuccessFile);
List<BuckEvent> events = listener.getEvents();
assertEquals(events.get(0),
configureTestEvent(BuildRuleEvent.started(cachingRule), buckEventBus));
assertEquals(events.get(1),
configureTestEvent(ArtifactCacheEvent.started(ArtifactCacheEvent.Operation.FETCH),
buckEventBus));
assertEquals(events.get(2),
configureTestEvent(ArtifactCacheEvent.finished(ArtifactCacheEvent.Operation.FETCH,
/* success */ false),
buckEventBus));
assertEquals(events.get(3),
configureTestEvent(ArtifactCacheEvent.started(ArtifactCacheEvent.Operation.STORE),
buckEventBus));
assertEquals(events.get(4),
configureTestEvent(ArtifactCacheEvent.finished(ArtifactCacheEvent.Operation.STORE,
/* success */ true),
buckEventBus));
assertEquals(events.get(5),
configureTestEvent(BuildRuleEvent.finished(cachingRule,
BuildRuleStatus.SUCCESS,
CacheResult.MISS,
Optional.of(BuildRuleSuccess.Type.BUILT_LOCALLY)),
buckEventBus));
}
@Test
@SuppressWarnings("deprecation")
public void testAbiRuleCanAvoidRebuild()
throws InterruptedException, ExecutionException, IOException {
BuildRuleParams buildRuleParams = new BuildRuleParams(buildTarget,
/* sortedDeps */ ImmutableSortedSet.<BuildRule>of(),
/* visibilityPatterns */ ImmutableSet.<BuildTargetPattern>of(),
/* pathRelativizer */ Functions.<String>identity());
TestAbstractCachingBuildRule buildRule = new TestAbstractCachingBuildRule(buildRuleParams);
// The EventBus should be updated with events indicating how the rule was built.
BuckEventBus buckEventBus = BuckEventBusFactory.newInstance();
FakeBuckEventListener listener = new FakeBuckEventListener();
buckEventBus.register(listener);
BuildContext buildContext = createMock(BuildContext.class);
expect(buildContext.getExecutor()).andReturn(MoreExecutors.sameThreadExecutor());
expect(buildContext.getEventBus()).andReturn(buckEventBus).anyTimes();
ProjectFilesystem projectFilesystem = createMock(ProjectFilesystem.class);
String pathToSuccessFile = buildRule.getPathToSuccessFile();
projectFilesystem.createParentDirs(pathToSuccessFile);
projectFilesystem.writeLinesToPath(
ImmutableList.of("bfcd53a794e7c732019e04e08b30b32e26e19d50"),
pathToSuccessFile);
expect(buildContext.getProjectFilesystem())
.andReturn(projectFilesystem)
.anyTimes();
replayAll();
ListenableFuture<BuildRuleSuccess> result = buildRule.build(buildContext);
assertTrue("We expect build() to be synchronous in this case, " +
"so the future should already be resolved.",
MoreFutures.isSuccess(result));
BuildRuleSuccess success = result.get();
assertEquals(BuildRuleSuccess.Type.MATCHING_DEPS_ABI_AND_RULE_KEY_NO_DEPS, success.getType());
assertTrue(buildRule.isAbiLoadedFromDisk());
List<BuckEvent> events = listener.getEvents();
assertEquals(events.get(0),
configureTestEvent(BuildRuleEvent.started(buildRule), buckEventBus));
assertEquals(events.get(1),
configureTestEvent(BuildRuleEvent.finished(buildRule,
BuildRuleStatus.SUCCESS,
CacheResult.HIT,
Optional.of(BuildRuleSuccess.Type.MATCHING_DEPS_ABI_AND_RULE_KEY_NO_DEPS)),
buckEventBus));
verifyAll();
}
@Test
public void testArtifactFetchedFromCache()
throws InterruptedException, ExecutionException, IOException {
ArtifactFetchedFromCacheScenario scenario = createArtifactFetchedFromCacheScenario(
/* isSuccessScenario */ true);
BuildableAbstractCachingBuildRule cachingRule = scenario.cachingRule;
ListenableFuture<BuildRuleSuccess> result = cachingRule.build(scenario.buildContext);
assertEquals(
"recordOutputFileDetailsAfterFetchedFromArtifactCache() should be invoked once.",
1,
cachingRule.numCallsToRecordOutputFileDetailsAfterFetchedFromArtifactCache);
ImmutableList<String> linesWrittenToSuccessFile = ImmutableList.copyOf(
scenario.lines.getValue());
assertEquals(
"The RuleKey should have been written to the .success file.",
ImmutableList.of(cachingRule.getRuleKey().toString()),
linesWrittenToSuccessFile);
assertTrue("We expect build() to be synchronous in this case, " +
"so the future should already be resolved.",
MoreFutures.isSuccess(result));
BuildRuleSuccess success = result.get();
assertEquals(BuildRuleSuccess.Type.FETCHED_FROM_CACHE, success.getType());
verifyAll();
}
@Test
public void testArtifactFetchedFromCacheFailsToRecordOutputFileDetails() throws IOException {
ArtifactFetchedFromCacheScenario scenario = createArtifactFetchedFromCacheScenario(
/* isSuccessScenario */ false);
BuildableAbstractCachingBuildRule cachingRule = scenario.cachingRule;
cachingRule.setRecordOutputFileDetailsAfterFetchedFromArtifactCacheShouldThrowIOException(
/* shouldThrow */ true);
ListenableFuture<BuildRuleSuccess> result = cachingRule.build(scenario.buildContext);
assertEquals(
"recordOutputFileDetailsAfterFetchedFromArtifactCache() should be invoked once.",
1,
cachingRule.numCallsToRecordOutputFileDetailsAfterFetchedFromArtifactCache);
Throwable failure = MoreFutures.getFailure(result);
assertTrue(failure instanceof IOException);
assertEquals("Failed to record output file details!", failure.getMessage());
verifyAll();
}
private ArtifactFetchedFromCacheScenario createArtifactFetchedFromCacheScenario(
boolean isSuccessScenario)
throws IOException {
Step step = new AbstractExecutionStep("exploding step") {
@Override
public int execute(ExecutionContext context) {
throw new UnsupportedOperationException("build step should not be executed");
}
};
String pathToOutputFile = "foo/bar/baz";
BuildableAbstractCachingBuildRule cachingRule = createRule(
/* deps */ ImmutableSet.<BuildRule>of(),
ImmutableList.<InputRule>of(),
ImmutableList.of(step),
/* ruleKeyOnDisk */ Optional.<RuleKey>absent(),
pathToOutputFile);
StepRunner stepRunner = createMock(StepRunner.class);
expect(stepRunner.getListeningExecutorService()).andReturn(MoreExecutors.sameThreadExecutor());
// Mock out all of the disk I/O.
File initialOutputFile = createMock(File.class);
ProjectFilesystem projectFilesystem = createMock(ProjectFilesystem.class);
expect(projectFilesystem.getFileForRelativePath(pathToOutputFile)).andReturn(initialOutputFile);
Capture<Iterable<String>> lines;
if (isSuccessScenario) {
String pathToSuccessFile = cachingRule.getPathToSuccessFile();
projectFilesystem.createParentDirs(pathToSuccessFile);
lines = new Capture<Iterable<String>>();
projectFilesystem.writeLinesToPath(capture(lines), eq(pathToSuccessFile));
} else {
lines = null;
}
// Simulate successfully fetching the output file from the ArtifactCache.
ArtifactCache artifactCache = createMock(ArtifactCache.class);
expect(artifactCache.fetch(cachingRule.getRuleKey(), initialOutputFile)).andReturn(true);
BuildContext buildContext = BuildContext.builder()
.setProjectRoot(createMock(File.class))
.setDependencyGraph(RuleMap.createGraphFromSingleRule(cachingRule))
.setStepRunner(stepRunner)
.setProjectFilesystem(projectFilesystem)
.setArtifactCache(artifactCache)
.setJavaPackageFinder(createMock(JavaPackageFinder.class))
.setEventBus(BuckEventBusFactory.newInstance())
.build();
replayAll();
ArtifactFetchedFromCacheScenario scenario = new ArtifactFetchedFromCacheScenario();
scenario.cachingRule = cachingRule;
scenario.buildContext = buildContext;
scenario.lines = lines;
return scenario;
}
private static class ArtifactFetchedFromCacheScenario {
BuildableAbstractCachingBuildRule cachingRule;
BuildContext buildContext;
Capture<Iterable<String>> lines;
}
// TODO(mbolin): Test that when the success files match, nothing is built and nothing is written
// back to the cache.
// TODO(mbolin): Test that when the value in the success file does not agree with the current
// value, the rule is rebuilt and the result is written back to the cache.
// TODO(mbolin): Test that a failure when executing the build steps is propagated appropriately.
// TODO(mbolin): Test what happens when the cache's methods throw an exception.
private static BuildableAbstractCachingBuildRule createRule(
ImmutableSet<BuildRule> deps,
Iterable<InputRule> inputRules,
List<Step> buildSteps,
Optional<RuleKey> ruleKeyOnDisk,
String pathToOutputFile) {
Comparator<BuildRule> comparator = RetainOrderComparator.createComparator(deps);
ImmutableSortedSet<BuildRule> sortedDeps = ImmutableSortedSet.copyOf(comparator, deps);
BuildRuleParams buildRuleParams = new BuildRuleParams(buildTarget,
sortedDeps,
/* visibilityPatterns */ ImmutableSet.<BuildTargetPattern>of(),
/* pathRelativizer */ Functions.<String>identity());
return new BuildableAbstractCachingBuildRule(buildRuleParams,
inputRules,
pathToOutputFile,
ruleKeyOnDisk,
buildSteps);
}
private static class BuildableAbstractCachingBuildRule extends AbstractCachingBuildRule {
private final Iterable<InputRule> inputRules;
private final String pathToOutputFile;
private final Optional<RuleKey> ruleKeyOnDisk;
private final List<Step> buildSteps;
private int numCallsToRecordOutputFileDetailsAfterFetchedFromArtifactCache;
private boolean recordOutputFileDetailsAfterFetchedFromArtifactCacheShouldThrowIOException;
private BuildableAbstractCachingBuildRule(BuildRuleParams params,
Iterable<InputRule> inputRules,
String pathToOutputFile,
Optional<RuleKey> ruleKeyOnDisk,
List<Step> buildSteps) {
super(params);
this.inputRules = inputRules;
this.pathToOutputFile = pathToOutputFile;
this.ruleKeyOnDisk = ruleKeyOnDisk;
this.buildSteps = buildSteps;
this.numCallsToRecordOutputFileDetailsAfterFetchedFromArtifactCache = 0;
this.recordOutputFileDetailsAfterFetchedFromArtifactCacheShouldThrowIOException = false;
}
@Override
public BuildRuleType getType() {
return BuildRuleType.JAVA_LIBRARY;
}
@Override
public Iterable<InputRule> getInputs() {
return inputRules;
}
@Override
public String getPathToOutputFile() {
return pathToOutputFile;
}
@Override
Optional<RuleKey> getRuleKeyOnDisk(ProjectFilesystem projectFilesystem) {
return ruleKeyOnDisk;
}
@Override
public List<Step> getBuildSteps(BuildContext context) throws IOException {
return buildSteps;
}
@Override
public Iterable<String> getInputsToCompareToOutput() {
throw new UnsupportedOperationException();
}
void setRecordOutputFileDetailsAfterFetchedFromArtifactCacheShouldThrowIOException(
boolean shouldThrow) {
recordOutputFileDetailsAfterFetchedFromArtifactCacheShouldThrowIOException = shouldThrow;
}
@Override
public void recordOutputFileDetailsAfterFetchFromArtifactCache(ArtifactCache cache,
ProjectFilesystem projectFilesystem) throws IOException {
numCallsToRecordOutputFileDetailsAfterFetchedFromArtifactCache++;
if (recordOutputFileDetailsAfterFetchedFromArtifactCacheShouldThrowIOException) {
throw new IOException("Failed to record output file details!");
}
}
}
/**
* {@link AbstractCachingBuildRule} that implements {@link AbiRule}.
*/
private static class TestAbstractCachingBuildRule extends AbstractCachingBuildRule
implements AbiRule {
private boolean isAbiLoadedFromDisk = false;
TestAbstractCachingBuildRule(BuildRuleParams buildRuleParams) {
super(buildRuleParams);
}
@Override
public Iterable<String> getInputsToCompareToOutput() {
throw new UnsupportedOperationException("method should not be called");
}
@Override
public List<Step> getBuildSteps(BuildContext context)
throws IOException {
throw new UnsupportedOperationException("method should not be called");
}
@Override
public BuildRuleType getType() {
throw new UnsupportedOperationException("method should not be called");
}
@Override
public RuleKey getRuleKey() {
return new RuleKey("bfcd53a794e7c732019e04e08b30b32e26e19d50");
}
@Override
public Optional<RuleKey> getRuleKeyWithoutDeps() {
return Optional.of(new RuleKey("efd7d450d9f1c3d9e43392dec63b1f31692305b9"));
}
@Override
public Optional<RuleKey> getRuleKeyWithoutDepsOnDisk(ProjectFilesystem projectFilesystem) {
return Optional.of(new RuleKey("efd7d450d9f1c3d9e43392dec63b1f31692305b9"));
}
@Override
public boolean initializeFromDisk(ProjectFilesystem projectFilesystem) {
isAbiLoadedFromDisk = true;
return true;
}
public boolean isAbiLoadedFromDisk() {
return isAbiLoadedFromDisk;
}
@Override
public Optional<Sha1HashCode> getAbiKeyForDeps() {
return Optional.of(new Sha1HashCode("92d6de0a59080284055bcde5d2923f144b216a59"));
}
@Override
public Optional<Sha1HashCode> getAbiKeyForDepsOnDisk(ProjectFilesystem projectFilesystem) {
return Optional.of(new Sha1HashCode("92d6de0a59080284055bcde5d2923f144b216a59"));
}
}
}