// Copyright 2018 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. #include #include "src/main/cpp/blaze_util.h" #include "src/main/cpp/blaze_util_platform.h" #include "src/main/cpp/option_processor.h" #include "src/main/cpp/util/file.h" #include "src/main/cpp/util/file_platform.h" #include "src/main/cpp/util/path.h" #include "src/main/cpp/util/strings.h" #include "src/main/cpp/workspace_layout.h" #include "googlemock/include/gmock/gmock.h" #include "googletest/include/gtest/gtest.h" namespace blaze { using std::string; using std::unordered_map; using std::vector; using ::testing::MatchesRegex; #if _WIN32 constexpr bool kIsWindows = true; #else constexpr bool kIsWindows = false; #endif class RcOptionsTest : public ::testing::Test { protected: RcOptionsTest() : test_file_dir_(blaze_util::JoinPath( blaze::GetPathEnv("TEST_TMPDIR"), ::testing::UnitTest::GetInstance()->current_test_info()->name())), workspace_layout_() { } const string test_file_dir_; const WorkspaceLayout workspace_layout_; void WriteRc(const string& filename, const string& contents) { bool success_mkdir = blaze_util::MakeDirectories(test_file_dir_, 0755); ASSERT_TRUE(success_mkdir) << "Failed to mkdir -p " << test_file_dir_; bool success_write = blaze_util::WriteFile( contents, blaze_util::JoinPath(test_file_dir_, filename)); ASSERT_TRUE(success_write) << "Failed to write " << filename; } std::unique_ptr Parse(const string& filename, RcFile::ParseError* error, std::string* error_text) { return RcFile::Parse( blaze_util::JoinPath(test_file_dir_, filename), &workspace_layout_, // Set workspace to test_file_dir_ so importing %workspace%/foo works. test_file_dir_, error, error_text); } void SuccessfullyParseRcWithExpectedArgs( const string& filename, const unordered_map>& expected_args_map) { RcFile::ParseError error; string error_text; std::unique_ptr rc = Parse(filename, &error, &error_text); EXPECT_EQ(error_text, ""); ASSERT_EQ(error, RcFile::ParseError::NONE); // Test that exactly each command in the expected map was in the results, // and that for each of these, exactly the expected args are found, in the // correct order. Note that this is not just an exercise in rewriting map // equality - the results have type RcOption, and the expected values // are just strings. This is ignoring the source_path for convenience. const RcFile::OptionMap& result = rc->options(); ASSERT_EQ(expected_args_map.size(), result.size()); for (const auto& command_args_pair : expected_args_map) { const string& expected_command = command_args_pair.first; const vector& expected_args = command_args_pair.second; const auto result_args_iter = result.find(expected_command); ASSERT_NE(result_args_iter, rc->options().end()); const std::vector& result_args = result_args_iter->second; ASSERT_EQ(result_args.size(), expected_args.size()); for (size_t i = 0; i < result_args.size(); ++i) { EXPECT_EQ(result_args[i].option, expected_args[i]); } } } }; TEST_F(RcOptionsTest, Empty) { WriteRc("empty.bazelrc", ""); unordered_map> no_expected_args; SuccessfullyParseRcWithExpectedArgs("empty.bazelrc", no_expected_args); } TEST_F(RcOptionsTest, Whitespace) { WriteRc("whitespace.bazelrc", " \n\t "); unordered_map> no_expected_args; SuccessfullyParseRcWithExpectedArgs("whitespace.bazelrc", no_expected_args); } TEST_F(RcOptionsTest, CommentedStartup) { WriteRc("commented_startup.bazelrc", "# startup foo"); unordered_map> no_expected_args; SuccessfullyParseRcWithExpectedArgs("commented_startup.bazelrc", no_expected_args); } TEST_F(RcOptionsTest, EmptyStartupLine) { WriteRc("empty_startup_line.bazelrc", "startup"); unordered_map> no_expected_args; SuccessfullyParseRcWithExpectedArgs("empty_startup_line.bazelrc", no_expected_args); } TEST_F(RcOptionsTest, StartupWithOnlyCommentedArg) { WriteRc("startup_with_comment.bazelrc", "startup # bar"); unordered_map> no_expected_args; SuccessfullyParseRcWithExpectedArgs("startup_with_comment.bazelrc", no_expected_args); } TEST_F(RcOptionsTest, SingleStartupArg) { WriteRc("startup_foo.bazelrc", "startup foo"); SuccessfullyParseRcWithExpectedArgs( "startup_foo.bazelrc", {{"startup", {"foo"}}}); } TEST_F(RcOptionsTest, SingleStartupArgWithComment) { WriteRc("startup_foo_and_comment.bazelrc", "startup foo # comment"); SuccessfullyParseRcWithExpectedArgs( "startup_foo_and_comment.bazelrc", {{"startup", {"foo"}}}); } TEST_F(RcOptionsTest, TwoStartupArgsOnOneLine) { WriteRc("startup_foo_bar.bazelrc", "startup foo bar"); SuccessfullyParseRcWithExpectedArgs( "startup_foo_bar.bazelrc", {{"startup", {"foo", "bar"}}}); } TEST_F(RcOptionsTest, TwoStartupArgsOnOneLineTabSeparated) { WriteRc("startup_with_tabs.bazelrc", "startup\tfoo\tbar"); SuccessfullyParseRcWithExpectedArgs( "startup_with_tabs.bazelrc", {{"startup", {"foo", "bar"}}}); } TEST_F(RcOptionsTest, StartupOptWithSimpleValue) { WriteRc("startup_opt_with_simple_value.bazelrc", "startup --opt=foo"); SuccessfullyParseRcWithExpectedArgs( "startup_opt_with_simple_value.bazelrc", {{"startup", {"--opt=foo"}}}); } TEST_F(RcOptionsTest, StartupQuotedArg) { WriteRc("startup_quoted_foo_bar.bazelrc", "startup \"foo bar\""); SuccessfullyParseRcWithExpectedArgs( "startup_quoted_foo_bar.bazelrc", {{"startup", {"foo bar"}}}); } TEST_F(RcOptionsTest, QuotedValueStartupArgAfterEquals) { WriteRc("startup_opt_quoted_arg.bazelrc", "startup --opt=\"foo bar\""); SuccessfullyParseRcWithExpectedArgs( "startup_opt_quoted_arg.bazelrc", {{"startup", {"--opt=foo bar"}}}); } TEST_F(RcOptionsTest, QuotedValueStartupArgAfterWhitespace) { WriteRc("startup_opt_quoted_arg_as_separate_token.bazelrc", "startup --opt \"foo bar\""); SuccessfullyParseRcWithExpectedArgs( "startup_opt_quoted_arg_as_separate_token.bazelrc", {{"startup", {"--opt", "foo bar"}}}); } TEST_F(RcOptionsTest, QuotedValueStartupArgOnNewLine) { WriteRc("startup_opt_quoted_arg_different_line.bazelrc", "startup --opt\n" "startup \"foo bar\""); SuccessfullyParseRcWithExpectedArgs( "startup_opt_quoted_arg_different_line.bazelrc", {{"startup", {"--opt", "foo bar"}}}); } TEST_F(RcOptionsTest, TwoOptStartup) { WriteRc("startup_two_options.bazelrc", "startup --opt1\n" "startup --opt2"); SuccessfullyParseRcWithExpectedArgs( "startup_two_options.bazelrc", {{"startup", {"--opt1", "--opt2"}}}); } TEST_F(RcOptionsTest, WhitespaceBeforeStartup) { WriteRc("whitespace_before_command.bazelrc", " startup foo\n" " # indented comments\n" "startup bar\n" "\tstartup \t baz"); SuccessfullyParseRcWithExpectedArgs( "whitespace_before_command.bazelrc", {{"startup", {"foo", "bar", "baz"}}}); } TEST_F(RcOptionsTest, StartupLineContinuation) { WriteRc("startup_line_continuation.bazelrc", "startup \\\n" "foo\n" "startup bar \\\n" "baz"); SuccessfullyParseRcWithExpectedArgs( "startup_line_continuation.bazelrc", {{"startup", {"foo", "bar", "baz"}}}); } TEST_F(RcOptionsTest, ManyArgStartup) { WriteRc("startup_with_many_args.bazelrc", "# Many arguments\n" "startup foo # First argument has reasons.\n" "startup --opt1 --opt2 # These arguments are split wide\n" "#startup --this_is_not_an_arg\n" "\n\n\n # A few empty lines for good measure\n" "startup\t \"string input Value 123. \" --bar"); SuccessfullyParseRcWithExpectedArgs( "startup_with_many_args.bazelrc", {{ "startup", {"foo", "--opt1", "--opt2", "string input Value 123. ", "--bar"} }}); } TEST_F(RcOptionsTest, MultipleCommands) { WriteRc("multiple_commands_intermixed.bazelrc", "startup foo\n" "build aaa\n" "startup bar baz\n" "build bbb\n" "build ccc\n"); SuccessfullyParseRcWithExpectedArgs( "multiple_commands_intermixed.bazelrc", {{"startup", {"foo", "bar", "baz"}}, {"build", {"aaa", "bbb", "ccc"}}}); } TEST_F(RcOptionsTest, SimpleImportFoo) { WriteRc("startup_foo.bazelrc", "startup foo"); WriteRc("import_simple.bazelrc", "import %workspace%/startup_foo.bazelrc"); SuccessfullyParseRcWithExpectedArgs( "import_simple.bazelrc", {{"startup", {"foo"}}}); } TEST_F(RcOptionsTest, ImportFooThenAddBar) { WriteRc("startup_foo.bazelrc", "startup foo"); WriteRc("import_foo_then_bar.bazelrc", "import %workspace%/startup_foo.bazelrc\n" "startup bar"); SuccessfullyParseRcWithExpectedArgs( "import_foo_then_bar.bazelrc", {{"startup", {"foo", "bar"}}}); } TEST_F(RcOptionsTest, StartupBarThenImportFoo) { WriteRc("startup_foo.bazelrc", "startup foo"); WriteRc("bar_then_import_foo.bazelrc", "startup bar\n" "import %workspace%/startup_foo.bazelrc"); SuccessfullyParseRcWithExpectedArgs( "bar_then_import_foo.bazelrc", {{"startup", {"bar", "foo"}}}); } TEST_F(RcOptionsTest, SimpleTryImportFoo) { WriteRc("startup_foo.bazelrc", "startup foo"); WriteRc("import_simple.bazelrc", "try-import %workspace%/startup_foo.bazelrc"); SuccessfullyParseRcWithExpectedArgs("import_simple.bazelrc", {{"startup", {"foo"}}}); } TEST_F(RcOptionsTest, ImportTryFooThenAddBar) { WriteRc("startup_foo.bazelrc", "startup foo"); WriteRc("import_foo_then_bar.bazelrc", "try-import %workspace%/startup_foo.bazelrc\n" "startup bar"); SuccessfullyParseRcWithExpectedArgs("import_foo_then_bar.bazelrc", {{"startup", {"foo", "bar"}}}); } TEST_F(RcOptionsTest, StartupBarThenTryImportFoo) { WriteRc("startup_foo.bazelrc", "startup foo"); WriteRc("bar_then_import_foo.bazelrc", "startup bar\n" "try-import %workspace%/startup_foo.bazelrc"); SuccessfullyParseRcWithExpectedArgs("bar_then_import_foo.bazelrc", {{"startup", {"bar", "foo"}}}); } // Most likely, import diamonds like this are unintended, and they might lead // to surprising doubled values for allow_multiple options. This causes a // warning in option_processor, which checks for duplicates across multiple rc // files. TEST_F(RcOptionsTest, ImportDiamond) { WriteRc("startup_foo.bazelrc", "startup foo"); WriteRc("import_foo_then_bar.bazelrc", "import %workspace%/startup_foo.bazelrc\n" "startup bar"); WriteRc("bar_then_import_foo.bazelrc", "startup bar\n" "import %workspace%/startup_foo.bazelrc"); WriteRc("import_diamond.bazelrc", "import %workspace%/import_foo_then_bar.bazelrc\n" "import %workspace%/bar_then_import_foo.bazelrc"); SuccessfullyParseRcWithExpectedArgs( "import_diamond.bazelrc", {{"startup", {"foo", "bar", "bar", "foo"}}}); } TEST_F(RcOptionsTest, ImportCycleFails) { WriteRc("import_cycle_1.bazelrc", "import %workspace%/import_cycle_2.bazelrc"); WriteRc("import_cycle_2.bazelrc", "import %workspace%/import_cycle_1.bazelrc"); RcFile::ParseError error; string error_text; std::unique_ptr rc = Parse("import_cycle_1.bazelrc", &error, &error_text); EXPECT_EQ(error, RcFile::ParseError::IMPORT_LOOP); ASSERT_THAT( error_text, MatchesRegex("Import loop detected:\n" " .*import_cycle_1.bazelrc\n" " .*import_cycle_2.bazelrc\n" " .*import_cycle_1.bazelrc\n")); } TEST_F(RcOptionsTest, LongImportCycleFails) { WriteRc("chain_to_cycle_1.bazelrc", "import %workspace%/chain_to_cycle_2.bazelrc"); WriteRc("chain_to_cycle_2.bazelrc", "import %workspace%/chain_to_cycle_3.bazelrc"); WriteRc("chain_to_cycle_3.bazelrc", "import %workspace%/chain_to_cycle_4.bazelrc"); WriteRc("chain_to_cycle_4.bazelrc", "import %workspace%/import_cycle_1.bazelrc"); WriteRc("import_cycle_1.bazelrc", "import %workspace%/import_cycle_2.bazelrc"); WriteRc("import_cycle_2.bazelrc", "import %workspace%/import_cycle_1.bazelrc"); RcFile::ParseError error; string error_text; std::unique_ptr rc = Parse("chain_to_cycle_1.bazelrc", &error, &error_text); EXPECT_EQ(error, RcFile::ParseError::IMPORT_LOOP); ASSERT_THAT( error_text, MatchesRegex("Import loop detected:\n" " .*chain_to_cycle_1.bazelrc\n" " .*chain_to_cycle_2.bazelrc\n" " .*chain_to_cycle_3.bazelrc\n" " .*chain_to_cycle_4.bazelrc\n" " .*import_cycle_1.bazelrc\n" " .*import_cycle_2.bazelrc\n" " .*import_cycle_1.bazelrc\n")); } TEST_F(RcOptionsTest, FileDoesNotExist) { RcFile::ParseError error; string error_text; std::unique_ptr rc = Parse("not_a_file.bazelrc", &error, &error_text); EXPECT_EQ(error, RcFile::ParseError::UNREADABLE_FILE); ASSERT_THAT( error_text, MatchesRegex(kIsWindows ? "Unexpected error reading \\.blazerc file '.*not_a_file\\.bazelrc':.*" : "Unexpected error reading \\.blazerc file '.*not_a_file\\.bazelrc': " "\\(error: 2\\): No such file or directory")); } TEST_F(RcOptionsTest, ImportedFileDoesNotExist) { WriteRc("import_fake_file.bazelrc", "import somefile"); RcFile::ParseError error; string error_text; std::unique_ptr rc = Parse("import_fake_file.bazelrc", &error, &error_text); EXPECT_EQ(error, RcFile::ParseError::UNREADABLE_FILE); if (kIsWindows) { ASSERT_THAT(error_text, MatchesRegex( "Unexpected error reading \\.blazerc file 'somefile':.*")); } else { ASSERT_EQ(error_text, "Unexpected error reading .blazerc file 'somefile': (error: 2): No such " "file or directory"); } } TEST_F(RcOptionsTest, TryImportedFileDoesNotExist) { WriteRc("try_import_fake_file.bazelrc", "try-import somefile"); unordered_map> no_expected_args; SuccessfullyParseRcWithExpectedArgs("try_import_fake_file.bazelrc", no_expected_args); } TEST_F(RcOptionsTest, ImportHasTooManyArgs) { WriteRc("bad_import.bazelrc", "import somefile bar"); RcFile::ParseError error; string error_text; std::unique_ptr rc = Parse("bad_import.bazelrc", &error, &error_text); EXPECT_EQ(error, RcFile::ParseError::INVALID_FORMAT); ASSERT_THAT( error_text, MatchesRegex("Invalid import declaration in .blazerc file " "'.*bad_import.bazelrc': 'import somefile bar' \\(are you " "in your source checkout/WORKSPACE\\?\\)")); } TEST_F(RcOptionsTest, TryImportHasTooManyArgs) { WriteRc("bad_import.bazelrc", "try-import somefile bar"); RcFile::ParseError error; string error_text; std::unique_ptr rc = Parse("bad_import.bazelrc", &error, &error_text); EXPECT_EQ(error, RcFile::ParseError::INVALID_FORMAT); ASSERT_THAT( error_text, MatchesRegex("Invalid import declaration in .blazerc file " "'.*bad_import.bazelrc': 'try-import somefile bar' \\(are " "you in your source checkout/WORKSPACE\\?\\)")); } // TODO(b/34811299) The tests below identify ways that '\' used as a line // continuation is broken. This is on top of user-reported cases where an // unintentional '\' made the command on the following line show up as // an argument, which lead to cryptic messages. There is no value added by '\', // since the following line could just repeat the command, so it might be best // to remove this feature entirely. // // For now, these tests serve as documentation of the brokenness, and to keep // broken behavior consistent before we get around to fixing it. TEST_F(RcOptionsTest, BadStartupLineContinuation_HasWhitespaceAfterSlash) { WriteRc("bad_startup_line_continuation.bazelrc", "startup foo \\ \n" "bar"); SuccessfullyParseRcWithExpectedArgs( "bad_startup_line_continuation.bazelrc", {{"startup", {"foo"}}}); // Does not contain "bar" from the next line. } TEST_F(RcOptionsTest, BadStartupLineContinuation_HasErroneousSlash) { WriteRc("bad_startup_line_continuation.bazelrc", "startup foo \\ bar"); SuccessfullyParseRcWithExpectedArgs( "bad_startup_line_continuation.bazelrc", // Whitespace between the slash and bar gets counted as part of the token. {{"startup", {"foo", " bar"}}}); } TEST_F(RcOptionsTest, BadStartupLineContinuation_HasCommentAfterSlash) { WriteRc("bad_startup_line_continuation.bazelrc", "startup foo \\ # comment\n" "bar"); SuccessfullyParseRcWithExpectedArgs( "bad_startup_line_continuation.bazelrc", // Whitespace between the slash and comment gets counted as a new token, // and the bar on the next line is ignored (it's an argumentless command). {{"startup", {"foo", " "}}}); } TEST_F(RcOptionsTest, BadStartupLineContinuation_InterpretsNextLineAsNewline) { WriteRc("bad_startup_line_continuation.bazelrc", "startup foo \\ #comment\n" "bar baz"); SuccessfullyParseRcWithExpectedArgs( "bad_startup_line_continuation.bazelrc", // Whitespace between the slash and comment gets counted as a new token, // and the bar on the next line treated as its own command, instead of as // a "startup" args. {{"startup", {"foo", " "}}, {"bar", {"baz"}}}); } } // namespace blaze