/*
* Copyright 2016-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.jvm.java.autodeps;
import static org.easymock.EasyMock.anyObject;
import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.expect;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.BuildTargetFactory;
import com.facebook.buck.rules.DefaultBuildTargetSourcePath;
import com.facebook.buck.rules.PathSourcePath;
import com.facebook.buck.rules.RuleKey;
import com.facebook.buck.rules.SourcePath;
import com.facebook.buck.rules.SourcePathResolver;
import com.facebook.buck.rules.SourcePathRuleFinder;
import com.facebook.buck.rules.keys.DefaultRuleKeyFactory;
import com.facebook.buck.testutil.FakeFileHashCache;
import com.facebook.buck.testutil.integration.TemporaryPaths;
import com.facebook.buck.timing.Clock;
import com.facebook.buck.timing.FakeClock;
import com.facebook.buck.zip.CustomZipOutputStream;
import com.facebook.buck.zip.ZipOutputStreams;
import com.google.common.base.Function;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
import com.google.common.io.Files;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import java.util.Optional;
import java.util.zip.ZipEntry;
import org.junit.Rule;
import org.junit.Test;
public class PrebuiltJarSymbolsFinderTest {
@Rule public TemporaryPaths tmp = new TemporaryPaths();
@Test
public void extractSymbolsFromBinaryJar() throws InterruptedException, IOException {
ImmutableSet<String> entries =
ImmutableSet.of(
"META-INF/",
"META-INF/MANIFEST.MF",
"com/",
"com/facebook/",
"com/facebook/buck/",
"com/facebook/buck/cli/",
"com/facebook/buck/cli/Main.class",
"com/facebook/buck/cli/Main$1.class",
"com/facebook/buck/cli/TestSelectorOptions.class",
"com/facebook/buck/cli/TestSelectorOptions$TestSelectorsOptionHandler$1.class",
"com/facebook/buck/cli/TestSelectorOptions$TestSelectorsOptionHandler.class");
PrebuiltJarSymbolsFinder finder = createFinderForFileWithEntries("real.jar", entries);
Symbols symbols = finder.extractSymbols();
assertEquals(
"Only entries that correspond to .class files for top-level types should be included.",
ImmutableSet.of("com.facebook.buck.cli.Main", "com.facebook.buck.cli.TestSelectorOptions"),
ImmutableSet.copyOf(symbols.provided));
}
@Test
public void extractSymbolsFromGeneratedBinaryJar() throws IOException {
PrebuiltJarSymbolsFinder finder = createFinderForGeneratedJar("//foo:jar_genrule");
Symbols symbols = finder.extractSymbols();
assertTrue(
"There should be no provided symbols if the binaryJar is not a PathSourcePath.",
Iterables.isEmpty(symbols.provided));
}
@Test
public void contentsOfBinaryJarShouldAffectRuleKey() throws InterruptedException, IOException {
// The path to the JAR file to use as the binaryJar of the PrebuiltJarSymbolsFinder.
final Path relativePathToJar = Paths.get("common.jar");
final Path absolutePathToJar = tmp.getRoot().resolve(relativePathToJar);
// Mock out calls to a SourcePathResolver so we can create a legitimate
// DefaultRuleKeyFactory.
final SourcePathRuleFinder ruleFinder = createMock(SourcePathRuleFinder.class);
final SourcePathResolver pathResolver = new SourcePathResolver(ruleFinder);
createMock(SourcePathResolver.class);
expect(ruleFinder.getRule(anyObject(SourcePath.class))).andReturn(Optional.empty()).anyTimes();
// Calculates the RuleKey for a JavaSymbolsRule with a PrebuiltJarSymbolsFinder whose binaryJar
// is a JAR file with the specified entries.
Function<ImmutableSet<String>, RuleKey> createRuleKey =
entries -> {
File jarFile = absolutePathToJar.toFile();
JavaSymbolsRule javaSymbolsRule;
FakeFileHashCache fileHashCache;
try {
PrebuiltJarSymbolsFinder finder =
createFinderForFileWithEntries(relativePathToJar.getFileName().toString(), entries);
HashCode hash = Files.hash(jarFile, Hashing.sha1());
Map<Path, HashCode> pathsToHashes = ImmutableMap.of(absolutePathToJar, hash);
fileHashCache = new FakeFileHashCache(pathsToHashes);
javaSymbolsRule =
new JavaSymbolsRule(
BuildTargetFactory.newInstance("//foo:rule"),
finder,
new ProjectFilesystem(tmp.getRoot()));
} catch (InterruptedException | IOException e) {
throw new RuntimeException(e);
}
RuleKey ruleKey =
new DefaultRuleKeyFactory(0, fileHashCache, pathResolver, ruleFinder)
.build(javaSymbolsRule);
jarFile.delete();
return ruleKey;
};
RuleKey key1 = createRuleKey.apply(ImmutableSet.of("entry1", "entry2"));
RuleKey key2 = createRuleKey.apply(ImmutableSet.of("entry1", "entry2"));
RuleKey key3 = createRuleKey.apply(ImmutableSet.of("entry1", "entry2", "entry3"));
assertNotNull(key1);
assertNotNull(key2);
assertNotNull(key3);
assertEquals(
"Two instances of a JavaSymbolsRule with the same inputs should have the same RuleKey.",
key1,
key2);
assertNotEquals(
"Changing the contents of the binaryJar for the PrebuiltJarSymbolsFinder should change "
+ "the RuleKey of the JavaSymbolsRule that contains it.",
key1,
key3);
}
@Test
public void generatedBinaryJarShouldNotAffectRuleKey() throws InterruptedException {
SourcePathResolver pathResolver = null;
SourcePathRuleFinder ruleFinder = null;
Path jarFile = tmp.getRoot().resolve("common.jar");
Map<Path, HashCode> pathsToHashes =
ImmutableMap.of(jarFile, HashCode.fromString(Strings.repeat("abcd", 10)));
FakeFileHashCache fileHashCache = new FakeFileHashCache(pathsToHashes);
JavaSymbolsRule javaSymbolsRule1 =
new JavaSymbolsRule(
BuildTargetFactory.newInstance("//foo:rule"),
createFinderForGeneratedJar("//foo:jar_genrule1"),
new ProjectFilesystem(tmp.getRoot()));
RuleKey key1 =
new DefaultRuleKeyFactory(0, fileHashCache, pathResolver, ruleFinder)
.build(javaSymbolsRule1);
JavaSymbolsRule javaSymbolsRule2 =
new JavaSymbolsRule(
BuildTargetFactory.newInstance("//foo:rule"),
createFinderForGeneratedJar("//foo:jar_genrule2"),
new ProjectFilesystem(tmp.getRoot()));
RuleKey key2 =
new DefaultRuleKeyFactory(0, fileHashCache, pathResolver, ruleFinder)
.build(javaSymbolsRule2);
assertNotNull(key1);
assertNotNull(key2);
assertEquals(
"Keys should match even though different BuildTargetSourcePaths are used.", key1, key2);
}
private PrebuiltJarSymbolsFinder createFinderForFileWithEntries(
String jarFileName, Iterable<String> entries) throws InterruptedException, IOException {
Clock clock = new FakeClock(1);
Path jarFile = tmp.newFile(jarFileName);
try (OutputStream stream =
new BufferedOutputStream(java.nio.file.Files.newOutputStream(jarFile));
CustomZipOutputStream out =
ZipOutputStreams.newOutputStream(
stream, ZipOutputStreams.HandleDuplicates.THROW_EXCEPTION, clock)) {
for (String entry : entries) {
out.putNextEntry(new ZipEntry(entry));
}
}
ProjectFilesystem filesystem = new ProjectFilesystem(tmp.getRoot());
SourcePath sourcePath = new PathSourcePath(filesystem, Paths.get(jarFileName));
return new PrebuiltJarSymbolsFinder(sourcePath);
}
private PrebuiltJarSymbolsFinder createFinderForGeneratedJar(String target) {
BuildTarget buildTarget = BuildTargetFactory.newInstance(target);
SourcePath sourcePath = new DefaultBuildTargetSourcePath(buildTarget);
return new PrebuiltJarSymbolsFinder(sourcePath);
}
}