blob: ff65a5168ca9acaf4f3f7719d0147e1f2b3008dc [file] [log] [blame]
// Part of the Crubit 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 "rs_bindings_from_cc/cmdline.h"
#include <fstream>
#include <initializer_list>
#include <string>
#include <utility>
#include <vector>
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "absl/flags/flag.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
#include "common/ffi_types.h"
#include "common/status_macros.h"
#include "common/status_test_matchers.h"
#include "rs_bindings_from_cc/bazel_types.h"
#include "rs_bindings_from_cc/cmdline_flags.h"
#include "rs_bindings_from_cc/ir.h"
namespace crubit {
namespace {
using ::testing::AllOf;
using ::testing::ElementsAre;
using ::testing::HasSubstr;
using ::testing::Pair;
using ::testing::UnorderedElementsAre;
absl::StatusOr<CmdlineArgs> TestCmdlineArgs(
std::string target, std::vector<std::string> public_headers,
absl::string_view target_args) {
auto args = CmdlineArgs{
.current_target = BazelLabel(std::move(target)),
.cc_out = "cc_out",
.rs_out = "rs_out",
.ir_out = "ir_out",
.namespaces_out = "namespaces_out",
.crubit_support_path_format = "<crubit/support/path/{header}>",
.clang_format_exe_path = "clang_format_exe_path",
.rustfmt_exe_path = "rustfmt_exe_path",
.rustfmt_config_path = "rustfmt_config_path",
.generate_source_location_in_doc_comment =
SourceLocationDocComment::Disabled};
std::transform(public_headers.begin(), public_headers.end(),
std::back_inserter(args.public_headers),
[](std::string header) { return HeaderName(header); });
CRUBIT_RETURN_IF_ERROR(internal::ParseTargetArgs(target_args, args));
CRUBIT_ASSIGN_OR_RETURN(Cmdline cmdline, Cmdline::Create(std::move(args)));
return std::move(cmdline).args();
}
absl::StatusOr<CmdlineArgs> TestCmdlineArgs(
std::vector<std::string> public_headers, absl::string_view target_args) {
return TestCmdlineArgs("//:target", std::move(public_headers),
std::move(target_args));
}
// Returns an example valid test command line.
absl::StatusOr<CmdlineArgs> TestCmdlineArgs() {
return TestCmdlineArgs("//:target", {"h1"},
R"([{"t": "//:target", "h": ["h1"]}])");
}
/// TestCmdlineArgs() above needs to be valid...
TEST(CmdlineTest, TestCmdlineArgs) { ASSERT_OK(TestCmdlineArgs().status()); }
TEST(CmdlineTest, BasicCorrectInput) {
absl::SetFlag(&FLAGS_do_nothing, false);
absl::SetFlag(&FLAGS_rs_out, "rs_out");
absl::SetFlag(&FLAGS_cc_out, "cc_out");
absl::SetFlag(&FLAGS_ir_out, "ir_out");
absl::SetFlag(&FLAGS_crubit_support_path_format,
"<crubit/support/path/{header}>");
absl::SetFlag(&FLAGS_clang_format_exe_path, "clang_format_exe_path");
absl::SetFlag(&FLAGS_rustfmt_exe_path, "rustfmt_exe_path");
absl::SetFlag(&FLAGS_rustfmt_config_path, "rustfmt_config_path");
absl::SetFlag(&FLAGS_public_headers, {"h1"});
absl::SetFlag(&FLAGS_target, "//:t1");
absl::SetFlag(&FLAGS_target_args, R"([{"t": "//:t1", "h": ["h1", "h2"]}])");
absl::SetFlag(&FLAGS_extra_rs_srcs, {"extra_file.rs"});
absl::SetFlag(&FLAGS_srcs_to_scan_for_instantiations,
{"scan_for_instantiations.rs"});
absl::SetFlag(&FLAGS_instantiations_out, "instantiations_out");
absl::SetFlag(&FLAGS_namespaces_out, "namespaces_out");
absl::SetFlag(&FLAGS_error_report_out, "error_report_out");
absl::SetFlag(&FLAGS_generate_source_location_in_doc_comment,
SourceLocationDocComment::Disabled);
ASSERT_OK_AND_ASSIGN(Cmdline cmdline, Cmdline::FromFlags());
const CmdlineArgs& args = cmdline.args();
EXPECT_EQ(args.cc_out, "cc_out");
EXPECT_EQ(args.rs_out, "rs_out");
EXPECT_EQ(args.ir_out, "ir_out");
EXPECT_EQ(args.namespaces_out, "namespaces_out");
EXPECT_EQ(args.crubit_support_path_format, "<crubit/support/path/{header}>");
EXPECT_EQ(args.clang_format_exe_path, "clang_format_exe_path");
EXPECT_EQ(args.rustfmt_exe_path, "rustfmt_exe_path");
EXPECT_EQ(args.rustfmt_config_path, "rustfmt_config_path");
EXPECT_EQ(args.instantiations_out, "instantiations_out");
EXPECT_EQ(args.error_report_out, "error_report_out");
EXPECT_EQ(args.do_nothing, false);
EXPECT_EQ(args.current_target.value(), "//:t1");
EXPECT_THAT(args.public_headers, ElementsAre(HeaderName("h1")));
EXPECT_THAT(args.extra_rs_srcs, ElementsAre("extra_file.rs"));
EXPECT_THAT(args.srcs_to_scan_for_instantiations,
ElementsAre("scan_for_instantiations.rs"));
EXPECT_THAT(
args.headers_to_targets,
UnorderedElementsAre(Pair(HeaderName("h1"), BazelLabel("//:t1")),
Pair(HeaderName("h2"), BazelLabel("//:t1"))));
EXPECT_EQ(args.generate_source_location_in_doc_comment,
SourceLocationDocComment::Disabled);
}
TEST(CmdlineTest, TargetArgsEmpty) {
CmdlineArgs args;
EXPECT_THAT(internal::ParseTargetArgs("", args),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("please specify --target_args")));
}
TEST(CmdlineTest, TargetArgsInvalidJson) {
CmdlineArgs args;
EXPECT_THAT(
internal::ParseTargetArgs("#!$%", args),
StatusIs(absl::StatusCode::kInvalidArgument,
AllOf(HasSubstr("--target_args"), HasSubstr("Invalid JSON"))));
}
TEST(CmdlineTest, TargetArgsIntInsteadOfTopLevelArray) {
CmdlineArgs args;
EXPECT_THAT(internal::ParseTargetArgs("123", args),
StatusIs(absl::StatusCode::kInvalidArgument,
AllOf(HasSubstr("--target_args"), HasSubstr("array"))));
}
TEST(CmdlineTest, TargetArgsIntInTopLevelArray) {
CmdlineArgs args;
EXPECT_THAT(internal::ParseTargetArgs("[123, 456]", args),
StatusIs(absl::StatusCode::kInvalidArgument,
AllOf(HasSubstr("--target_args"))));
}
TEST(CmdlineTest, TargetArgsIntInsteadOfHeadersArray) {
CmdlineArgs args;
EXPECT_THAT(internal::ParseTargetArgs(R"([{"t": "//:t1", "h": 123}])", args),
StatusIs(absl::StatusCode::kInvalidArgument,
AllOf(HasSubstr("--target_args"), HasSubstr(".h"),
HasSubstr("array"))));
}
TEST(CmdlineTest, TargetArgsMissingTarget) {
CmdlineArgs args;
EXPECT_THAT(internal::ParseTargetArgs(R"([{"h": ["h1", "h2"]}])", args),
StatusIs(absl::StatusCode::kInvalidArgument,
AllOf(HasSubstr("--target_args"), HasSubstr(".t"),
HasSubstr("missing"))));
}
TEST(CmdlineTest, TargetArgsMissingHeader) {
EXPECT_THAT(TestCmdlineArgs("//:t1", {"h1"}, R"([{"t": "//:t1"}])"),
StatusIs(absl::StatusCode::kInvalidArgument,
AllOf(HasSubstr("--target_args"),
HasSubstr("Couldn't find header"))));
}
TEST(CmdlineTest, TargetArgsEmptyHeader) {
CmdlineArgs args;
EXPECT_THAT(
internal::ParseTargetArgs(R"([{"t": "t1", "h": ["", "h2"]}])", args),
StatusIs(absl::StatusCode::kInvalidArgument,
AllOf(HasSubstr("--target_args"), HasSubstr("`h`"),
HasSubstr("empty string"))));
}
TEST(CmdlineTest, TargetArgsEmptyTarget) {
CmdlineArgs args;
EXPECT_THAT(
internal::ParseTargetArgs(R"([{"t": "", "h": ["h1", "h2"]}])", args),
StatusIs(absl::StatusCode::kInvalidArgument,
AllOf(HasSubstr("--target_args"), HasSubstr("`t`"),
HasSubstr("empty string"))));
}
TEST(CmdlineTest, TargetArgsIntInsteadOfTarget) {
CmdlineArgs args;
EXPECT_THAT(
internal::ParseTargetArgs(R"([{"t": 123, "h": ["h1", "h2"]}])", args),
StatusIs(absl::StatusCode::kInvalidArgument,
AllOf(HasSubstr("--target_args"), HasSubstr(".t"),
HasSubstr("string"))));
}
TEST(CmdlineTest, TargetArgsIntInsteadOfHeader) {
CmdlineArgs args;
EXPECT_THAT(
internal::ParseTargetArgs(R"([{"t": "//:t1", "h": [123, "h2"]}])", args),
StatusIs(absl::StatusCode::kInvalidArgument,
AllOf(HasSubstr("--target_args"), HasSubstr(".h"),
HasSubstr("string"))));
}
TEST(CmdlineTest, TargetArgsDuplicateHeader) {
CmdlineArgs args;
ASSERT_OK(internal::ParseTargetArgs(R"([
{"t": "//:t1", "h": ["h1"]},
{"t": "//:t2", "h": ["h1", "h2"]} ])",
args));
EXPECT_THAT(
args.headers_to_targets,
UnorderedElementsAre(Pair(HeaderName("h1"), BazelLabel("//:t1")),
Pair(HeaderName("h2"), BazelLabel("//:t2"))));
}
TEST(CmdlineTest, PublicHeadersEmpty) {
constexpr absl::string_view kTargetsAndHeaders = R"([
{"t": "//:target1", "h": ["a.h", "b.h"]}
])";
ASSERT_THAT(TestCmdlineArgs({}, std::string(kTargetsAndHeaders)),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("please specify --public_headers")));
}
TEST(CmdlineTest, PublicHeadersWhereFirstHeaderMissingInMap) {
constexpr absl::string_view kTargetsAndHeaders = R"([
{"t": "//:target1", "h": ["a.h", "b.h"]}
])";
ASSERT_THAT(
TestCmdlineArgs({"missing-in-map.h"}, std::string(kTargetsAndHeaders)),
StatusIs(
absl::StatusCode::kInvalidArgument,
AllOf(HasSubstr("missing-in-map.h"), HasSubstr("Couldn't find"))));
}
TEST(CmdlineTest, PublicHeadersWhereSecondHeaderMissingInMap) {
constexpr absl::string_view kTargetsAndHeaders = R"([
{"t": "//:target1", "h": ["a.h", "b.h"]}
])";
ASSERT_THAT(
TestCmdlineArgs({"a.h", "missing.h"}, kTargetsAndHeaders),
StatusIs(absl::StatusCode::kInvalidArgument,
AllOf(HasSubstr("missing.h"), HasSubstr("Couldn't find"))));
}
TEST(CmdlineTest, PublicHeadersCoveringMultipleTargets) {
constexpr absl::string_view kTargetsAndHeaders = R"([
{"t": "//:target1", "h": ["a.h", "b.h"]},
{"t": "//:target2", "h": ["c.h", "d.h"]}
])";
ASSERT_OK_AND_ASSIGN(
CmdlineArgs args,
TestCmdlineArgs("//:target1", {"a.h", "c.h"}, kTargetsAndHeaders));
EXPECT_EQ(args.current_target.value(), "//:target1");
EXPECT_THAT(
args.headers_to_targets,
UnorderedElementsAre(Pair(HeaderName("a.h"), BazelLabel("//:target1")),
Pair(HeaderName("b.h"), BazelLabel("//:target1")),
Pair(HeaderName("c.h"), BazelLabel("//:target2")),
Pair(HeaderName("d.h"), BazelLabel("//:target2"))));
}
TEST(CmdlineTest, TargetArgsIntInsteadOfFeaturesArray) {
ASSERT_THAT(TestCmdlineArgs({"h1"}, R"([{"t": "t1", "f": 123}])"),
StatusIs(absl::StatusCode::kInvalidArgument,
AllOf(HasSubstr("--target_args"), HasSubstr(".f"),
HasSubstr("array"))));
}
TEST(CmdlineTest, TargetArgsEmptyFeature) {
ASSERT_THAT(TestCmdlineArgs({"h1"}, R"([{"t": "t1", "f": ["", "h2"]}])"),
StatusIs(absl::StatusCode::kInvalidArgument,
AllOf(HasSubstr("--target_args"), HasSubstr("`f`"),
HasSubstr("empty string"))));
}
TEST(CmdlineTest, TargetArgsIntInsteadOfFeature) {
ASSERT_THAT(
TestCmdlineArgs({"h1"}, R"([{"t": "t1", "f": [123, "experimental"]}])"),
StatusIs(absl::StatusCode::kInvalidArgument,
AllOf(HasSubstr("--target_args"), HasSubstr(".f"),
HasSubstr("string"))));
}
TEST(CmdlineTest, InstantiationsOutEmpty) {
ASSERT_OK_AND_ASSIGN(CmdlineArgs args, TestCmdlineArgs());
args.srcs_to_scan_for_instantiations = {"lib.rs"};
args.instantiations_out = "";
EXPECT_THAT(
Cmdline::Create(std::move(args)),
StatusIs(
absl::StatusCode::kInvalidArgument,
HasSubstr(
"please specify both --rust_sources and --instantiations_out "
"when requesting a template instantiation mode")));
}
TEST(CmdlineTest, RustSourcesEmpty) {
ASSERT_OK_AND_ASSIGN(CmdlineArgs args, TestCmdlineArgs());
args.srcs_to_scan_for_instantiations = {};
args.instantiations_out = "instantiations_out";
EXPECT_THAT(
Cmdline::Create(std::move(args)),
StatusIs(
absl::StatusCode::kInvalidArgument,
HasSubstr(
"please specify both --rust_sources and --instantiations_out "
"when requesting a template instantiation mode")));
}
TEST(CmdlineTest, CcOutEmpty) {
ASSERT_OK_AND_ASSIGN(CmdlineArgs args, TestCmdlineArgs());
args.cc_out = "";
EXPECT_THAT(Cmdline::Create(std::move(args)),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("please specify --cc_out")));
}
TEST(CmdlineTest, RsOutEmpty) {
ASSERT_OK_AND_ASSIGN(CmdlineArgs args, TestCmdlineArgs());
args.rs_out = "";
EXPECT_THAT(Cmdline::Create(std::move(args)),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("please specify --rs_out")));
}
TEST(CmdlineTest, IrOutEmpty) {
ASSERT_OK_AND_ASSIGN(CmdlineArgs args, TestCmdlineArgs());
args.ir_out = "";
EXPECT_OK(Cmdline::Create(std::move(args)));
}
TEST(CmdlineTest, ClangFormatExePathEmpty) {
ASSERT_OK_AND_ASSIGN(CmdlineArgs args, TestCmdlineArgs());
args.clang_format_exe_path = "";
EXPECT_THAT(Cmdline::Create(std::move(args)),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("please specify --clang_format_exe_path")));
}
TEST(CmdlineTest, RustfmtExePathEmpty) {
ASSERT_OK_AND_ASSIGN(CmdlineArgs args, TestCmdlineArgs());
args.rustfmt_exe_path = "";
EXPECT_THAT(Cmdline::Create(std::move(args)),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("please specify --rustfmt_exe_path")));
}
TEST(CmdlineTest, SupportPathEmpty) {
ASSERT_OK_AND_ASSIGN(CmdlineArgs args, TestCmdlineArgs());
args.crubit_support_path_format = "";
EXPECT_THAT(
Cmdline::Create(std::move(args)),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("please specify --crubit_support_path_format")));
}
TEST(CmdlineTest, SupportPathNoPlaceholder) {
ASSERT_OK_AND_ASSIGN(CmdlineArgs args, TestCmdlineArgs());
args.crubit_support_path_format = "<crubit/support/path>";
EXPECT_THAT(Cmdline::Create(std::move(args)),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("cannot find `{header}` placeholder in "
"crubit_support_path_format")));
}
// A mutable test argv, which doesn't leak memory.
class Args {
public:
explicit Args(std::vector<std::string> args) {
storage_ = std::move(args);
for (std::string& arg : storage_) {
argv_back_.push_back(arg.data());
}
argc_ = argv_back_.size();
argv_ = argv_back_.data();
}
std::vector<absl::string_view> argv_vector() const {
auto span = absl::MakeConstSpan(argv_, argc_);
std::vector<absl::string_view> result;
result.reserve(span.size());
for (const char* arg : span) {
result.push_back(arg);
}
return result;
}
char**& argv() { return argv_; }
int& argc() { return argc_; }
private:
// storage_ and argv_back_ need to be separate, because the argv can have
// new strings inserted into it (which should _not_ be freed the same way
// as the old strings).
std::vector<std::string> storage_;
std::vector<char*> argv_back_;
int argc_;
char** argv_;
};
TEST(PreprocessTargetArgsTest, Noop) {
Args args({"binary", "foo", "bar"});
PreprocessTargetArgs(args.argc(), args.argv());
EXPECT_THAT(args.argv_vector(), ElementsAre("binary", "foo", "bar"));
}
TEST(PreprocessTargetArgsTest, TargetToArg) {
Args args({"binary", "--target_to_arg", R"({"k": "v"})", "--target_to_arg",
R"({"k2": "v2"})", "other_args"});
PreprocessTargetArgs(args.argc(), args.argv());
EXPECT_THAT(
args.argv_vector(),
ElementsAre("binary", R"(--target_args=[{"k": "v"},{"k2": "v2"}])",
"other_args"));
}
std::string Paramfile(absl::string_view contents) {
std::string path = absl::StrCat(
testing::TempDir(), "/",
testing::UnitTest::GetInstance()->current_test_info()->name(), ".param");
std::ofstream f(path);
f << contents;
f.close();
return absl::StrCat("@", path);
}
TEST(ExpandParamfilesTest, Noop) {
Args args({"binary", "foo", "bar"});
ExpandParamfiles(args.argc(), args.argv());
EXPECT_THAT(args.argv_vector(), ElementsAre("binary", "foo", "bar"));
}
TEST(ExpandParamfilesTest, Expand) {
Args args({"binary", "foo", Paramfile("arg1\narg2\n"), "bar"});
ExpandParamfiles(args.argc(), args.argv());
EXPECT_THAT(args.argv_vector(),
ElementsAre("binary", "foo", "arg1", "arg2", "bar"));
}
TEST(ExpandParamfilesTest, Nested) {
Args args({"binary", Paramfile("@unexpanded")});
ExpandParamfiles(args.argc(), args.argv());
EXPECT_THAT(args.argv_vector(), ElementsAre("binary", "@unexpanded"));
}
TEST(ExpandParamfilesTest, Escapes) {
std::string unescaped_contents = "\"'\n\f\v\r";
std::string paramfile_contents = "";
paramfile_contents.reserve(unescaped_contents.size() * 2);
for (char c : unescaped_contents) {
paramfile_contents += '\\';
paramfile_contents += c;
}
Args args({"binary", Paramfile(paramfile_contents)});
ExpandParamfiles(args.argc(), args.argv());
EXPECT_THAT(args.argv_vector(), ElementsAre("binary", unescaped_contents));
}
// Backslash escapes should be read right to left -- so for instance, while
// the two bytes `\'` become the single byte `'`, the three bytes `\\'` become
// the two bytes `\'`.
TEST(ExpandParamfilesTest, BackslashEscape) {
Args args({"binary", Paramfile(R"(\\\')")});
ExpandParamfiles(args.argc(), args.argv());
EXPECT_THAT(args.argv_vector(), ElementsAre("binary", R"(\')"));
}
} // namespace
} // namespace crubit