Add unit tests for rc parsing.

These provide some testing for the following cases:
- tokenization
- recognizing comments
- grouping of different lines by command
- import ordering
- import cycles
- bad imports

There's still room for more, in particular in the multi-command case, but this feels like a good start.

Also identified some surprising behaviors that should be fixed. Leaving them tested as documentation of their broken nature.

RELNOTES: None.
PiperOrigin-RevId: 188355929
diff --git a/src/test/cpp/rc_options_test.cc b/src/test/cpp/rc_options_test.cc
new file mode 100644
index 0000000..46511e6
--- /dev/null
+++ b/src/test/cpp/rc_options_test.cc
@@ -0,0 +1,466 @@
+// 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 <vector>
+
+#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/strings.h"
+#include "src/main/cpp/workspace_layout.h"
+#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<RcFile> 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<string, vector<string>>& expected_args_map) {
+    RcFile::ParseError error;
+    string error_text;
+    std::unique_ptr<RcFile> 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<string>& 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<RcOption>& 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<string, vector<string>> no_expected_args;
+  SuccessfullyParseRcWithExpectedArgs("empty.bazelrc", no_expected_args);
+}
+
+TEST_F(RcOptionsTest, Whitespace) {
+  WriteRc("whitespace.bazelrc",
+          "      \n\t      ");
+  unordered_map<string, vector<string>> no_expected_args;
+  SuccessfullyParseRcWithExpectedArgs("whitespace.bazelrc", no_expected_args);
+}
+
+TEST_F(RcOptionsTest, CommentedStartup) {
+  WriteRc("commented_startup.bazelrc",
+          "# startup foo");
+  unordered_map<string, vector<string>> no_expected_args;
+  SuccessfullyParseRcWithExpectedArgs("whitespace.bazelrc", no_expected_args);
+}
+
+TEST_F(RcOptionsTest, EmptyStartupLine) {
+  WriteRc("empty_startup_line.bazelrc",
+          "startup");
+  unordered_map<string, vector<string>> no_expected_args;
+  SuccessfullyParseRcWithExpectedArgs("empty_startup_line.bazelrc",
+                                      no_expected_args);
+}
+
+TEST_F(RcOptionsTest, StartupWithOnlyCommentedArg) {
+  WriteRc("startup_with_comment.bazelrc",
+          "startup # bar");
+  unordered_map<string, vector<string>> 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<RcFile> 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<RcFile> 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<RcFile> 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<RcFile> 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<RcFile> 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
+