mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-09-24 20:10:13 +01:00
I'm refactoring logic to try to make these files more manageable, particularly as I'm looking at ways to use more threads. I'm renaming FileTestBase::TestContext to TestFile and FileTestBase::TestFile to TestFile::Split to try to be more consistent in how we talk about test files and file splits in general. This change is not expected to change any behavior, it's just refactoring. This PR has two commits: - Copying files for viewing deltas in GH - Moving logic The delta of the two commits is the main PR. The second commit can be viewed on its own to see the delta versus file_test_base.* files where the logic currently rests (there's still a reordering in run_test.cpp as part of making one function static).
192 lines
7.0 KiB
C++
192 lines
7.0 KiB
C++
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
|
// Exceptions. See /LICENSE for license information.
|
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
|
|
#include "testing/file_test/run_test.h"
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include "llvm/ADT/ScopeExit.h"
|
|
#include "llvm/ADT/SmallVector.h"
|
|
#include "llvm/ADT/StringMap.h"
|
|
#include "llvm/ADT/StringRef.h"
|
|
#include "llvm/Support/PrettyStackTrace.h"
|
|
#include "testing/file_test/file_test_base.h"
|
|
#include "testing/file_test/test_file.h"
|
|
|
|
namespace Carbon::Testing {
|
|
|
|
// While these are marked as "internal" APIs, they seem to work and be pretty
|
|
// widely used for their exact documented behavior.
|
|
using ::testing::internal::CaptureStderr;
|
|
using ::testing::internal::CaptureStdout;
|
|
using ::testing::internal::GetCapturedStderr;
|
|
using ::testing::internal::GetCapturedStdout;
|
|
|
|
static constexpr llvm::StringLiteral StdinFilename = "STDIN";
|
|
|
|
// Does replacements in ARGS for %s and %t.
|
|
static auto DoArgReplacements(
|
|
|
|
llvm::SmallVector<std::string>& test_args,
|
|
const llvm::StringMap<std::string>& replacements,
|
|
const llvm::SmallVector<TestFile::Split>& split_files) -> ErrorOr<Success> {
|
|
for (auto* it = test_args.begin(); it != test_args.end(); ++it) {
|
|
auto percent = it->find("%");
|
|
if (percent == std::string::npos) {
|
|
continue;
|
|
}
|
|
|
|
if (percent + 1 >= it->size()) {
|
|
return ErrorBuilder() << "% is not allowed on its own: " << *it;
|
|
}
|
|
char c = (*it)[percent + 1];
|
|
switch (c) {
|
|
case 's': {
|
|
if (*it != "%s") {
|
|
return ErrorBuilder() << "%s must be the full argument: " << *it;
|
|
}
|
|
it = test_args.erase(it);
|
|
for (const auto& split : split_files) {
|
|
const std::string& filename = split.filename;
|
|
if (filename == StdinFilename || filename.ends_with(".h")) {
|
|
continue;
|
|
}
|
|
it = test_args.insert(it, filename);
|
|
++it;
|
|
}
|
|
// Back up once because the for loop will advance.
|
|
--it;
|
|
break;
|
|
}
|
|
case 't': {
|
|
char* tmpdir = getenv("TEST_TMPDIR");
|
|
CARBON_CHECK(tmpdir != nullptr);
|
|
it->replace(percent, 2, llvm::formatv("{0}/temp_file", tmpdir));
|
|
break;
|
|
}
|
|
case '{': {
|
|
auto end_brace = it->find('}', percent);
|
|
if (end_brace == std::string::npos) {
|
|
return ErrorBuilder() << "%{ without closing }: " << *it;
|
|
}
|
|
llvm::StringRef substr(&*(it->begin() + percent + 2),
|
|
end_brace - percent - 2);
|
|
auto replacement = replacements.find(substr);
|
|
if (replacement == replacements.end()) {
|
|
return ErrorBuilder()
|
|
<< "unknown substitution: %{" << substr << "}: " << *it;
|
|
}
|
|
it->replace(percent, end_brace - percent + 1, replacement->second);
|
|
break;
|
|
}
|
|
default:
|
|
return ErrorBuilder() << "%" << c << " is not supported: " << *it;
|
|
}
|
|
}
|
|
return Success();
|
|
}
|
|
|
|
auto ProcessTestFileAndRun(FileTestBase* test_base, std::mutex* output_mutex,
|
|
bool dump_output, bool running_autoupdate)
|
|
-> ErrorOr<TestFile> {
|
|
// Load expected output.
|
|
CARBON_ASSIGN_OR_RETURN(
|
|
TestFile test_file,
|
|
ProcessTestFile(test_base->test_name(), running_autoupdate));
|
|
|
|
// Process arguments.
|
|
if (test_file.test_args.empty()) {
|
|
test_file.test_args = test_base->GetDefaultArgs();
|
|
test_file.test_args.append(test_file.extra_args);
|
|
}
|
|
CARBON_RETURN_IF_ERROR(DoArgReplacements(test_file.test_args,
|
|
test_base->GetArgReplacements(),
|
|
test_file.file_splits));
|
|
|
|
// stdin needs to exist on-disk for compatibility. We'll use a pointer for it.
|
|
FILE* input_stream = nullptr;
|
|
auto erase_input_on_exit = llvm::make_scope_exit([&input_stream]() {
|
|
if (input_stream) {
|
|
// fclose should delete the tmpfile.
|
|
fclose(input_stream);
|
|
input_stream = nullptr;
|
|
}
|
|
});
|
|
|
|
// Create the files in-memory.
|
|
llvm::IntrusiveRefCntPtr<llvm::vfs::InMemoryFileSystem> fs =
|
|
new llvm::vfs::InMemoryFileSystem;
|
|
for (const auto& split : test_file.file_splits) {
|
|
if (split.filename == StdinFilename) {
|
|
input_stream = tmpfile();
|
|
fwrite(split.content.c_str(), sizeof(char), split.content.size(),
|
|
input_stream);
|
|
rewind(input_stream);
|
|
} else if (!fs->addFile(split.filename, /*ModificationTime=*/0,
|
|
llvm::MemoryBuffer::getMemBuffer(
|
|
split.content, split.filename,
|
|
/*RequiresNullTerminator=*/false))) {
|
|
return ErrorBuilder() << "File is repeated: " << split.filename;
|
|
}
|
|
}
|
|
// Convert the arguments to StringRef and const char* to match the
|
|
// expectations of PrettyStackTraceProgram and Run.
|
|
llvm::SmallVector<llvm::StringRef> test_args_ref;
|
|
llvm::SmallVector<const char*> test_argv_for_stack_trace;
|
|
test_args_ref.reserve(test_file.test_args.size());
|
|
test_argv_for_stack_trace.reserve(test_file.test_args.size() + 1);
|
|
for (const auto& arg : test_file.test_args) {
|
|
test_args_ref.push_back(arg);
|
|
test_argv_for_stack_trace.push_back(arg.c_str());
|
|
}
|
|
// Add a trailing null so that this is a proper argv.
|
|
test_argv_for_stack_trace.push_back(nullptr);
|
|
|
|
// Add a stack trace entry for the test invocation.
|
|
llvm::PrettyStackTraceProgram stack_trace_entry(
|
|
test_argv_for_stack_trace.size() - 1, test_argv_for_stack_trace.data());
|
|
|
|
// Execution must be serialized for either serial tests or console output.
|
|
std::unique_lock<std::mutex> output_lock;
|
|
if (output_mutex &&
|
|
(test_file.capture_console_output || !test_base->AllowParallelRun())) {
|
|
output_lock = std::unique_lock<std::mutex>(*output_mutex);
|
|
}
|
|
|
|
// Conditionally capture console output. We use a scope exit to ensure the
|
|
// captures terminate even on run failures.
|
|
if (test_file.capture_console_output) {
|
|
CaptureStderr();
|
|
CaptureStdout();
|
|
}
|
|
|
|
// Prepare string streams to capture output. In order to address casting
|
|
// constraints, we split calls to Run as a ternary based on whether we want to
|
|
// capture output.
|
|
llvm::raw_svector_ostream output_stream(test_file.actual_stdout);
|
|
llvm::raw_svector_ostream error_stream(test_file.actual_stderr);
|
|
|
|
ErrorOr<FileTestBase::RunResult> run_result =
|
|
dump_output ? test_base->Run(test_args_ref, fs, input_stream,
|
|
llvm::outs(), llvm::errs())
|
|
: test_base->Run(test_args_ref, fs, input_stream,
|
|
output_stream, error_stream);
|
|
|
|
// Ensure stdout/stderr are always fetched, even when discarded on error.
|
|
if (test_file.capture_console_output) {
|
|
// No need to flush stderr.
|
|
llvm::outs().flush();
|
|
test_file.actual_stdout += GetCapturedStdout();
|
|
test_file.actual_stderr += GetCapturedStderr();
|
|
}
|
|
|
|
if (!run_result.ok()) {
|
|
return std::move(run_result).error();
|
|
}
|
|
test_file.run_result = std::move(*run_result);
|
|
return test_file;
|
|
}
|
|
|
|
} // namespace Carbon::Testing
|