blob: 57a166f214cc0a5dcab69100918b8f7af4a3676d [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 <string>
#include <string_view>
#include <vector>
#include "base/command_line.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "gn/commands.h"
#include "gn/filesystem_utils.h"
#include "gn/input_file.h"
#include "gn/location.h"
#include "gn/setup.h"
#include "gn/standard_out.h"
#include "gn/switches.h"
#include "gn/target.h"
#include "gn/test_with_scheduler.h"
#include "gn/test_with_scope.h"
#include "util/test/test.h"
using SuggestTest = TestWithScheduler;
struct TestProject {
base::ScopedTempDir in_temp_dir;
base::ScopedTempDir build_temp_dir;
base::FilePath in_path;
base::FilePath build_path;
Setup setup;
TestProject(std::map<SourceFile, std::string> files) {
EXPECT_TRUE(in_temp_dir.CreateUniqueTempDir());
in_path = base::MakeAbsoluteFilePath(in_temp_dir.GetPath());
EXPECT_TRUE(build_temp_dir.CreateUniqueTempDir());
build_path = base::MakeAbsoluteFilePath(build_temp_dir.GetPath());
files.try_emplace(SourceFile("//.gn"),
"buildconfig = \"//BUILDCONFIG.gn\"\n");
files.try_emplace(SourceFile("//BUILDCONFIG.gn"), R"(
set_default_toolchain("//toolchain:default")
set_defaults("executable") {
include_dirs = [ "//" ]
}
set_defaults("source_set") {
include_dirs = [ "//" ]
}
)");
files.try_emplace(SourceFile("//toolchain/BUILD.gn"), R"(
toolchain("default") {
tool("cxx") {
command = "cxx"
outputs = [ "{{source_out_dir}}/{{source_file_part}}.o" ]
}
tool("link") {
command = "link"
outputs = [ "{{root_out_dir}}/{{target_output_name}}{{output_extension}}" ]
}
tool("stamp") {
command = "stamp"
}
}
)");
for (const auto& [file, content] : files) {
base::FilePath full_path = in_path.AppendASCII(file.value().substr(2));
base::CreateDirectory(full_path.DirName());
WriteFile(full_path, content, nullptr);
}
base::CommandLine cmdline(base::CommandLine::NO_PROGRAM);
cmdline.AppendSwitchPath(switches::kRoot, in_path);
EXPECT_TRUE(setup.DoSetup(FilePathToUTF8(build_path), true, cmdline));
EXPECT_TRUE(setup.Run());
}
TestProject(std::string build_gn)
: TestProject(std::map<SourceFile, std::string>{
{SourceFile("//BUILD.gn"), std::move(build_gn)}}) {}
std::vector<const Target*> targets() {
return setup.builder().GetAllResolvedTargets();
}
const Label& default_toolchain() {
return setup.loader()->default_toolchain_label();
}
std::string Read(const SourceFile& file) const {
std::string content;
base::FilePath full_path = in_path.AppendASCII(file.value().substr(2));
base::ReadFileToString(full_path, &content);
return content;
}
};
TEST_F(SuggestTest, ResolveModuleName) {
TestWithScope setup_scope;
SourceDir current_dir("//");
Label default_toolchain(SourceDir("//toolchain/"), "default");
Err err;
Target target(setup_scope.settings(), Label(SourceDir("//foo/"), "bar"));
target.set_module_name("my_module");
std::vector<const Target*> all_targets = {&target};
{
auto [results, ok] = commands::ResolveSuggestionToTarget(
setup_scope.build_settings(), all_targets, default_toolchain,
"my_module");
std::vector<std::pair<const Target*, commands::ApiScope>> expected = {
{&target, commands::ApiScope::kPublic}};
EXPECT_EQ(expected, results);
EXPECT_TRUE(ok);
}
// Test resolving module name "my_module_Private"
{
auto [results, ok] = commands::ResolveSuggestionToTarget(
setup_scope.build_settings(), all_targets, default_toolchain,
"my_module_Private");
std::vector<std::pair<const Target*, commands::ApiScope>> expected = {
{&target, commands::ApiScope::kPrivate}};
EXPECT_EQ(expected, results);
EXPECT_TRUE(ok);
}
}
TEST_F(SuggestTest, ResolveTargetName) {
TestWithScope setup_scope;
SourceDir current_dir("//");
Label default_toolchain = setup_scope.toolchain()->label();
Err err;
Target target(
setup_scope.settings(),
Label(SourceDir("//"), "hello", setup_scope.toolchain()->label().dir(),
setup_scope.toolchain()->label().name()));
Target target_gcc(
setup_scope.settings(),
Label(SourceDir("//"), "hello", SourceDir("//build/toolchain/"), "gcc"));
std::vector<const Target*> all_targets = {&target, &target_gcc};
// Test resolving "//:hello"
auto [results_label, ok_label] = commands::ResolveSuggestionToTarget(
setup_scope.build_settings(), all_targets,
setup_scope.toolchain()->label(), "//:hello");
std::vector<std::pair<const Target*, commands::ApiScope>> expected_label = {
{&target, commands::ApiScope::kPublic}};
EXPECT_EQ(expected_label, results_label);
EXPECT_TRUE(ok_label);
// Test resolving "//:hello(//build/toolchain:gcc)"
auto [results_toolchain, ok_toolchain] = commands::ResolveSuggestionToTarget(
setup_scope.build_settings(), all_targets, default_toolchain,
"//:hello(//build/toolchain:gcc)");
std::vector<std::pair<const Target*, commands::ApiScope>> expected_toolchain =
{{&target_gcc, commands::ApiScope::kPublic}};
EXPECT_EQ(expected_toolchain, results_toolchain);
EXPECT_TRUE(ok_toolchain);
}
TEST_F(SuggestTest, ResolveFileName) {
TestWithScope setup_scope;
SourceDir current_dir("//");
Label default_toolchain = setup_scope.toolchain()->label();
Label current_toolchain(SourceDir("//build/toolchain/"), "gcc");
Label secondary_toolchain(SourceDir("//build/toolchain/"), "clang");
Err err;
// Follow standard practice to create temporary directories in tests.
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
base::FilePath root_dir = temp_dir.GetPath();
setup_scope.build_settings()->SetRootPath(root_dir);
base::WriteFile(root_dir.AppendASCII("public.h"), "", 0);
base::WriteFile(root_dir.AppendASCII("private.h"), "", 0);
base::WriteFile(root_dir.AppendASCII("implicit_public.h"), "", 0);
base::WriteFile(root_dir.AppendASCII("no_target.h"), "", 0);
base::WriteFile(root_dir.AppendASCII("simple.h"), "", 0);
base::WriteFile(root_dir.AppendASCII("default_toolchain.h"), "", 0);
base::WriteFile(root_dir.AppendASCII("secondary_toolchain.h"), "", 0);
base::FilePath inc_dir = root_dir.AppendASCII("include_dir");
ASSERT_TRUE(base::CreateDirectory(inc_dir));
base::WriteFile(inc_dir.AppendASCII("my_header.h"), "", 0);
Target explicit_target(
setup_scope.settings(),
Label(SourceDir("//"), "explicit", current_toolchain.dir(),
current_toolchain.name()));
explicit_target.set_all_headers_public(false);
explicit_target.sources().push_back(SourceFile("//private.h"));
explicit_target.public_headers().push_back(SourceFile("//public.h"));
explicit_target.public_headers().push_back(
SourceFile("//nonexistent_file.h"));
Target implicit_target(
setup_scope.settings(),
Label(SourceDir("//"), "implicit", default_toolchain.dir(),
default_toolchain.name()));
implicit_target.set_all_headers_public(true);
implicit_target.sources().push_back(SourceFile("//implicit_public.h"));
implicit_target.sources().push_back(SourceFile("//private.cc"));
Target simple_default(
setup_scope.settings(),
Label(SourceDir("//"), "simple", default_toolchain.dir(),
default_toolchain.name()));
simple_default.public_headers().push_back(SourceFile("//public.h"));
simple_default.public_headers().push_back(
SourceFile("//default_toolchain.h"));
Target simple_secondary(
setup_scope.settings(),
Label(SourceDir("//"), "simple", secondary_toolchain.dir(),
secondary_toolchain.name()));
simple_secondary.public_headers().push_back(SourceFile("//public.h"));
simple_secondary.public_headers().push_back(
SourceFile("//default_toolchain.h"));
simple_secondary.public_headers().push_back(
SourceFile("//secondary_toolchain.h"));
Target generated(setup_scope.settings(),
Label(SourceDir("//"), "generated", current_toolchain.dir(),
current_toolchain.name()));
generated.set_output_type(Target::ACTION);
generated.SetToolchain(setup_scope.toolchain());
generated.action_values().outputs() =
SubstitutionList::MakeForTest("//out/Debug/generated_file.h");
Err resolve_err;
ASSERT_TRUE(generated.OnResolvedWithoutChecks(&resolve_err));
Target included_target(
setup_scope.settings(),
Label(SourceDir("//"), "included_target", current_toolchain.dir(),
current_toolchain.name()));
included_target.set_output_type(Target::SOURCE_SET);
included_target.SetToolchain(setup_scope.toolchain());
included_target.set_all_headers_public(true);
included_target.sources().push_back(SourceFile("//include_dir/my_header.h"));
ASSERT_TRUE(included_target.OnResolvedWithoutChecks(&resolve_err));
Target consumer(setup_scope.settings(),
Label(SourceDir("//"), "consumer", current_toolchain.dir(),
current_toolchain.name()));
consumer.set_output_type(Target::SOURCE_SET);
consumer.SetToolchain(setup_scope.toolchain());
consumer.set_all_headers_public(true);
consumer.public_headers().push_back(
SourceFile("//out/Debug/generated_file.h"));
consumer.config_values().include_dirs().push_back(
SourceDir("//include_dir/"));
ASSERT_TRUE(consumer.OnResolvedWithoutChecks(&resolve_err));
std::vector<const Target*> all_targets = {&explicit_target, &implicit_target,
&simple_default, &simple_secondary,
&generated, &included_target};
{
auto [results, ok] = commands::ResolveSuggestionToTarget(
setup_scope.build_settings(), all_targets, current_toolchain,
"//public.h");
std::vector<std::pair<const Target*, commands::ApiScope>> expected = {
{&explicit_target, commands::ApiScope::kPublic}};
EXPECT_TRUE(ok);
EXPECT_EQ(expected, results);
}
{
auto [results, ok] = commands::ResolveSuggestionToTarget(
setup_scope.build_settings(), all_targets, current_toolchain,
"../../private.h");
std::vector<std::pair<const Target*, commands::ApiScope>> expected = {
{&explicit_target, commands::ApiScope::kPrivate}};
EXPECT_TRUE(ok);
EXPECT_EQ(expected, results);
}
{
auto [results, ok] = commands::ResolveSuggestionToTarget(
setup_scope.build_settings(), all_targets, current_toolchain,
"//implicit_public.h");
std::vector<std::pair<const Target*, commands::ApiScope>> expected = {
{&implicit_target, commands::ApiScope::kPublic}};
EXPECT_TRUE(ok);
EXPECT_EQ(expected, results);
}
{
auto [results, ok] = commands::ResolveSuggestionToTarget(
setup_scope.build_settings(), all_targets, current_toolchain,
"nonexistent_file.h");
EXPECT_FALSE(ok);
}
{
auto [results, ok] = commands::ResolveSuggestionToTarget(
setup_scope.build_settings(), all_targets, current_toolchain,
"//out/Debug/generated_file.h");
std::vector<std::pair<const Target*, commands::ApiScope>> expected = {
{&generated, commands::ApiScope::kPublic}};
EXPECT_TRUE(ok);
EXPECT_EQ(expected, results);
}
all_targets.push_back(&consumer);
{
auto [results, ok] = commands::ResolveSuggestionToTarget(
setup_scope.build_settings(), all_targets, current_toolchain,
"//out/Debug/generated_file.h");
std::vector<std::pair<const Target*, commands::ApiScope>> expected = {
{&consumer, commands::ApiScope::kPublic}};
EXPECT_TRUE(ok);
EXPECT_EQ(expected, results);
}
{
auto [results, ok] = commands::ResolveSuggestionToTarget(
setup_scope.build_settings(), all_targets, current_toolchain,
"//no_target.h");
std::vector<std::pair<const Target*, commands::ApiScope>> expected_targets;
EXPECT_TRUE(ok);
EXPECT_EQ(expected_targets, results);
}
{
auto [results, ok] = commands::ResolveSuggestionToTarget(
setup_scope.build_settings(), all_targets, current_toolchain,
"//default_toolchain.h");
std::vector<std::pair<const Target*, commands::ApiScope>> expected_targets =
{
{&simple_secondary, commands::ApiScope::kPublic},
{&simple_default, commands::ApiScope::kPublic},
};
EXPECT_TRUE(ok);
EXPECT_EQ(expected_targets, results);
}
{
auto [results, ok] = commands::ResolveSuggestionToTarget(
setup_scope.build_settings(), all_targets, current_toolchain,
"//secondary_toolchain.h");
std::vector<std::pair<const Target*, commands::ApiScope>> expected_targets =
{{{&simple_secondary, commands::ApiScope::kPublic}}};
EXPECT_TRUE(ok);
EXPECT_EQ(expected_targets, results);
}
{
auto [results, ok] = commands::ResolveSuggestionToTarget(
setup_scope.build_settings(), all_targets, current_toolchain,
"my_header.h", &consumer);
EXPECT_TRUE(ok);
std::vector<std::pair<const Target*, commands::ApiScope>> expected_targets =
{{{&included_target, commands::ApiScope::kPublic}}};
EXPECT_EQ(expected_targets, results);
}
}
TEST_F(SuggestTest, OutputSuggestions) {
TestWithScope setup_scope;
Label default_toolchain = setup_scope.toolchain()->label();
InputFile build_file(SourceFile("//BUILD.gn"));
Location dummy_loc(&build_file, 1, 1);
std::vector<const Target*> all_targets;
auto set_visibility = [&](Target* target, std::string_view pattern) {
Value visibility_value(nullptr, Value::LIST);
visibility_value.list_value().push_back(
Value(nullptr, std::string(pattern)));
Err err;
EXPECT_TRUE(
target->visibility().Set(SourceDir("//"), "", visibility_value, &err));
};
auto create_target = [&](std::string_view name, Target::OutputType type,
auto fn) {
auto target = std::make_unique<Target>(
setup_scope.settings(),
Label(SourceDir("//"), name, default_toolchain.dir(),
default_toolchain.name()));
target->set_output_type(type);
target->SetToolchain(setup_scope.toolchain());
target->set_user_friendly_location(dummy_loc);
if (type == Target::SOURCE_SET) {
Target::ModuleType module_type;
module_type.set(Target::HAS_MODULEMAP);
target->set_module_type(module_type);
target->set_module_name(std::string(name));
target->public_headers().push_back(
SourceFile("//" + std::string(name) + ".h"));
}
fn(target.get());
Err err;
EXPECT_TRUE(target->OnResolvedWithoutChecks(&err));
all_targets.push_back(target.get());
return target;
};
auto includer = create_target("includer", Target::GROUP, [](Target*) {});
auto run_suggest = [&](std::string_view want) {
std::string output;
auto collect = [&](std::string_view s, TextDecoration, HtmlEscaping) {
output.append(s);
};
commands::OutputSuggestions(all_targets, setup_scope.build_settings(),
default_toolchain, "//:includer", want,
collect);
return output;
};
auto visible = create_target("visible", Target::SOURCE_SET,
[&](Target* t) { t->visibility().SetPublic(); });
auto visible_group =
create_target("visible_group", Target::GROUP, [&](Target* t) {
t->public_deps().push_back(LabelTargetPair(visible.get()));
t->visibility().SetPublic();
});
// Prefer the real target over the group that exposes it.
EXPECT_EQ(
"Suggestion: Add public_deps = [ \":visible\" ] to :includer (defined "
"at //BUILD.gn:1)\n"
" (`gn edit \"add public_deps :visible\" //:includer`)\n",
run_suggest(visible->module_name()));
auto invisible =
create_target("invisible", Target::SOURCE_SET, [&](Target* t) {});
EXPECT_EQ(
"Warning: //:invisible is not visible to //:includer\n"
"Suggestion: Carefully consider whether you want to change the "
"visibility so that you can depend on it\n"
"Suggestion: Add public_deps = [ \":invisible\" ] to :includer (defined "
"at //BUILD.gn:1)\n"
" (`gn edit \"add public_deps :invisible\" //:includer`)\n",
run_suggest(invisible->module_name()));
auto exposer_invisible =
create_target("exposer_invisible", Target::GROUP, [&](Target* t) {
t->private_deps().push_back(LabelTargetPair(invisible.get()));
});
EXPECT_EQ(
"Warning: //:invisible is exposed via the following targets, but none "
"are visible to //:includer\n"
"Suggestion: Carefully consider whether you want to change the "
"visibility so that you can depend on one of them\n"
"Suggestion: Add one of the following to public_deps in :includer "
"(defined at //BUILD.gn:1):\n"
"* :exposer_invisible (`gn edit \"add public_deps :exposer_invisible\" "
"//:includer`)\n"
"* :invisible (`gn edit \"add public_deps :invisible\" //:includer`)\n",
run_suggest(invisible->module_name()));
auto exposer_visible =
create_target("exposer_visible", Target::GROUP, [&](Target* t) {
t->private_deps().push_back(LabelTargetPair(invisible.get()));
t->visibility().SetPublic();
});
EXPECT_EQ(
"Suggestion: Add public_deps = [ \":exposer_visible\" ] to :includer "
"(defined at //BUILD.gn:1)\n"
" (`gn edit \"add public_deps :exposer_visible\" //:includer`)\n",
run_suggest(invisible->module_name()));
auto exposer_visible2 =
create_target("exposer_visible2", Target::GROUP, [&](Target* t) {
t->private_deps().push_back(LabelTargetPair(exposer_visible.get()));
t->visibility().SetPublic();
});
EXPECT_EQ(
"Warning: //:invisible is exposed via multiple targets\n"
"Suggestion: Clean up the visibility so that only one of the below "
"targets is visible to //:includer\n"
"Suggestion: Add one of the following to public_deps in :includer "
"(defined at //BUILD.gn:1):\n"
"* :exposer_visible (`gn edit \"add public_deps :exposer_visible\" "
"//:includer`)\n"
"* :exposer_visible2 (`gn edit \"add public_deps :exposer_visible2\" "
"//:includer`)\n",
run_suggest(invisible->module_name()));
auto exposer_specific =
create_target("exposer_specific", Target::GROUP, [&](Target* t) {
t->private_deps().push_back(LabelTargetPair(exposer_visible.get()));
set_visibility(t, "//:includer");
});
EXPECT_EQ(
"Suggestion: Add public_deps = [ \":exposer_specific\" ] to :includer "
"(defined at //BUILD.gn:1)\n"
" (`gn edit \"add public_deps :exposer_specific\" //:includer`)\n",
run_suggest(invisible->module_name()));
auto cyclic = create_target("cyclic", Target::SOURCE_SET, [&](Target* t) {
t->public_deps().push_back(LabelTargetPair(includer.get()));
t->visibility().SetPublic();
});
EXPECT_EQ(
"Warning: //:cyclic depends on //:includer, so adding this dependency "
"will create a dependency loop:\n"
" //:includer ->\n"
" //:cyclic ->\n"
" //:includer\n"
"Suggestion: Find the part of the dependency chain where there is no "
"#include and remove that dependency.\n"
"Suggestion: Add public_deps = [ \":cyclic\" ] to :includer (defined at "
"//BUILD.gn:1)\n"
" (`gn edit \"add public_deps :cyclic\" //:includer`)\n",
run_suggest(cyclic->module_name()));
auto cyclic_circular_includes = create_target(
"cyclic_circular_includes", Target::STATIC_LIBRARY, [&](Target* t) {
t->public_deps().push_back(LabelTargetPair(includer.get()));
t->visibility().SetPublic();
t->allow_circular_includes_from().insert(includer->label());
});
EXPECT_EQ(
"Warning: //:cyclic_circular_includes depends on //:includer, so adding "
"this "
"dependency will create a dependency loop:\n"
" //:includer ->\n"
" //:cyclic_circular_includes ->\n"
" //:includer\n"
"Suggestion: :cyclic_circular_includes (defined at //BUILD.gn:1) "
"declares "
"allow_circular_includes_from, which is bad style. Instead, you should "
"remove allow_circular_includes_from by doing the following:\n"
"source_set(\"cyclic_circular_includes_sources\") {\n"
" # All attributes from :cyclic_circular_includes except public_deps, "
"and any link options\n"
" # Note that some public_deps may need to be added back based on "
"#includes of headers.\n"
"}\n"
"\n"
"static_library(\"cyclic_circular_includes\") {\n"
" public_deps = [ \":cyclic_circular_includes_sources\" ]\n"
" # public_deps, and any link variables from :cyclic_circular_includes\n"
"}\n"
"Suggestion: Add public_deps = [ \":cyclic_circular_includes_sources\" ] "
"to :includer (defined at //BUILD.gn:1)\n"
" (`gn edit \"add public_deps :cyclic_circular_includes_sources\" "
"//:includer`)\n",
run_suggest(cyclic_circular_includes->module_name()));
auto private_target =
create_target("private_target", Target::SOURCE_SET, [&](Target* t) {
t->set_all_headers_public(false);
t->sources().push_back(SourceFile("//private_target.h"));
t->visibility().SetPublic();
});
EXPECT_EQ(
"Warning: \"private_target_Private\" is in the private API of "
"//:private_target\n"
"Suggestion: Move \"private_target.h\" from `sources` to `public` "
"in :private_target (defined at //BUILD.gn:1)\n"
" (`gn edit \"move sources public private_target.h\" "
"//:private_target`)\n"
"Suggestion: Add public_deps = [ \":private_target\" ] to :includer "
"(defined at //BUILD.gn:1)\n"
" (`gn edit \"add public_deps :private_target\" //:includer`)\n",
run_suggest("private_target_Private"));
}
TEST_F(SuggestTest, ApplyValidSuggestion) {
TestProject project({
{SourceFile("//BUILD.gn"), R"(executable("includer") {
sources = [ "includer.cc" ]
}
source_set("included") {
sources = [ "included.h" ]
}
)"},
{SourceFile("//includer.cc"), ""},
{SourceFile("//included.h"), ""},
});
std::string output;
auto collect = [&](std::string_view s, TextDecoration, HtmlEscaping) {
output.append(s);
};
commands::SuggestResult result = commands::OutputSuggestions(
project.targets(), &project.setup.build_settings(),
project.default_toolchain(), "//includer.cc", "//included.h", collect,
true, &project.setup);
EXPECT_EQ(commands::SuggestResult::kSuccess, result);
EXPECT_EQ(
"[APPLIED] Suggestion: Add deps = [ \":included\" ] to :includer "
"(defined at //BUILD.gn:1)\n",
output);
std::string expected_build_gn = R"(executable("includer") {
sources = [ "includer.cc" ]
deps = [ ":included" ]
}
source_set("included") {
sources = [ "included.h" ]
}
)";
EXPECT_EQ(expected_build_gn, project.Read(SourceFile("//BUILD.gn")));
}
TEST_F(SuggestTest, CheckAppliesSuggestions) {
TestProject project({
{SourceFile("//BUILD.gn"), R"(
group("all") {
deps = [
"//included",
"//includer",
]
}
)"},
{SourceFile("//includer/BUILD.gn"), R"(executable("includer") {
sources = [ "includer.cc" ]
}
)"},
{SourceFile("//included/BUILD.gn"), R"(source_set("included") {
sources = [ "included.h" ]
}
)"},
{SourceFile("//includer/includer.cc"),
"#include \"included/included.h\""},
{SourceFile("//included/included.h"), ""},
});
std::string output;
auto collect = [&](std::string_view s, TextDecoration, HtmlEscaping) {
output.append(s);
};
EXPECT_TRUE(commands::CheckPublicHeaders(
&project.setup.build_settings(), project.targets(), project.targets(),
false, false, false, true, &project.setup, collect));
EXPECT_EQ(
"ERROR at //includer/includer.cc:1:11: Include not allowed.\n"
"#include \"included/included.h\"\n"
" ^\n"
"[APPLIED] Suggestion: Add deps = [ \"//included:included\" ] to "
":includer (defined at //includer/BUILD.gn:1)\n",
output);
std::string expected_build_gn = R"(executable("includer") {
sources = [ "includer.cc" ]
deps = [ "//included" ]
}
)";
EXPECT_EQ(expected_build_gn, project.Read(SourceFile("//includer/BUILD.gn")));
}