/* * Copyright 2015-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.keys; import static org.junit.Assert.assertThat; import com.facebook.buck.io.ProjectFilesystem; import com.facebook.buck.model.BuildTargetFactory; import com.facebook.buck.parser.NoSuchBuildTargetException; import com.facebook.buck.rules.AddToRuleKey; import com.facebook.buck.rules.BuildRule; import com.facebook.buck.rules.BuildRuleParams; import com.facebook.buck.rules.BuildRuleResolver; import com.facebook.buck.rules.DefaultTargetNodeToBuildRuleTransformer; import com.facebook.buck.rules.FakeBuildRule; import com.facebook.buck.rules.FakeBuildRuleParamsBuilder; import com.facebook.buck.rules.NonHashableSourcePathContainer; import com.facebook.buck.rules.NoopBuildRule; import com.facebook.buck.rules.PathSourcePath; import com.facebook.buck.rules.RuleKey; import com.facebook.buck.rules.RuleKeyAppendable; import com.facebook.buck.rules.RuleKeyObjectSink; import com.facebook.buck.rules.SourcePath; import com.facebook.buck.rules.SourcePathResolver; import com.facebook.buck.rules.SourcePathRuleFinder; import com.facebook.buck.rules.TargetGraph; import com.facebook.buck.shell.ExportFileBuilder; import com.facebook.buck.shell.GenruleBuilder; import com.facebook.buck.testutil.FakeFileHashCache; import com.facebook.buck.testutil.FakeProjectFilesystem; import com.facebook.buck.util.cache.DefaultFileHashCache; import com.facebook.buck.util.cache.FileHashCache; import com.facebook.buck.util.cache.StackedFileHashCache; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSortedSet; import com.google.common.hash.HashCode; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import java.util.HashMap; import java.util.List; import org.hamcrest.Matchers; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; public class InputBasedRuleKeyFactoryTest { @Rule public ExpectedException expectedException = ExpectedException.none(); @Test public void ruleKeyDoesNotChangeWhenOnlyDependencyRuleKeyChanges() throws Exception { ProjectFilesystem filesystem = new FakeProjectFilesystem(); BuildRuleResolver resolver = new BuildRuleResolver(TargetGraph.EMPTY, new DefaultTargetNodeToBuildRuleTransformer()); SourcePathRuleFinder ruleFinder = new SourcePathRuleFinder(resolver); SourcePathResolver pathResolver = new SourcePathResolver(ruleFinder); Path depOutput = Paths.get("output"); FakeBuildRule dep = resolver.addToIndex( new FakeBuildRule(BuildTargetFactory.newInstance("//:dep"), filesystem, pathResolver)); dep.setOutputFile(depOutput.toString()); filesystem.writeContentsToPath( "hello", pathResolver.getRelativePath(dep.getSourcePathToOutput())); FakeFileHashCache hashCache = new FakeFileHashCache(ImmutableMap.of(filesystem.resolve(depOutput), HashCode.fromInt(0))); BuildRule rule = GenruleBuilder.newGenruleBuilder(BuildTargetFactory.newInstance("//:rule")) .setOut("out") .setSrcs(ImmutableList.of(dep.getSourcePathToOutput())) .build(resolver, filesystem); RuleKey inputKey1 = new InputBasedRuleKeyFactory(0, hashCache, pathResolver, ruleFinder).build(rule); RuleKey inputKey2 = new InputBasedRuleKeyFactory(0, hashCache, pathResolver, ruleFinder).build(rule); assertThat(inputKey1, Matchers.equalTo(inputKey2)); } @Test public void ruleKeyChangesIfInputContentsFromPathSourceChanges() throws Exception { BuildRuleResolver resolver = new BuildRuleResolver(TargetGraph.EMPTY, new DefaultTargetNodeToBuildRuleTransformer()); SourcePathRuleFinder ruleFinder = new SourcePathRuleFinder(resolver); SourcePathResolver pathResolver = new SourcePathResolver(ruleFinder); FakeProjectFilesystem filesystem = new FakeProjectFilesystem(); Path output = Paths.get("output"); BuildRule rule = ExportFileBuilder.newExportFileBuilder(BuildTargetFactory.newInstance("//:rule")) .setOut("out") .setSrc(new PathSourcePath(filesystem, output)) .build(resolver, filesystem); // Build a rule key with a particular hash set for the output for the above rule. FakeFileHashCache hashCache = new FakeFileHashCache(ImmutableMap.of(filesystem.resolve(output), HashCode.fromInt(0))); RuleKey inputKey1 = new InputBasedRuleKeyFactory(0, hashCache, pathResolver, ruleFinder).build(rule); // Now, build a rule key with a different hash for the output for the above rule. hashCache = new FakeFileHashCache(ImmutableMap.of(filesystem.resolve(output), HashCode.fromInt(1))); RuleKey inputKey2 = new InputBasedRuleKeyFactory(0, hashCache, pathResolver, ruleFinder).build(rule); assertThat(inputKey1, Matchers.not(Matchers.equalTo(inputKey2))); } @Test public void ruleKeyChangesIfInputContentsFromBuildTargetSourcePathChanges() throws Exception { BuildRuleResolver resolver = new BuildRuleResolver(TargetGraph.EMPTY, new DefaultTargetNodeToBuildRuleTransformer()); SourcePathRuleFinder ruleFinder = new SourcePathRuleFinder(resolver); SourcePathResolver pathResolver = new SourcePathResolver(ruleFinder); FakeProjectFilesystem filesystem = new FakeProjectFilesystem(); BuildRule dep = GenruleBuilder.newGenruleBuilder(BuildTargetFactory.newInstance("//:dep")) .setOut("out") .build(resolver, filesystem); BuildRule rule = ExportFileBuilder.newExportFileBuilder(BuildTargetFactory.newInstance("//:rule")) .setOut("out") .setSrc(dep.getSourcePathToOutput()) .build(resolver, filesystem); // Build a rule key with a particular hash set for the output for the above rule. FakeFileHashCache hashCache = new FakeFileHashCache( ImmutableMap.of( pathResolver.getAbsolutePath( Preconditions.checkNotNull(dep.getSourcePathToOutput())), HashCode.fromInt(0))); RuleKey inputKey1 = new InputBasedRuleKeyFactory(0, hashCache, pathResolver, ruleFinder).build(rule); // Now, build a rule key with a different hash for the output for the above rule. hashCache = new FakeFileHashCache( ImmutableMap.of( pathResolver.getAbsolutePath( (Preconditions.checkNotNull(dep.getSourcePathToOutput()))), HashCode.fromInt(1))); RuleKey inputKey2 = new InputBasedRuleKeyFactory(0, hashCache, pathResolver, ruleFinder).build(rule); assertThat(inputKey1, Matchers.not(Matchers.equalTo(inputKey2))); } @Test public void ruleKeyChangesIfInputContentsFromPathSourcePathInRuleKeyAppendableChanges() { BuildRuleResolver resolver = new BuildRuleResolver(TargetGraph.EMPTY, new DefaultTargetNodeToBuildRuleTransformer()); SourcePathRuleFinder ruleFinder = new SourcePathRuleFinder(resolver); SourcePathResolver pathResolver = new SourcePathResolver(ruleFinder); final FakeProjectFilesystem filesystem = new FakeProjectFilesystem(); final Path output = Paths.get("output"); BuildRuleParams params = new FakeBuildRuleParamsBuilder("//:rule").setProjectFilesystem(filesystem).build(); BuildRule rule = new NoopBuildRule(params) { @AddToRuleKey RuleKeyAppendableWithInput input = new RuleKeyAppendableWithInput(new PathSourcePath(filesystem, output)); }; // Build a rule key with a particular hash set for the output for the above rule. FakeFileHashCache hashCache = new FakeFileHashCache(ImmutableMap.of(filesystem.resolve(output), HashCode.fromInt(0))); RuleKey inputKey1 = new InputBasedRuleKeyFactory(0, hashCache, pathResolver, ruleFinder).build(rule); // Now, build a rule key with a different hash for the output for the above rule. hashCache = new FakeFileHashCache(ImmutableMap.of(filesystem.resolve(output), HashCode.fromInt(1))); RuleKey inputKey2 = new InputBasedRuleKeyFactory(0, hashCache, pathResolver, ruleFinder).build(rule); assertThat(inputKey1, Matchers.not(Matchers.equalTo(inputKey2))); } @Test public void ruleKeyChangesIfInputContentsFromBuildTargetSourcePathInRuleKeyAppendableChanges() throws Exception { BuildRuleResolver resolver = new BuildRuleResolver(TargetGraph.EMPTY, new DefaultTargetNodeToBuildRuleTransformer()); SourcePathRuleFinder ruleFinder = new SourcePathRuleFinder(resolver); SourcePathResolver pathResolver = new SourcePathResolver(ruleFinder); final FakeProjectFilesystem filesystem = new FakeProjectFilesystem(); final BuildRule dep = GenruleBuilder.newGenruleBuilder(BuildTargetFactory.newInstance("//:dep")) .setOut("out") .build(resolver, filesystem); BuildRuleParams params = new FakeBuildRuleParamsBuilder("//:rule") .setDeclaredDeps(ImmutableSortedSet.of(dep)) .setProjectFilesystem(filesystem) .build(); BuildRule rule = new NoopBuildRule(params) { @AddToRuleKey RuleKeyAppendableWithInput input = new RuleKeyAppendableWithInput(dep.getSourcePathToOutput()); }; // Build a rule key with a particular hash set for the output for the above rule. FakeFileHashCache hashCache = new FakeFileHashCache( ImmutableMap.of( pathResolver.getAbsolutePath( Preconditions.checkNotNull(dep.getSourcePathToOutput())), HashCode.fromInt(0))); RuleKey inputKey1 = new InputBasedRuleKeyFactory(0, hashCache, pathResolver, ruleFinder).build(rule); // Now, build a rule key with a different hash for the output for the above rule. hashCache = new FakeFileHashCache( ImmutableMap.of( pathResolver.getAbsolutePath( Preconditions.checkNotNull(dep.getSourcePathToOutput())), HashCode.fromInt(1))); RuleKey inputKey2 = new InputBasedRuleKeyFactory(0, hashCache, pathResolver, ruleFinder).build(rule); assertThat(inputKey1, Matchers.not(Matchers.equalTo(inputKey2))); } @Test public void ruleKeyDoesNotChangeIfNonHashingSourcePathContentChanges() throws NoSuchBuildTargetException { BuildRuleResolver resolver = new BuildRuleResolver(TargetGraph.EMPTY, new DefaultTargetNodeToBuildRuleTransformer()); SourcePathRuleFinder ruleFinder = new SourcePathRuleFinder(resolver); SourcePathResolver pathResolver = new SourcePathResolver(ruleFinder); final ProjectFilesystem fileSystem = new FakeProjectFilesystem(); // Build a rule key with a particular hash set for the output for the above rule. Path filePath = fileSystem.getPath("file.txt"); FakeFileHashCache hashCache = new FakeFileHashCache(new HashMap<>()); hashCache.set(filePath.toAbsolutePath(), HashCode.fromInt(0)); PathSourcePath sourcePath = new PathSourcePath(fileSystem, filePath); NonHashableSourcePathContainer nonHashablePath = new NonHashableSourcePathContainer(sourcePath); RuleKey inputKey1 = computeRuleKey(hashCache, pathResolver, ruleFinder, nonHashablePath); hashCache.set(filePath.toAbsolutePath(), HashCode.fromInt(1)); RuleKey inputKey2 = computeRuleKey(hashCache, pathResolver, ruleFinder, nonHashablePath); assertThat(inputKey1, Matchers.equalTo(inputKey2)); } @Test public void nestedSizeLimitExceptionHandled() throws Exception { BuildRuleResolver resolver = new BuildRuleResolver(TargetGraph.EMPTY, new DefaultTargetNodeToBuildRuleTransformer()); RuleKeyFieldLoader fieldLoader = new RuleKeyFieldLoader(0); SourcePathRuleFinder ruleFinder = new SourcePathRuleFinder(resolver); SourcePathResolver pathResolver = new SourcePathResolver(ruleFinder); final FakeProjectFilesystem filesystem = new FakeProjectFilesystem(); FileHashCache hashCache = new StackedFileHashCache( ImmutableList.of(DefaultFileHashCache.createDefaultFileHashCache(filesystem))); Path inputFile = filesystem.getPath("input"); filesystem.writeBytesToPath(new byte[1024], inputFile); BuildRuleParams params = new FakeBuildRuleParamsBuilder("//:rule").setProjectFilesystem(filesystem).build(); BuildRule rule = new NoopBuildRule(params) { @AddToRuleKey NestedRuleKeyAppendableWithInput input = new NestedRuleKeyAppendableWithInput(new PathSourcePath(filesystem, inputFile)); }; // Verify rule key isn't calculated. expectedException.expect(SizeLimiter.SizeLimitException.class); new InputBasedRuleKeyFactory(fieldLoader, hashCache, pathResolver, ruleFinder, 200).build(rule); } @Test public void ruleKeyNotCalculatedIfSizeLimitHit() throws Exception { BuildRuleResolver resolver = new BuildRuleResolver(TargetGraph.EMPTY, new DefaultTargetNodeToBuildRuleTransformer()); RuleKeyFieldLoader fieldLoader = new RuleKeyFieldLoader(0); SourcePathRuleFinder ruleFinder = new SourcePathRuleFinder(resolver); SourcePathResolver pathResolver = new SourcePathResolver(ruleFinder); FakeProjectFilesystem filesystem = new FakeProjectFilesystem(); FileHashCache hashCache = new StackedFileHashCache( ImmutableList.of(DefaultFileHashCache.createDefaultFileHashCache(filesystem))); // Create input that passes size limit. Path input = filesystem.getPath("input"); filesystem.writeBytesToPath(new byte[1024], input); // Construct rule which uses input. BuildRule rule = ExportFileBuilder.newExportFileBuilder(BuildTargetFactory.newInstance("//:rule")) .setOut("out") .setSrc(new PathSourcePath(filesystem, input)) .build(resolver, filesystem); // Verify rule key isn't calculated. expectedException.expect(SizeLimiter.SizeLimitException.class); new InputBasedRuleKeyFactory(fieldLoader, hashCache, pathResolver, ruleFinder, 200).build(rule); } @Test public void ruleKeyNotCalculatedIfSizeLimitHitWithMultipleInputs() throws Exception { BuildRuleResolver resolver = new BuildRuleResolver(TargetGraph.EMPTY, new DefaultTargetNodeToBuildRuleTransformer()); RuleKeyFieldLoader fieldLoader = new RuleKeyFieldLoader(0); SourcePathRuleFinder ruleFinder = new SourcePathRuleFinder(resolver); SourcePathResolver pathResolver = new SourcePathResolver(ruleFinder); FakeProjectFilesystem filesystem = new FakeProjectFilesystem(); FileHashCache hashCache = new StackedFileHashCache( ImmutableList.of(DefaultFileHashCache.createDefaultFileHashCache(filesystem))); // Create inputs that combine to pass size limit. Path input1 = filesystem.getPath("input1"); filesystem.writeBytesToPath(new byte[150], input1); Path input2 = filesystem.getPath("input2"); filesystem.writeBytesToPath(new byte[150], input2); // Construct rule which uses inputs. BuildRule rule = GenruleBuilder.newGenruleBuilder(BuildTargetFactory.newInstance("//:rule")) .setOut("out") .setSrcs( ImmutableList.of( new PathSourcePath(filesystem, input1), new PathSourcePath(filesystem, input2))) .build(resolver, filesystem); // Verify rule key isn't calculated. expectedException.expect(SizeLimiter.SizeLimitException.class); new InputBasedRuleKeyFactory(fieldLoader, hashCache, pathResolver, ruleFinder, 200).build(rule); } @Test public void ruleKeyNotCalculatedIfSizeLimitHitWithDirectory() throws Exception { BuildRuleResolver resolver = new BuildRuleResolver(TargetGraph.EMPTY, new DefaultTargetNodeToBuildRuleTransformer()); RuleKeyFieldLoader fieldLoader = new RuleKeyFieldLoader(0); SourcePathRuleFinder ruleFinder = new SourcePathRuleFinder(resolver); SourcePathResolver pathResolver = new SourcePathResolver(ruleFinder); FakeProjectFilesystem filesystem = new FakeProjectFilesystem(); FileHashCache hashCache = new StackedFileHashCache( ImmutableList.of(DefaultFileHashCache.createDefaultFileHashCache(filesystem))); // Create a directory of files which combine to pass size limit. Path input = filesystem.getPath("input"); filesystem.mkdirs(input); filesystem.writeBytesToPath(new byte[150], input.resolve("file1")); filesystem.writeBytesToPath(new byte[150], input.resolve("file2")); // Construct rule which uses inputs. BuildRule rule = ExportFileBuilder.newExportFileBuilder(BuildTargetFactory.newInstance("//:rule")) .setOut("out") .setSrc(new PathSourcePath(filesystem, input)) .build(resolver, filesystem); // Verify rule key isn't calculated. expectedException.expect(SizeLimiter.SizeLimitException.class); new InputBasedRuleKeyFactory(fieldLoader, hashCache, pathResolver, ruleFinder, 200).build(rule); } @Test public void ruleKeysForOversizedRules() throws Exception { BuildRuleResolver resolver = new BuildRuleResolver(TargetGraph.EMPTY, new DefaultTargetNodeToBuildRuleTransformer()); RuleKeyFieldLoader fieldLoader = new RuleKeyFieldLoader(0); SourcePathRuleFinder ruleFinder = new SourcePathRuleFinder(resolver); SourcePathResolver pathResolver = new SourcePathResolver(ruleFinder); FakeProjectFilesystem filesystem = new FakeProjectFilesystem(); FileHashCache hashCache = new StackedFileHashCache( ImmutableList.of(DefaultFileHashCache.createDefaultFileHashCache(filesystem))); final int sizeLimit = 200; InputBasedRuleKeyFactory factory = new InputBasedRuleKeyFactory(fieldLoader, hashCache, pathResolver, ruleFinder, sizeLimit); // Create rule with inputs that make it go past the size limit, and verify the rule key factory // doesn't create a rule key. final int tooLargeRuleSize = 300; assertThat(tooLargeRuleSize, Matchers.greaterThan(sizeLimit)); Path tooLargeInput = filesystem.getPath("too_large_input"); filesystem.writeBytesToPath(new byte[tooLargeRuleSize], tooLargeInput); BuildRule tooLargeRule = ExportFileBuilder.newExportFileBuilder(BuildTargetFactory.newInstance("//:large_rule")) .setOut("out") .setSrc(new PathSourcePath(filesystem, tooLargeInput)) .build(resolver, filesystem); expectedException.expect(SizeLimiter.SizeLimitException.class); factory.build(tooLargeRule); } @Test public void ruleKeysForUndersizedRules() throws Exception { BuildRuleResolver resolver = new BuildRuleResolver(TargetGraph.EMPTY, new DefaultTargetNodeToBuildRuleTransformer()); RuleKeyFieldLoader fieldLoader = new RuleKeyFieldLoader(0); SourcePathRuleFinder ruleFinder = new SourcePathRuleFinder(resolver); SourcePathResolver pathResolver = new SourcePathResolver(ruleFinder); FakeProjectFilesystem filesystem = new FakeProjectFilesystem(); FileHashCache hashCache = new StackedFileHashCache( ImmutableList.of(DefaultFileHashCache.createDefaultFileHashCache(filesystem))); final int sizeLimit = 200; InputBasedRuleKeyFactory factory = new InputBasedRuleKeyFactory(fieldLoader, hashCache, pathResolver, ruleFinder, sizeLimit); // Create a rule that doesn't pass the size limit and verify it creates a rule key. final int smallEnoughRuleSize = 100; assertThat(smallEnoughRuleSize, Matchers.lessThan(sizeLimit)); Path input = filesystem.getPath("small_enough_input"); filesystem.writeBytesToPath(new byte[smallEnoughRuleSize], input); BuildRule smallEnoughRule = ExportFileBuilder.newExportFileBuilder(BuildTargetFactory.newInstance("//:small_rule")) .setOut("out") .setSrc(new PathSourcePath(filesystem, input)) .build(resolver, filesystem); factory.build(smallEnoughRule); } private static class RuleKeyAppendableWithInput implements RuleKeyAppendable { private final SourcePath input; public RuleKeyAppendableWithInput(SourcePath input) { this.input = input; } @Override public void appendToRuleKey(RuleKeyObjectSink sink) { sink.setReflectively("input", input); } } private static class NestedRuleKeyAppendableWithInput implements RuleKeyAppendable { private final RuleKeyAppendable appendable; public NestedRuleKeyAppendableWithInput(SourcePath input) { this.appendable = new RuleKeyAppendableWithInput(input); } @Override public void appendToRuleKey(RuleKeyObjectSink sink) { sink.setReflectively("input", appendable); } } RuleKey computeRuleKey( FileHashCache hashCache, SourcePathResolver resolver, SourcePathRuleFinder ruleFinder, Object... objects) { return new InputBasedRuleKeyFactory(0, hashCache, resolver, ruleFinder) .build( new FakeBuildRule("//fake:target", resolver) { @AddToRuleKey List<Object> ruleObjects = Arrays.asList(objects); }); } }