blob: 5bd52ffede31945d7b04d4335819a685477c3dd7 [file] [log] [blame]
// Copyright 2019 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.blackbox.tests.manageddirs;
import static com.google.common.truth.Truth.assertThat;
import com.google.devtools.build.lib.blackbox.framework.BuilderRunner;
import com.google.devtools.build.lib.blackbox.framework.PathUtils;
import com.google.devtools.build.lib.blackbox.framework.ProcessResult;
import com.google.devtools.build.lib.blackbox.junit.AbstractBlackBoxTest;
import com.google.devtools.build.lib.util.ResourceFileLoader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import org.junit.Before;
import org.junit.Test;
/** Tests for managed directories. */
public class ManagedDirectoriesBlackBoxTest extends AbstractBlackBoxTest {
private Random random;
private Integer currentDebugId;
@Override
@Before
public void setUp() throws Exception {
random = new Random(17);
super.setUp();
}
@Test
public void testBuildProject() throws Exception {
generateProject();
buildExpectRepositoryRuleCalled();
checkProjectFiles();
}
@Test
public void testNodeModulesDeleted() throws Exception {
generateProject();
buildExpectRepositoryRuleCalled();
checkProjectFiles();
Path nodeModules = context().getWorkDir().resolve("node_modules");
assertThat(nodeModules.toFile().isDirectory()).isTrue();
PathUtils.deleteTree(nodeModules);
buildExpectRepositoryRuleCalled();
checkProjectFiles();
}
@Test
public void testNodeModulesDeletedAndRecreated() throws Exception {
generateProject();
buildExpectRepositoryRuleCalled();
checkProjectFiles();
Path nodeModules = context().getWorkDir().resolve("node_modules");
assertThat(nodeModules.toFile().isDirectory()).isTrue();
Path nodeModulesBackup = context().getWorkDir().resolve("node_modules_backup");
PathUtils.copyTree(nodeModules, nodeModulesBackup);
PathUtils.deleteTree(nodeModules);
PathUtils.copyTree(nodeModulesBackup, nodeModules);
buildExpectRepositoryRuleNotCalled();
checkProjectFiles();
}
@Test
public void testBuildProjectFetchNotRecalled() throws Exception {
generateProject();
buildExpectRepositoryRuleCalled();
checkProjectFiles();
buildExpectRepositoryRuleNotCalled();
checkProjectFiles();
}
private BuilderRunner bazel() {
return bazel(false);
}
private BuilderRunner bazel(boolean watchFs) {
currentDebugId = random.nextInt();
BuilderRunner bazel = context().bazel().withEnv("DEBUG_ID", String.valueOf(currentDebugId));
if (watchFs) {
bazel.withFlags("--watchfs=true");
}
return bazel;
}
@Test
public void testChangeOfFileTextUnderNodeModules() throws Exception {
generateProject();
buildExpectRepositoryRuleCalled();
checkProjectFiles();
Path nodeModules = context().getWorkDir().resolve("node_modules");
Path modulePackageJson = nodeModules.resolve("example-module/package.json");
assertThat(modulePackageJson.toFile().exists()).isTrue();
// Assert that non-structural changes are not detected.
PathUtils.append(modulePackageJson, "# comment");
buildExpectRepositoryRuleNotCalled();
checkProjectFiles();
}
@Test
public void testLoadIsNotCalledForManagedDirectories() throws Exception {
generateProject();
Path workspaceFile = context().getWorkDir().resolve(WORKSPACE);
PathUtils.append(workspaceFile, "load('@non_existing//:target.bzl', 'some_symbol')");
// Test that there is error when loading, so we parsed managed directories successfully.
ProcessResult result = bazel().shouldFail().build("//...");
assertThat(findPattern(result, "ERROR: Failed to load Starlark extension")).isTrue();
}
@Test
public void testWithBazelTools() throws Exception {
generateProject();
Path workspaceFile = context().getWorkDir().resolve(WORKSPACE);
PathUtils.append(
workspaceFile,
"load(\"@bazel_tools//tools/build_defs/repo:http.bzl\", \"http_archive\", \"http_file\")");
buildExpectRepositoryRuleCalled();
checkProjectFiles();
}
@Test
public void testAddManagedDirectoriesLater() throws Exception {
// Start the server, have things cached.
context().write("BUILD", "");
bazel().build("//...");
// Now that we generate the project and have managed directories updated, we are also testing,
// that managed directories are re-read correctly from the changed file.
generateProject();
buildExpectRepositoryRuleCalled();
checkProjectFiles();
// Test everything got cached.
buildExpectRepositoryRuleNotCalled();
checkProjectFiles();
}
@Test
public void testFilesUnderChangedManagedDirectoriesRefreshed() throws Exception {
doTestFilesUnderManagedDirectoriesRefreshed(false);
}
@Test
public void testFilesUnderChangedManagedDirectoriesRefreshedWatchFs() throws Exception {
doTestFilesUnderManagedDirectoriesRefreshed(true);
}
private void doTestFilesUnderManagedDirectoriesRefreshed(boolean watchFs) throws Exception {
generateProject();
buildExpectRepositoryRuleCalled(false, watchFs);
checkProjectFiles();
// Now remove the ManagedDirectories, and change the package version - it should still work.
List<String> properWorkspaceText = context().read("WORKSPACE");
context()
.write(
"WORKSPACE",
"workspace(name = \"fine_grained_user_modules\")",
"load(\":use_node_modules.bzl\", \"generate_fine_grained_node_modules\")",
"generate_fine_grained_node_modules(name = \"generated_node_modules\",",
"package_json = \"//:package.json\",)");
Path packageJson =
PathUtils.resolve(context().getWorkDir(), "node_modules", "example-module", "package.json");
assertThat(packageJson.toFile().exists()).isTrue();
// Now we are building it without managed directories, both managed directories and
// RepositoryDirectoryValue will be dirty - we expect repository rule to be called again.
buildExpectRepositoryRuleCalled(false, watchFs);
checkProjectFiles();
// Now change files directly in generated area, and build.
List<String> oldPackageJson = PathUtils.readFile(packageJson);
PathUtils.writeFile(
packageJson,
"{",
" \"license\": \"MIT\",",
" \"main\": \"example-module.js\",",
" \"name\": \"example-module\",",
" \"repository\": {",
" \"type\": \"git\",",
" \"url\": \"aaa\",",
" },",
" \"version\": \"7.7.7\"",
"}");
Path build = context().getWorkDir().resolve("BUILD");
List<String> oldBuild = PathUtils.readFile(build);
PathUtils.writeFile(
build,
"load(\":test_rule.bzl\", \"test_rule\")",
"test_rule(",
" name = \"test_generated_deps\",",
" module_source = \"@generated_node_modules//:example-module\",",
" version = \"7.7.7\"",
")");
// Test rule inputs has changed, so the build is not cached; however, the repository rule
// is not rerun, since it's inputs (including managed directories settings) were not changed,
// so debug_id is the same.
buildExpectRepositoryRuleNotCalled();
checkProjectFiles("7.7.7");
// And is cached.
buildExpectRepositoryRuleNotCalled();
// Now change just the managed directories and see the generated version comes up.
PathUtils.writeFile(
context().getWorkDir().resolve(WORKSPACE), properWorkspaceText.toArray(new String[0]));
PathUtils.writeFile(build, oldBuild.toArray(new String[0]));
buildExpectRepositoryRuleCalled(false, watchFs);
checkProjectFiles("0.2.0");
}
@Test
public void testManagedDirectoriesSettingsAndManagedDirectoriesFilesChangeSimultaneously()
throws Exception {
doTestManagedDirectoriesSettingsAndManagedDirectoriesFilesChangeSimultaneously(false);
}
@Test
public void testManagedDirectoriesSettingsAndManagedDirectoriesFilesChangeSimultaneouslyWatchFs()
throws Exception {
doTestManagedDirectoriesSettingsAndManagedDirectoriesFilesChangeSimultaneously(true);
}
private void doTestManagedDirectoriesSettingsAndManagedDirectoriesFilesChangeSimultaneously(
boolean watchFs) throws Exception {
generateProject();
buildExpectRepositoryRuleCalled(false, watchFs);
checkProjectFiles();
// Modify managed directories somehow.
context()
.write(
"WORKSPACE",
"workspace(name = \"fine_grained_user_modules\",",
"managed_directories = {'@generated_node_modules': ['node_modules', 'something']})",
"load(\":use_node_modules.bzl\", \"generate_fine_grained_node_modules\")",
"generate_fine_grained_node_modules(name = \"generated_node_modules\",",
"package_json = \"//:package.json\",)");
Path packageJson =
PathUtils.resolve(context().getWorkDir(), "node_modules", "example-module", "package.json");
assertThat(packageJson.toFile().exists()).isTrue();
// Modify generated package.json under the managed directory.
List<String> oldPackageJson = PathUtils.readFile(packageJson);
PathUtils.writeFile(
packageJson,
"{",
" \"license\": \"MIT\",",
" \"main\": \"example-module.js\",",
" \"name\": \"example-module\",",
" \"repository\": {",
" \"type\": \"git\",",
" \"url\": \"aaa\",",
" },",
" \"version\": \"7.7.7\"",
"}");
// Expect files under managed directories be regenerated
// and changes under managed directories be lost.
buildExpectRepositoryRuleCalled(false, watchFs);
checkProjectFiles();
}
@Test
public void testRepositoryOverrideWithManagedDirectories() throws Exception {
generateProject();
Path override = context().getTmpDir().resolve("override");
PathUtils.writeFile(override.resolve(WORKSPACE));
// Just define some similar target.
PathUtils.writeFile(
override.resolve("BUILD"),
"genrule(",
" name = \"example-module\",",
" srcs = [],",
" cmd = \"touch $(location package.json)\",",
" outs = [\"package.json\"],",
" visibility = ['//visibility:public'],",
")");
BuilderRunner bazel =
bazel().withFlags("--override_repository=generated_node_modules=" + override.toString());
ProcessResult result = bazel.shouldFail().build("@generated_node_modules//:example-module");
assertThat(result.errString())
.contains(
"ERROR: Overriding repositories is not allowed"
+ " for the repositories with managed directories."
+ "\nThe following overridden external repositories"
+ " have managed directories: @generated_node_modules");
// Assert the result stays the same even when managed directories has not changed.
result = bazel.shouldFail().build("@generated_node_modules//:example-module");
assertThat(result.errString())
.contains(
"ERROR: Overriding repositories is not allowed"
+ " for the repositories with managed directories."
+ "\nThe following overridden external repositories"
+ " have managed directories: @generated_node_modules");
}
@Test
public void testRepositoryOverrideChangeToConflictWithManagedDirectories() throws Exception {
generateProject();
buildExpectRepositoryRuleCalled();
checkProjectFiles();
Path override = context().getTmpDir().resolve("override");
PathUtils.writeFile(override.resolve(WORKSPACE));
// Just define some similar target.
PathUtils.writeFile(
override.resolve("BUILD"),
"genrule(",
" name = \"example-module\",",
" srcs = [],",
" cmd = \"touch $(location package.json)\",",
" outs = [\"package.json\"],",
" visibility = ['//visibility:public'],",
")");
// Now the overrides change.
BuilderRunner bazel =
bazel().withFlags("--override_repository=generated_node_modules=" + override.toString());
ProcessResult result = bazel.shouldFail().build("@generated_node_modules//:example-module");
assertThat(result.errString())
.contains(
"ERROR: Overriding repositories is not allowed"
+ " for the repositories with managed directories."
+ "\nThe following overridden external repositories"
+ " have managed directories: @generated_node_modules");
}
/**
* The test to verify that WORKSPACE file can not be a symlink when managed directories are used.
*
* <p>The test of the case, when WORKSPACE file is a symlink, but not managed directories are
* used, is in {@link WorkspaceBlackBoxTest#testWorkspaceFileIsSymlink()}
*/
@Test
public void testWorkspaceSymlinkThrowsWithManagedDirectories() throws Exception {
generateProject();
Path workspaceFile = context().getWorkDir().resolve(WORKSPACE);
assertThat(workspaceFile.toFile().delete()).isTrue();
Path tempWorkspace = Files.createTempFile(context().getTmpDir(), WORKSPACE, "");
PathUtils.writeFile(
tempWorkspace,
"workspace(name = \"fine_grained_user_modules\",",
"managed_directories = {'@generated_node_modules': ['node_modules']})",
"",
"load(\":use_node_modules.bzl\", \"generate_fine_grained_node_modules\")",
"",
"generate_fine_grained_node_modules(",
" name = \"generated_node_modules\",",
" package_json = \"//:package.json\",",
")");
Files.createSymbolicLink(workspaceFile, tempWorkspace);
ProcessResult result = bazel().shouldFail().build("//...");
assertThat(
findPattern(
result,
"WORKSPACE file can not be a symlink if incrementally updated directories are"
+ " used."))
.isTrue();
}
private void generateProject() throws IOException {
writeProjectFile("BUILD.test", "BUILD");
writeProjectFile("WORKSPACE.test", "WORKSPACE");
writeProjectFile("bazelignore.test", ".bazelignore");
writeProjectFile("package.json", "package.json");
writeProjectFile("test_rule.bzl", "test_rule.bzl");
writeProjectFile("use_node_modules.bzl", "use_node_modules.bzl");
}
private void writeProjectFile(String oldName, String newName) throws IOException {
String text = ResourceFileLoader.loadResource(ManagedDirectoriesBlackBoxTest.class, oldName);
assertThat(text).isNotNull();
assertThat(text).isNotEmpty();
context().write(newName, text);
}
private void checkProjectFiles() throws IOException {
checkProjectFiles("0.2.0");
}
private void checkProjectFiles(String version) throws IOException {
Path nodeModules = context().getWorkDir().resolve("node_modules");
assertThat(nodeModules.toFile().exists()).isTrue();
assertThat(nodeModules.toFile().isDirectory()).isTrue();
Path exampleModule = nodeModules.resolve("example-module");
assertThat(exampleModule.toFile().exists()).isTrue();
assertThat(exampleModule.toFile().isDirectory()).isTrue();
Path packageJson = exampleModule.resolve("package.json");
assertThat(packageJson.toFile().exists()).isTrue();
assertThat(packageJson.toFile().isDirectory()).isFalse();
List<String> text = PathUtils.readFile(packageJson);
assertThat(text.stream().anyMatch(s -> s.trim().equals("\"name\": \"example-module\",")))
.isTrue();
String versionString = String.format("\"version\": \"%s\"", version);
assertThat(text.stream().anyMatch(s -> s.trim().equals(versionString))).isTrue();
}
private String getDebugId(BuilderRunner bazel) throws Exception {
Path path = context().resolveExecRootPath(bazel, "external/generated_node_modules/debug_id");
List<String> lines = PathUtils.readFile(path);
assertThat(lines.size()).isEqualTo(1);
return lines.get(0);
}
private ProcessResult buildExpectRepositoryRuleCalled() throws Exception {
return buildExpectRepositoryRuleCalled(false, false);
}
private ProcessResult buildExpectRepositoryRuleCalled(boolean debug, boolean watchFs)
throws Exception {
BuilderRunner bazel = bazel(watchFs);
if (debug) {
bazel.enableDebug();
}
ProcessResult result = bazel.build("//...");
buildSucceeded(result);
debugIdShouldBeUpdated(bazel);
return result;
}
private ProcessResult buildExpectRepositoryRuleNotCalled() throws Exception {
return buildExpectRepositoryRuleNotCalled(false);
}
private ProcessResult buildExpectRepositoryRuleNotCalled(boolean debug) throws Exception {
BuilderRunner bazel = bazel();
if (debug) {
bazel.enableDebug();
}
ProcessResult result = bazel.build("//...");
buildSucceeded(result);
debugIdShouldNotBeUpdated(bazel);
return result;
}
private void debugIdShouldBeUpdated(BuilderRunner bazel) throws Exception {
assertThat(getDebugId(bazel)).isEqualTo(String.valueOf(currentDebugId));
}
private void debugIdShouldNotBeUpdated(BuilderRunner bazel) throws Exception {
assertThat(getDebugId(bazel)).isNotEqualTo(String.valueOf(currentDebugId));
}
private void buildSucceeded(ProcessResult result) {
assertThat(findPattern(result, "INFO: Build completed successfully")).isTrue();
}
private void buildFailed(ProcessResult result) {
assertThat(findPattern(result, "FAILED: Build did NOT complete successfully")).isTrue();
}
private boolean findPattern(ProcessResult result, String pattern) {
String[] lines = result.errString().split("\n");
return Arrays.stream(lines).anyMatch(s -> s.contains(pattern));
}
}