blob: ff16d3b133d05f9fd4ac17aee0088b6ffc54cc64 [file]
// 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