/*
* Copyright 2012-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.shell;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.junit.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
import com.facebook.buck.testutil.integration.ProjectWorkspace;
import com.facebook.buck.testutil.integration.ProjectWorkspace.ProcessResult;
import com.facebook.buck.testutil.integration.TemporaryPaths;
import com.facebook.buck.testutil.integration.TestDataHelper;
import com.facebook.buck.util.HumanReadableException;
import com.facebook.buck.util.environment.Platform;
import com.google.common.io.CharStreams;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
public class GenruleIntegrationTest {
@Rule public TemporaryPaths temporaryFolder = new TemporaryPaths();
@Rule public ExpectedException exception = ExpectedException.none();
// When these tests fail, the failures contain buck output that is easy to confuse with the output
// of the instance of buck that's running the test. This prepends each line with "> ".
private String quoteOutput(String output) {
output = output.trim();
output = "> " + output;
output = output.replace("\n", "\n> ");
return output;
}
@Test
public void testIfCommandExitsZeroThenGenruleFails() throws IOException {
assumeTrue(
"This genrule uses the 'bash' argument, which is not supported on Windows. ",
Platform.detect() != Platform.WINDOWS);
ProjectWorkspace workspace =
TestDataHelper.createProjectWorkspaceForScenario(
this, "genrule_failing_command", temporaryFolder);
workspace.setUp();
ProcessResult buildResult = workspace.runBuckCommand("build", "//:fail", "--verbose", "10");
buildResult.assertFailure();
/* We want to make sure we failed for the right reason. The expected should contain something
* like the following:
*
* BUILD FAILED: //:fail failed with exit code 1:
* (cd /tmp/junit12345/buck-out/gen/fail__srcs && /bin/bash -e -c 'false; echo >&2 hi')
*
* We should match all that, except for the specific temp dir.
*/
// "(?s)" enables multiline matching for ".*". Parens have to be escaped.
String outputPattern =
"(?s).*BUILD FAILED: //:fail failed with exit code 1:(?s).*"
+ "\\(cd .*/buck-out/gen/fail__srcs && "
+ "/bin/bash -e .*/buck-out/tmp/genrule-[0-9]*\\.sh\\)(?s).*";
assertTrue(
"Unexpected output:\n" + quoteOutput(buildResult.getStderr()),
buildResult.getStderr().matches(outputPattern));
}
@Test
public void genruleWithEmptyOutParameterFails() throws IOException {
ProjectWorkspace workspace =
TestDataHelper.createProjectWorkspaceForScenario(
this, "genrule_empty_out", temporaryFolder);
workspace.setUp();
exception.expect(HumanReadableException.class);
exception.expectMessage(
"The 'out' parameter of genrule //:genrule is '', which is not a valid file name.");
workspace.runBuckCommand("build", "//:genrule");
}
@Test
public void genruleWithAbsoluteOutParameterFails() throws IOException {
ProjectWorkspace workspace =
TestDataHelper.createProjectWorkspaceForScenario(
this, "genrule_absolute_out", temporaryFolder);
workspace.setUp();
exception.expect(HumanReadableException.class);
exception.expectMessage(
"The 'out' parameter of genrule //:genrule is '/tmp/file', "
+ "which is not a valid file name.");
workspace.runBuckCommand("build", "//:genrule");
}
@Test
public void genruleDirectoryOutput() throws IOException {
ProjectWorkspace workspace =
TestDataHelper.createProjectWorkspaceForScenario(
this, "genrule_directory_output", temporaryFolder);
workspace.setUp();
workspace.enableDirCache();
workspace.runBuckCommand("build", "//:mkdir").assertSuccess();
assertThat(Files.isDirectory(workspace.resolve("buck-out/gen/mkdir/directory")), equalTo(true));
assertThat(
workspace.getFileContents("buck-out/gen/mkdir/directory/file"),
equalTo("something" + System.lineSeparator()));
workspace.runBuckCommand("clean").assertSuccess();
assertThat(Files.isDirectory(workspace.resolve("buck-out/gen")), equalTo(false));
// Retrieving the genrule output from the local cache should recreate the directory contents.
workspace.runBuckCommand("build", "//:mkdir").assertSuccess();
workspace.getBuildLog().assertTargetWasFetchedFromCache("//:mkdir");
assertThat(Files.isDirectory(workspace.resolve("buck-out/gen/mkdir/directory")), equalTo(true));
assertThat(
workspace.getFileContents("buck-out/gen/mkdir/directory/file"),
equalTo("something" + System.lineSeparator()));
}
@Test
public void genruleWithBigCommand() throws IOException {
ProjectWorkspace workspace =
TestDataHelper.createProjectWorkspaceForScenario(
this, "genrule_big_command", temporaryFolder);
workspace.setUp();
workspace.runBuckCommand("build", "//:big").assertSuccess();
Path outputPath = workspace.resolve("buck-out/gen/big/file");
assertThat(Files.isRegularFile(outputPath), equalTo(true));
int stringSize = 1000;
StringBuilder expectedOutput = new StringBuilder();
for (int i = 0; i < stringSize; ++i) {
expectedOutput.append("X");
}
expectedOutput.append(System.lineSeparator());
assertThat(workspace.getFileContents(outputPath), equalTo(expectedOutput.toString()));
}
@Test
public void genruleDirectoryOutputIsCleanedBeforeBuildAndCacheFetch() throws IOException {
ProjectWorkspace workspace =
TestDataHelper.createProjectWorkspaceForScenario(
this, "genrule_directory_output_cleaned", temporaryFolder);
workspace.setUp();
workspace.enableDirCache();
workspace.copyFile("BUCK.1", "BUCK");
workspace.runBuckCommand("build", "//:mkdir_another").assertSuccess();
workspace.getBuildLog().assertTargetBuiltLocally("//:mkdir_another");
assertTrue(
"mkdir_another should be built",
Files.isRegularFile(
workspace.resolve("buck-out/gen/mkdir_another/another_directory/file")));
workspace.runBuckCommand("build", "//:mkdir").assertSuccess();
workspace.getBuildLog().assertTargetBuiltLocally("//:mkdir");
assertTrue(
"BUCK.1 should create its output",
Files.isRegularFile(workspace.resolve("buck-out/gen/mkdir/directory/one")));
assertFalse(
"BUCK.1 should not touch the output of BUCK.2",
Files.isRegularFile(workspace.resolve("buck-out/gen/mkdir/directory/two")));
assertTrue(
"output of mkdir_another should still exist",
Files.isRegularFile(
workspace.resolve("buck-out/gen/mkdir_another/another_directory/file")));
workspace.copyFile("BUCK.2", "BUCK");
workspace.runBuckCommand("build", "//:mkdir").assertSuccess();
workspace.getBuildLog().assertTargetBuiltLocally("//:mkdir");
assertFalse(
"Output of BUCK.1 should be deleted before output of BUCK.2 is built",
Files.isRegularFile(workspace.resolve("buck-out/gen/mkdir/directory/one")));
assertTrue(
"BUCK.2 should create its output",
Files.isRegularFile(workspace.resolve("buck-out/gen/mkdir/directory/two")));
assertTrue(
"output of mkdir_another should still exist",
Files.isRegularFile(
workspace.resolve("buck-out/gen/mkdir_another/another_directory/file")));
workspace.copyFile("BUCK.1", "BUCK");
workspace.runBuckCommand("build", "//:mkdir").assertSuccess();
workspace.getBuildLog().assertTargetWasFetchedFromCache("//:mkdir");
assertTrue(
"Output of BUCK.1 should be fetched from the cache",
Files.isRegularFile(workspace.resolve("buck-out/gen/mkdir/directory/one")));
assertFalse(
"Output of BUCK.2 should be deleted before output of BUCK.1 is fetched from cache",
Files.isRegularFile(workspace.resolve("buck-out/gen/mkdir/directory/two")));
assertTrue(
"output of mkdir_another should still exist",
Files.isRegularFile(
workspace.resolve("buck-out/gen/mkdir_another/another_directory/file")));
assertThat(Files.isDirectory(workspace.resolve("buck-out/gen/mkdir/directory")), equalTo(true));
}
@Test
public void genruleCleansEntireOutputDirectory() throws IOException {
ProjectWorkspace workspace =
TestDataHelper.createProjectWorkspaceForScenario(
this, "genrule_robust_cleaning", temporaryFolder);
workspace.setUp();
workspace.copyFile("BUCK.1", "BUCK");
workspace.runBuckCommand("build", "//:write").assertSuccess();
workspace.getBuildLog().assertTargetBuiltLocally("//:write");
assertTrue(
"write should be built", Files.isRegularFile(workspace.resolve("buck-out/gen/write/one")));
workspace.copyFile("BUCK.2", "BUCK");
workspace.runBuckCommand("build", "//:write").assertSuccess();
workspace.getBuildLog().assertTargetBuiltLocally("//:write");
assertFalse(
"Output of BUCK.1 should be deleted before output of BUCK.2 is built",
Files.isRegularFile(workspace.resolve("buck-out/gen/write/one")));
assertTrue(
"BUCK.2 should create its output",
Files.isRegularFile(workspace.resolve("buck-out/gen/write/two")));
}
@Test
public void genruleDirectorySourcePath() throws IOException {
ProjectWorkspace workspace =
TestDataHelper.createProjectWorkspaceForScenario(
this, "genrule_directory_source_path", temporaryFolder);
workspace.setUp();
ProcessResult buildResult = workspace.runBuckCommand("build", "//:cpdir");
buildResult.assertSuccess();
assertThat(Files.isDirectory(workspace.resolve("buck-out/gen/cpdir/copy")), equalTo(true));
assertThat(
Files.isRegularFile(workspace.resolve("buck-out/gen/cpdir/copy/hello")), equalTo(true));
}
@Test
public void twoGenrulesWithTheSameOutputFileShouldNotOverwriteOneAnother() throws IOException {
ProjectWorkspace workspace =
TestDataHelper.createProjectWorkspaceForScenario(
this, "genrule_overwrite", temporaryFolder);
workspace.setUp();
// The two genrules run in this test have the same inputs and same output name
Path output = workspace.buildAndReturnOutput("//:genrule-one");
String originalOutput = new String(Files.readAllBytes(output), UTF_8);
output = workspace.buildAndReturnOutput("//:genrule-two");
String updatedOutput = new String(Files.readAllBytes(output), UTF_8);
assertNotEquals(originalOutput, updatedOutput);
// Finally, reinvoke the first rule.
output = workspace.buildAndReturnOutput("//:genrule-one");
String originalOutput2 = new String(Files.readAllBytes(output), UTF_8);
assertEquals(originalOutput, originalOutput2);
}
@Test
public void executableGenrule() throws IOException {
ProjectWorkspace workspace =
TestDataHelper.createProjectWorkspaceForScenario(
this, "genrule_executable", temporaryFolder);
workspace.setUp();
ProcessResult buildResult = workspace.runBuckCommand("run", "//:binary");
buildResult.assertSuccess();
}
@Test
public void genruleZipOutputsAreScrubbed() throws IOException {
ProjectWorkspace workspace =
TestDataHelper.createProjectWorkspaceForScenario(
this, "genrule_zip_scrubber", temporaryFolder);
workspace.setUp();
Path outputOne = workspace.buildAndReturnOutput("//:genrule-one");
Path outputTwo = workspace.buildAndReturnOutput("//:genrule-two");
assertZipsAreEqual(outputOne, outputTwo);
}
@Test
public void genruleZipOutputsExtendedTimestampsAreScrubbed() throws IOException {
ProjectWorkspace workspace =
TestDataHelper.createProjectWorkspaceForScenario(
this, "genrule_zip_scrubber", temporaryFolder);
workspace.setUp();
Path outputOne = workspace.buildAndReturnOutput("//:extended-time-one");
Path outputTwo = workspace.buildAndReturnOutput("//:extended-time-two");
assertZipsAreEqual(outputOne, outputTwo);
}
private void assertZipsAreEqual(Path zipPathOne, Path zipPathTwo) throws IOException {
try (ZipFile zipOne = new ZipFile(zipPathOne.toFile());
ZipFile zipTwo = new ZipFile(zipPathTwo.toFile())) {
Enumeration<? extends ZipEntry> entriesOne = zipOne.entries(), entriesTwo = zipTwo.entries();
while (entriesOne.hasMoreElements()) {
assertTrue(entriesTwo.hasMoreElements());
ZipEntry entryOne = entriesOne.nextElement(), entryTwo = entriesTwo.nextElement();
assertEquals(zipEntryDebugString(entryOne), zipEntryDebugString(entryTwo));
assertEquals(zipEntryData(zipOne, entryOne), zipEntryData(zipTwo, entryTwo));
}
assertFalse(entriesTwo.hasMoreElements());
}
assertEquals(
new String(Files.readAllBytes(zipPathOne)), new String(Files.readAllBytes(zipPathTwo)));
}
private String zipEntryDebugString(ZipEntry entryOne) {
return "<ZE name="
+ entryOne.getName()
+ " crc="
+ entryOne.getCrc()
+ " comment="
+ entryOne.getComment()
+ " size="
+ entryOne.getSize()
+ " atime="
+ entryOne.getLastAccessTime()
+ " mtime="
+ entryOne.getLastModifiedTime()
+ " ctime="
+ entryOne.getCreationTime();
}
private String zipEntryData(ZipFile zip, ZipEntry entry) throws IOException {
return CharStreams.toString(new InputStreamReader(zip.getInputStream(entry)));
}
}