Ver Fonte

Add support for testing with stdin (#4819)

In order to write language-server tests, we need some way to pass stdin
input. This adds support for a split "// --- STDIN" which will be
provided as a temp file for testing.

Note this does more stdin -> input_stream style renaming, this is just
bugging me more since I know shadowing works but it can be subtle to
read, particularly since I'm now making direct use of stdin in a handful
of spots.
Jon Ross-Perkins há 1 ano atrás
pai
commit
e393c769af

+ 6 - 4
explorer/file_test.cpp

@@ -29,7 +29,8 @@ class ExplorerFileTest : public FileTestBase {
 
   auto Run(const llvm::SmallVector<llvm::StringRef>& test_args,
            llvm::IntrusiveRefCntPtr<llvm::vfs::InMemoryFileSystem>& fs,
-           llvm::raw_pwrite_stream& stdout, llvm::raw_pwrite_stream& stderr)
+           FILE* /*input_stream*/, llvm::raw_pwrite_stream& output_stream,
+           llvm::raw_pwrite_stream& error_stream)
       -> ErrorOr<RunResult> override {
     // Add the prelude.
     llvm::ErrorOr<std::unique_ptr<llvm::MemoryBuffer>> prelude =
@@ -52,9 +53,10 @@ class ExplorerFileTest : public FileTestBase {
       args.push_back(arg.data());
     }
 
-    int exit_code = ExplorerMain(
-        args.size(), args.data(), /*install_path=*/"", PreludePath, stdout,
-        stderr, check_trace_output() ? stdout : trace_stream_, *fs);
+    int exit_code =
+        ExplorerMain(args.size(), args.data(), /*install_path=*/"", PreludePath,
+                     output_stream, error_stream,
+                     check_trace_output() ? output_stream : trace_stream_, *fs);
 
     return {{.success = exit_code == EXIT_SUCCESS}};
   }

+ 32 - 24
testing/file_test/README.md

@@ -50,9 +50,11 @@ class MyFileTest : public FileTestBase {
   // Called as part of individual test executions.
   auto Run(const llvm::SmallVector<llvm::StringRef>& test_args,
            const llvm::SmallVector<TestFile>& test_files,
-           llvm::raw_pwrite_stream& stdout, llvm::raw_pwrite_stream& stderr)
+           FILE* input_stream, llvm::raw_pwrite_stream& output_stream,
+           llvm::raw_pwrite_stream& error_stream)
       -> ErrorOr<RunResult> override {
-    return MyFunctionality(test_args, stdout, stderr);
+    return MyFunctionality(test_args, input_stream, output_stream,
+                           error_stream);
   }
 
   // Provides arguments which are used in tests that don't provide ARGS.
@@ -108,21 +110,22 @@ Supported comment markers are:
     // NOAUTOUPDATE
     ```
 
-    Controls whether the checks in the file will be autoupdated if --autoupdate
-    is passed. Exactly one of these markers must be present. If the file uses
-    splits, the marker must currently be before any splits.
+    Controls whether the checks in the file will be autoupdated if
+    `--autoupdate` is passed. Exactly one of these markers must be present. If
+    the file uses splits, the marker must currently be before any splits.
 
-    When autoupdating, CHECKs will be inserted starting below AUTOUPDATE. When a
-    CHECK has line information, autoupdate will try to insert the CHECK
-    immediately next to the line it's associated with, with stderr CHECKs
-    preceding the line and stdout CHECKs following the line. When that happens,
-    any subsequent CHECK lines without line information, or that refer to lines
-    appearing earlier, will immediately follow. As an exception, if no STDOUT
-    check line refers to any line in the test, all STDOUT check lines are placed
-    at the end of the file instead of immediately after AUTOUPDATE.
+    When autoupdating, `CHECK`s will be inserted starting below `AUTOUPDATE`.
+    When a `CHECK` has line information, autoupdate will try to insert the
+    `CHECK` immediately next to the line it's associated with, with stderr
+    `CHECK`s preceding the line and stdout `CHECK`s following the line. When
+    that happens, any subsequent `CHECK` lines without line information, or that
+    refer to lines appearing earlier, will immediately follow. As an exception,
+    if no `STDOUT` check line refers to any line in the test, all `STDOUT` check
+    lines are placed at the end of the file instead of immediately after
+    `AUTOUPDATE`.
 
     When using split files, if the last split file is named
-    `// --- AUTOUPDATE-SPLIT`, all CHECKs will be added there; no line
+    `// --- AUTOUPDATE-SPLIT`, all `CHECK`s will be added there; no line
     associations occur.
 
 -   ```
@@ -149,8 +152,8 @@ Supported comment markers are:
         Replaces some implementation-specific identifier with a value. (Mappings
         provided by way of an optional `MyFileTest::GetArgReplacements`)
 
-    ARGS can be specified at most once. If not provided, the FileTestBase child
-    is responsible for providing default arguments.
+    `ARGS` can be specified at most once. If not provided, the `FileTestBase`
+    child is responsible for providing default arguments.
 
 -   ```
     // EXTRA-ARGS: <arguments>
@@ -173,28 +176,33 @@ Supported comment markers are:
     This should be avoided because we are partly ensuring that streams are an
     API, but is helpful when wrapping Clang, where stderr is used directly.
 
-    SET-CAPTURE-CONSOLE-OUTPUT can be specified at most once.
+    `SET-CAPTURE-CONSOLE-OUTPUT` can be specified at most once.
 
 -   ```
     // SET-CHECK-SUBSET
     ```
 
-    By default, all lines of output must have a CHECK match. Adding this as a
+    By default, all lines of output must have a `CHECK` match. Adding this as a
     option sets it so that non-matching lines are ignored. All provided
-    CHECK:STDOUT: and CHECK:STDERR: lines must still have a match in output.
+    `CHECK:STDOUT:` and `CHECK:STDERR:` lines must still have a match in output.
 
-    SET-CHECK-SUBSET can be specified at most once.
+    `SET-CHECK-SUBSET`can be specified at most once.
 
 -   ```
     // --- <filename>
     ```
 
     By default, all file content is provided to the test as a single file in
-    test_files. Using this marker allows the file to be split into multiple
-    files which will all be passed to test_files.
+    `test_files`. Using this marker allows the file to be split into multiple
+    files which will all be passed to `test_files`.
 
-    Files are not created on disk; it's expected the child will create an
-    InMemoryFilesystem if needed.
+    Files are not created on disk; instead, content is passed in through the
+    `fs` passed to `Run`.
+
+    If the filename is `STDIN`, it will be provided as `input_stream` instead of
+    in `test_files`. Currently, autoupdate can place `CHECK` lines in the
+    `STDIN` split; use `AUTOUPDATE-SPLIT` to avoid that (see `AUTOUPDATE` for
+    information).
 
 -   ```
     // CHECK:STDOUT: <output line>

+ 3 - 3
testing/file_test/autoupdate.h

@@ -39,7 +39,7 @@ class FileTestAutoupdater {
       const llvm::SmallVector<llvm::StringRef>& filenames,
       int autoupdate_line_number, bool autoupdate_split,
       const llvm::SmallVector<FileTestLine>& non_check_lines,
-      llvm::StringRef stdout, llvm::StringRef stderr,
+      llvm::StringRef actual_stdout, llvm::StringRef actual_stderr,
       const std::optional<RE2>& default_file_re,
       const llvm::SmallVector<LineNumberReplacement>& line_number_replacements,
       std::function<void(std::string&)> do_extra_check_replacements)
@@ -56,8 +56,8 @@ class FileTestAutoupdater {
         do_extra_check_replacements_(std::move(do_extra_check_replacements)),
         // BuildCheckLines should only be called after other member
         // initialization.
-        stdout_(BuildCheckLines(stdout, "STDOUT")),
-        stderr_(BuildCheckLines(stderr, "STDERR")),
+        stdout_(BuildCheckLines(actual_stdout, "STDOUT")),
+        stderr_(BuildCheckLines(actual_stderr, "STDERR")),
         any_attached_stdout_lines_(llvm::any_of(
             stdout_.lines,
             [&](const CheckLine& line) { return line.line_number() != -1; })),

+ 41 - 22
testing/file_test/file_test_base.cpp

@@ -58,6 +58,8 @@ using ::testing::Matcher;
 using ::testing::MatchesRegex;
 using ::testing::StrEq;
 
+static constexpr llvm::StringLiteral StdinFilename = "STDIN";
+
 // Reads a file to string.
 static auto ReadFile(std::string_view path) -> ErrorOr<std::string> {
   std::ifstream proto_file{std::string(path)};
@@ -186,15 +188,15 @@ auto FileTestBase::TestBody() -> void {
   }
   SCOPED_TRACE(update_message);
   if (context.check_subset) {
-    EXPECT_THAT(SplitOutput(context.stdout),
+    EXPECT_THAT(SplitOutput(context.actual_stdout),
                 IsSupersetOf(context.expected_stdout));
-    EXPECT_THAT(SplitOutput(context.stderr),
+    EXPECT_THAT(SplitOutput(context.actual_stderr),
                 IsSupersetOf(context.expected_stderr));
 
   } else {
-    EXPECT_THAT(SplitOutput(context.stdout),
+    EXPECT_THAT(SplitOutput(context.actual_stdout),
                 ElementsAreArray(context.expected_stdout));
-    EXPECT_THAT(SplitOutput(context.stderr),
+    EXPECT_THAT(SplitOutput(context.actual_stderr),
                 ElementsAreArray(context.expected_stderr));
   }
 
@@ -232,8 +234,9 @@ auto FileTestBase::RunAutoupdater(const TestContext& context, bool dry_run)
              GetBazelCommand(BazelMode::Test, test_name_),
              GetBazelCommand(BazelMode::Dump, test_name_),
              context.input_content, filenames, *context.autoupdate_line_number,
-             context.autoupdate_split, context.non_check_lines, context.stdout,
-             context.stderr, GetDefaultFileRE(expected_filenames),
+             context.autoupdate_split, context.non_check_lines,
+             context.actual_stdout, context.actual_stderr,
+             GetDefaultFileRE(expected_filenames),
              GetLineNumberReplacements(expected_filenames),
              [&](std::string& line) { DoExtraCheckReplacements(line); })
       .Run(dry_run);
@@ -266,7 +269,8 @@ auto FileTestBase::DumpOutput() -> ErrorOr<Success> {
     return ErrorBuilder() << "Error updating " << test_name_ << ": "
                           << run_result.error();
   }
-  llvm::errs() << banner << context.stdout << banner << "= Exit with success: "
+  llvm::errs() << banner << context.actual_stdout << banner
+               << "= Exit with success: "
                << (context.run_result.success ? "true" : "false") << "\n"
                << banner;
   return Success();
@@ -297,18 +301,32 @@ auto FileTestBase::ProcessTestFileAndRun(TestContext& context)
   CARBON_RETURN_IF_ERROR(
       DoArgReplacements(context.test_args, context.test_files));
 
+  // stdin needs to exist on-disk for compatibility. We'll use a pointer for it.
+  FILE* input_stream = nullptr;
+  auto erase_input_on_exit = llvm::make_scope_exit([&input_stream]() {
+    if (input_stream) {
+      // fclose should delete the tmpfile.
+      fclose(input_stream);
+      input_stream = nullptr;
+    }
+  });
+
   // Create the files in-memory.
   llvm::IntrusiveRefCntPtr<llvm::vfs::InMemoryFileSystem> fs =
       new llvm::vfs::InMemoryFileSystem;
   for (const auto& test_file : context.test_files) {
-    if (!fs->addFile(test_file.filename, /*ModificationTime=*/0,
-                     llvm::MemoryBuffer::getMemBuffer(
-                         test_file.content, test_file.filename,
-                         /*RequiresNullTerminator=*/false))) {
+    if (test_file.filename == StdinFilename) {
+      input_stream = tmpfile();
+      fwrite(test_file.content.c_str(), sizeof(char), test_file.content.size(),
+             input_stream);
+      rewind(input_stream);
+    } else if (!fs->addFile(test_file.filename, /*ModificationTime=*/0,
+                            llvm::MemoryBuffer::getMemBuffer(
+                                test_file.content, test_file.filename,
+                                /*RequiresNullTerminator=*/false))) {
       return ErrorBuilder() << "File is repeated: " << test_file.filename;
     }
   }
-
   // Convert the arguments to StringRef and const char* to match the
   // expectations of PrettyStackTraceProgram and Run.
   llvm::SmallVector<llvm::StringRef> test_args_ref;
@@ -340,20 +358,21 @@ auto FileTestBase::ProcessTestFileAndRun(TestContext& context)
     if (context.capture_console_output) {
       // No need to flush stderr.
       llvm::outs().flush();
-      context.stdout += GetCapturedStdout();
-      context.stderr += GetCapturedStderr();
+      context.actual_stdout += GetCapturedStdout();
+      context.actual_stderr += GetCapturedStderr();
     }
   });
 
   // Prepare string streams to capture output. In order to address casting
   // constraints, we split calls to Run as a ternary based on whether we want to
   // capture output.
-  llvm::raw_svector_ostream stdout(context.stdout);
-  llvm::raw_svector_ostream stderr(context.stderr);
+  llvm::raw_svector_ostream output_stream(context.actual_stdout);
+  llvm::raw_svector_ostream error_stream(context.actual_stderr);
   CARBON_ASSIGN_OR_RETURN(
       context.run_result,
-      context.dump_output ? Run(test_args_ref, fs, llvm::outs(), llvm::errs())
-                          : Run(test_args_ref, fs, stdout, stderr));
+      context.dump_output
+          ? Run(test_args_ref, fs, input_stream, llvm::outs(), llvm::errs())
+          : Run(test_args_ref, fs, input_stream, output_stream, error_stream));
 
   return Success();
 }
@@ -380,10 +399,11 @@ auto FileTestBase::DoArgReplacements(
         it = test_args.erase(it);
         for (const auto& file : test_files) {
           const std::string& filename = file.filename;
-          if (!filename.ends_with(".h")) {
-            it = test_args.insert(it, filename);
-            ++it;
+          if (filename == StdinFilename || filename.ends_with(".h")) {
+            continue;
           }
+          it = test_args.insert(it, filename);
+          ++it;
         }
         // Back up once because the for loop will advance.
         --it;
@@ -546,7 +566,6 @@ static auto AddTestFile(llvm::StringRef filename, std::string* content,
                         llvm::SmallVector<FileTestBase::TestFile>* test_files)
     -> ErrorOr<Success> {
   CARBON_RETURN_IF_ERROR(ReplaceContentKeywords(filename, content));
-
   test_files->push_back(
       {.filename = filename.str(), .content = std::move(*content)});
   content->clear();

+ 12 - 7
testing/file_test/file_test_base.h

@@ -68,9 +68,13 @@ class FileTestBase : public testing::Test {
   explicit FileTestBase(std::mutex* output_mutex, llvm::StringRef test_name)
       : output_mutex_(output_mutex), test_name_(test_name) {}
 
-  // Implemented by children to run the test. For example, TestBody validates
-  // stdout and stderr. Children should use fs for file content, and may add
-  // more files.
+  // Implemented by children to run the test. The framework will validate the
+  // content written to `output_stream` and `error_stream`. Children should use
+  // `fs` for file content, and may add more files.
+  //
+  // If there is a split test file named "STDIN", then its contents will be
+  // provided at `input_stream` instead of `fs`. Otherwise, `input_stream` will
+  // be null.
   //
   // Any test expectations should be called from ValidateRun, not Run.
   //
@@ -78,8 +82,9 @@ class FileTestBase : public testing::Test {
   // RunResult otherwise.
   virtual auto Run(const llvm::SmallVector<llvm::StringRef>& test_args,
                    llvm::IntrusiveRefCntPtr<llvm::vfs::InMemoryFileSystem>& fs,
-                   llvm::raw_pwrite_stream& stdout,
-                   llvm::raw_pwrite_stream& stderr) -> ErrorOr<RunResult> = 0;
+                   FILE* input_stream, llvm::raw_pwrite_stream& output_stream,
+                   llvm::raw_pwrite_stream& error_stream)
+      -> ErrorOr<RunResult> = 0;
 
   // Implemented by children to do post-Run test expectations. Only called when
   // testing. Does not need to be provided if only CHECK test expectations are
@@ -174,8 +179,8 @@ class FileTestBase : public testing::Test {
     llvm::SmallVector<testing::Matcher<std::string>> expected_stderr;
 
     // stdout and stderr from Run. 16 is arbitrary but a required value.
-    llvm::SmallString<16> stdout;
-    llvm::SmallString<16> stderr;
+    llvm::SmallString<16> actual_stdout;
+    llvm::SmallString<16> actual_stderr;
 
     RunResult run_result = {.success = false};
   };

+ 92 - 68
testing/file_test/file_test_base_test.cpp

@@ -23,7 +23,8 @@ class FileTestBaseTest : public FileTestBase {
 
   auto Run(const llvm::SmallVector<llvm::StringRef>& test_args,
            llvm::IntrusiveRefCntPtr<llvm::vfs::InMemoryFileSystem>& fs,
-           llvm::raw_pwrite_stream& stdout, llvm::raw_pwrite_stream& stderr)
+           FILE* input_stream, llvm::raw_pwrite_stream& output_stream,
+           llvm::raw_pwrite_stream& error_stream)
       -> ErrorOr<RunResult> override;
 
   auto GetArgReplacements() -> llvm::StringMap<std::string> override {
@@ -55,13 +56,13 @@ class FileTestBaseTest : public FileTestBase {
 
 // Prints arguments so that they can be validated in tests.
 static auto PrintArgs(llvm::ArrayRef<llvm::StringRef> args,
-                      llvm::raw_pwrite_stream& stdout) -> void {
+                      llvm::raw_pwrite_stream& output_stream) -> void {
   llvm::ListSeparator sep;
-  stdout << args.size() << " args: ";
+  output_stream << args.size() << " args: ";
   for (auto arg : args) {
-    stdout << sep << "`" << arg << "`";
+    output_stream << sep << "`" << arg << "`";
   }
-  stdout << "\n";
+  output_stream << "\n";
 }
 
 // Verifies arguments are well-structured, and returns the files in them.
@@ -85,113 +86,131 @@ static auto GetFilesFromArgs(llvm::ArrayRef<llvm::StringRef> args,
 struct TestParams {
   // These are the arguments to `Run()`.
   llvm::vfs::InMemoryFileSystem& fs;
-  llvm::raw_pwrite_stream& stdout;
-  llvm::raw_pwrite_stream& stderr;
+  FILE* input_stream;
+  llvm::raw_pwrite_stream& output_stream;
+  llvm::raw_pwrite_stream& error_stream;
 
   // This is assigned after construction.
   llvm::ArrayRef<llvm::StringRef> files;
 };
 
-// Does printing and returns expected results for alternating_files.carbon.
+// Prints and returns expected results for alternating_files.carbon.
 static auto TestAlternatingFiles(TestParams& params)
     -> ErrorOr<FileTestBaseTest::RunResult> {
-  params.stdout << "unattached message 1\n"
-                << "a.carbon:2: message 2\n"
-                << "b.carbon:5: message 3\n"
-                << "a.carbon:2: message 4\n"
-                << "b.carbon:5: message 5\n"
-                << "unattached message 6\n";
-  params.stderr << "unattached message 1\n"
-                << "a.carbon:2: message 2\n"
-                << "b.carbon:5: message 3\n"
-                << "a.carbon:2: message 4\n"
-                << "b.carbon:5: message 5\n"
-                << "unattached message 6\n";
+  params.output_stream << "unattached message 1\n"
+                       << "a.carbon:2: message 2\n"
+                       << "b.carbon:5: message 3\n"
+                       << "a.carbon:2: message 4\n"
+                       << "b.carbon:5: message 5\n"
+                       << "unattached message 6\n";
+  params.error_stream << "unattached message 1\n"
+                      << "a.carbon:2: message 2\n"
+                      << "b.carbon:5: message 3\n"
+                      << "a.carbon:2: message 4\n"
+                      << "b.carbon:5: message 5\n"
+                      << "unattached message 6\n";
   return {{.success = true}};
 }
 
-// Does printing and returns expected results for capture_console_output.carbon.
+// Prints and returns expected results for capture_console_output.carbon.
 static auto TestCaptureConsoleOutput(TestParams& params)
     -> ErrorOr<FileTestBaseTest::RunResult> {
   llvm::errs() << "llvm::errs\n";
-  params.stderr << "params.stderr\n";
+  params.error_stream << "params.error_stream\n";
   llvm::outs() << "llvm::outs\n";
-  params.stdout << "params.stdout\n";
+  params.output_stream << "params.output_stream\n";
   return {{.success = true}};
 }
 
-// Does printing and returns expected results for example.carbon.
+// Prints and returns expected results for example.carbon.
 static auto TestExample(TestParams& params)
     -> ErrorOr<FileTestBaseTest::RunResult> {
   int delta_line = 10;
-  params.stdout << "something\n"
-                << "\n"
-                << "example.carbon:" << delta_line + 1 << ": Line delta\n"
-                << "example.carbon:" << delta_line << ": Negative line delta\n"
-                << "+*[]{}\n"
-                << "Foo baz\n";
+  params.output_stream << "something\n"
+                       << "\n"
+                       << "example.carbon:" << delta_line + 1
+                       << ": Line delta\n"
+                       << "example.carbon:" << delta_line
+                       << ": Negative line delta\n"
+                       << "+*[]{}\n"
+                       << "Foo baz\n";
   return {{.success = true}};
 }
 
-// Does printing and returns expected results for fail_example.carbon.
+// Prints and returns expected results for fail_example.carbon.
 static auto TestFailExample(TestParams& params)
     -> ErrorOr<FileTestBaseTest::RunResult> {
-  params.stderr << "Oops\n";
+  params.error_stream << "Oops\n";
   return {{.success = false}};
 }
 
-// Does printing and returns expected results for
+// Prints and returns expected results for
 // file_only_re_multi_file.carbon.
 static auto TestFileOnlyREMultiFile(TestParams& params)
     -> ErrorOr<FileTestBaseTest::RunResult> {
   int msg_count = 0;
-  params.stdout << "unattached message " << ++msg_count << "\n"
-                << "file: a.carbon\n"
-                << "unattached message " << ++msg_count << "\n"
-                << "line: 3: attached message " << ++msg_count << "\n"
-                << "unattached message " << ++msg_count << "\n"
-                << "line: 8: late message " << ++msg_count << "\n"
-                << "unattached message " << ++msg_count << "\n"
-                << "file: b.carbon\n"
-                << "line: 2: attached message " << ++msg_count << "\n"
-                << "unattached message " << ++msg_count << "\n"
-                << "line: 7: late message " << ++msg_count << "\n"
-                << "unattached message " << ++msg_count << "\n";
+  params.output_stream << "unattached message " << ++msg_count << "\n"
+                       << "file: a.carbon\n"
+                       << "unattached message " << ++msg_count << "\n"
+                       << "line: 3: attached message " << ++msg_count << "\n"
+                       << "unattached message " << ++msg_count << "\n"
+                       << "line: 8: late message " << ++msg_count << "\n"
+                       << "unattached message " << ++msg_count << "\n"
+                       << "file: b.carbon\n"
+                       << "line: 2: attached message " << ++msg_count << "\n"
+                       << "unattached message " << ++msg_count << "\n"
+                       << "line: 7: late message " << ++msg_count << "\n"
+                       << "unattached message " << ++msg_count << "\n";
   return {{.success = true}};
 }
 
-// Does printing and returns expected results for file_only_re_one_file.carbon.
+// Prints and returns expected results for file_only_re_one_file.carbon.
 static auto TestFileOnlyREOneFile(TestParams& params)
     -> ErrorOr<FileTestBaseTest::RunResult> {
-  params.stdout << "unattached message 1\n"
-                << "file: file_only_re_one_file.carbon\n"
-                << "line: 1\n"
-                << "unattached message 2\n";
+  params.output_stream << "unattached message 1\n"
+                       << "file: file_only_re_one_file.carbon\n"
+                       << "line: 1\n"
+                       << "unattached message 2\n";
   return {{.success = true}};
 }
 
-// Does printing and returns expected results for no_line_number.carbon.
+// Prints and returns expected results for no_line_number.carbon.
 static auto TestNoLineNumber(TestParams& params)
     -> ErrorOr<FileTestBaseTest::RunResult> {
-  params.stdout << "a.carbon: msg1\n"
-                   "msg2\n"
-                   "b.carbon: msg3\n"
-                   "msg4\n"
-                   "a.carbon: msg5\n";
+  params.output_stream << "a.carbon: msg1\n"
+                          "msg2\n"
+                          "b.carbon: msg3\n"
+                          "msg4\n"
+                          "a.carbon: msg5\n";
   return {{.success = true}};
 }
 
-// Does printing and returns expected results for unattached_multi_file.carbon.
+// Prints and returns expected results for stdin.carbon.
+static auto TestStdin(TestParams& params)
+    -> ErrorOr<FileTestBaseTest::RunResult> {
+  CARBON_CHECK(params.input_stream);
+  constexpr int ReadSize = 256;
+  char buf[ReadSize];
+  while (feof(params.input_stream) == 0) {
+    auto read = fread(&buf, sizeof(char), ReadSize, params.input_stream);
+    if (read > 0) {
+      params.error_stream.write(buf, read);
+    }
+  }
+  return {{.success = true}};
+}
+
+// Prints and returns expected results for unattached_multi_file.carbon.
 static auto TestUnattachedMultiFile(TestParams& params)
     -> ErrorOr<FileTestBaseTest::RunResult> {
-  params.stdout << "unattached message 1\n"
-                << "unattached message 2\n";
-  params.stderr << "unattached message 3\n"
-                << "unattached message 4\n";
+  params.output_stream << "unattached message 1\n"
+                       << "unattached message 2\n";
+  params.error_stream << "unattached message 3\n"
+                      << "unattached message 4\n";
   return {{.success = true}};
 }
 
-// Does printing and returns expected results for:
+// Prints and returns expected results for:
 // - fail_multi_success_overall_fail.carbon
 // - multi_success.carbon
 // - multi_success_and_fail.carbon
@@ -220,8 +239,8 @@ static auto EchoFileContent(TestParams& params)
     for (int line_number = 1; !buffer.empty(); ++line_number) {
       auto [line, remainder] = buffer.split('\n');
       if (!line.empty() && !line.starts_with("//")) {
-        params.stdout << test_file << ":" << line_number << ": " << line
-                      << "\n";
+        params.output_stream << test_file << ":" << line_number << ": " << line
+                             << "\n";
       }
       buffer = remainder;
     }
@@ -232,9 +251,9 @@ static auto EchoFileContent(TestParams& params)
 auto FileTestBaseTest::Run(
     const llvm::SmallVector<llvm::StringRef>& test_args,
     llvm::IntrusiveRefCntPtr<llvm::vfs::InMemoryFileSystem>& fs,
-    llvm::raw_pwrite_stream& stdout, llvm::raw_pwrite_stream& stderr)
-    -> ErrorOr<RunResult> {
-  PrintArgs(test_args, stdout);
+    FILE* input_stream, llvm::raw_pwrite_stream& output_stream,
+    llvm::raw_pwrite_stream& error_stream) -> ErrorOr<RunResult> {
+  PrintArgs(test_args, output_stream);
 
   auto filename = std::filesystem::path(test_name().str()).filename();
   if (filename == "args.carbon") {
@@ -254,6 +273,8 @@ auto FileTestBaseTest::Run(
           .Case("file_only_re_one_file.carbon", &TestFileOnlyREOneFile)
           .Case("file_only_re_multi_file.carbon", &TestFileOnlyREMultiFile)
           .Case("no_line_number.carbon", &TestNoLineNumber)
+          .Case("stdin.carbon", &TestStdin)
+          .Case("stdin_and_autoupdate_split.carbon", &TestStdin)
           .Case("unattached_multi_file.carbon", &TestUnattachedMultiFile)
           .Case("fail_multi_success_overall_fail.carbon",
                 [&](TestParams&) {
@@ -273,7 +294,10 @@ auto FileTestBaseTest::Run(
           .Default(&EchoFileContent);
 
   // Call the appropriate test function for the file.
-  TestParams params = {.fs = *fs, .stdout = stdout, .stderr = stderr};
+  TestParams params = {.fs = *fs,
+                       .input_stream = input_stream,
+                       .output_stream = output_stream,
+                       .error_stream = error_stream};
   CARBON_ASSIGN_OR_RETURN(params.files, GetFilesFromArgs(test_args, *fs));
   return test_fn(params);
 }

+ 2 - 2
testing/file_test/testdata/capture_console_output.carbon

@@ -8,9 +8,9 @@
 // TIP:   bazel test //testing/file_test:file_test_base_test --test_arg=--file_tests=testing/file_test/testdata/capture_console_output.carbon
 // TIP: To dump output, run:
 // TIP:   bazel run //testing/file_test:file_test_base_test -- --dump_output --file_tests=testing/file_test/testdata/capture_console_output.carbon
-// CHECK:STDERR: params.stderr
+// CHECK:STDERR: params.error_stream
 // CHECK:STDERR: llvm::errs
 
 // CHECK:STDOUT: 2 args: `default_args`, `capture_console_output.carbon`
-// CHECK:STDOUT: params.stdout
+// CHECK:STDOUT: params.output_stream
 // CHECK:STDOUT: llvm::outs

+ 30 - 0
testing/file_test/testdata/stdin.carbon

@@ -0,0 +1,30 @@
+// 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
+
+// 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/stdin.carbon
+// TIP: To dump output, run:
+// TIP:   bazel run //testing/file_test:file_test_base_test -- --dump_output --file_tests=testing/file_test/testdata/stdin.carbon
+
+// --- a.carbon
+
+// --- STDIN
+
+S
+t
+d
+i
+n
+
+// --- AUTOUPDATE-SPLIT
+
+// CHECK:STDERR:
+// CHECK:STDERR: S
+// CHECK:STDERR: t
+// CHECK:STDERR: d
+// CHECK:STDERR: i
+// CHECK:STDERR: n
+// CHECK:STDERR:
+// CHECK:STDOUT: 2 args: `default_args`, `a.carbon`

+ 7 - 6
toolchain/testing/file_test.cpp

@@ -35,7 +35,8 @@ class ToolchainFileTest : public FileTestBase {
 
   auto Run(const llvm::SmallVector<llvm::StringRef>& test_args,
            llvm::IntrusiveRefCntPtr<llvm::vfs::InMemoryFileSystem>& fs,
-           llvm::raw_pwrite_stream& stdout, llvm::raw_pwrite_stream& stderr)
+           FILE* input_stream, llvm::raw_pwrite_stream& output_stream,
+           llvm::raw_pwrite_stream& error_stream)
       -> ErrorOr<RunResult> override {
     CARBON_ASSIGN_OR_RETURN(auto prelude, installation_.ReadPreludeManifest());
     if (!is_no_prelude()) {
@@ -46,16 +47,16 @@ class ToolchainFileTest : public FileTestBase {
 
     Driver driver({.fs = fs,
                    .installation = &installation_,
-                   .input_stream = nullptr,
-                   .output_stream = &stdout,
-                   .error_stream = &stderr});
+                   .input_stream = input_stream,
+                   .output_stream = &output_stream,
+                   .error_stream = &error_stream});
     auto driver_result = driver.RunCommand(test_args);
     // If any diagnostics have been produced, add a trailing newline to make the
     // last diagnostic match intermediate diagnostics (that have a newline
     // separator between them). This reduces churn when adding new diagnostics
     // to test cases.
-    if (stderr.tell() > 0) {
-      stderr << '\n';
+    if (error_stream.tell() > 0) {
+      error_stream << '\n';
     }
 
     RunResult result{