Delayed generated_file() writes if they collect validations metadata When a generated_file() target is written to the Ninja build plan, delay its output file write if `collect_validations_metadata` is true. Instead, the writes will be performed after all targets have been resolved to ensure there are no runtime crashes when performing the metadata walk. Note that the flag can be set directly in the target definition, or inherited from the .gn default value set through experimental_collect_validations_metadata. - builder_record.h: Add a comment explaining why metadata walks that include validations must be delayed. - ninja_generated_file_target_writer.cc: If a target has collect_validations_metadata enabled, do not write its output file yet, instead set a boolean flag to indicate the delay. - ninja_target_writer.cc: Take an optional pointer to a boolean flag to receive notification of delayed writes for generated_file() targets. Pass the pointer to NinjaGeneratedFileWriter properly. - setup.cc: Add SetPostResolutionCallback() to register a callback to invoke after all items have been resolved in the graph (i.e. after the first scheduler.Run() call). - setup_unittest.cc: Add new unit-test that verify that metadata walks with validations do not crash and return the correct values (or errors in case of missing deps). - command_gen.cc: Record delayed generated_file() target pointers during graph resolution, then use a post-resolution callback to write all delayed generated_file() outputs in parallel. Benchmarking shows that: - For a medium Fuchsia project (which currently relies on validations metadata walks), using `gn gen --experimental-collect-validations-metadata` is about 15% slower with this patch applied (18.55s to 20.3s) - For a default Android/arm64 Chromium build, `gn gen --experimental-collect-validations-metadata` there is no statical difference (around 3s each). Bug: 566346002 Change-Id: I58ad0c3bcec71ceec80cf5a617ef00e86415734d Reviewed-on: https://gn-review.googlesource.com/c/gn/+/27086 Reviewed-by: Takuto Ikuta <tikuta@google.com> Reviewed-by: Matt Stark <msta@google.com> Commit-Queue: David Turner <digit@google.com>
diff --git a/src/gn/builder_record.h b/src/gn/builder_record.h index b2bef55..fbb4eab 100644 --- a/src/gn/builder_record.h +++ b/src/gn/builder_record.h
@@ -116,9 +116,25 @@ // bar, but computing `bar.output` can only be done when `bar` is // Resolved. // -// Another issue is that metadata walks, performed when writing -// the build statement for generated_file() targets, will walk over -// validation dependencies as well. This requires these to be resolved. +// Another issue is that metadata walks for generated_file() targets +// that use `collect_validations_metadata = true` must be delayed +// after *all* targets have been resolved. Consider the following +// example, where `A` is a generated_file() target that sets +// this flag to true: +// +// A ---validations--> B --validations--> C --deps--> D +// +// As soon as C is defined, even if D is not loaded yet, the rules +// described here imply that B can be resolved immediately, then +// A finalized immediately. This will trigger a metadata walk +// starting from A, that will try to visit the C --> D dependency +// edge, which is still nullptr since C has not been resolved yet +// (because D is not loaded or resolved yet). This results in a +// runtime crash. +// +// To avoid this, these metadata walks MUST be delayed *after* all +// items in the graph have been resolved, and cannot be performed +// in the NinjaGeneratedFileTargetWriter::Run() function itself. // class BuilderRecord { public:
diff --git a/src/gn/command_gen.cc b/src/gn/command_gen.cc index de8b89f..4e991c3 100644 --- a/src/gn/command_gen.cc +++ b/src/gn/command_gen.cc
@@ -18,6 +18,7 @@ #include "gn/compile_commands_writer.h" #include "gn/eclipse_writer.h" #include "gn/filesystem_utils.h" +#include "gn/generated_file_writer.h" #include "gn/json_project_writer.h" #include "gn/label_pattern.h" #include "gn/ninja_outputs_writer.h" @@ -91,6 +92,10 @@ NinjaOutputsMap ninja_outputs_map; + // The list of generated_file() targets whose file writes have been + // delayed because they have collect_validations_metadata == true. + std::vector<const Target*> delayed_generated_file_targets; + std::unique_ptr<ResolvedTargetData> resolved = std::make_unique<ResolvedTargetData>(); @@ -104,8 +109,10 @@ std::vector<OutputFile>* ninja_outputs = write_info->want_ninja_outputs ? &target_ninja_outputs : nullptr; - std::string rule = - NinjaTargetWriter::RunAndWriteFile(target, resolved, ninja_outputs); + bool delayed_generated_file_write = false; + + std::string rule = NinjaTargetWriter::RunAndWriteFile( + target, resolved, ninja_outputs, &delayed_generated_file_write); { std::lock_guard<std::mutex> lock(write_info->lock); @@ -120,6 +127,9 @@ write_info->ninja_outputs_map.emplace(target, std::move(target_ninja_outputs)); } + + if (delayed_generated_file_write) + write_info->delayed_generated_file_targets.push_back(target); } } @@ -816,6 +826,27 @@ ItemResolvedAndGeneratedCallback(&write_info, record); }); + // Set a post-resolution callback that will be used to schedule + // the writes of delayed generated_file() outputs which require + // the full resolution of all targets. + // See https://issuetracker.google.com/566346002 + setup->SetPostResolutionCallback([&write_info](Scheduler& scheduler) -> bool { + if (write_info.delayed_generated_file_targets.empty()) + return true; + + scheduler.IncrementWorkCount(); + for (const Target* target : write_info.delayed_generated_file_targets) { + scheduler.ScheduleWork([target, &scheduler]() { + Err err; + if (!WriteGeneratedFileToDisk(target, &err)) { + scheduler.FailWithError(err); + } + }); + } + scheduler.DecrementWorkCount(); + return scheduler.Run(); + }); + // Do the actual load. This will also write out the target ninja files. if (!setup->Run()) return 1;
diff --git a/src/gn/ninja_generated_file_target_writer.cc b/src/gn/ninja_generated_file_target_writer.cc index 09aa271..b5508f6 100644 --- a/src/gn/ninja_generated_file_target_writer.cc +++ b/src/gn/ninja_generated_file_target_writer.cc
@@ -23,9 +23,18 @@ void NinjaGeneratedFileTargetWriter::Run() { // Write the file. - Err err; - if (!WriteGeneratedFileToDisk(target_, &err)) { - g_scheduler->FailWithError(err); + if (target_->collect_validations_metadata()) { + // Do not write the file yet because some transitive dependencies + // might not be fully resolved, causing runtime crashes during the + // write. The |delayed_generated_file_write_| pointer must be set + // when validations metadata is collected. + CHECK(delayed_generated_file_write_ptr_); + *delayed_generated_file_write_ptr_ = true; + } else { + Err err; + if (!WriteGeneratedFileToDisk(target_, &err)) { + g_scheduler->FailWithError(err); + } } // A generated_file target should generate a phony target with dependencies
diff --git a/src/gn/ninja_generated_file_target_writer.h b/src/gn/ninja_generated_file_target_writer.h index d39f5fd..4765d52 100644 --- a/src/gn/ninja_generated_file_target_writer.h +++ b/src/gn/ninja_generated_file_target_writer.h
@@ -13,9 +13,15 @@ NinjaGeneratedFileTargetWriter(const Target* target, std::ostream& out); ~NinjaGeneratedFileTargetWriter() override; + void SetDelayedGeneratedFileWritePtr(bool* flag_ptr) { + delayed_generated_file_write_ptr_ = flag_ptr; + } + void Run() override; private: + bool* delayed_generated_file_write_ptr_ = nullptr; + NinjaGeneratedFileTargetWriter(const NinjaGeneratedFileTargetWriter&) = delete; NinjaGeneratedFileTargetWriter& operator=(
diff --git a/src/gn/ninja_generated_file_target_writer_unittest.cc b/src/gn/ninja_generated_file_target_writer_unittest.cc index 481db80..b32f939 100644 --- a/src/gn/ninja_generated_file_target_writer_unittest.cc +++ b/src/gn/ninja_generated_file_target_writer_unittest.cc
@@ -59,7 +59,9 @@ ASSERT_TRUE(target.OnResolved(&err)) << err.message(); std::ostringstream out; + bool delayed_write = false; NinjaGeneratedFileTargetWriter writer(&target, out); + writer.SetDelayedGeneratedFileWritePtr(&delayed_write); writer.Run(); const char expected[] = @@ -67,4 +69,66 @@ "phony/foo/dep2 || " "phony/foo/bundle_data_dep phony/foo/datadep\n"; EXPECT_EQ(expected, out.str()); + EXPECT_FALSE(delayed_write); +} + +TEST_F(NinjaGeneratedFileTargetWriterTest, RunWithDelayedGeneratedFileWrite) { + Err err; + TestWithScope setup; + + Target target(setup.settings(), Label(SourceDir("//foo/"), "bar")); + target.set_output_type(Target::GENERATED_FILE); + target.visibility().SetPublic(); + target.action_values().outputs() = + SubstitutionList::MakeForTest("//out/Debug/foo.json"); + target.set_contents(Value(nullptr, true)); + target.set_output_conversion(Value(nullptr, "json")); + target.set_collect_validations_metadata(true); + + Target dep(setup.settings(), Label(SourceDir("//foo/"), "dep")); + dep.set_output_type(Target::ACTION); + dep.visibility().SetPublic(); + dep.SetToolchain(setup.toolchain()); + ASSERT_TRUE(dep.OnResolved(&err)); + + Target dep2(setup.settings(), Label(SourceDir("//foo/"), "dep2")); + dep2.set_output_type(Target::ACTION); + dep2.visibility().SetPublic(); + dep2.SetToolchain(setup.toolchain()); + ASSERT_TRUE(dep2.OnResolved(&err)); + + Target bundle_data_dep(setup.settings(), + Label(SourceDir("//foo/"), "bundle_data_dep")); + bundle_data_dep.sources().push_back(SourceFile("//foo/some_data.txt")); + bundle_data_dep.set_output_type(Target::BUNDLE_DATA); + bundle_data_dep.visibility().SetPublic(); + bundle_data_dep.SetToolchain(setup.toolchain()); + ASSERT_TRUE(bundle_data_dep.OnResolved(&err)); + + Target datadep(setup.settings(), Label(SourceDir("//foo/"), "datadep")); + datadep.set_output_type(Target::ACTION); + datadep.visibility().SetPublic(); + datadep.SetToolchain(setup.toolchain()); + ASSERT_TRUE(datadep.OnResolved(&err)); + + target.public_deps().push_back(LabelTargetPair(&dep)); + target.public_deps().push_back(LabelTargetPair(&dep2)); + target.public_deps().push_back(LabelTargetPair(&bundle_data_dep)); + target.data_deps().push_back(LabelTargetPair(&datadep)); + + target.SetToolchain(setup.toolchain()); + ASSERT_TRUE(target.OnResolved(&err)) << err.message(); + + std::ostringstream out; + NinjaGeneratedFileTargetWriter writer(&target, out); + bool delayed_write = false; + writer.SetDelayedGeneratedFileWritePtr(&delayed_write); + writer.Run(); + + const char expected[] = + "build phony/foo/bar: phony foo.json phony/foo/dep " + "phony/foo/dep2 || " + "phony/foo/bundle_data_dep phony/foo/datadep\n"; + EXPECT_EQ(expected, out.str()); + EXPECT_TRUE(delayed_write); }
diff --git a/src/gn/ninja_target_writer.cc b/src/gn/ninja_target_writer.cc index 604f2f5..d19a3aa 100644 --- a/src/gn/ninja_target_writer.cc +++ b/src/gn/ninja_target_writer.cc
@@ -100,7 +100,8 @@ std::string NinjaTargetWriter::RunAndWriteFile( const Target* target, ResolvedTargetData* resolved, - std::vector<OutputFile>* ninja_outputs) { + std::vector<OutputFile>* ninja_outputs, + bool* delayed_generated_file_write) { const Settings* settings = target->settings(); ScopedTrace trace(TraceItem::TRACE_FILE_WRITE_NINJA, @@ -110,6 +111,9 @@ if (g_scheduler->verbose_logging()) g_scheduler->Log("Computing", target->label().GetUserVisibleName(true)); + if (delayed_generated_file_write) + *delayed_generated_file_write = false; + // It's ridiculously faster to write to a string and then write that to // disk in one operation than to use an fstream here. StringOutputBuffer storage; @@ -161,6 +165,7 @@ NinjaGeneratedFileTargetWriter writer(target, rules); writer.SetResolvedTargetData(resolved); writer.SetNinjaOutputs(ninja_outputs); + writer.SetDelayedGeneratedFileWritePtr(delayed_generated_file_write); writer.Run(); } else if (target->IsBinary()) { needs_file_write = true;
diff --git a/src/gn/ninja_target_writer.h b/src/gn/ninja_target_writer.h index 043e834..747aa74 100644 --- a/src/gn/ninja_target_writer.h +++ b/src/gn/ninja_target_writer.h
@@ -49,10 +49,17 @@ // // If |ninja_outputs| is not nullptr, it will be set with the list of // Ninja output paths generated by the corresponding writer. + // + // If |delayed_generated_file_write| is not null, and the target is + // a GENERATED_FILE with collect_validations_metadata enabled, then + // this function will not write the collected result to a file, but instead + // write true to |*delayed_generated_file_write|. This is to avoid runtime + // crashes (see https://issuetracker.google.com/566346002). static std::string RunAndWriteFile( const Target* target, ResolvedTargetData* resolved = nullptr, - std::vector<OutputFile>* ninja_outputs = nullptr); + std::vector<OutputFile>* ninja_outputs = nullptr, + bool* delayed_generated_file_write = nullptr); // Writes a stamp or phony rule for the target's public inputs if it exports // any. This does nothing if resolved->ExportsPublicInputs(target) returns
diff --git a/src/gn/setup.cc b/src/gn/setup.cc index 26a805c..c4c2b8b 100644 --- a/src/gn/setup.cc +++ b/src/gn/setup.cc
@@ -581,6 +581,20 @@ RunPreMessageLoop(); if (!scheduler_.Run()) return false; + + // Check for bad items in the graph now, as the + // optional second Run() call requires all items + // to be properly resolved. + Err err; + if (!builder_.CheckForBadItems(&err)) { + err.PrintToStdout(); + return false; + } + + // Perform a second run if needed. + if (post_resolution_callback_ && !post_resolution_callback_(scheduler_)) + return false; + return RunPostMessageLoop(cmdline); } @@ -598,10 +612,6 @@ bool Setup::RunPostMessageLoop(const base::CommandLine& cmdline) { Err err; - if (!builder_.CheckForBadItems(&err)) { - err.PrintToStdout(); - return false; - } if (!build_settings_.build_args().VerifyAllOverridesUsed(&err)) { if (cmdline.HasSwitch(switches::kFailOnUnusedArgs)) {
diff --git a/src/gn/setup.h b/src/gn/setup.h index 7f13c44..c6a190b 100644 --- a/src/gn/setup.h +++ b/src/gn/setup.h
@@ -5,6 +5,7 @@ #ifndef TOOLS_GN_SETUP_H_ #define TOOLS_GN_SETUP_H_ +#include <functional> #include <memory> #include <vector> @@ -67,6 +68,16 @@ // a build directory. bool DoSetupForEditing(); + // A callback that will be invoked by Run() once all items in the graph + // have been resolved (and no bad graph items were found). This can be + // used to schedule more work and then call |scheduler.Run()|. + using PostResolutionCallback = std::function<bool(Scheduler&)>; + + // Set a PostResolutionCallback, which will be invoked by Run() + void SetPostResolutionCallback(PostResolutionCallback&& callback) { + post_resolution_callback_ = std::move(callback); + } + // Runs the load, returning true on success. On failure, prints the error // and returns false. This includes both RunPreMessageLoop() and // RunPostMessageLoop(). @@ -221,6 +232,8 @@ std::vector<LabelPattern> export_compile_commands_; + PostResolutionCallback post_resolution_callback_; + Setup(const Setup&) = delete; Setup& operator=(const Setup&) = delete; };
diff --git a/src/gn/setup_unittest.cc b/src/gn/setup_unittest.cc index 0eee1e7..0b323c3 100644 --- a/src/gn/setup_unittest.cc +++ b/src/gn/setup_unittest.cc
@@ -9,10 +9,12 @@ #include "base/files/file_util.h" #include "base/files/scoped_temp_dir.h" #include "gn/builder_record.h" +#include "gn/commands.h" #include "gn/filesystem_utils.h" #include "gn/switches.h" #include "gn/test_with_scheduler.h" #include "util/build_config.h" +#include "util/msg_loop.h" using SetupTest = TestWithScheduler; @@ -21,6 +23,12 @@ base::WriteFile(file, data.data(), data.size())); } +static std::string ReadFile(const base::FilePath& file) { + std::string data; + CHECK(base::ReadFileToString(file, &data)); + return data; +} + TEST_F(SetupTest, DotGNFileIsGenDep) { base::CommandLine cmdline(base::CommandLine::NO_PROGRAM); @@ -616,3 +624,259 @@ setup.DoSetupWithErr(FilePathToUTF8(build_dir), true, cmdline, &err)); EXPECT_FALSE(err.has_error()); } + +// Convenience class to save the current process base::CommandLine +// on construction, modify it through the get() method, then restore +// the saved version on scope exit. This is useful for tests that need +// to modify the process global singleton temporarily. +class ScopedCommandLineForTest { + public: + ScopedCommandLineForTest() + : command_line_ref_(*base::CommandLine::ForCurrentProcess()), + saved_command_line_(command_line_ref_) {} + + ~ScopedCommandLineForTest() { command_line_ref_ = saved_command_line_; } + + base::CommandLine& get() { return command_line_ref_; } + + private: + base::CommandLine& command_line_ref_; + base::CommandLine saved_command_line_; +}; + +// A test that verifies that generated_file() generation doesn't crash. +// See the commands in builder_record.h regarding metadata walks +// requiring all items in the graph to be fully resolved to run safely. +// +// The test creates a graph that looks like: +// +// A ---validation--> +// B --validation--> +// C --deps--> +// D_0 --deps--> +// D_1 --deps--> .. --deps--> D_19 +// +// Where A will be finalized (written to the Ninja build plan) early while one +// of D_0 or D_19 is still undefined during the load. +// +// There is a potential race condition because GN loads build files concurrently +// and according to the state transition rules described in builder_record.h, +// A could be finalized as soon as C is defined, even if some of its +// dependencies (D_0 to D_19) are still undefined or being loaded in other +// threads, thus that some of the --deps--> edges between D_n and D_n+1 would be +// nullptr. (see https://crbug.com/gn/566346002 for more details). +// +// A's finalization calls NinjaGeneratedTargetWriter::Run() which delays +// metadata walks if collect_validations_metadata is true. If it didn't do +// that, starting a metadata walk would crash at runtime when trying to +// traverse the nullptr edge. +// +// This test sets --threads=4 to ensure that all dependencies cannot be +// defined when A is finalized. Removing the delay in +// NinjaGeneratedTargetWriter::Run() makes it crash consistently. +// +// The test also verifies that the generated file exists with the right content, +// i.e. that metadata collection was done properly, even if it was delayed. +TEST(GenCommandTest, ValidationMetadataRace) { + MsgLoop msg_loop; + ScopedCommandLineForTest global_cmdline; + + base::ScopedTempDir in_temp_dir; + ASSERT_TRUE(in_temp_dir.CreateUniqueTempDir()); + base::FilePath in_path = base::MakeAbsoluteFilePath(in_temp_dir.GetPath()); + + WriteFile(in_path.Append(FILE_PATH_LITERAL(".gn")), + "buildconfig = \"//BUILDCONFIG.gn\"\n"); + + WriteFile(in_path.Append(FILE_PATH_LITERAL("BUILDCONFIG.gn")), + "set_default_toolchain(\"//:toolchain\")\n"); + + std::string build_gn = R"( +toolchain("toolchain") { + tool("stamp") { + command = "touch {{output}}" + } +} + +generated_file("A") { + outputs = [ "$target_out_dir/A.json" ] + data_keys = [ "key" ] + deps = [] + validations = [ ":B" ] + collect_validations_metadata = true +} + +group("B") { + deps = [] + validations = [ ":C" ] + metadata = { + key = [ "value_b" ] + } +} + +group("C") { + deps = [ ":D_0" ] + metadata = { + key = [ "value_c" ] + } +} +)"; + + // Create a chain of groups: + // D_0 --deps--> D_1 --deps--> D_2 --deps--> D_3 .... --deps--> D_19 + int chain_length = 20; + for (int i = 0; i < chain_length; i++) { + build_gn += "group(\"D_" + std::to_string(i) + "\") {\n"; + if (i < chain_length - 1) { + build_gn += " deps = [ \":D_" + std::to_string(i + 1) + "\" ]\n"; + } + build_gn += "}\n"; + } + + build_gn += R"( +group("default") { + deps = [ ":A" ] +} +)"; + + WriteFile(in_path.Append(FILE_PATH_LITERAL("BUILD.gn")), build_gn); + + base::ScopedTempDir build_temp_dir; + ASSERT_TRUE(build_temp_dir.CreateUniqueTempDir()); + + global_cmdline.get().AppendSwitchPath(switches::kRoot, in_path); + global_cmdline.get().AppendSwitch(switches::kFailOnUnusedArgs); + global_cmdline.get().AppendSwitch(switches::kQuiet); + global_cmdline.get().AppendSwitch(switches::kThreads, "4"); + + std::vector<std::string> args; + args.push_back(FilePathToUTF8(build_temp_dir.GetPath())); + + int exit_code = commands::RunGen(args); + EXPECT_EQ(0, exit_code); + + // Now verify that the file was written with the right content + std::string data = ReadFile( + build_temp_dir.GetPath().Append(FILE_PATH_LITERAL("obj/A.json"))); + EXPECT_EQ(data, "value_c\nvalue_b\n"); +} + +// A variant of the previous test above where the last +// dependency in the chain will depend on a target that is +// never defined. +// +// The test creates a graph that looks like: +// +// A ---validation--> +// B --validation--> +// C --deps--> +// D_0 --deps--> +// D_1 --deps--> .. --deps--> D_19 --> //missing:target +// +// Where //missing:target will never be defined. +// +// The test checks that an error is correctly reported in this case, +// to verify that delaying the metadata collection didn't delay +// proper checks. +TEST(GenCommandTest, ValidationMetadataRaceMissingTarget) { + MsgLoop msg_loop; + ScopedCommandLineForTest global_cmdline; + + base::ScopedTempDir in_temp_dir; + ASSERT_TRUE(in_temp_dir.CreateUniqueTempDir()); + base::FilePath in_path = base::MakeAbsoluteFilePath(in_temp_dir.GetPath()); + + WriteFile(in_path.Append(FILE_PATH_LITERAL(".gn")), + "buildconfig = \"//BUILDCONFIG.gn\"\n"); + + WriteFile(in_path.Append(FILE_PATH_LITERAL("BUILDCONFIG.gn")), + "set_default_toolchain(\"//:toolchain\")\n"); + + std::string build_gn = R"( +toolchain("toolchain") { + tool("stamp") { + command = "touch {{output}}" + } +} + +generated_file("A") { + outputs = [ "$target_out_dir/A.json" ] + data_keys = [ "key" ] + deps = [] + validations = [ ":B" ] + collect_validations_metadata = true +} + +group("B") { + deps = [] + validations = [ ":C" ] + metadata = { + key = [ "value_b" ] + } +} + +group("C") { + deps = [ ":D_0" ] + metadata = { + key = [ "value_c" ] + } +} +)"; + + // Create a chain of groups: + // D_0 --deps--> D_1 --deps--> D_2 --deps--> D_3 .... --deps--> D_19 + // Where D_19 depends on a missing target in an existing file. + int chain_length = 20; + for (int i = 0; i < chain_length; i++) { + build_gn += "group(\"D_" + std::to_string(i) + "\") {\n"; + if (i < chain_length - 1) { + build_gn += " deps = [ \":D_" + std::to_string(i + 1) + "\" ]\n"; + } else { + build_gn += " deps = [ \"//missing:target\" ]\n"; + } + build_gn += "}\n"; + } + + build_gn += R"( +group("default") { + deps = [ ":A" ] +} +)"; + + WriteFile(in_path.Append(FILE_PATH_LITERAL("BUILD.gn")), build_gn); + + // Create the missing directory and empty BUILD.gn + ASSERT_TRUE( + base::CreateDirectory(in_path.Append(FILE_PATH_LITERAL("missing")))); + WriteFile(in_path.Append(FILE_PATH_LITERAL("missing/BUILD.gn")), ""); + + base::ScopedTempDir build_temp_dir; + ASSERT_TRUE(build_temp_dir.CreateUniqueTempDir()); + + global_cmdline.get().AppendSwitchPath(switches::kRoot, in_path); + global_cmdline.get().AppendSwitch(switches::kFailOnUnusedArgs); + global_cmdline.get().AppendSwitch(switches::kQuiet); + global_cmdline.get().AppendSwitch(switches::kThreads, "4"); + + std::vector<std::string> args; + args.push_back(FilePathToUTF8(build_temp_dir.GetPath())); + + { + // Use a ScopedBufferedOutput class to prevent the test from printing + // an "ERROR unresolved dependencies." message. + ScopedBufferedOutput buffered_out; + int exit_code = commands::RunGen(args); + EXPECT_NE(0, exit_code); + + auto output_items = buffered_out.GetItems(); + ASSERT_EQ(3u, output_items.size()); + EXPECT_EQ(output_items[0].output, "ERROR "); + EXPECT_EQ(output_items[0].decoration, DECORATION_RED); + EXPECT_EQ(output_items[1].output, "Unresolved dependencies.\n"); + EXPECT_EQ(output_items[1].decoration, DECORATION_NONE); + EXPECT_EQ( + output_items[2].output, + "//:D_19(//:toolchain)\n needs //missing:target(//:toolchain)\n\n"); + EXPECT_EQ(output_items[2].decoration, DECORATION_NONE); + } +}