// 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; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.common.eventbus.EventBus; import com.google.devtools.build.lib.cmdline.Label; import com.google.devtools.build.lib.cmdline.PackageIdentifier; import com.google.devtools.build.lib.events.Reporter; import com.google.devtools.build.lib.packages.util.PackageFactoryApparatus; import com.google.devtools.build.lib.packages.util.PackageFactoryTestBase; import com.google.devtools.build.lib.syntax.Type; import com.google.devtools.build.lib.testutil.TestUtils; import com.google.devtools.build.lib.vfs.Path; import com.google.devtools.build.lib.vfs.PathFragment; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** * Unit tests for {@code PackageFactory}. */ @RunWith(JUnit4.class) public class PackageFactoryTest extends PackageFactoryTestBase { @Test public void testCreatePackage() throws Exception { Path buildFile = scratch.file("/pkgname/BUILD", "# empty build file "); Package pkg = packages.createPackage("pkgname", buildFile); assertThat(pkg.getName()).isEqualTo("pkgname"); assertThat(Sets.newHashSet(pkg.getTargets(Rule.class))).isEmpty(); } @Test public void testCreatePackageIsolatedFromOuterErrors() throws Exception { ExecutorService e = Executors.newCachedThreadPool(); final Semaphore beforeError = new Semaphore(0); final Semaphore afterError = new Semaphore(0); Reporter reporter = new Reporter(new EventBus()); ParsingTracker parser = new ParsingTracker(beforeError, afterError, reporter); final Logger log = Logger.getLogger(PackageFactory.class.getName()); log.addHandler(parser); Level originalLevel = log.getLevel(); log.setLevel(Level.FINE); e.execute(new ErrorReporter(reporter, beforeError, afterError)); e.execute(parser); // wait for all to finish e.shutdown(); assertThat(e.awaitTermination(TestUtils.WAIT_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS)) .isTrue(); log.removeHandler(parser); log.setLevel(originalLevel); assertThat(parser.hasParsed()).isTrue(); } @Test public void testBadRuleName() throws Exception { events.setFailFast(false); Path buildFile = scratch.file("/badrulename/BUILD", "cc_library(name = 3)"); Package pkg = packages.createPackage("badrulename", buildFile); events.assertContainsError("cc_library 'name' attribute must be a string"); assertThat(pkg.containsErrors()).isTrue(); } @Test public void testNoRuleName() throws Exception { events.setFailFast(false); Path buildFile = scratch.file("/badrulename/BUILD", "cc_library()"); Package pkg = packages.createPackage("badrulename", buildFile); events.assertContainsError("cc_library rule has no 'name' attribute"); assertThat(pkg.containsErrors()).isTrue(); } @Test public void testBadPackageName() throws Exception { try { // PathFragment parsing de-double slashes, and normalization of the path fragment removes // up reference (/../), so use triple dot /.../ that will always be a forbidden package name. packages.createPackage("not even a legal/.../label", emptyBuildFile("not even a legal/.../label")); fail(); } catch (NoSuchPackageException e) { assertThat(e) .hasMessageThat() .contains( "no such package 'not even a legal/.../label': " + "illegal package name: 'not even a legal/.../label' "); } } @Test public void testColonInExportsFilesTargetName() throws Exception { events.setFailFast(false); Path path = scratch.file( "/googledata/cafe/BUILD", "exports_files(['houseads/house_ads:ca-aol_parenting_html'])"); Package pkg = packages.createPackage("googledata/cafe", path); events.assertContainsError("target names may not contain ':'"); assertThat(pkg.getTargets(FileTarget.class).toString()) .doesNotContain("houseads/house_ads:ca-aol_parenting_html"); assertThat(pkg.containsErrors()).isTrue(); } @Test public void testPackageNameWithPROTECTEDIsOk() throws Exception { events.setFailFast(false); // One "PROTECTED": assertThat(isValidPackageName("foo/PROTECTED/bar")).isTrue(); // Multiple "PROTECTED"s: assertThat(isValidPackageName("foo/PROTECTED/bar/PROTECTED/wiz")).isTrue(); } @Test public void testDuplicateRuleName() throws Exception { events.setFailFast(false); Path buildFile = scratch.file( "/duplicaterulename/BUILD", "# -*- python -*-", "proto_library(name = 'spell_proto', srcs = ['spell.proto'], cc_api_version = 2)", "cc_library(name = 'spell_proto')"); Package pkg = packages.createPackage("duplicaterulename", buildFile); events.assertContainsError( "cc_library rule 'spell_proto' in package " + "'duplicaterulename' conflicts with existing proto_library rule"); assertThat(pkg.containsErrors()).isTrue(); } @Test public void testDuplicatedDependencies() throws Exception { events.setFailFast(false); Path buildFile = scratch.file( "/has_dupe/BUILD", "cc_library(name='dep')", "cc_library(name='has_dupe', deps=[':dep', ':dep'])"); Package pkg = packages.createPackage("has_dupe", buildFile); events.assertContainsError( "Label '//has_dupe:dep' is duplicated in the 'deps' " + "attribute of rule 'has_dupe'"); assertThat(pkg.containsErrors()).isTrue(); assertThat(pkg.getRule("has_dupe")).isNotNull(); assertThat(pkg.getRule("dep")).isNotNull(); assertThat(pkg.getRule("has_dupe").containsErrors()).isTrue(); assertThat(pkg.getRule("dep").containsErrors()).isTrue(); // because all rules in an // errant package are // themselves errant. } @Test public void testPrefixWithinSameRule1() throws Exception { events.setFailFast(false); Path buildFile = scratch.file( "/fruit/orange/BUILD", "genrule(name='orange', srcs=[], outs=['a', 'a/b'], cmd='')"); packages.createPackage("fruit/orange", buildFile); events.assertContainsError("rule 'orange' has conflicting output files 'a/b' and 'a"); } @Test public void testPrefixWithinSameRule2() throws Exception { events.setFailFast(false); Path buildFile = scratch.file( "/fruit/orange/BUILD", "genrule(name='orange', srcs=[], outs=['a/b', 'a'], cmd='')"); packages.createPackage("fruit/orange", buildFile); events.assertContainsError("rule 'orange' has conflicting output files 'a' and 'a/b"); } @Test public void testPrefixBetweenRules1() throws Exception { events.setFailFast(false); Path buildFile = scratch.file( "/fruit/kiwi/BUILD", "genrule(name='kiwi1', srcs=[], outs=['a'], cmd='')", "genrule(name='kiwi2', srcs=[], outs=['a/b'], cmd='')"); packages.createPackage("fruit/kiwi", buildFile); events.assertContainsError( "output file 'a/b' of rule 'kiwi2' conflicts " + "with output file 'a' of rule 'kiwi1'"); } @Test public void testPrefixBetweenRules2() throws Exception { events.setFailFast(false); Path buildFile = scratch.file( "/fruit/kiwi/BUILD", "genrule(name='kiwi1', srcs=[], outs=['a/b'], cmd='')", "genrule(name='kiwi2', srcs=[], outs=['a'], cmd='')"); packages.createPackage("fruit/kiwi", buildFile); events.assertContainsError( "output file 'a' of rule 'kiwi2' conflicts " + "with output file 'a/b' of rule 'kiwi1'"); } @Test public void testPackageConstant() throws Exception { Path buildFile = scratch.file("/pina/BUILD", "cc_library(name=PACKAGE_NAME + '-colada')"); Package pkg = packages.createPackage("pina", buildFile); events.assertNoWarningsOrErrors(); assertThat(pkg.containsErrors()).isFalse(); assertThat(pkg.getRule("pina-colada")).isNotNull(); assertThat(pkg.getRule("pina-colada").containsErrors()).isFalse(); assertThat(Sets.newHashSet(pkg.getTargets(Rule.class)).size()).isSameAs(1); } @Test public void testPackageConstantIsForbidden() throws Exception { events.setFailFast(false); Path buildFile = scratch.file("/pina/BUILD", "cc_library(name=PACKAGE_NAME + '-colada')"); packages.createPackage("pina", buildFile, "--incompatible_package_name_is_a_function=true"); events.assertContainsError("The value 'PACKAGE_NAME' has been removed"); } @Test public void testPackageNameFunction() throws Exception { Path buildFile = scratch.file("/pina/BUILD", "cc_library(name=package_name() + '-colada')"); Package pkg = packages.createPackage("pina", buildFile); events.assertNoWarningsOrErrors(); assertThat(pkg.containsErrors()).isFalse(); assertThat(pkg.getRule("pina-colada")).isNotNull(); assertThat(pkg.getRule("pina-colada").containsErrors()).isFalse(); assertThat(Sets.newHashSet(pkg.getTargets(Rule.class)).size()).isSameAs(1); } @Test public void testPackageConstantInExternalRepository() throws Exception { Path buildFile = scratch.file( "/external/a/b/BUILD", "genrule(name='c', srcs=[], outs=['ao'], cmd=REPOSITORY_NAME + ' ' + PACKAGE_NAME)"); Package pkg = packages.createPackage( PackageIdentifier.create("@a", PathFragment.create("b")), buildFile, events.reporter()); Rule c = pkg.getRule("c"); assertThat(AggregatingAttributeMapper.of(c).get("cmd", Type.STRING)).isEqualTo("@a b"); } @Test public void testPackageConstantInExternalRepositoryIsForbidden() throws Exception { events.setFailFast(false); Path buildFile = scratch.file( "/external/a/b/BUILD", "genrule(name='c', srcs=[], outs=['ao'], cmd=REPOSITORY_NAME)"); packages.createPackage( PackageIdentifier.create("@a", PathFragment.create("b")), buildFile, events.reporter(), "--incompatible_package_name_is_a_function=true"); events.assertContainsError("The value 'REPOSITORY_NAME' has been removed"); } @Test public void testPackageFunctionInExternalRepository() throws Exception { Path buildFile = scratch.file( "/external/a/b/BUILD", "genrule(name='c', srcs=[], outs=['o'], cmd=repository_name() + ' ' + package_name())"); Package pkg = packages.createPackage( PackageIdentifier.create("@a", PathFragment.create("b")), buildFile, events.reporter()); Rule c = pkg.getRule("c"); assertThat(AggregatingAttributeMapper.of(c).get("cmd", Type.STRING)).isEqualTo("@a b"); } @Test public void testMultipleDuplicateRuleName() throws Exception { events.setFailFast(false); Path buildFile = scratch.file( "/multipleduplicaterulename/BUILD", "# -*- python -*-", "proto_library(name = 'spellcheck_proto',", " srcs = ['spellcheck.proto'],", " cc_api_version = 2)", "cc_library(name = 'spellcheck_proto')", "proto_library(name = 'spell_proto',", " srcs = ['spell.proto'],", " cc_api_version = 2)", "cc_library(name = 'spell_proto')"); Package pkg = packages.createPackage("multipleduplicaterulename", buildFile); events.assertContainsError( "cc_library rule 'spellcheck_proto' in package " + "'multipleduplicaterulename' conflicts with existing proto_library rule"); events.assertContainsError( "cc_library rule 'spell_proto' in package " + "'multipleduplicaterulename' conflicts with existing proto_library rule"); assertThat(pkg.containsErrors()).isTrue(); } @Test public void testBuildFileTargetExists() throws Exception { Path buildFile = scratch.file("/foo/BUILD", ""); Package pkg = packages.createPackage("foo", buildFile); Target target = pkg.getTarget("BUILD"); assertThat(target.getName()).isEqualTo("BUILD"); // Test that it's memoized: assertThat(pkg.getTarget("BUILD")).isSameAs(target); } @Test public void testCreationOfInputFiles() throws Exception { Path buildFile = scratch.file( "/foo/BUILD", "exports_files(['Z'])", "cc_library(name='W', deps=['X', 'Y'])", "cc_library(name='X', srcs=['X'])", "cc_library(name='Y')"); Package pkg = packages.createPackage("foo", buildFile); assertThat(pkg.containsErrors()).isFalse(); // X is a rule with a circular self-dependency. assertThat(pkg.getTarget("X").getClass()).isSameAs(Rule.class); // Y is a rule assertThat(pkg.getTarget("Y").getClass()).isSameAs(Rule.class); // Z is a file assertThat(pkg.getTarget("Z").getClass()).isSameAs(InputFile.class); // A is nothing try { pkg.getTarget("A"); fail(); } catch (NoSuchTargetException e) { assertThat(e) .hasMessage( "no such target '//foo:A': " + "target 'A' not declared in package 'foo' defined by /foo/BUILD"); } // These are the only input files: BUILD, Z Set inputFiles = Sets.newTreeSet(); for (InputFile inputFile : pkg.getTargets(InputFile.class)) { inputFiles.add(inputFile.getName()); } assertThat(Lists.newArrayList(inputFiles)).containsExactly("BUILD", "Z").inOrder(); } @Test public void testThirdPartyLicenseError() throws Exception { events.setFailFast(false); Path buildFile = scratch.file("/third_party/foo/BUILD", "# line 1", "cc_library(name='bar')", "# line 3"); Package pkg = packages.createPackage("third_party/foo", buildFile); events.assertContainsError( "third-party rule '//third_party/foo:bar' lacks a license " + "declaration with one of the following types: " + "notice, reciprocal, permissive, restricted, unencumbered, by_exception_only"); assertThat(pkg.containsErrors()).isTrue(); } @Test public void testThirdPartyLicenseExportsFileError() throws Exception { events.setFailFast(false); Path buildFile = scratch.file("/third_party/foo/BUILD", "exports_files(['bar'])"); Package pkg = packages.createPackage("third_party/foo", buildFile); events.assertContainsError( "third-party file 'bar' lacks a license " + "declaration with one of the following types: " + "notice, reciprocal, permissive, restricted, unencumbered, by_exception_only"); assertThat(pkg.containsErrors()).isTrue(); } @Test public void testDuplicateRuleIsNotAddedToPackage() throws Exception { events.setFailFast(false); Path path = scratch.file( "/dup/BUILD", "proto_library(name = 'dup_proto',", " srcs = ['dup.proto'],", " cc_api_version = 2)", "", "cc_library(name = 'dup_proto',", " srcs = ['dup.pb.cc', 'dup.pb.h'])"); Package pkg = packages.createPackage("dup", path); events.assertContainsError( "cc_library rule 'dup_proto' in package 'dup' " + "conflicts with existing proto_library rule"); assertThat(pkg.containsErrors()).isTrue(); Rule dupProto = pkg.getRule("dup_proto"); // Check that the first rule of the given name "wins", and that each of the // "winning" rule's outputs is a member of the package. assertThat(dupProto.getRuleClass()).isEqualTo("proto_library"); for (OutputFile out : dupProto.getOutputFiles()) { assertThat(pkg.getTargets(FileTarget.class)).contains(out); } } @Test public void testConflictingRuleDoesNotUpdatePackage() throws Exception { events.setFailFast(false); // In this test, rule2's outputs conflict with rule1, so rule2 is rejected. // However, we must check that neither rule2, nor any of its inputs or // outputs is a member of the package, and that the conflicting output file // "out2" still has rule1 as its getGeneratingRule(). Path path = scratch.file( "/conflict/BUILD", "genrule(name = 'rule1',", " cmd = '',", " srcs = ['in1', 'in2'],", " outs = ['out1', 'out2'])", "genrule(name = 'rule2',", " cmd = '',", " srcs = ['in3', 'in4'],", " outs = ['out3', 'out2'])"); Package pkg = packages.createPackage("conflict", path); events.assertContainsError( "generated file 'out2' in rule 'rule2' " + "conflicts with existing generated file from rule 'rule1'"); assertThat(pkg.containsErrors()).isTrue(); assertThat(pkg.getRule("rule2")).isNull(); // Ensure that rule2's "out2" didn't overwrite rule1's: assertThat(((OutputFile) pkg.getTarget("out2")).getGeneratingRule()) .isSameAs(pkg.getRule("rule1")); // None of rule2, its inputs, or its outputs should belong to pkg: List found = new ArrayList<>(); for (String targetName : ImmutableList.of("rule2", "in3", "in4", "out3")) { try { found.add(pkg.getTarget(targetName)); } catch (NoSuchTargetException e) { /* good! */ } } assertThat(found).isEmpty(); } // Was: Regression test for bug "Rules declared after an error in // a package should be considered 'in error'". // Now: Regression test for bug "Why aren't ERRORS considered // fatal?*" @Test public void testAllRulesInErrantPackageAreInError() throws Exception { events.setFailFast(false); Path path = scratch.file( "/error/BUILD", "genrule(name = 'rule1',", " cmd = ':',", " outs = ['out.1'])", "list = ['bad']", "PopulateList(list)", // undefined => error "genrule(name = 'rule2',", " cmd = ':',", " outs = list)"); Package pkg = packages.createPackage("error", path); events.assertContainsError("name 'PopulateList' is not defined"); assertThat(pkg.containsErrors()).isTrue(); // rule1 would be fine but is still marked as in error: assertThat(pkg.getRule("rule1").containsErrors()).isTrue(); // rule2 is considered "in error" because it's after an error. // Indeed, it has the wrong "outs" set because the call to PopulateList // failed. Rule rule2 = pkg.getRule("rule2"); assertThat(rule2.containsErrors()).isTrue(); assertThat(Sets.newHashSet(rule2.getOutputFiles())) .isEqualTo(Sets.newHashSet(pkg.getTarget("bad"))); } @Test public void testHelpfulErrorForMissingExportsFiles() throws Exception { Path path = scratch.file("/x/BUILD", "cc_library(name='x', srcs=['x.cc'])"); scratch.file("/x/x.cc"); scratch.file("/x/y.cc"); scratch.file("/x/dir/dummy"); Package pkg = packages.createPackage("x", path); assertThat(pkg.getTarget("x.cc")).isNotNull(); // existing and mentioned. try { pkg.getTarget("y.cc"); // existing but not mentioned. fail(); } catch (NoSuchTargetException e) { assertThat(e) .hasMessage( "no such target '//x:y.cc': " + "target 'y.cc' not declared in package 'x'; " + "however, a source file of this name exists. " + "(Perhaps add 'exports_files([\"y.cc\"])' to x/BUILD?) " + "defined by /x/BUILD"); } try { pkg.getTarget("z.cc"); // non-existent and unmentioned. fail(); } catch (NoSuchTargetException e) { assertThat(e) .hasMessage( "no such target '//x:z.cc': " + "target 'z.cc' not declared in package 'x' (did you mean 'x.cc'?) " + "defined by /x/BUILD"); } try { pkg.getTarget("dir"); // existing directory but not mentioned. fail(); } catch (NoSuchTargetException e) { assertThat(e) .hasMessage( "no such target '//x:dir': target 'dir' not declared in package 'x'; " + "however, a source directory of this name exists. " + "(Perhaps add 'exports_files([\"dir\"])' to x/BUILD, " + "or define a filegroup?) defined by /x/BUILD"); } } @Test public void testTestSuitesImplicitlyDependOnAllRulesInPackage() throws Exception { Path path = scratch.file( "/x/BUILD", "java_test(name='j')", "test_suite(name='t1')", "test_suite(name='t2', tests=['//foo'])", "test_suite(name='t3', tests=['//foo'])", "cc_test(name='c')"); Package pkg = packages.createPackage("x", path); // Things to note: // - the t1 refers to both :j and :c, even though :c is a forward reference. // - $implicit_tests is empty unless tests=[] assertThat(attributes(pkg.getRule("t1")).get("$implicit_tests", BuildType.LABEL_LIST)) .containsExactlyElementsIn( Sets.newHashSet( Label.parseAbsolute("//x:c", ImmutableMap.of()), Label.parseAbsolute("//x:j", ImmutableMap.of()))); assertThat(attributes(pkg.getRule("t2")).get("$implicit_tests", BuildType.LABEL_LIST)) .isEmpty(); assertThat(attributes(pkg.getRule("t3")).get("$implicit_tests", BuildType.LABEL_LIST)) .isEmpty(); } @Test public void testGlobDirectoryExclusion() throws Exception { emptyFile("/fruit/data/apple"); emptyFile("/fruit/data/pear"); emptyFile("/fruit/data/berry/black"); emptyFile("/fruit/data/berry/blue"); Path file = scratch.file( "/fruit/BUILD", "cc_library(name = 'yes', srcs = glob(['data/*']))", "cc_library(name = 'no', srcs = glob(['data/*'], exclude_directories=0))"); Package pkg = packages.eval("fruit", file); events.assertNoWarningsOrErrors(); List