// Copyright 2015 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.lib.packages.util; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.devtools.build.lib.cmdline.PackageIdentifier; import com.google.devtools.build.lib.events.Event; import com.google.devtools.build.lib.events.EventHandler; import com.google.devtools.build.lib.events.Location; import com.google.devtools.build.lib.events.util.EventCollectionApparatus; import com.google.devtools.build.lib.packages.AttributeMap; import com.google.devtools.build.lib.packages.GlobCache; import com.google.devtools.build.lib.packages.OutputFile; import com.google.devtools.build.lib.packages.Package; import com.google.devtools.build.lib.packages.RawAttributeMapper; import com.google.devtools.build.lib.packages.Rule; import com.google.devtools.build.lib.syntax.Printer; import com.google.devtools.build.lib.testutil.Scratch; import com.google.devtools.build.lib.testutil.TestUtils; import com.google.devtools.build.lib.util.Pair; import com.google.devtools.build.lib.vfs.Dirent; import com.google.devtools.build.lib.vfs.FileSystem; import com.google.devtools.build.lib.vfs.FileSystemUtils; import com.google.devtools.build.lib.vfs.Path; import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem; import java.io.FileNotFoundException; import java.io.IOException; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.concurrent.Semaphore; import java.util.logging.Handler; import java.util.logging.LogRecord; import org.junit.Before; /** * Base class for PackageFactory tests. */ public abstract class PackageFactoryTestBase { protected Scratch scratch; protected EventCollectionApparatus events = new EventCollectionApparatus(); protected PackageFactoryApparatus packages = createPackageFactoryApparatus(); protected com.google.devtools.build.lib.packages.Package expectEvalSuccess(String... content) throws InterruptedException, IOException { Path file = scratch.file("/pkg/BUILD", content); Package pkg = packages.eval("pkg", file); assertFalse(pkg.containsErrors()); return pkg; } protected void expectEvalError(String expectedError, String... content) throws Exception { events.setFailFast(false); Path file = scratch.file("/pkg/BUILD", content); Package pkg = packages.eval("pkg", file); assertTrue("Expected evaluation error, but none was not reported", pkg.containsErrors()); events.assertContainsError(expectedError); } protected abstract PackageFactoryApparatus createPackageFactoryApparatus(); protected Path throwOnReaddir = null; protected static AttributeMap attributes(Rule rule) { return RawAttributeMapper.of(rule); } protected static void assertOutputFileForRule(Package pkg, Collection<String> outNames, Rule rule) throws Exception { for (String outName : outNames) { OutputFile out = (OutputFile) pkg.getTarget(outName); assertThat(rule.getOutputFiles()).contains(out); assertSame(rule, out.getGeneratingRule()); assertEquals(outName, out.getName()); assertEquals("generated file", out.getTargetKind()); } assertThat(rule.getOutputFiles()).hasSize(outNames.size()); } protected static void assertEvaluates(Package pkg, List<String> expected, String... include) throws Exception { assertEvaluates(pkg, expected, ImmutableList.copyOf(include), Collections.<String>emptyList()); } protected static void assertEvaluates( Package pkg, List<String> expected, List<String> include, List<String> exclude) throws Exception { GlobCache globCache = new GlobCache( pkg.getFilename().getParentDirectory(), pkg.getPackageIdentifier(), PackageFactoryApparatus.createEmptyLocator(), null, TestUtils.getPool(), -1); assertThat(globCache.globUnsorted(include, exclude, false)).containsExactlyElementsIn(expected); } @Before public final void initializeFileSystem() throws Exception { FileSystem fs = new InMemoryFileSystem() { @Override public Collection<Dirent> readdir(Path path, boolean followSymlinks) throws IOException { if (path.equals(throwOnReaddir)) { throw new FileNotFoundException(path.getPathString()); } return super.readdir(path, followSymlinks); } }; Path tmpPath = fs.getPath("/tmp"); scratch = new Scratch(tmpPath); } protected Path emptyBuildFile(String packageName) { return emptyFile(getPathPrefix() + "/" + packageName + "/BUILD"); } protected Path emptyFile(String path) { try { return scratch.file(path); } catch (IOException e) { throw new IllegalStateException(e); } } protected boolean isValidPackageName(String packageName) throws Exception { // Write a license decl just in case it's a third_party package: Path buildFile = scratch.file( getPathPrefix() + "/" + packageName + "/BUILD", "licenses(['notice'])"); Package pkg = packages.createPackage(packageName, buildFile); return !pkg.containsErrors(); } /******************************************************************** * * * Test "glob" function in build language * * * ********************************************************************/ protected void assertGlobFails(String globCallExpression, String expectedError) throws Exception { Package pkg = buildPackageWithGlob(globCallExpression); events.assertContainsError(expectedError); assertTrue(pkg.containsErrors()); } private Package buildPackageWithGlob(String globCallExpression) throws Exception { scratch.deleteFile("/dummypackage/BUILD"); Path file = scratch.file("/dummypackage/BUILD", "x = " + globCallExpression); return packages.eval("dummypackage", file); } private List<Pair<String, Boolean>> createGlobCacheKeys( List<String> expressions, boolean excludeDirs) { List<Pair<String, Boolean>> keys = Lists.newArrayListWithCapacity(expressions.size()); for (String expression : expressions) { keys.add(Pair.of(expression, excludeDirs)); } return keys; } /** * Test globbing in the context of a package, using the build language. * We use the specially setup "globs" test package and the files beneath it. * @param result the expected list of filenames that match the glob * @param includes an include pattern for the glob * @param excludes an exclude pattern for the glob * @param excludeDirs an exclude_directories flag for the glob * @throws Exception if the glob doesn't match the expected result. */ protected void assertGlobMatches( List<String> result, List<String> includes, List<String> excludes, boolean excludeDirs) throws Exception { // The BUILD language, unlike Skylark, doesn't have fail(), so instead, // we rely on boolean short circuit logic to only try to evaluate // the undefined identifier this_will_fail if the result isn't as expected, // in which case an error occurs (which we test in testGlobNegativeTest). Pair<Package, GlobCache> evaluated = evaluateGlob( includes, excludes, excludeDirs, Printer.format("(result == sorted(%r)) or this_will_fail()", result)); Package pkg = evaluated.first; GlobCache globCache = evaluated.second; // Ensure all of the patterns are recorded against this package: assertTrue(globCache.getKeySet().containsAll(createGlobCacheKeys(includes, excludeDirs))); assertTrue(globCache.getKeySet().containsAll(createGlobCacheKeys(excludes, excludeDirs))); assertFalse(pkg.containsErrors()); } /** * Evaluate a glob() call against a test directory and BUILD code to process the results. * @param includes a list of glob patterns; glob will include these files. * @param excludes a list of glob patterns to exclude even if previously included. * @param excludeDirs true if directories should be excluded from the match. * @param resultAssertion code in the BUILD language that can access the variable result, * to which the result of the glob will be bound, and that may contain an assertion on it. * @return a Package and a GlobCache. * @throws Exception if the processResult code causes a failure. */ private Pair<Package, GlobCache> evaluateGlob( List<String> includes, List<String> excludes, boolean excludeDirs, String resultAssertion) throws Exception { Path globsDir = scratch.dir("/globs"); globsDir.getChild("subdir").createDirectory(); for (String file : ImmutableList.of("Wombat1.java", "Wombat2.java", "subdir/Wombat3.java")) { FileSystemUtils.createEmptyFile(globsDir.getRelative(file)); } Path file = scratch.file( "/globs/BUILD", Printer.format( "result = glob(%r, exclude=%r, exclude_directories=%r)", includes, excludes, excludeDirs ? 1 : 0), resultAssertion); return packages.evalAndReturnGlobCache("globs", file, packages.ast(file)); } protected void assertGlobProducesError(String pattern, boolean errorExpected) throws Exception { events.setFailFast(false); Package pkg = evaluateGlob(ImmutableList.of(pattern), Collections.<String>emptyList(), false, "").first; assertEquals(errorExpected, pkg.containsErrors()); boolean foundError = false; for (Event event : events.collector()) { if (event.getMessage().contains("glob")) { if (!errorExpected) { fail("error not expected for glob pattern " + pattern + ", but got: " + event); return; } foundError = errorExpected; break; } } assertEquals(errorExpected, foundError); } /** Runnable that asks for parsing of build file and synchronizes it with * ErrorReporter. It consumes log messages from PackageFactory to release * first semaphore when parsing is started and waits for second semaphore * before it ends. */ protected class ParsingTracker extends Handler implements Runnable { private final Semaphore parsingStarted; private final Semaphore errorReported; private final EventHandler eventHandler; private boolean first = true; private boolean parsedOK; public ParsingTracker(Semaphore first, Semaphore second, EventHandler eventHandler) { this.eventHandler = eventHandler; parsingStarted = first; errorReported = second; } @Override public void run() { try { Path buildFile = scratch.file( getPathPrefix() + "/isolated/BUILD", "# -*- python -*-", "", "java_library(name = 'mylib',", " srcs = 'java/A.java')"); packages.createPackage( PackageIdentifier.createInMainRepo("isolated"), buildFile, eventHandler); parsedOK = true; } catch (Exception e) { e.printStackTrace(); } } public boolean hasParsed() { return parsedOK; } @Override public void close() throws SecurityException {} @Override public void flush() {} @Override public void publish(LogRecord record) { if (!record.getMessage().contains("isolated")) { return; } if (first) { parsingStarted.release(); first = false; } else { try { errorReported.acquire(); } catch (InterruptedException e) { e.printStackTrace(); fail("parsing thread interrupted"); } } } } protected abstract String getPathPrefix(); /** Process interfering with parsing of build files. * It waits until parsing of some BUILD file is started and then reports * arbitrary error. It signals that error was submitted so the parsing can be * finished at the end. */ protected class ErrorReporter implements Runnable { private final EventHandler eventHandler; private final Semaphore parsingStarted; private final Semaphore errorReported; public ErrorReporter(EventHandler eventHandler, Semaphore first, Semaphore second) { this.eventHandler = eventHandler; parsingStarted = first; errorReported = second; } @Override public void run() { try { parsingStarted.acquire(); eventHandler.handle( Event.error(Location.fromFile(scratch.file("dummy")), "Error from other " + "thread")); errorReported.release(); } catch (InterruptedException e) { e.printStackTrace(); fail("ErrorReporter thread interrupted"); } catch (IOException e) { e.printStackTrace(); fail("ErrorReporter thread failed with IOException"); } } } }