| // Copyright 2026 The GN Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "gn/edit_command.h" |
| |
| #include <string> |
| #include <string_view> |
| #include <vector> |
| |
| #include "base/files/file_util.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "gn/err.h" |
| #include "gn/filesystem_utils.h" |
| #include "gn/setup.h" |
| #include "gn/test_with_scheduler.h" |
| #include "util/test/test.h" |
| |
| namespace commands { |
| namespace { |
| struct Edited; |
| std::string Pretty(const Edited& edited); |
| |
| struct Edited { |
| Edited(std::string_view contents, EditState edit_state = EditState()) |
| : contents_(contents.starts_with('\n') ? contents.substr(1) : contents), |
| edit_state_(std::move(edit_state)) {} |
| |
| bool operator==(const Edited& other) const { |
| return Pretty(*this) == Pretty(other); |
| } |
| |
| std::string contents_; |
| EditState edit_state_; |
| }; |
| |
| std::string Pretty(const Edited& edited) { |
| std::string res = edited.contents_; |
| if (!edited.edit_state_.needs_manual_review.empty()) { |
| res += "\nNeeds manual review: " + |
| testing::Pretty(edited.edit_state_.needs_manual_review); |
| } |
| if (!edited.edit_state_.warnings.empty()) { |
| res += "\nWarnings: " + testing::Pretty(edited.edit_state_.warnings); |
| } |
| if (!edited.edit_state_.needs_fix_deps.empty()) { |
| res += "\nNeeds check --fix: " + |
| testing::Pretty(edited.edit_state_.needs_fix_deps); |
| } |
| return res; |
| } |
| |
| // Runs an edit command on matching the given target |
| // patterns, and returns the formatted output file contents. |
| Result<Edited> DoEdit(std::string command, |
| std::vector<std::string> patterns, |
| const std::string& before) { |
| base::ScopedTempDir temp_dir; |
| if (!temp_dir.CreateUniqueTempDir()) { |
| return Err(Location(), "Failed to create temp dir"); |
| } |
| base::FilePath root_path = base::MakeAbsoluteFilePath(temp_dir.GetPath()); |
| |
| base::FilePath build_gn_path = root_path.AppendASCII("BUILD.gn"); |
| if (!WriteFile(build_gn_path, before, nullptr)) { |
| return Err(Location(), "Failed to write BUILD.gn"); |
| } |
| base::FilePath dot_gn_path = root_path.AppendASCII(".gn"); |
| if (!WriteFile(dot_gn_path, "", nullptr)) { |
| return Err(Location(), "Failed to write .gn"); |
| } |
| |
| Setup setup; |
| setup.build_settings().SetRootPath(root_path); |
| |
| std::vector<std::string> args; |
| args.push_back(std::move(command)); |
| for (auto& p : patterns) { |
| args.push_back(std::move(p)); |
| } |
| |
| auto result = RunEditImpl(args, setup); |
| if (result.has_error()) { |
| return result.error(); |
| } |
| |
| std::string after; |
| if (!base::ReadFileToString(build_gn_path, &after)) { |
| return Err(Location(), "Failed to read BUILD.gn"); |
| } |
| return Edited(after, std::move(result->second)); |
| } |
| |
| // Runs an edit command matching all targets in the root BUILD.gn ("//:*"). |
| Result<Edited> DoEdit(std::string command, const std::string& before) { |
| return DoEdit(std::move(command), {"//:*"}, before); |
| } |
| |
| } // namespace |
| |
| using EditCommandTest = TestWithScheduler; |
| |
| TEST_F(EditCommandTest, MultipleTargetsSubset) { |
| EXPECT_SUCCESS(DoEdit("set testonly true", {"//:foo"}, |
| R"( |
| executable("foo") { |
| testonly = false |
| } |
| executable("bar") { |
| testonly = false |
| } |
| )"), |
| Edited(R"( |
| executable("foo") { |
| testonly = true |
| } |
| |
| executable("bar") { |
| testonly = false |
| } |
| )")); |
| } |
| |
| TEST_F(EditCommandTest, PatternNeverMatched) { |
| EXPECT_FAILURE(DoEdit("set testonly true", {"//:nonexistent"}, |
| R"( |
| executable("foo") { |
| } |
| )"), |
| "Target(s) not found: //:nonexistent"); |
| } |
| |
| TEST_F(EditCommandTest, AddSubcommand) { |
| EXPECT_SUCCESS(DoEdit("add deps //add1 //add2 //add3 :dep2", |
| R"( |
| executable("foo") { |
| deps = [ "//dep1" ] |
| deps += [ "//:dep2" ] |
| if (is_linux) { |
| deps += [ "//add3" ] |
| } |
| } |
| )"), |
| Edited(R"( |
| executable("foo") { |
| deps = [ |
| "//add1", |
| "//add2", |
| "//add3", |
| "//dep1", |
| ] |
| deps += [ "//:dep2" ] |
| if (is_linux) { |
| deps += [] |
| } |
| } |
| )")); |
| |
| // Adding to a target where attribute is not defined should it at the end |
| EXPECT_SUCCESS(DoEdit("add deps //base", |
| R"( |
| executable("foo") { |
| sources = [ "foo.cc" ] |
| } |
| )"), |
| Edited(R"( |
| executable("foo") { |
| sources = [ "foo.cc" ] |
| deps = [ "//base" ] |
| } |
| )")); |
| |
| // Attribute defined only conditionally should hoist to start of block and |
| // convert = to += |
| EXPECT_SUCCESS(DoEdit("add deps //base", |
| R"( |
| executable("foo") { |
| if (is_linux) { |
| deps = [ "//dep" ] |
| } |
| } |
| )"), |
| Edited(R"( |
| executable("foo") { |
| deps = [ "//base" ] |
| |
| if (is_linux) { |
| deps += [ "//dep" ] |
| } |
| } |
| )")); |
| |
| EXPECT_SUCCESS(DoEdit("add deps //base", |
| R"( |
| executable("foo") { |
| deps = foo + bar + [ "//baz" ] |
| } |
| )"), |
| Edited(R"( |
| executable("foo") { |
| deps = foo + bar + [ |
| "//base", |
| "//baz", |
| ] |
| } |
| )")); |
| |
| EXPECT_SUCCESS(DoEdit("add deps //base", |
| R"( |
| executable("foo") { |
| deps = other_deps |
| } |
| )"), |
| Edited(R"( |
| executable("foo") { |
| deps = [ "//base" ] + other_deps |
| } |
| )")); |
| } |
| |
| TEST_F(EditCommandTest, DeleteSubcommand) { |
| EXPECT_SUCCESS(DoEdit("delete", {"//:bar"}, |
| R"( |
| executable("foo") { |
| sources = [ "foo.cc" ] |
| } |
| executable("bar") { |
| sources = [ "bar.cc" ] |
| } |
| )"), |
| Edited(R"( |
| executable("foo") { |
| sources = [ "foo.cc" ] |
| } |
| )")); |
| |
| EXPECT_SUCCESS(DoEdit("delete", {"//:bar"}, |
| R"( |
| if (is_win) { |
| executable("bar") { |
| sources = [ "bar.cc" ] |
| } |
| } else { |
| executable("bar") { |
| sources = [ "bar.cc" ] |
| } |
| } |
| )"), |
| Edited(R"( |
| if (is_win) { |
| # TODO(gn edit: delete): This would normally be deleted but is conditional. |
| # Manual intervention is required to decide whether it should actually be |
| # deleted. |
| executable("bar") { |
| sources = [ "bar.cc" ] |
| } |
| } else { |
| # TODO(gn edit: delete): This would normally be deleted but is conditional. |
| # Manual intervention is required to decide whether it should actually be |
| # deleted. |
| executable("bar") { |
| sources = [ "bar.cc" ] |
| } |
| } |
| )", |
| EditState({Label(SourceDir("//"), "bar")}))); |
| } |
| |
| TEST_F(EditCommandTest, MoveSubcommand) { |
| EXPECT_SUCCESS( |
| DoEdit("move deps public_deps //a //b //nonexistent", {"//:foo"}, |
| R"( |
| executable("foo") { |
| deps = [ |
| "//a", |
| "//b", |
| "//c", |
| ] |
| public_deps = [ "//d" ] |
| } |
| )"), |
| Edited(R"( |
| executable("foo") { |
| deps = [ "//c" ] |
| public_deps = [ |
| "//a", |
| "//b", |
| "//d", |
| ] |
| } |
| )", |
| EditState{{}, |
| {Err(Location(), |
| "Target \"//:foo\" does not contain the value " |
| "\"//nonexistent\" in attribute \"deps\".")}})); |
| } |
| |
| TEST_F(EditCommandTest, NewSubcommand) { |
| // Append new target to the end of the file. |
| EXPECT_SUCCESS(DoEdit("new source_set", {"//:bar"}, |
| R"( |
| executable("foo") { |
| sources = [ "foo.cc" ] |
| } |
| )"), |
| Edited(R"( |
| executable("foo") { |
| sources = [ "foo.cc" ] |
| } |
| |
| source_set("bar") { |
| } |
| )")); |
| |
| // Insert before a relative target. |
| EXPECT_SUCCESS(DoEdit("new source_set before foo", {"//:bar"}, |
| R"( |
| executable("foo") { |
| sources = [ "foo.cc" ] |
| } |
| )"), |
| Edited(R"( |
| source_set("bar") { |
| } |
| |
| executable("foo") { |
| sources = [ "foo.cc" ] |
| } |
| )")); |
| |
| // Insert after a relative target. |
| EXPECT_SUCCESS(DoEdit("new source_set after foo", {"//:bar"}, |
| R"( |
| executable("foo") { |
| sources = [ "foo.cc" ] |
| } |
| |
| executable("baz") { |
| sources = [ "baz.cc" ] |
| } |
| )"), |
| Edited(R"( |
| executable("foo") { |
| sources = [ "foo.cc" ] |
| } |
| |
| source_set("bar") { |
| } |
| |
| executable("baz") { |
| sources = [ "baz.cc" ] |
| } |
| )")); |
| |
| // Insert multiple new targets after a relative target. |
| EXPECT_SUCCESS(DoEdit("new source_set after foo", {"//:bar", "//:qux"}, |
| R"( |
| executable("foo") { |
| sources = [ "foo.cc" ] |
| } |
| |
| executable("baz") { |
| sources = [ "baz.cc" ] |
| } |
| )"), |
| Edited(R"( |
| executable("foo") { |
| sources = [ "foo.cc" ] |
| } |
| |
| source_set("bar") { |
| } |
| |
| source_set("qux") { |
| } |
| |
| executable("baz") { |
| sources = [ "baz.cc" ] |
| } |
| )")); |
| |
| // Error when target already exists. |
| EXPECT_FAILURE(DoEdit("new source_set", {"//:foo"}, |
| R"( |
| executable("foo") { |
| sources = [ "foo.cc" ] |
| } |
| )")); |
| |
| // Error when relative target not found. |
| EXPECT_FAILURE(DoEdit("new source_set before nonexistent", {"//:bar"}, |
| R"( |
| executable("foo") { |
| sources = [ "foo.cc" ] |
| } |
| )")); |
| |
| // Error when no explicit target name specified (e.g. glob pattern). |
| EXPECT_FAILURE(DoEdit("new source_set", {"//*"}, |
| R"( |
| executable("foo") { |
| sources = [ "foo.cc" ] |
| } |
| )")); |
| } |
| |
| TEST_F(EditCommandTest, RemoveAttributeSubcommand) { |
| EXPECT_SUCCESS(DoEdit("remove testonly", |
| R"( |
| executable("foo") { |
| sources = [ "foo.cc" ] |
| testonly = true |
| } |
| )"), |
| Edited(R"( |
| executable("foo") { |
| sources = [ "foo.cc" ] |
| } |
| )")); |
| |
| EXPECT_SUCCESS( |
| DoEdit("remove nonexistent_attribute", {"//:foo"}, |
| R"( |
| executable("foo") { |
| } |
| )"), |
| Edited(R"( |
| executable("foo") { |
| } |
| )", |
| EditState{{}, |
| {Err(Location(), |
| "Target \"//:foo\" does not contain the " |
| "attribute \"nonexistent_attribute\".")}})); |
| } |
| |
| TEST_F(EditCommandTest, RemoveFromAttributeSubcommand) { |
| EXPECT_SUCCESS(DoEdit("remove deps //base :bar", |
| R"( |
| executable("foo") { |
| deps = [ |
| "//base", |
| "//:bar", |
| "//other:bar", |
| ] |
| } |
| )"), |
| Edited(R"( |
| executable("foo") { |
| deps = [ "//other:bar" ] |
| } |
| )")); |
| |
| EXPECT_SUCCESS(DoEdit("remove deps //base //nonexistent:glob", |
| R"( |
| executable("foo") { |
| deps = [ "//base" ] + [ "//foo:bar" ] |
| } |
| )"), |
| Edited(R"( |
| executable("foo") { |
| deps = [] + [ "//foo:bar" ] |
| } |
| )")); |
| |
| EXPECT_SUCCESS(DoEdit("remove deps //nonexistent", {"//:foo"}, |
| R"( |
| executable("foo") { |
| deps = [ "//base" ] |
| } |
| )"), |
| Edited(R"( |
| executable("foo") { |
| deps = [ "//base" ] |
| } |
| )", |
| EditState{{}, |
| {Err(Location(), |
| "Target \"//:foo\" does not contain the " |
| "value \"//nonexistent\" in attribute " |
| "\"deps\".")}})); |
| } |
| |
| TEST_F(EditCommandTest, RenameSubcommand) { |
| EXPECT_SUCCESS(DoEdit("rename srcs sources", |
| R"( |
| executable("foo") { |
| # This comment should be preserved |
| srcs = [ "foo.cc" ] |
| if (is_linux) { |
| srcs += [ "linux.cc" ] |
| } |
| } |
| )"), |
| Edited(R"( |
| executable("foo") { |
| # This comment should be preserved |
| sources = [ "foo.cc" ] |
| if (is_linux) { |
| sources += [ "linux.cc" ] |
| } |
| } |
| )")); |
| |
| EXPECT_SUCCESS(DoEdit("rename nonexistent new_attr", {"//:foo"}, |
| R"( |
| executable("foo") { |
| sources = [ "foo.cc" ] |
| } |
| )"), |
| Edited(R"( |
| executable("foo") { |
| sources = [ "foo.cc" ] |
| } |
| )", |
| EditState{{}, |
| {Err(Location(), |
| "Target \"//:foo\" does not contain the " |
| "attribute \"nonexistent\".")}})); |
| } |
| |
| TEST_F(EditCommandTest, SetSubcommand) { |
| // New bool attribute |
| EXPECT_SUCCESS(DoEdit("set testonly true", |
| R"( |
| executable("foo") { |
| } |
| )"), |
| Edited(R"( |
| executable("foo") { |
| testonly = true |
| } |
| )")); |
| |
| // Replacing existing attribute |
| EXPECT_SUCCESS(DoEdit("set testonly false", |
| R"( |
| executable("foo") { |
| testonly = true |
| } |
| )"), |
| Edited(R"( |
| executable("foo") { |
| testonly = false |
| } |
| )")); |
| |
| // String attribute |
| EXPECT_SUCCESS(DoEdit("set label \"//foo:bar\"", |
| R"( |
| executable("foo") { |
| } |
| )"), |
| Edited(R"( |
| executable("foo") { |
| label = "//foo:bar" |
| } |
| )")); |
| |
| // Int attribute |
| EXPECT_SUCCESS(DoEdit("set assert_no_deps 42", |
| R"( |
| executable("foo") { |
| } |
| )"), |
| Edited(R"( |
| executable("foo") { |
| assert_no_deps = 42 |
| } |
| )")); |
| |
| // Multiple values setting a list (replaces first, deletes modification, |
| // adds review for conditional) |
| EXPECT_SUCCESS(DoEdit("set deps //foo //bar", |
| R"( |
| executable("foo") { |
| deps = [ "//bar" ] |
| deps += [ "//baz" ] |
| if (is_linux) { |
| deps += [ "//linux" ] |
| } |
| } |
| )"), |
| Edited( |
| R"( |
| executable("foo") { |
| deps = [ |
| "//bar", |
| "//foo", |
| ] |
| |
| if (is_linux) { |
| # TODO(gn edit: set deps //foo //bar): This would normally be deleted but is |
| # conditional. Manual intervention is required to decide whether it should |
| # actually be deleted. |
| deps += [ "//linux" ] |
| } |
| } |
| )", |
| EditState({Label(SourceDir("//"), "foo")}))); |
| |
| // Forced list attribute (appends new list, adds review for conditional) |
| EXPECT_SUCCESS(DoEdit("set deps:list //foo", |
| R"( |
| executable("foo") { |
| if (is_linux) { |
| deps = [ "//linux" ] |
| public_deps = [ "//linux" ] |
| } |
| } |
| )"), |
| Edited( |
| R"( |
| executable("foo") { |
| if (is_linux) { |
| # TODO(gn edit: set deps:list //foo): This would normally be deleted but is |
| # conditional. Manual intervention is required to decide whether it should |
| # actually be deleted. |
| deps = [ "//linux" ] |
| public_deps = [ "//linux" ] |
| } |
| deps = [ "//foo" ] |
| } |
| )", |
| EditState({Label(SourceDir("//"), "foo")}))); |
| } |
| |
| TEST_F(EditCommandTest, ShardSubcommand) { |
| // Shards target across sources and subdirectory files, preserving |
| // conditionals, attributes, and visibility. |
| EXPECT_SUCCESS(DoEdit("shard", {"//:foo"}, |
| R"( |
| static_library("foo") { |
| sources = [ |
| "a.cc", |
| "a.h", |
| "foo.h", |
| "util/foo-bar.cc", |
| "util/foo-bar.h", |
| ] |
| if (is_win) { |
| sources += [ "c_win.cc" ] |
| } |
| defines = [ "ENABLE_FEATURE" ] |
| deps = [ "//base" ] |
| testonly = true |
| visibility = [ "//..." ] |
| } |
| )"), |
| Edited(R"( |
| group("foo") { |
| public_deps = [ |
| ":a", |
| ":c_win", |
| ":foo_foo", |
| ":util_foo_bar", |
| ] |
| |
| testonly = true |
| visibility = [ "//..." ] |
| } |
| static_library("a") { |
| sources = [ |
| "a.cc", |
| "a.h", |
| ] |
| if (is_win) { |
| sources += [] |
| } |
| defines = [ "ENABLE_FEATURE" ] |
| |
| testonly = true |
| visibility = [ "//..." ] |
| } |
| static_library("c_win") { |
| sources = [] |
| if (is_win) { |
| sources += [ "c_win.cc" ] |
| } |
| defines = [ "ENABLE_FEATURE" ] |
| |
| testonly = true |
| visibility = [ "//..." ] |
| } |
| static_library("foo_foo") { |
| sources = [ "foo.h" ] |
| if (is_win) { |
| sources += [] |
| } |
| defines = [ "ENABLE_FEATURE" ] |
| |
| testonly = true |
| visibility = [ "//..." ] |
| } |
| static_library("util_foo_bar") { |
| sources = [ |
| "util/foo-bar.cc", |
| "util/foo-bar.h", |
| ] |
| if (is_win) { |
| sources += [] |
| } |
| defines = [ "ENABLE_FEATURE" ] |
| |
| testonly = true |
| visibility = [ "//..." ] |
| } |
| )", |
| EditState({}, {}, |
| {Label(SourceDir("//"), "a"), |
| Label(SourceDir("//"), "c_win"), |
| Label(SourceDir("//"), "foo_foo"), |
| Label(SourceDir("//"), "util_foo_bar")}))); |
| |
| // Sharding with explicit shard target type and custom group type. |
| EXPECT_SUCCESS(DoEdit("shard source_set static_library", {"//:foo"}, |
| R"( |
| static_library("foo") { |
| sources = [ |
| "a.cc", |
| "b.cc", |
| ] |
| } |
| )"), |
| Edited(R"( |
| static_library("foo") { |
| public_deps = [ |
| ":a", |
| ":b", |
| ] |
| } |
| source_set("a") { |
| sources = [ "a.cc" ] |
| } |
| source_set("b") { |
| sources = [ "b.cc" ] |
| } |
| )", |
| EditState({}, {}, |
| {Label(SourceDir("//"), "a"), |
| Label(SourceDir("//"), "b")}))); |
| |
| // Single source target emits a warning and is not sharded. |
| EXPECT_SUCCESS( |
| DoEdit("shard", {"//:foo"}, |
| R"( |
| static_library("foo") { |
| sources = [ "foo.cc" ] |
| deps = [ "//base" ] |
| } |
| )"), |
| Edited(R"( |
| static_library("foo") { |
| sources = [ "foo.cc" ] |
| deps = [ "//base" ] |
| } |
| )", |
| EditState( |
| {}, {Err(Location(), |
| "Target \"//:foo\" does not need to be sharded.")}))); |
| |
| // Absolute source paths produce an error. |
| EXPECT_FAILURE(DoEdit("shard", {"//:foo"}, |
| R"( |
| static_library("foo") { |
| sources = [ |
| "//base/a.cc", |
| "b.cc", |
| ] |
| } |
| )")); |
| } |
| |
| } // namespace commands |