Преглед изворни кода

Allow args, includes, and splits in file_test includes (#5610)

This builds a mechanism for the toolchain to construct more complex
min_prelude files, which in turn should allow the toolchain to stop
special-casing the min_prelude directory. For example, instead of:

```
      args.insert(args.end(), {"--custom-core",
                               "--exclude-dump-file-prefix=include_files/"});
```

This should allow (in a `min_prelude` file):

```
// EXTRA-ARGS: --custom-core --exclude-dump-file-prefix=include_files/
```

Then when that min_prelude is included, it'd be used.

But also, this should allow sharing between min_prelude files with use
of `INCLUDE-FILE`, which as we make progressively more complex
min_preludes might become useful.
Jon Ross-Perkins пре 11 месеци
родитељ
комит
51b4abec20

+ 5 - 1
testing/file_test/BUILD

@@ -62,7 +62,11 @@ file_test(
     name = "file_test_base_test",
     size = "small",
     srcs = ["file_test_base_test.cpp"],
-    tests = glob(["testdata/**"]),
+    data = glob(["testdata/include_files/**"]),
+    tests = glob(
+        ["testdata/**"],
+        exclude = ["testdata/include_files/**"],
+    ),
     deps = [
         ":file_test_base",
         "//common:ostream",

+ 13 - 5
testing/file_test/README.md

@@ -202,18 +202,26 @@ Supported comment markers are:
     // EXTRA-ARGS: <arguments>
     ```
 
-    Same as `ARGS`, including substitution behavior, but appends to the default
-    argument list instead of replacing it.
+    Same as `ARGS`, including substitution behavior, but appends to the argument
+    list instead of replacing it.
 
-    `EXTRA-ARGS` can be specified at most once, and a test cannot specify both
-    `ARGS` and `EXTRA-ARGS`.
+    `EXTRA-ARGS` can be repeated, and provided with `ARGS`.
 
 -   ```
     // INCLUDE-FILE: <path/from/repository/root>
     ```
 
     Includes the specified file in the test's virtual file system and adds the
-    path to the file's splits.
+    included file's content to the current file's splits. Included files can
+    use:
+
+    -   `// ARGS`
+    -   `// EXTRA-ARGS`
+    -   `// INCLUDE-FILE`
+    -   `// --- <filename>`
+
+    Included files will use `include_files/<filename>` for their name when a
+    split isn't provided.
 
     This can be used to provide a minimal `Core` package (and `prelude` library)
     in toolchain tests. See the

+ 18 - 16
testing/file_test/file_test_base_test.cpp

@@ -126,6 +126,19 @@ static auto TestCaptureConsoleOutput(TestParams& params)
   return {{.success = true}};
 }
 
+// Prints and returns expected results for escaping.carbon.
+static auto TestEscaping(TestParams& params)
+    -> ErrorOr<FileTestBaseTest::RunResult> {
+  params.error_stream << "carriage return\r\n"
+                         "{one brace}\n"
+                         "{{two braces}}\n"
+                         "[one bracket]\n"
+                         "[[two brackets]]\n"
+                         "end of line whitespace   \n"
+                         "\ttabs\t\n";
+  return {{.success = true}};
+}
+
 // Prints and returns expected results for example.carbon.
 static auto TestExample(TestParams& params)
     -> ErrorOr<FileTestBaseTest::RunResult> {
@@ -189,19 +202,6 @@ static auto TestNoLineNumber(TestParams& params)
   return {{.success = true}};
 }
 
-// Prints and returns expected results for escaping.carbon.
-static auto TestEscaping(TestParams& params)
-    -> ErrorOr<FileTestBaseTest::RunResult> {
-  params.error_stream << "carriage return\r\n"
-                         "{one brace}\n"
-                         "{{two braces}}\n"
-                         "[one bracket]\n"
-                         "[[two brackets]]\n"
-                         "end of line whitespace   \n"
-                         "\ttabs\t\n";
-  return {{.success = true}};
-}
-
 // Prints and returns expected results for unattached_multi_file.carbon.
 static auto TestUnattachedMultiFile(TestParams& params)
     -> ErrorOr<FileTestBaseTest::RunResult> {
@@ -269,9 +269,11 @@ auto FileTestBaseTest::Run(
   PrintArgs(test_args, output_stream);
 
   auto filename = std::filesystem::path(test_name().str()).filename();
-  if (filename == "args.carbon" || filename == "include_file.carbon") {
-    // 'args.carbon' and 'include_file.carbon' have custom arguments, so don't
-    // do regular argument validation for them.
+  if (filename == "args.carbon" || filename == "include_args.carbon" ||
+      filename == "include_extra_args.carbon" ||
+      filename == "include_args_and_extra_args.carbon") {
+    // These files are testing argument behavior, which doesn't work with the
+    // default test logic.
     return {{.success = true}};
   }
 

+ 1 - 1
testing/file_test/run_test.cpp

@@ -102,8 +102,8 @@ auto RunTestFile(const FileTestBase& test_base, bool dump_output,
   // Process arguments.
   if (test_file.test_args.empty()) {
     test_file.test_args = test_base.GetDefaultArgs();
-    test_file.test_args.append(test_file.extra_args);
   }
+  test_file.test_args.append(test_file.extra_args);
   CARBON_RETURN_IF_ERROR(DoArgReplacements(
       test_file.test_args, test_base.GetArgReplacements(), all_splits));
 

+ 210 - 112
testing/file_test/test_file.cpp

@@ -16,6 +16,7 @@
 #include "llvm/ADT/StringExtras.h"
 #include "llvm/Support/JSON.h"
 #include "testing/base/file_helpers.h"
+#include "testing/file_test/line.h"
 
 namespace Carbon::Testing {
 
@@ -28,7 +29,7 @@ using ::testing::StrEq;
 static auto TryConsumeConflictMarker(bool running_autoupdate,
                                      llvm::StringRef line,
                                      llvm::StringRef line_trimmed,
-                                     bool* inside_conflict_marker)
+                                     bool& inside_conflict_marker)
     -> ErrorOr<bool> {
   bool is_start = line.starts_with("<<<<<<<");
   bool is_middle = line.starts_with("=======") || line.starts_with("|||||||");
@@ -41,7 +42,7 @@ static auto TryConsumeConflictMarker(bool running_autoupdate,
 
   // Autoupdate tracks conflict markers for context, and will discard
   // conflicting lines when it can autoupdate them.
-  if (*inside_conflict_marker) {
+  if (inside_conflict_marker) {
     if (is_start) {
       return ErrorBuilder() << "Unexpected conflict marker inside conflict:\n"
                             << line;
@@ -50,7 +51,7 @@ static auto TryConsumeConflictMarker(bool running_autoupdate,
       return true;
     }
     if (is_end) {
-      *inside_conflict_marker = false;
+      inside_conflict_marker = false;
       return true;
     }
 
@@ -66,7 +67,7 @@ static auto TryConsumeConflictMarker(bool running_autoupdate,
            << line;
   } else {
     if (is_start) {
-      *inside_conflict_marker = true;
+      inside_conflict_marker = true;
       return true;
     }
     if (is_middle || is_end) {
@@ -114,10 +115,10 @@ static auto ExtractFilePathFromUri(llvm::StringRef uri)
 // When `FROM_FILE_SPLIT` is used in path `textDocument.text`, populate the
 // value from the split matching the `uri`. Only used for
 // `textDocument/didOpen`.
-static auto AutoFillDidOpenParams(llvm::json::Object* params,
+static auto AutoFillDidOpenParams(llvm::json::Object& params,
                                   llvm::ArrayRef<TestFile::Split> splits)
     -> ErrorOr<Success> {
-  auto* text_document = params->getObject("textDocument");
+  auto* text_document = params.getObject("textDocument");
   if (text_document == nullptr) {
     return Success();
   }
@@ -144,12 +145,12 @@ static auto AutoFillDidOpenParams(llvm::json::Object* params,
 }
 
 // Reformats `[[@LSP:` and similar keyword as an LSP call with headers.
-static auto ReplaceLspKeywordAt(std::string* content, size_t keyword_pos,
+static auto ReplaceLspKeywordAt(std::string& content, size_t keyword_pos,
                                 int& lsp_call_id,
                                 llvm::ArrayRef<TestFile::Split> splits)
     -> ErrorOr<size_t> {
   llvm::StringRef content_at_keyword =
-      llvm::StringRef(*content).substr(keyword_pos);
+      llvm::StringRef(content).substr(keyword_pos);
 
   auto [keyword, body_start] = content_at_keyword.split(":");
   if (body_start.empty()) {
@@ -198,7 +199,7 @@ static auto ReplaceLspKeywordAt(std::string* content, size_t keyword_pos,
     if (extra_content_label == "params" &&
         method_or_id == "textDocument/didOpen") {
       CARBON_RETURN_IF_ERROR(
-          AutoFillDidOpenParams(parsed_extra_content.getAsObject(), splits));
+          AutoFillDidOpenParams(*parsed_extra_content.getAsObject(), splits));
     }
   }
 
@@ -231,17 +232,17 @@ static auto ReplaceLspKeywordAt(std::string* content, size_t keyword_pos,
                               .str();
   int keyword_len =
       (body_start.data() + body_end + LspEnd.size()) - keyword.data();
-  content->replace(keyword_pos, keyword_len, json_with_header);
+  content.replace(keyword_pos, keyword_len, json_with_header);
   return keyword_pos + json_with_header.size();
 }
 
 // Replaces the keyword at the given position. Returns the position to start a
 // find for the next keyword.
-static auto ReplaceContentKeywordAt(std::string* content, size_t keyword_pos,
+static auto ReplaceContentKeywordAt(std::string& content, size_t keyword_pos,
                                     llvm::StringRef test_name, int& lsp_call_id,
                                     llvm::ArrayRef<TestFile::Split> splits)
     -> ErrorOr<size_t> {
-  auto keyword = llvm::StringRef(*content).substr(keyword_pos);
+  auto keyword = llvm::StringRef(content).substr(keyword_pos);
 
   // Line replacements aren't handled here.
   static constexpr llvm::StringLiteral Line = "[[@LINE";
@@ -253,7 +254,7 @@ static auto ReplaceContentKeywordAt(std::string* content, size_t keyword_pos,
   // Replaced with the actual test name.
   static constexpr llvm::StringLiteral TestName = "[[@TEST_NAME]]";
   if (keyword.starts_with(TestName)) {
-    content->replace(keyword_pos, TestName.size(), test_name);
+    content.replace(keyword_pos, TestName.size(), test_name);
     return keyword_pos + test_name.size();
   }
 
@@ -270,12 +271,12 @@ static auto ReplaceContentKeywordAt(std::string* content, size_t keyword_pos,
 // TEST_NAME is the only content keyword at present, but we do validate that
 // other names are reserved.
 static auto ReplaceContentKeywords(llvm::StringRef filename,
-                                   std::string* content,
+                                   std::string& content,
                                    llvm::ArrayRef<TestFile::Split> splits)
     -> ErrorOr<Success> {
   static constexpr llvm::StringLiteral Prefix = "[[@";
 
-  auto keyword_pos = content->find(Prefix);
+  auto keyword_pos = content.find(Prefix);
   // Return early if not finding anything.
   if (keyword_pos == std::string::npos) {
     return Success();
@@ -303,44 +304,45 @@ static auto ReplaceContentKeywords(llvm::StringRef filename,
         auto keyword_end,
         ReplaceContentKeywordAt(content, keyword_pos, test_name, lsp_call_id,
                                 splits));
-    keyword_pos = content->find(Prefix, keyword_end);
+    keyword_pos = content.find(Prefix, keyword_end);
   }
   return Success();
 }
 
 // Adds a file. Used for both split and unsplit test files.
-static auto AddSplit(llvm::StringRef filename, std::string* content,
-                     llvm::SmallVector<TestFile::Split>* file_splits)
+static auto AddSplit(llvm::StringRef filename, std::string& content,
+                     llvm::SmallVector<TestFile::Split>& file_splits)
     -> ErrorOr<Success> {
   CARBON_RETURN_IF_ERROR(
-      ReplaceContentKeywords(filename, content, *file_splits));
-  file_splits->push_back(
-      {.filename = filename.str(), .content = std::move(*content)});
-  content->clear();
+      ReplaceContentKeywords(filename, content, file_splits));
+  file_splits.push_back(
+      {.filename = filename.str(), .content = std::move(content)});
+  content.clear();
   return Success();
 }
 
 // Process file split ("---") lines when found. Returns true if the line is
-// consumed.
+// consumed. `non_check_lines` is only provided for the main file, and will be
+// null for includes.
 static auto TryConsumeSplit(llvm::StringRef line, llvm::StringRef line_trimmed,
-                            bool found_autoupdate, int* line_index,
-                            SplitState* split,
-                            llvm::SmallVector<TestFile::Split>* file_splits,
+                            bool missing_autoupdate, int& line_index,
+                            SplitState& split,
+                            llvm::SmallVector<TestFile::Split>& file_splits,
                             llvm::SmallVector<FileTestLine>* non_check_lines)
     -> ErrorOr<bool> {
   if (!line_trimmed.consume_front("// ---")) {
-    if (!split->has_splits() && !line_trimmed.starts_with("//") &&
+    if (!split.has_splits() && !line_trimmed.starts_with("//") &&
         !line_trimmed.empty()) {
-      split->found_code_pre_split = true;
+      split.found_code_pre_split = true;
     }
 
     // Add the line to the current file's content (which may not be a split
     // file).
-    split->add_content(line);
+    split.add_content(line);
     return false;
   }
 
-  if (!found_autoupdate) {
+  if (missing_autoupdate) {
     // If there's a split, all output is appended at the end of each file
     // before AUTOUPDATE. We may want to change that, but it's not
     // necessary to handle right now.
@@ -350,12 +352,12 @@ static auto TryConsumeSplit(llvm::StringRef line, llvm::StringRef line_trimmed,
   }
 
   // On a file split, add the previous file, then start a new one.
-  if (split->has_splits()) {
+  if (split.has_splits()) {
     CARBON_RETURN_IF_ERROR(
-        AddSplit(split->filename, &split->content, file_splits));
+        AddSplit(split.filename, split.content, file_splits));
   } else {
-    split->content.clear();
-    if (split->found_code_pre_split) {
+    split.content.clear();
+    if (split.found_code_pre_split) {
       // For the first split, we make sure there was no content prior.
       return Error(
           "When using split files, there must be no content before the first "
@@ -363,16 +365,18 @@ static auto TryConsumeSplit(llvm::StringRef line, llvm::StringRef line_trimmed,
     }
   }
 
-  ++split->file_index;
-  split->filename = line_trimmed.trim();
-  if (split->filename.empty()) {
+  ++split.file_index;
+  split.filename = line_trimmed.trim();
+  if (split.filename.empty()) {
     return Error("Missing filename for split.");
   }
   // The split line is added to non_check_lines for retention in autoupdate, but
   // is not added to the test file content.
-  *line_index = 0;
-  non_check_lines->push_back(
-      FileTestLine(split->file_index, *line_index, line));
+  line_index = 0;
+  if (non_check_lines) {
+    non_check_lines->push_back(
+        FileTestLine(split.file_index, line_index, line));
+  }
   return true;
 }
 
@@ -497,20 +501,26 @@ static auto TransformExpectation(int line_index, llvm::StringRef in)
 }
 
 // Once all content is processed, do any remaining split processing.
-static auto FinishSplit(llvm::StringRef test_name, SplitState* split,
-                        llvm::SmallVector<TestFile::Split>* file_splits)
+static auto FinishSplit(llvm::StringRef filename, bool is_include_file,
+                        SplitState& split,
+                        llvm::SmallVector<TestFile::Split>& file_splits)
     -> ErrorOr<Success> {
-  if (split->has_splits()) {
-    return AddSplit(split->filename, &split->content, file_splits);
+  if (split.has_splits()) {
+    return AddSplit(split.filename, split.content, file_splits);
   } else {
     // If no file splitting happened, use the main file as the test file.
     // There will always be a `/` unless tests are in the repo root.
-    return AddSplit(test_name.drop_front(test_name.rfind("/") + 1),
-                    &split->content, file_splits);
+    std::string split_name = std::filesystem::path(filename.str()).filename();
+    if (is_include_file) {
+      split_name.insert(0, "include_files/");
+    }
+    return AddSplit(split_name, split.content, file_splits);
   }
 }
 
 // Process CHECK lines when found. Returns true if the line is consumed.
+// `expected_stdout` and `expected_stderr` are null in included files, where
+// it's an error to use `CHECK`.
 static auto TryConsumeCheck(
     bool running_autoupdate, int line_index, llvm::StringRef line,
     llvm::StringRef line_trimmed,
@@ -521,6 +531,11 @@ static auto TryConsumeCheck(
     return false;
   }
 
+  if (!expected_stdout) {
+    return ErrorBuilder() << "Included files can't add CHECKs: "
+                          << line_trimmed;
+  }
+
   // Don't build expectations when doing an autoupdate. We don't want to
   // break the autoupdate on an invalid CHECK line.
   if (!running_autoupdate) {
@@ -542,50 +557,58 @@ static auto TryConsumeCheck(
 // Processes ARGS and EXTRA-ARGS lines when found. Returns true if the line is
 // consumed.
 static auto TryConsumeArgs(llvm::StringRef line, llvm::StringRef line_trimmed,
-                           llvm::SmallVector<std::string>* args,
-                           llvm::SmallVector<std::string>* extra_args)
+                           llvm::SmallVector<std::string>& args)
     -> ErrorOr<bool> {
-  llvm::SmallVector<std::string>* arg_list = nullptr;
-  if (line_trimmed.consume_front("// ARGS: ")) {
-    arg_list = args;
-  } else if (line_trimmed.consume_front("// EXTRA-ARGS: ")) {
-    arg_list = extra_args;
-  } else {
+  if (!line_trimmed.consume_front("// ARGS: ")) {
     return false;
   }
 
-  if (!args->empty() || !extra_args->empty()) {
-    return ErrorBuilder() << "ARGS / EXTRA-ARGS specified multiple times: "
-                          << line.str();
+  if (!args.empty()) {
+    return ErrorBuilder() << "ARGS specified multiple times: " << line.str();
   }
 
   // Split the line into arguments.
   std::pair<llvm::StringRef, llvm::StringRef> cursor =
       llvm::getToken(line_trimmed);
   while (!cursor.first.empty()) {
-    arg_list->push_back(std::string(cursor.first));
+    args.push_back(std::string(cursor.first));
     cursor = llvm::getToken(cursor.second);
   }
 
   return true;
 }
+static auto TryConsumeExtraArgs(llvm::StringRef line_trimmed,
+                                llvm::SmallVector<std::string>& extra_args)
+    -> ErrorOr<bool> {
+  if (!line_trimmed.consume_front("// EXTRA-ARGS: ")) {
+    return false;
+  }
 
-static auto TryConsumeIncludeFile(
-    llvm::StringRef line_trimmed,
-    llvm::SmallVector<TestFile::Split>* include_files) -> ErrorOr<bool> {
+  // Split the line into arguments.
+  std::pair<llvm::StringRef, llvm::StringRef> cursor =
+      llvm::getToken(line_trimmed);
+  while (!cursor.first.empty()) {
+    extra_args.push_back(std::string(cursor.first));
+    cursor = llvm::getToken(cursor.second);
+  }
+
+  return true;
+}
+
+static auto TryConsumeIncludeFile(llvm::StringRef line_trimmed,
+                                  llvm::SmallVector<std::string>& include_files)
+    -> ErrorOr<bool> {
   if (!line_trimmed.consume_front("// INCLUDE-FILE: ")) {
     return false;
   }
 
-  std::filesystem::path path = std::string(line_trimmed);
-  CARBON_ASSIGN_OR_RETURN(std::string content, ReadFile(path));
-  include_files->push_back(
-      {.filename = std::filesystem::path("include_files") / path.filename(),
-       .content = content});
+  include_files.push_back(line_trimmed.str());
   return true;
 }
 
 // Processes AUTOUPDATE lines when found. Returns true if the line is consumed.
+// `found_autoupdate` and `autoupdate_line_number` are only provided for the
+// main file; it's an error to have autoupdate in included files.
 static auto TryConsumeAutoupdate(int line_index, llvm::StringRef line_trimmed,
                                  bool* found_autoupdate,
                                  std::optional<int>* autoupdate_line_number)
@@ -595,6 +618,10 @@ static auto TryConsumeAutoupdate(int line_index, llvm::StringRef line_trimmed,
   if (line_trimmed != Autoupdate && line_trimmed != NoAutoupdate) {
     return false;
   }
+  if (!found_autoupdate) {
+    return ErrorBuilder() << "Included files can't control autoupdate: "
+                          << line_trimmed;
+  }
   if (*found_autoupdate) {
     return Error("Multiple AUTOUPDATE/NOAUTOUPDATE settings found");
   }
@@ -606,12 +633,16 @@ static auto TryConsumeAutoupdate(int line_index, llvm::StringRef line_trimmed,
 }
 
 // Processes SET-* lines when found. Returns true if the line is consumed.
+// If `flag` is null, we're in an included file where the flag can't be set.
 static auto TryConsumeSetFlag(llvm::StringRef line_trimmed,
                               llvm::StringLiteral flag_name, bool* flag)
     -> ErrorOr<bool> {
   if (!line_trimmed.consume_front("// ") || line_trimmed != flag_name) {
     return false;
   }
+  if (!flag) {
+    return ErrorBuilder() << "Included files can't set flag: " << line_trimmed;
+  }
   if (*flag) {
     return ErrorBuilder() << flag_name << " was specified multiple times";
   }
@@ -619,49 +650,62 @@ static auto TryConsumeSetFlag(llvm::StringRef line_trimmed,
   return true;
 }
 
-auto ProcessTestFile(llvm::StringRef test_name, bool running_autoupdate)
-    -> ErrorOr<TestFile> {
-  TestFile test_file;
-
-  // Store the file so that file_splits can use references to content.
-  CARBON_ASSIGN_OR_RETURN(test_file.input_content, ReadFile(test_name.str()));
-
-  // Original file content, and a cursor for walking through it.
-  llvm::StringRef file_content = test_file.input_content;
-  llvm::StringRef cursor = file_content;
-
-  // Whether either AUTOUDPATE or NOAUTOUPDATE was found.
-  bool found_autoupdate = false;
-
+// Process content for either the main file (with `test_file` and
+// `found_autoupdate` provided) or an included file (with those arguments null).
+//
+// - `found_autoupdate` is set to true when either `AUTOUPDATE` or
+//   `NOAUTOUPDATE` are found.
+// - `args` is set from `ARGS`.
+// - `extra_args` accumulates `EXTRA-ARGS`.
+// - `splits` accumulates split form for the test (`// --- <filename>`, or the
+//   full file named as `filename` when there are no splits in the file).
+// - `include_files` accumulates `INCLUDE-FILE`.
+static auto ProcessFileContent(llvm::StringRef filename,
+                               llvm::StringRef content_cursor,
+                               bool running_autoupdate, TestFile* test_file,
+                               bool* found_autoupdate,
+                               llvm::SmallVector<std::string>& args,
+                               llvm::SmallVector<std::string>& extra_args,
+                               llvm::SmallVector<TestFile::Split>& splits,
+                               llvm::SmallVector<std::string>& include_files)
+    -> ErrorOr<Success> {
   // The index in the current test file. Will be reset on splits.
   int line_index = 0;
 
-  SplitState split;
-
   // When autoupdating, we track whether we're inside conflict markers.
   // Otherwise conflict markers are errors.
   bool inside_conflict_marker = false;
 
-  while (!cursor.empty()) {
-    auto [line, next_cursor] = cursor.split("\n");
-    cursor = next_cursor;
+  SplitState split_state;
+
+  while (!content_cursor.empty()) {
+    auto [line, next_cursor] = content_cursor.split("\n");
+    content_cursor = next_cursor;
     auto line_trimmed = line.ltrim();
 
     bool is_consumed = false;
+
     CARBON_ASSIGN_OR_RETURN(
         is_consumed,
         TryConsumeConflictMarker(running_autoupdate, line, line_trimmed,
-                                 &inside_conflict_marker));
+                                 inside_conflict_marker));
     if (is_consumed) {
       continue;
     }
 
     // At this point, remaining lines are part of the test input.
+
+    // We need to consume a split, but the main file has a little more handling.
+    bool missing_autoupdate = false;
+    llvm::SmallVector<FileTestLine>* non_check_lines = nullptr;
+    if (test_file) {
+      missing_autoupdate = !*found_autoupdate;
+      non_check_lines = &test_file->non_check_lines;
+    }
     CARBON_ASSIGN_OR_RETURN(
         is_consumed,
-        TryConsumeSplit(line, line_trimmed, found_autoupdate, &line_index,
-                        &split, &test_file.file_splits,
-                        &test_file.non_check_lines));
+        TryConsumeSplit(line, line_trimmed, missing_autoupdate, line_index,
+                        split_state, splits, non_check_lines));
     if (is_consumed) {
       continue;
     }
@@ -674,63 +718,99 @@ auto ProcessTestFile(llvm::StringRef test_name, bool running_autoupdate)
     }
 
     CARBON_ASSIGN_OR_RETURN(
-        is_consumed, TryConsumeCheck(running_autoupdate, line_index, line,
-                                     line_trimmed, &test_file.expected_stdout,
-                                     &test_file.expected_stderr));
+        is_consumed,
+        TryConsumeCheck(running_autoupdate, line_index, line, line_trimmed,
+                        test_file ? &test_file->expected_stdout : nullptr,
+                        test_file ? &test_file->expected_stderr : nullptr));
     if (is_consumed) {
       continue;
     }
 
-    // At this point, lines are retained as non-CHECK lines.
-    test_file.non_check_lines.push_back(
-        FileTestLine(split.file_index, line_index, line));
+    if (test_file) {
+      // At this point, lines are retained as non-CHECK lines.
+      test_file->non_check_lines.push_back(
+          FileTestLine(split_state.file_index, line_index, line));
+    }
 
-    CARBON_ASSIGN_OR_RETURN(
-        is_consumed, TryConsumeArgs(line, line_trimmed, &test_file.test_args,
-                                    &test_file.extra_args));
+    CARBON_ASSIGN_OR_RETURN(is_consumed,
+                            TryConsumeArgs(line, line_trimmed, args));
     if (is_consumed) {
       continue;
     }
-    CARBON_ASSIGN_OR_RETURN(
-        is_consumed,
-        TryConsumeIncludeFile(line_trimmed, &test_file.include_file_splits));
+    CARBON_ASSIGN_OR_RETURN(is_consumed,
+                            TryConsumeExtraArgs(line_trimmed, extra_args));
     if (is_consumed) {
       continue;
     }
+    CARBON_ASSIGN_OR_RETURN(is_consumed,
+                            TryConsumeIncludeFile(line_trimmed, include_files));
+    if (is_consumed) {
+      continue;
+    }
+
     CARBON_ASSIGN_OR_RETURN(
         is_consumed,
-        TryConsumeAutoupdate(line_index, line_trimmed, &found_autoupdate,
-                             &test_file.autoupdate_line_number));
+        TryConsumeAutoupdate(
+            line_index, line_trimmed, found_autoupdate,
+            test_file ? &test_file->autoupdate_line_number : nullptr));
     if (is_consumed) {
       continue;
     }
     CARBON_ASSIGN_OR_RETURN(
         is_consumed,
-        TryConsumeSetFlag(line_trimmed, "SET-CAPTURE-CONSOLE-OUTPUT",
-                          &test_file.capture_console_output));
+        TryConsumeSetFlag(
+            line_trimmed, "SET-CAPTURE-CONSOLE-OUTPUT",
+            test_file ? &test_file->capture_console_output : nullptr));
     if (is_consumed) {
       continue;
     }
-    CARBON_ASSIGN_OR_RETURN(is_consumed,
-                            TryConsumeSetFlag(line_trimmed, "SET-CHECK-SUBSET",
-                                              &test_file.check_subset));
+    CARBON_ASSIGN_OR_RETURN(
+        is_consumed,
+        TryConsumeSetFlag(line_trimmed, "SET-CHECK-SUBSET",
+                          test_file ? &test_file->check_subset : nullptr));
     if (is_consumed) {
       continue;
     }
   }
 
+  CARBON_RETURN_IF_ERROR(FinishSplit(filename, /*is_include_file=*/!test_file,
+                                     split_state, splits));
+
+  if (test_file) {
+    test_file->has_splits = split_state.has_splits();
+  }
+  return Success();
+}
+
+auto ProcessTestFile(llvm::StringRef test_name, bool running_autoupdate)
+    -> ErrorOr<TestFile> {
+  TestFile test_file;
+
+  // Store the original content, to avoid a read when autoupdating.
+  CARBON_ASSIGN_OR_RETURN(test_file.input_content, ReadFile(test_name.str()));
+
+  // Whether either AUTOUDPATE or NOAUTOUPDATE was found.
+  bool found_autoupdate = false;
+
+  // INCLUDE-FILE uses, accumulated across both the main file and any includes
+  // (recursively).
+  llvm::SmallVector<std::string> include_files;
+
+  // Process the main file.
+  CARBON_RETURN_IF_ERROR(ProcessFileContent(
+      test_name, test_file.input_content, running_autoupdate, &test_file,
+      &found_autoupdate, test_file.test_args, test_file.extra_args,
+      test_file.file_splits, include_files));
+
   if (!found_autoupdate) {
     return ErrorBuilder() << "Missing AUTOUPDATE/NOAUTOUPDATE setting: "
                           << test_name;
   }
 
-  test_file.has_splits = split.has_splits();
-  CARBON_RETURN_IF_ERROR(
-      FinishSplit(test_name, &split, &test_file.file_splits));
+  constexpr llvm::StringLiteral AutoupdateSplit = "AUTOUPDATE-SPLIT";
 
   // Validate AUTOUPDATE-SPLIT use, and remove it from test files if present.
   if (test_file.has_splits) {
-    constexpr llvm::StringLiteral AutoupdateSplit = "AUTOUPDATE-SPLIT";
     for (const auto& test_file :
          llvm::ArrayRef(test_file.file_splits).drop_back()) {
       if (test_file.filename == AutoupdateSplit) {
@@ -754,6 +834,24 @@ auto ProcessTestFile(llvm::StringRef test_name, bool running_autoupdate)
     test_file.expected_stderr.push_back(StrEq(""));
   }
 
+  // Process includes. This can add entries to `include_files`.
+  for (size_t i = 0; i < include_files.size(); ++i) {
+    const auto& filename = include_files[i];
+    CARBON_ASSIGN_OR_RETURN(std::string content, ReadFile(filename));
+    // Note autoupdate never touches included files.
+    CARBON_RETURN_IF_ERROR(ProcessFileContent(
+        filename, content, /*running_autoupdate=*/false,
+        /*test_file=*/nullptr,
+        /*found_autoupdate=*/nullptr, test_file.test_args, test_file.extra_args,
+        test_file.include_file_splits, include_files));
+  }
+
+  for (const auto& split : test_file.include_file_splits) {
+    if (split.filename == AutoupdateSplit) {
+      return Error("AUTOUPDATE-SPLIT is disallowed in included files");
+    }
+  }
+
   return std::move(test_file);
 }
 

+ 6 - 5
testing/file_test/testdata/include_file.carbon → testing/file_test/testdata/include_args.carbon

@@ -1,12 +1,13 @@
 // 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-FILE: testing/file_test/testdata/no_line_number.carbon
+//
+// INCLUDE-FILE: testing/file_test/testdata/include_files/args.carbon
+//
 // AUTOUPDATE
 // TIP: To test this file alone, run:
-// TIP:   bazel test //testing/file_test:file_test_base_test --test_arg=--file_tests=testing/file_test/testdata/include_file.carbon
+// TIP:   bazel test //testing/file_test:file_test_base_test --test_arg=--file_tests=testing/file_test/testdata/include_args.carbon
 // TIP: To dump output, run:
-// TIP:   bazel run //testing/file_test:file_test_base_test -- --dump_output --file_tests=testing/file_test/testdata/include_file.carbon
+// TIP:   bazel run //testing/file_test:file_test_base_test -- --dump_output --file_tests=testing/file_test/testdata/include_args.carbon
 
-// CHECK:STDOUT: 3 args: `default_args`, `include_file.carbon`, `include_files/no_line_number.carbon`
+// CHECK:STDOUT: 3 args: `foo`, `bar`, `baz`

+ 14 - 0
testing/file_test/testdata/include_args_and_extra_args.carbon

@@ -0,0 +1,14 @@
+// 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-FILE: testing/file_test/testdata/include_files/extra_args.carbon
+// ARGS: foo
+//
+// AUTOUPDATE
+// TIP: To test this file alone, run:
+// TIP:   bazel test //testing/file_test:file_test_base_test --test_arg=--file_tests=testing/file_test/testdata/include_args_and_extra_args.carbon
+// TIP: To dump output, run:
+// TIP:   bazel run //testing/file_test:file_test_base_test -- --dump_output --file_tests=testing/file_test/testdata/include_args_and_extra_args.carbon
+
+// CHECK:STDOUT: 3 args: `foo`, `bar`, `baz`

+ 13 - 0
testing/file_test/testdata/include_empty.carbon

@@ -0,0 +1,13 @@
+// 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-FILE: testing/file_test/testdata/include_files/empty.carbon
+//
+// AUTOUPDATE
+// TIP: To test this file alone, run:
+// TIP:   bazel test //testing/file_test:file_test_base_test --test_arg=--file_tests=testing/file_test/testdata/include_empty.carbon
+// TIP: To dump output, run:
+// TIP:   bazel run //testing/file_test:file_test_base_test -- --dump_output --file_tests=testing/file_test/testdata/include_empty.carbon
+
+// CHECK:STDOUT: 3 args: `default_args`, `include_empty.carbon`, `empty.carbon`

+ 14 - 0
testing/file_test/testdata/include_extra_args.carbon

@@ -0,0 +1,14 @@
+// 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
+//
+// EXTRA-ARGS: foo
+// INCLUDE-FILE: testing/file_test/testdata/include_files/extra_args.carbon
+//
+// AUTOUPDATE
+// TIP: To test this file alone, run:
+// TIP:   bazel test //testing/file_test:file_test_base_test --test_arg=--file_tests=testing/file_test/testdata/include_extra_args.carbon
+// TIP: To dump output, run:
+// TIP:   bazel run //testing/file_test:file_test_base_test -- --dump_output --file_tests=testing/file_test/testdata/include_extra_args.carbon
+
+// CHECK:STDOUT: 6 args: `default_args`, `include_extra_args.carbon`, `include_files/extra_args.carbon`, `foo`, `bar`, `baz`

+ 5 - 0
testing/file_test/testdata/include_files/args.carbon

@@ -0,0 +1,5 @@
+// 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
+//
+// ARGS: foo bar baz

+ 5 - 0
testing/file_test/testdata/include_files/empty.carbon

@@ -0,0 +1,5 @@
+// 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
+
+// --- empty.carbon

+ 5 - 0
testing/file_test/testdata/include_files/extra_args.carbon

@@ -0,0 +1,5 @@
+// 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
+//
+// EXTRA-ARGS: bar baz

+ 5 - 0
testing/file_test/testdata/include_files/no_split.carbon

@@ -0,0 +1,5 @@
+// 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
+
+no split

+ 13 - 0
testing/file_test/testdata/include_files/recursive.carbon

@@ -0,0 +1,13 @@
+// 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-FILE: testing/file_test/testdata/include_files/split.carbon
+
+// --- c.carbon
+
+c
+
+// --- d.carbon
+
+d

+ 11 - 0
testing/file_test/testdata/include_files/split.carbon

@@ -0,0 +1,11 @@
+// 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
+
+// --- a.carbon
+
+a
+
+// --- b.carbon
+
+b

+ 14 - 0
testing/file_test/testdata/include_no_split.carbon

@@ -0,0 +1,14 @@
+// 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-FILE: testing/file_test/testdata/include_files/no_split.carbon
+//
+// AUTOUPDATE
+// TIP: To test this file alone, run:
+// TIP:   bazel test //testing/file_test:file_test_base_test --test_arg=--file_tests=testing/file_test/testdata/include_no_split.carbon
+// TIP: To dump output, run:
+// TIP:   bazel run //testing/file_test:file_test_base_test -- --dump_output --file_tests=testing/file_test/testdata/include_no_split.carbon
+
+// CHECK:STDOUT: 3 args: `default_args`, `include_no_split.carbon`, `include_files/no_split.carbon`
+// CHECK:STDOUT: include_files/no_split.carbon:5: no split

+ 17 - 0
testing/file_test/testdata/include_recursive.carbon

@@ -0,0 +1,17 @@
+// 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-FILE: testing/file_test/testdata/include_files/recursive.carbon
+//
+// AUTOUPDATE
+// TIP: To test this file alone, run:
+// TIP:   bazel test //testing/file_test:file_test_base_test --test_arg=--file_tests=testing/file_test/testdata/include_recursive.carbon
+// TIP: To dump output, run:
+// TIP:   bazel run //testing/file_test:file_test_base_test -- --dump_output --file_tests=testing/file_test/testdata/include_recursive.carbon
+
+// CHECK:STDOUT: 6 args: `default_args`, `include_recursive.carbon`, `c.carbon`, `d.carbon`, `a.carbon`, `b.carbon`
+// CHECK:STDOUT: c.carbon:2: c
+// CHECK:STDOUT: d.carbon:2: d
+// CHECK:STDOUT: a.carbon:2: a
+// CHECK:STDOUT: b.carbon:2: b

+ 15 - 0
testing/file_test/testdata/include_split.carbon

@@ -0,0 +1,15 @@
+// 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-FILE: testing/file_test/testdata/include_files/split.carbon
+//
+// AUTOUPDATE
+// TIP: To test this file alone, run:
+// TIP:   bazel test //testing/file_test:file_test_base_test --test_arg=--file_tests=testing/file_test/testdata/include_split.carbon
+// TIP: To dump output, run:
+// TIP:   bazel run //testing/file_test:file_test_base_test -- --dump_output --file_tests=testing/file_test/testdata/include_split.carbon
+
+// CHECK:STDOUT: 4 args: `default_args`, `include_split.carbon`, `a.carbon`, `b.carbon`
+// CHECK:STDOUT: a.carbon:2: a
+// CHECK:STDOUT: b.carbon:2: b