// 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 "googletest/include/gtest/gtest.h" namespace blaze { using std::string; using std::unordered_map; using std::vector; class RcOptionsTest : public ::testing::Test { protected: RcOptionsTest() : test_file_dir_(blaze::GetEnv("TEST_TMPDIR")), workspace_layout_() {} const string test_file_dir_; const WorkspaceLayout workspace_layout_; void WriteRc(const string& filename, const string& contents) { bool success = blaze_util::WriteFile( contents, blaze_util::JoinPath(test_file_dir_, filename)); ASSERT_TRUE(success) << "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 rewritting 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]); } } } }; // Effectively empty file tests 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("whitespace.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); } // Single command tests - testing tokenization and accumulation of arguments. 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"} }}); } // Testing which commands different args belong to. 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"}}}); } // Successful import tests 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"}}}); } // Consider making this an error, or at least a warning - most likely, import // diamonds like this are unintended, and they might lead to surprising doubled // values for allow_multiple options. 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"}}}); } // Testing failure modes 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); string expected_error; blaze_util::StringPrintf( &expected_error, "Import loop detected:\n %s\n %s\n %s\n", blaze_util::JoinPath(test_file_dir_, "import_cycle_1.bazelrc").c_str(), blaze_util::JoinPath(test_file_dir_, "import_cycle_2.bazelrc").c_str(), blaze_util::JoinPath(test_file_dir_, "import_cycle_1.bazelrc").c_str()); ASSERT_EQ(error_text, expected_error); } 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); string expected_error; blaze_util::StringPrintf( &expected_error, "Import loop detected:\n %s\n %s\n %s\n %s\n %s\n %s\n %s\n", blaze_util::JoinPath(test_file_dir_, "chain_to_cycle_1.bazelrc").c_str(), blaze_util::JoinPath(test_file_dir_, "chain_to_cycle_2.bazelrc").c_str(), blaze_util::JoinPath(test_file_dir_, "chain_to_cycle_3.bazelrc").c_str(), blaze_util::JoinPath(test_file_dir_, "chain_to_cycle_4.bazelrc").c_str(), blaze_util::JoinPath(test_file_dir_, "import_cycle_1.bazelrc").c_str(), blaze_util::JoinPath(test_file_dir_, "import_cycle_2.bazelrc").c_str(), blaze_util::JoinPath(test_file_dir_, "import_cycle_1.bazelrc").c_str()); ASSERT_EQ(error_text, expected_error); } 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); string expected_error; blaze_util::StringPrintf( &expected_error, "Unexpected error reading .blazerc file '%s'", blaze_util::JoinPath(test_file_dir_, "not_a_file.bazelrc").c_str()); ASSERT_EQ(error_text, expected_error); } 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); ASSERT_EQ(error_text, "Unexpected error reading .blazerc file 'somefile'"); } 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); string expected_error; blaze_util::StringPrintf( &expected_error, "Invalid import declaration in .blazerc file '%s': " "'import somefile bar' (are you in your source checkout/WORKSPACE?)", blaze_util::JoinPath(test_file_dir_, "bad_import.bazelrc").c_str()); ASSERT_EQ(error_text, expected_error); } // 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