blob: 900ce8b0c7c5b5ad6e1112e55509cea80a6979bf [file] [log] [blame]
//===- unittests/BuildSystem/BuildSystemFrontendTest.cpp ------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
#include "TempDir.h"
#include "llbuild/Basic/FileSystem.h"
#include "llbuild/BuildSystem/BuildDescription.h"
#include "llbuild/BuildSystem/BuildFile.h"
#include "llbuild/BuildSystem/BuildSystemFrontend.h"
#include "llvm/Support/ErrorHandling.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/SourceMgr.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/Support/Path.h"
#include "llvm/ADT/StringRef.h"
#include "gtest/gtest.h"
#include <unordered_set>
#include <mutex>
using namespace llbuild;
using namespace llbuild::basic;
using namespace llbuild::buildsystem;
using namespace llvm;
// TODO Move this into some kind of libtestSupport?
#define ASSERT_NO_ERROR(code) \
do { \
std::error_code __ec; \
ASSERT_FALSE(__ec = (code)) << __ec.message(); \
} while (0)
namespace {
/// Records delegate callbacks and makes them available via ``getTrace()``
/// and ``checkTrace()``.
///
/// Allows for command skipping via ``commandsToSkip``.
class TestBuildSystemFrontendDelegate : public BuildSystemFrontendDelegate {
using super = BuildSystemFrontendDelegate;
FileSystem& fs;
std::mutex traceMutex;
std::string traceData;
raw_string_ostream traceStream;
public:
TestBuildSystemFrontendDelegate(SourceMgr& sourceMgr,
const BuildSystemInvocation& invocation,
FileSystem& fs):
BuildSystemFrontendDelegate(sourceMgr, invocation, "client", 0),
fs(fs),
traceStream(traceData)
{
}
std::unordered_set<std::string> commandsToSkip;
void clearTrace() {
std::lock_guard<std::mutex> lock(traceMutex);
traceStream.flush();
traceData.clear();
}
const std::string& getTrace() {
std::lock_guard<std::mutex> lock(traceMutex);
return traceStream.str();
}
bool checkTrace(StringRef expected) {
expected = expected.ltrim();
bool result = expected == getTrace();
if (!result) {
errs() << "error: failed to match expected trace.\n\n"
<< "Actual:\n" << getTrace() << "\n"
<< "Expected:\n" << expected << "\n";
TmpDir tmpDir;
std::string outputActual = tmpDir.str() + "/actual.log";
{
std::error_code ec;
llvm::raw_fd_ostream os(outputActual, ec, llvm::sys::fs::F_Text);
assert(!ec);
os << getTrace();
os.close();
assert(!os.has_error());
}
std::string outputExpected = tmpDir.str() + "/expected.log";
{
std::error_code ec;
llvm::raw_fd_ostream os(outputExpected, ec, llvm::sys::fs::F_Text);
assert(!ec);
os << expected;
os.close();
assert(!os.has_error());
}
errs() << "Diff:\n";
system(("diff '" + outputActual + "' '"+ outputExpected +"'").c_str());
}
return result;
}
virtual void hadCommandFailure() override {
{
std::lock_guard<std::mutex> lock(traceMutex);
traceStream << __FUNCTION__ << "\n";
}
super::hadCommandFailure();
}
virtual void commandPreparing(Command* command) override {
{
std::lock_guard<std::mutex> lock(traceMutex);
traceStream << __FUNCTION__ << ": " << command->getName() << "\n";
}
super::commandPreparing(command);
}
virtual bool shouldCommandStart(Command* command) override {
{
std::lock_guard<std::mutex> lock(traceMutex);
traceStream << __FUNCTION__ << ": " << command->getName() << "\n";
}
if (commandsToSkip.find(command->getName()) != commandsToSkip.end()) {
return false;
}
return super::shouldCommandStart(command);
}
virtual void commandStarted(Command* command) override {
{
std::lock_guard<std::mutex> lock(traceMutex);
traceStream << __FUNCTION__ << ": " << command->getName() << "\n";
}
super::commandStarted(command);
}
virtual void commandFinished(Command* command, CommandResult result) override {
{
std::lock_guard<std::mutex> lock(traceMutex);
traceStream << __FUNCTION__ << ": " << command->getName() << ": " << (int)result << "\n";
}
super::commandFinished(command, result);
}
virtual void commandProcessStarted(Command* command, ProcessHandle handle) override {
{
std::lock_guard<std::mutex> lock(traceMutex);
traceStream << __FUNCTION__ << ": " << command->getName() << "\n";
}
super::commandProcessStarted(command, handle);
}
virtual void commandProcessHadError(Command* command, ProcessHandle handle,
const Twine& message) override {
{
std::lock_guard<std::mutex> lock(traceMutex);
traceStream << __FUNCTION__ << ": " << command->getName() << ": " << message << "\n";
}
super::commandProcessHadError(command, handle, message);
}
virtual void commandProcessFinished(Command* command, ProcessHandle handle, CommandResult result,
int exitStatus) override {
{
std::lock_guard<std::mutex> lock(traceMutex);
traceStream << __FUNCTION__ << ": " << command->getName() << ": " << exitStatus << "\n";
}
super::commandProcessFinished(command, handle, result, exitStatus);
}
virtual FileSystem& getFileSystem() override {
return fs;
}
virtual std::unique_ptr<Tool> lookupTool(StringRef name) override {
return nullptr;
}
virtual void cycleDetected(const std::vector<core::Rule*>& items) override { }
};
/// Sets up state and provides utils for build system frontend tests.
class BuildSystemFrontendTest : public ::testing::Test {
protected:
TmpDir tempDir{"FrontendTest"};
std::unique_ptr<FileSystem> fs = createLocalFileSystem();
SourceMgr sourceMgr;
BuildSystemInvocation invocation;
virtual void SetUp() override {
invocation.chdirPath = tempDir.str();
invocation.traceFilePath = tempDir.str() + "/trace.log";
}
void writeBuildFile(StringRef s) {
std::error_code ec;
raw_fd_ostream os(std::string(tempDir.str()) + "/build.llbuild", ec,
llvm::sys::fs::F_Text);
ASSERT_NO_ERROR(ec);
os << s;
}
};
// We have commands { 3 -> 2 -> 1 }. We'd like to skip 2, but we still
// want 1 and 3 to run.
TEST_F(BuildSystemFrontendTest, commandSkipping) {
writeBuildFile(R"END(
client:
name: client
targets:
"": ["3"]
commands:
1:
tool: shell
outputs: ["1"]
args: touch 1
2:
tool: shell
inputs: ["1"]
outputs: ["2"]
args: touch 2
3:
tool: shell
inputs: ["2"]
outputs: ["3"]
args: touch 3
)END");
{
TestBuildSystemFrontendDelegate delegate(sourceMgr, invocation, *fs);
delegate.commandsToSkip.insert("2");
BuildSystemFrontend frontend(delegate, invocation);
ASSERT_TRUE(frontend.build(""));
ASSERT_TRUE(delegate.checkTrace(R"END(
commandPreparing: 3
commandPreparing: 2
commandPreparing: 1
shouldCommandStart: 1
commandStarted: 1
commandProcessStarted: 1
commandProcessFinished: 1: 0
commandFinished: 1: 0
shouldCommandStart: 2
commandFinished: 2: 3
shouldCommandStart: 3
commandStarted: 3
commandProcessStarted: 3
commandProcessFinished: 3: 0
commandFinished: 3: 0
)END"));
ASSERT_FALSE(fs->getFileInfo(tempDir.str() + "/1").isMissing());
ASSERT_TRUE(fs->getFileInfo(tempDir.str() + "/2").isMissing());
ASSERT_FALSE(fs->getFileInfo(tempDir.str() + "/3").isMissing());
}
// If we rebuild incrementally without skipping, we expect to run 2 and
// re-run 3. 1 doesn't have to run at all, so we don't expect to get asked
// if it should start.
{
TestBuildSystemFrontendDelegate delegate(sourceMgr, invocation, *fs);
BuildSystemFrontend frontend(delegate, invocation);
ASSERT_TRUE(frontend.build(""));
ASSERT_TRUE(delegate.checkTrace(R"END(
commandPreparing: 2
shouldCommandStart: 2
commandStarted: 2
commandProcessStarted: 2
commandProcessFinished: 2: 0
commandFinished: 2: 0
commandPreparing: 3
shouldCommandStart: 3
commandStarted: 3
commandProcessStarted: 3
commandProcessFinished: 3: 0
commandFinished: 3: 0
)END"));
ASSERT_FALSE(fs->getFileInfo(tempDir.str() + "/2").isMissing());
}
}
// We have commands { 2 -> 1 }, where two requires 1's output. We'd like to
// skip 1, we expect 2 to run and fail because 1 did not produce output.
TEST_F(BuildSystemFrontendTest, commandSkippingFailure) {
writeBuildFile(R"END(
client:
name: client
targets:
"": ["2"]
commands:
1:
tool: shell
outputs: ["1"]
args: touch 1
2:
tool: shell
inputs: ["1"]
outputs: ["2"]
args: cp 1 2
)END");
{
TestBuildSystemFrontendDelegate delegate(sourceMgr, invocation, *fs);
delegate.commandsToSkip.insert("1");
BuildSystemFrontend frontend(delegate, invocation);
ASSERT_FALSE(frontend.build(""));
ASSERT_EQ(1u, delegate.getNumFailedCommands());
ASSERT_TRUE(delegate.checkTrace(R"END(
commandPreparing: 2
commandPreparing: 1
shouldCommandStart: 1
commandFinished: 1: 3
shouldCommandStart: 2
commandStarted: 2
commandProcessStarted: 2
commandProcessFinished: 2: 256
commandFinished: 2: 1
hadCommandFailure
)END"));
ASSERT_TRUE(fs->getFileInfo(tempDir.str() + "/1").isMissing());
ASSERT_TRUE(fs->getFileInfo(tempDir.str() + "/2").isMissing());
}
// If we rebuild incrementally without skipping, we expect to run 1 and 2.
{
TestBuildSystemFrontendDelegate delegate(sourceMgr, invocation, *fs);
BuildSystemFrontend frontend(delegate, invocation);
ASSERT_TRUE(frontend.build(""));
ASSERT_TRUE(delegate.checkTrace(R"END(
commandPreparing: 2
commandPreparing: 1
shouldCommandStart: 1
commandStarted: 1
commandProcessStarted: 1
commandProcessFinished: 1: 0
commandFinished: 1: 0
shouldCommandStart: 2
commandStarted: 2
commandProcessStarted: 2
commandProcessFinished: 2: 0
commandFinished: 2: 0
)END"));
ASSERT_FALSE(fs->getFileInfo(tempDir.str() + "/1").isMissing());
ASSERT_FALSE(fs->getFileInfo(tempDir.str() + "/2").isMissing());
}
}
// Built-in commands don't process skipped inputs the same way as external
// commands (for now). Add explicit tests to make sure they behave as expected.
TEST_F(BuildSystemFrontendTest, commandSkipping_NonExternalCommands_mkdir) {
writeBuildFile(R"END(
client:
name: client
targets:
"": ["2"]
commands:
1:
tool: mkdir
outputs: ["1"]
2:
tool: mkdir
inputs: ["1"]
outputs: ["2"]
)END");
TestBuildSystemFrontendDelegate delegate(sourceMgr, invocation, *fs);
delegate.commandsToSkip.insert("1");
BuildSystemFrontend frontend(delegate, invocation);
ASSERT_TRUE(frontend.build(""));
ASSERT_TRUE(delegate.checkTrace(R"END(
commandPreparing: 2
commandPreparing: 1
shouldCommandStart: 1
commandFinished: 1: 3
shouldCommandStart: 2
commandStarted: 2
commandFinished: 2: 0
)END"));
}
// Built-in commands don't process skipped inputs the same way as external
// commands (for now). Add explicit tests to make sure they behave as expected.
TEST_F(BuildSystemFrontendTest, commandSkipping_NonExternalCommands_symlink) {
writeBuildFile(R"END(
client:
name: client
targets:
"": ["3"]
commands:
1:
tool: symlink
outputs: ["1"]
contents: "x"
2:
tool: symlink
inputs: ["1"]
outputs: ["2"]
contents: "y"
3:
tool: shell
inputs: ["2"]
outputs: ["3"]
args: rm 2
)END");
// We need to delete the symlink ourselves, because llvm's remove()
// currently refuses to delete symlinks.
TestBuildSystemFrontendDelegate delegate(sourceMgr, invocation, *fs);
delegate.commandsToSkip.insert("1");
BuildSystemFrontend frontend(delegate, invocation);
ASSERT_TRUE(frontend.build(""));
ASSERT_TRUE(delegate.checkTrace(R"END(
commandPreparing: 3
commandPreparing: 2
commandPreparing: 1
shouldCommandStart: 1
commandFinished: 1: 3
shouldCommandStart: 2
commandStarted: 2
commandFinished: 2: 0
shouldCommandStart: 3
commandStarted: 3
commandProcessStarted: 3
commandProcessFinished: 3: 0
commandFinished: 3: 0
)END"));
}
}