// Copyright 2016 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.bazel.repository.skylark; 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.devtools.build.lib.analysis.ConfiguredRuleClassProvider; import com.google.devtools.build.lib.analysis.ConfiguredTarget; import com.google.devtools.build.lib.analysis.util.AnalysisMock; import com.google.devtools.build.lib.analysis.util.BuildViewTestCase; import com.google.devtools.build.lib.bazel.repository.downloader.HttpDownloader; import com.google.devtools.build.lib.packages.BuildFileContainsErrorsException; import com.google.devtools.build.lib.packages.NoSuchPackageException; import com.google.devtools.build.lib.rules.cpp.FdoSupportFunction; import com.google.devtools.build.lib.rules.cpp.FdoSupportValue; import com.google.devtools.build.lib.rules.repository.LocalRepositoryFunction; import com.google.devtools.build.lib.rules.repository.LocalRepositoryRule; import com.google.devtools.build.lib.rules.repository.RepositoryDelegatorFunction; import com.google.devtools.build.lib.rules.repository.RepositoryFunction; import com.google.devtools.build.lib.rules.repository.RepositoryLoaderFunction; import com.google.devtools.build.lib.skyframe.SkyFunctions; import com.google.devtools.build.lib.testutil.TestRuleClassProvider; import com.google.devtools.build.skyframe.SkyFunction; import com.google.devtools.build.skyframe.SkyFunctionName; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.Mockito; /** * Integration test for skylark repository not as heavyweight than shell integration tests. */ @RunWith(JUnit4.class) public class SkylarkRepositoryIntegrationTest extends BuildViewTestCase { // The RuleClassProvider loaded with the SkylarkRepositoryModule private ConfiguredRuleClassProvider ruleProvider = null; // The Analysis mock injected with the SkylarkRepositoryFunction private AnalysisMock analysisMock = null; /** * Proxy to the real analysis mock to overwrite {@code #getSkyFunctions(BlazeDirectories)} to * inject the SkylarkRepositoryFunction in the list of SkyFunctions. In Bazel, this function is * injected by the corresponding @{code BlazeModule}. */ private static class CustomAnalysisMock extends AnalysisMock.Delegate { CustomAnalysisMock(AnalysisMock proxied) { super(proxied); } @Override public ImmutableMap<SkyFunctionName, SkyFunction> getSkyFunctions() { // Add both the local repository and the skylark repository functions // The RepositoryCache mock injected with the SkylarkRepositoryFunction HttpDownloader downloader = Mockito.mock(HttpDownloader.class); RepositoryFunction localRepositoryFunction = new LocalRepositoryFunction(); SkylarkRepositoryFunction skylarkRepositoryFunction = new SkylarkRepositoryFunction(downloader); ImmutableMap<String, RepositoryFunction> repositoryHandlers = ImmutableMap.of(LocalRepositoryRule.NAME, localRepositoryFunction); RepositoryDelegatorFunction function = new RepositoryDelegatorFunction( repositoryHandlers, skylarkRepositoryFunction, new AtomicBoolean(true)); function.setClientEnvironment(ImmutableMap.<String, String>of()); return ImmutableMap.of( SkyFunctions.REPOSITORY_DIRECTORY, function, SkyFunctions.REPOSITORY, new RepositoryLoaderFunction(), FdoSupportValue.SKYFUNCTION, new FdoSupportFunction()); } } @Override protected AnalysisMock getAnalysisMock() { if (analysisMock == null) { analysisMock = new CustomAnalysisMock(super.getAnalysisMock()); } return analysisMock; } @Override protected ConfiguredRuleClassProvider getRuleClassProvider() { // We inject the repository module in our test rule class provider. if (ruleProvider == null) { ConfiguredRuleClassProvider.Builder builder = new ConfiguredRuleClassProvider.Builder(); TestRuleClassProvider.addStandardRules(builder); builder.addSkylarkModule(SkylarkRepositoryModule.class); ruleProvider = builder.build(); } return ruleProvider; } @Override protected void invalidatePackages() throws InterruptedException { // Repository shuffling breaks access to config-needed paths like //tools/jdk:toolchain and // these tests don't do anything interesting with configurations anyway. So exempt them. invalidatePackages(/*alsoConfigs=*/false); } @Test public void testSkylarkLocalRepository() throws Exception { // A simple test that recreates local_repository with Skylark. scratch.file("/repo2/WORKSPACE"); scratch.file("/repo2/bar.txt"); scratch.file("/repo2/BUILD", "filegroup(name='bar', srcs=['bar.txt'], path='foo')"); scratch.file( "def.bzl", "def _impl(repository_ctx):", " repository_ctx.symlink(repository_ctx.attr.path, '')", "", "repo = repository_rule(", " implementation=_impl,", " local=True,", " attrs={'path': attr.string(mandatory=True)})"); scratch.file(rootDirectory.getRelative("BUILD").getPathString()); scratch.overwriteFile(rootDirectory.getRelative("WORKSPACE").getPathString(), new ImmutableList.Builder<String>() .addAll(analysisMock.getWorkspaceContents(mockToolsConfig)) .add("load('//:def.bzl', 'repo')") .add("repo(name='foo', path='/repo2')") .build()); invalidatePackages(); ConfiguredTarget target = getConfiguredTarget("@foo//:bar"); Object path = target.getTarget().getAssociatedRule().getAttributeContainer().getAttr("path"); assertThat(path).isEqualTo("foo"); } @Test public void testSkylarkSymlinkFileFromRepository() throws Exception { scratch.file("/repo2/bar.txt", "filegroup(name='bar', srcs=['foo.txt'], path='foo')"); scratch.file("/repo2/BUILD"); scratch.file("/repo2/WORKSPACE"); scratch.file( "def.bzl", "def _impl(repository_ctx):", " repository_ctx.symlink(Label('@repo2//:bar.txt'), 'BUILD')", " repository_ctx.file('foo.txt', 'foo')", "", "repo = repository_rule(", " implementation=_impl,", " local=True)"); scratch.file(rootDirectory.getRelative("BUILD").getPathString()); scratch.overwriteFile(rootDirectory.getRelative("WORKSPACE").getPathString(), new ImmutableList.Builder<String>() .addAll(analysisMock.getWorkspaceContents(mockToolsConfig)) .add("local_repository(name='repo2', path='/repo2')") .add("load('//:def.bzl', 'repo')") .add("repo(name='foo')") .build()); invalidatePackages(); ConfiguredTarget target = getConfiguredTarget("@foo//:bar"); Object path = target.getTarget().getAssociatedRule().getAttributeContainer().getAttr("path"); assertThat(path).isEqualTo("foo"); } @Test public void testSkylarkRepositoryTemplate() throws Exception { scratch.file("/repo2/bar.txt", "filegroup(name='{target}', srcs=['foo.txt'], path='{path}')"); scratch.file("/repo2/BUILD"); scratch.file("/repo2/WORKSPACE"); scratch.file( "def.bzl", "def _impl(repository_ctx):", " repository_ctx.template('BUILD', Label('@repo2//:bar.txt'), " + "{'{target}': 'bar', '{path}': 'foo'})", " repository_ctx.file('foo.txt', 'foo')", "", "repo = repository_rule(", " implementation=_impl,", " local=True)"); scratch.file(rootDirectory.getRelative("BUILD").getPathString()); scratch.overwriteFile(rootDirectory.getRelative("WORKSPACE").getPathString(), new ImmutableList.Builder<String>() .addAll(analysisMock.getWorkspaceContents(mockToolsConfig)) .add("local_repository(name='repo2', path='/repo2')") .add("load('//:def.bzl', 'repo')") .add("repo(name='foo')") .build()); invalidatePackages(); ConfiguredTarget target = getConfiguredTarget("@foo//:bar"); Object path = target.getTarget().getAssociatedRule().getAttributeContainer().getAttr("path"); assertThat(path).isEqualTo("foo"); } @Test public void testSkylarkRepositoryName() throws Exception { // Variation of the template rule to test the repository_ctx.name field. scratch.file("/repo2/bar.txt", "filegroup(name='bar', srcs=['foo.txt'], path='{path}')"); scratch.file("/repo2/BUILD"); scratch.file("/repo2/WORKSPACE"); scratch.file( "def.bzl", "def _impl(repository_ctx):", " repository_ctx.template('BUILD', Label('@repo2//:bar.txt'), " + "{'{path}': repository_ctx.name})", " repository_ctx.file('foo.txt', 'foo')", "", "repo = repository_rule(", " implementation=_impl,", " local=True)"); scratch.file(rootDirectory.getRelative("BUILD").getPathString()); scratch.overwriteFile(rootDirectory.getRelative("WORKSPACE").getPathString(), new ImmutableList.Builder<String>() .addAll(analysisMock.getWorkspaceContents(mockToolsConfig)) .add("local_repository(name='repo2', path='/repo2')") .add("load('//:def.bzl', 'repo')") .add("repo(name='foobar')") .build()); invalidatePackages(); ConfiguredTarget target = getConfiguredTarget("@foobar//:bar"); Object path = target.getTarget().getAssociatedRule().getAttributeContainer().getAttr("path"); assertThat(path).isEqualTo("foobar"); } @Test public void testCycleErrorWhenCallingRandomTarget() throws Exception { reporter.removeHandler(failFastHandler); scratch.file("/repo2/data.txt", "data"); scratch.file("/repo2/BUILD", "exports_files_(['data.txt'])"); scratch.file("/repo2/def.bzl", "def macro():", " print('bleh')"); scratch.file("/repo2/WORKSPACE"); scratch.overwriteFile( rootDirectory.getRelative("WORKSPACE").getPathString(), "load('@foo//:def.bzl', 'repo')", "repo(name='foobar')", "local_repository(name='foo', path='/repo2')"); try { invalidatePackages(); getTarget("@foobar//:data.txt"); fail(); } catch (BuildFileContainsErrorsException e) { // This is expected } assertDoesNotContainEvent("cycle"); assertContainsEvent("Maybe repository 'foo' was defined later in your WORKSPACE file?"); assertContainsEvent("Failed to load Skylark extension '@foo//:def.bzl'."); } @Test public void testCycleErrorWhenCallingCycleTarget() throws Exception { reporter.removeHandler(failFastHandler); scratch.file("/repo2/data.txt", "data"); scratch.file("/repo2/BUILD", "exports_files_(['data.txt'])"); scratch.file("/repo2/def.bzl", "def macro():", " print('bleh')"); scratch.file("/repo2/WORKSPACE"); scratch.overwriteFile( rootDirectory.getRelative("WORKSPACE").getPathString(), "load('@foo//:def.bzl', 'repo')", "repo(name='foobar')", "local_repository(name='foo', path='/repo2')"); try { invalidatePackages(); getTarget("@foo//:data.txt"); fail(); } catch (BuildFileContainsErrorsException e) { // This is expected } assertDoesNotContainEvent("cycle"); assertContainsEvent("Maybe repository 'foo' was defined later in your WORKSPACE file?"); assertContainsEvent("Failed to load Skylark extension '@foo//:def.bzl'."); } @Test public void testCycleErrorInWorkspaceFileWithExternalRepo() throws Exception { try (OutputStream output = scratch.resolve("WORKSPACE").getOutputStream(true /* append */)) { output.write(( "\nload('//foo:bar.bzl', 'foobar')" + "\ngit_repository(name = 'git_repo')").getBytes(StandardCharsets.UTF_8)); } scratch.file("BUILD", ""); scratch.file("foo/BUILD", ""); scratch.file( "foo/bar.bzl", "load('@git_repo//xyz:foo.bzl', 'rule_from_git')", "rule_from_git(name = 'foobar')"); invalidatePackages(); try { getTarget("@//:git_repo"); fail(); } catch (AssertionError expected) { assertThat(expected.getMessage()).contains("Failed to load Skylark extension " + "'@git_repo//xyz:foo.bzl'.\n" + "It usually happens when the repository is not defined prior to being used.\n" + "Maybe repository 'git_repo' was defined later in your WORKSPACE file?"); } } @Test public void testLoadDoesNotHideWorkspaceError() throws Exception { reporter.removeHandler(failFastHandler); scratch.file("/repo2/data.txt", "data"); scratch.file("/repo2/BUILD", "exports_files_(['data.txt'])"); scratch.file("/repo2/def.bzl", "def macro():", " print('bleh')"); scratch.file("/repo2/WORKSPACE"); scratch.overwriteFile(rootDirectory.getRelative("WORKSPACE").getPathString(), new ImmutableList.Builder<String>() .addAll(analysisMock.getWorkspaceContents(mockToolsConfig)) .add("local_repository(name='bleh')") .add("local_repository(name='foo', path='/repo2')") .add("load('@foo//:def.bzl', 'repo')") .add("repo(name='foobar')") .build()); try { invalidatePackages(); getTarget("@foo//:data.txt"); fail(); } catch (NoSuchPackageException e) { // This is expected assertThat(e.getMessage()).contains("Could not load //external package"); } assertContainsEvent("missing value for mandatory attribute 'path' in 'local_repository' rule"); } @Test public void testLoadDoesNotHideWorkspaceFunction() throws Exception { scratch.file("def.bzl", "def macro():", " print('bleh')"); scratch.overwriteFile(rootDirectory.getRelative("WORKSPACE").getPathString(), new ImmutableList.Builder<String>() .addAll(analysisMock.getWorkspaceContents(mockToolsConfig)) .add("workspace(name='bleh')") .add("load('//:def.bzl', 'macro')") .build()); scratch.file("data.txt"); scratch.file("BUILD", "filegroup(", " name='files', ", " srcs=['data.txt'])"); invalidatePackages(); assertThat(getRuleContext(getConfiguredTarget("//:files")).getWorkspaceName()) .isEqualTo("bleh"); } @Test public void testSkylarkRepositoryCannotOverrideBuiltInAttribute() throws Exception { scratch.file( "def.bzl", "def _impl(ctx):", " print(ctx.attr.name)", "", "repo = repository_rule(", " implementation=_impl,", " attrs={'name': attr.string(mandatory=True)})"); scratch.file(rootDirectory.getRelative("BUILD").getPathString()); scratch.overwriteFile( rootDirectory.getRelative("WORKSPACE").getPathString(), "load('//:def.bzl', 'repo')", "repo(name='foo')"); invalidatePackages(); try { getConfiguredTarget("@foo//:bar"); fail(); } catch (AssertionError e) { assertThat(e.getMessage()).contains("There is already a built-in attribute 'name' " + "which cannot be overridden"); } } @Test public void testMultipleLoadSameExtension() throws Exception { scratch.overwriteFile(rootDirectory.getRelative("WORKSPACE").getPathString(), new ImmutableList.Builder<String>() .addAll(analysisMock.getWorkspaceContents(mockToolsConfig)) .add("load('//:def.bzl', 'f1')") .add("f1()") .add("load('//:def.bzl', 'f2')") .add("f2()") .add("load('//:def.bzl', 'f1')") .add("f1()") .add("local_repository(name = 'foo', path = '')") .build()); scratch.file( rootDirectory.getRelative("BUILD").getPathString(), "filegroup(name = 'bar', srcs = [])"); scratch.file( rootDirectory.getRelative("def.bzl").getPathString(), "def f1():", " print('f1')", "", "def f2():", " print('f2')"); invalidatePackages(); // Just request the last external repository to force the whole loading. getConfiguredTarget("@foo//:bar"); } }