// Copyright 2018 The Chromium 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/setup.h"

#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "gn/builder_record.h"
#include "gn/filesystem_utils.h"
#include "gn/switches.h"
#include "gn/test_with_scheduler.h"
#include "util/build_config.h"

using SetupTest = TestWithScheduler;

static void WriteFile(const base::FilePath& file, const std::string& data) {
  CHECK_EQ(static_cast<int>(data.size()),  // Way smaller than INT_MAX.
           base::WriteFile(file, data.data(), data.size()));
}

TEST_F(SetupTest, DotGNFileIsGenDep) {
  base::CommandLine cmdline(base::CommandLine::NO_PROGRAM);

  // Create a temp directory containing a .gn file and a BUILDCONFIG.gn file,
  // pass it as --root.
  base::ScopedTempDir in_temp_dir;
  ASSERT_TRUE(in_temp_dir.CreateUniqueTempDir());
  base::FilePath in_path = base::MakeAbsoluteFilePath(in_temp_dir.GetPath());
  base::FilePath dot_gn_name = in_path.Append(FILE_PATH_LITERAL(".gn"));
  WriteFile(dot_gn_name, "buildconfig = \"//BUILDCONFIG.gn\"\n");
  WriteFile(in_path.Append(FILE_PATH_LITERAL("BUILDCONFIG.gn")), "");
  cmdline.AppendSwitchPath(switches::kRoot, in_path);

  // Create another temp dir for writing the generated files to.
  base::ScopedTempDir build_temp_dir;
  ASSERT_TRUE(build_temp_dir.CreateUniqueTempDir());

  // Run setup and check that the .gn file is in the scheduler's gen deps.
  Setup setup;
  EXPECT_TRUE(
      setup.DoSetup(FilePathToUTF8(build_temp_dir.GetPath()), true, cmdline));
  std::vector<base::FilePath> gen_deps = g_scheduler->GetGenDependencies();
  ASSERT_EQ(1u, gen_deps.size());
  EXPECT_EQ(gen_deps[0], base::MakeAbsoluteFilePath(dot_gn_name));
}

TEST_F(SetupTest, EmptyScriptExecutableDoesNotGenerateError) {
  base::CommandLine cmdline(base::CommandLine::NO_PROGRAM);

  const char kDotfileContents[] = R"(
buildconfig = "//BUILDCONFIG.gn"
script_executable = ""
)";

  // Create a temp directory containing a .gn file and a BUILDCONFIG.gn file,
  // pass it as --root.
  base::ScopedTempDir in_temp_dir;
  ASSERT_TRUE(in_temp_dir.CreateUniqueTempDir());
  base::FilePath in_path = base::MakeAbsoluteFilePath(in_temp_dir.GetPath());
  base::FilePath dot_gn_name = in_path.Append(FILE_PATH_LITERAL(".gn"));
  WriteFile(dot_gn_name, kDotfileContents);

  WriteFile(in_path.Append(FILE_PATH_LITERAL("BUILDCONFIG.gn")), "");
  cmdline.AppendSwitchPath(switches::kRoot, in_path);

  // Create another temp dir for writing the generated files to.
  base::ScopedTempDir build_temp_dir;
  ASSERT_TRUE(build_temp_dir.CreateUniqueTempDir());

  // Run setup and check that the .gn file is in the scheduler's gen deps.
  Setup setup;
  Err err;
  EXPECT_TRUE(setup.DoSetupWithErr(FilePathToUTF8(build_temp_dir.GetPath()),
                                   true, cmdline, &err));
}

#if defined(OS_WIN)
TEST_F(SetupTest, MissingScriptExeGeneratesSetupErrorOnWindows) {
  base::CommandLine cmdline(base::CommandLine::NO_PROGRAM);

  const char kDotfileContents[] = R"(
buildconfig = "//BUILDCONFIG.gn"
script_executable = "this_does_not_exist"
)";

  // Create a temp directory containing a .gn file and a BUILDCONFIG.gn file,
  // pass it as --root.
  base::ScopedTempDir in_temp_dir;
  ASSERT_TRUE(in_temp_dir.CreateUniqueTempDir());
  base::FilePath in_path = base::MakeAbsoluteFilePath(in_temp_dir.GetPath());
  base::FilePath dot_gn_name = in_path.Append(FILE_PATH_LITERAL(".gn"));
  WriteFile(dot_gn_name, kDotfileContents);

  WriteFile(in_path.Append(FILE_PATH_LITERAL("BUILDCONFIG.gn")), "");
  cmdline.AppendSwitchPath(switches::kRoot, in_path);

  // Create another temp dir for writing the generated files to.
  base::ScopedTempDir build_temp_dir;
  ASSERT_TRUE(build_temp_dir.CreateUniqueTempDir());

  // Run setup and check that the .gn file is in the scheduler's gen deps.
  Setup setup;
  Err err;
  EXPECT_FALSE(setup.DoSetupWithErr(FilePathToUTF8(build_temp_dir.GetPath()),
                                    true, cmdline, &err));
  EXPECT_TRUE(err.has_error());
}
#endif  // defined(OS_WIN)

static void RunExtensionCheckTest(std::string extension,
                                  bool success,
                                  const std::string& expected_error_message) {
  base::CommandLine cmdline(base::CommandLine::NO_PROGRAM);

  // Create a temp directory containing a .gn file and a BUILDCONFIG.gn file,
  // pass it as --root.
  base::ScopedTempDir in_temp_dir;
  ASSERT_TRUE(in_temp_dir.CreateUniqueTempDir());
  base::FilePath in_path = base::MakeAbsoluteFilePath(in_temp_dir.GetPath());
  base::FilePath dot_gn_name = in_path.Append(FILE_PATH_LITERAL(".gn"));
  WriteFile(dot_gn_name,
            "buildconfig = \"//BUILDCONFIG.gn\"\n\
      build_file_extension = \"" +
                extension + "\"");
  WriteFile(in_path.Append(FILE_PATH_LITERAL("BUILDCONFIG.gn")), "");
  cmdline.AppendSwitchPath(switches::kRoot, in_path);

  // Create another temp dir for writing the generated files to.
  base::ScopedTempDir build_temp_dir;
  ASSERT_TRUE(build_temp_dir.CreateUniqueTempDir());

  // Run setup and check that its status.
  Setup setup;
  Err err;
  EXPECT_EQ(success,
            setup.DoSetupWithErr(FilePathToUTF8(build_temp_dir.GetPath()), true,
                                 cmdline, &err));
  EXPECT_EQ(success, !err.has_error());
}

TEST_F(SetupTest, NoSeparatorInExtension) {
  RunExtensionCheckTest(
      "hello" + std::string(1, base::FilePath::kSeparators[0]) + "world", false,
#if defined(OS_WIN)
      "Build file extension 'hello\\world' cannot contain a path separator"
#else
      "Build file extension 'hello/world' cannot contain a path separator"
#endif
  );
}

TEST_F(SetupTest, Extension) {
  RunExtensionCheckTest("yay", true, "");
}

TEST_F(SetupTest, AddExportCompileCommands) {
  base::CommandLine cmdline(base::CommandLine::NO_PROGRAM);

  // Provide a project default export compile command list.
  const char kDotfileContents[] = R"(
buildconfig = "//BUILDCONFIG.gn"
export_compile_commands = [ "//base/*" ]
)";

  // Create a temp directory containing the build.
  base::ScopedTempDir in_temp_dir;
  ASSERT_TRUE(in_temp_dir.CreateUniqueTempDir());
  base::FilePath in_path = base::MakeAbsoluteFilePath(in_temp_dir.GetPath());
  base::FilePath dot_gn_name = in_path.Append(FILE_PATH_LITERAL(".gn"));
  WriteFile(dot_gn_name, kDotfileContents);

  WriteFile(in_path.Append(FILE_PATH_LITERAL("BUILDCONFIG.gn")), "");
  cmdline.AppendSwitch(switches::kRoot, FilePathToUTF8(in_path));

  // Two additions to the compile commands list.
  cmdline.AppendSwitch(switches::kAddExportCompileCommands,
                       "//tools:doom_melon");
  cmdline.AppendSwitch(switches::kAddExportCompileCommands, "//src/gn:*");

  // Create another temp dir for writing the generated files to.
  base::ScopedTempDir build_temp_dir;
  ASSERT_TRUE(build_temp_dir.CreateUniqueTempDir());

  // Run setup and check that the .gn file is in the scheduler's gen deps.
  Setup setup;
  Err err;
  EXPECT_TRUE(setup.DoSetupWithErr(FilePathToUTF8(build_temp_dir.GetPath()),
                                   true, cmdline, &err));

  // The export compile commands should have three items.
  const std::vector<LabelPattern>& export_cc = setup.export_compile_commands();
  ASSERT_EQ(3u, export_cc.size());
  EXPECT_EQ("//base/*", export_cc[0].Describe());
  EXPECT_EQ("//tools:doom_melon", export_cc[1].Describe());
  EXPECT_EQ("//src/gn:*", export_cc[2].Describe());
}

TEST_F(SetupTest, RootPatternsInGnConfig) {
  base::CommandLine cmdline(base::CommandLine::NO_PROGRAM);

  // Provide a default root pattern for all top-level targets from //BUILD.gn
  const char kDotfileContents[] = R"(
buildconfig = "//BUILDCONFIG.gn"
root_patterns = [ "//:*" ]
)";

  // Create a temp directory containing the build.
  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("BUILDCONFIG.gn")), "");
  WriteFile(in_path.Append(FILE_PATH_LITERAL(".gn")), kDotfileContents);

  cmdline.AppendSwitch(switches::kRoot, FilePathToUTF8(in_path));

  // Create another temp dir for writing the generated files to.
  base::ScopedTempDir build_temp_dir;
  ASSERT_TRUE(build_temp_dir.CreateUniqueTempDir());

  // Run setup and check that the .gn file is in the scheduler's gen deps.
  Setup setup;
  Err err;
  EXPECT_TRUE(setup.DoSetupWithErr(FilePathToUTF8(build_temp_dir.GetPath()),
                                   true, cmdline, &err));

  const std::vector<LabelPattern>& root_patterns =
      setup.build_settings().root_patterns();
  ASSERT_EQ(1u, root_patterns.size());
  EXPECT_EQ("//.:*", root_patterns[0].Describe());
}

TEST_F(SetupTest, RootPatternsOnCommandLineOverrideGnConfig) {
  base::CommandLine cmdline(base::CommandLine::NO_PROGRAM);

  // Provide a default root pattern for only //:foo
  const char kDotfileContents[] = R"(
buildconfig = "//BUILDCONFIG.gn"
root_patterns = [ "//:foo" ]
)";

  // Create a temp directory containing the build.
  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("BUILDCONFIG.gn")), "");
  WriteFile(in_path.Append(FILE_PATH_LITERAL(".gn")), kDotfileContents);

  cmdline.AppendSwitch(switches::kRoot, FilePathToUTF8(in_path));

  // Override the default root pattern list.
  cmdline.AppendSwitch(switches::kRootPattern, "//:bar");
  cmdline.AppendSwitch(switches::kRootPattern, "//:qux");

  // Create another temp dir for writing the generated files to.
  base::ScopedTempDir build_temp_dir;
  ASSERT_TRUE(build_temp_dir.CreateUniqueTempDir());

  // Run setup and check that the .gn file is in the scheduler's gen deps.
  Setup setup;
  Err err;
  EXPECT_TRUE(setup.DoSetupWithErr(FilePathToUTF8(build_temp_dir.GetPath()),
                                   true, cmdline, &err));

  const std::vector<LabelPattern>& root_patterns =
      setup.build_settings().root_patterns();
  ASSERT_EQ(2u, root_patterns.size());
  EXPECT_EQ("//.:bar", root_patterns[0].Describe());
  EXPECT_EQ("//.:qux", root_patterns[1].Describe());
}

TEST_F(SetupTest, RootPatternsFiltersPatterns) {
  base::CommandLine cmdline(base::CommandLine::NO_PROGRAM);

  const char kDotfileContents[] = R"(
buildconfig = "//BUILDCONFIG.gn"
root_patterns = [ "//:foo" ]
)";

  const char kBuildConfigContents[] = R"(
set_default_toolchain("//:toolchain")
)";

  const char kBuildGnContents[] = R"(
group("foo") {
  deps = [ ":bar" ]
}

group("bar") {
}

group("zoo") {
}

group("qux") {
}

# Minimal default toolchain definition for this test. Non-functional.
toolchain("toolchain") {
  tool("stamp") {
    command = "stamp"
  }
}
)";

  // Create a temp directory containing the build.
  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("BUILD.gn")), kBuildGnContents);
  WriteFile(in_path.Append(FILE_PATH_LITERAL("BUILDCONFIG.gn")),
            kBuildConfigContents);
  WriteFile(in_path.Append(FILE_PATH_LITERAL(".gn")), kDotfileContents);

  cmdline.AppendSwitch(switches::kRoot, FilePathToUTF8(in_path));

  // Create another temp dir for writing the generated files to.
  base::ScopedTempDir build_temp_dir;
  ASSERT_TRUE(build_temp_dir.CreateUniqueTempDir());

  // Run setup and check that the .gn file is in the scheduler's gen deps.
  Setup setup;
  Err err;
  EXPECT_TRUE(setup.DoSetupWithErr(FilePathToUTF8(build_temp_dir.GetPath()),
                                   true, cmdline, &err));

  const std::vector<LabelPattern>& root_patterns =
      setup.build_settings().root_patterns();
  ASSERT_EQ(1u, root_patterns.size());
  EXPECT_EQ("//.:foo", root_patterns[0].Describe());

  // Now build the graph, then verify it only includes //:foo and //:bar
  ASSERT_TRUE(setup.Run(cmdline));

  SourceDir top_dir("//");

  const BuilderRecord* foo_record =
      setup.builder().GetRecord(Label(top_dir, "foo", top_dir, "toolchain"));
  const BuilderRecord* bar_record =
      setup.builder().GetRecord(Label(top_dir, "bar", top_dir, "toolchain"));
  const BuilderRecord* qux_record =
      setup.builder().GetRecord(Label(top_dir, "qux", top_dir, "toolchain"));
  const BuilderRecord* zoo_record =
      setup.builder().GetRecord(Label(top_dir, "zoo", top_dir, "toolchain"));

  // All four targets were added as build graph records.
  ASSERT_TRUE(foo_record);
  ASSERT_TRUE(bar_record);
  ASSERT_TRUE(zoo_record);
  ASSERT_TRUE(qux_record);

  // But only foo and bar should be generated in the Ninja plan.
  EXPECT_TRUE(foo_record->should_generate());
  EXPECT_TRUE(bar_record->should_generate());
  EXPECT_FALSE(qux_record->should_generate());
  EXPECT_FALSE(zoo_record->should_generate());
}

TEST_F(SetupTest, ArgsGnRelativeAndAbsoluteImports) {
  base::CommandLine cmdline(base::CommandLine::NO_PROGRAM);

  const char kDotfileContents[] = R"(
    buildconfig = "//BUILDCONFIG.gn"
    script_executable = ""
  )";

  // Create a temp directory containing a .gn file and a BUILDCONFIG.gn file,
  // pass it as --root.
  base::ScopedTempDir in_temp_dir;
  ASSERT_TRUE(in_temp_dir.CreateUniqueTempDir());
  base::FilePath in_path = base::MakeAbsoluteFilePath(in_temp_dir.GetPath());
  base::FilePath dot_gn_name = in_path.Append(FILE_PATH_LITERAL(".gn"));
  WriteFile(dot_gn_name, kDotfileContents);

  WriteFile(in_path.Append(FILE_PATH_LITERAL("BUILDCONFIG.gn")), "");
  cmdline.AppendSwitchPath(switches::kRoot, in_path);

  WriteFile(in_path.Append(FILE_PATH_LITERAL("build_defines.gni")),
            "variable1 = true");

  // Create another temp dir and write the args.gn with imports.
  base::ScopedTempDir build_temp_dir;
  ASSERT_TRUE(build_temp_dir.CreateUniqueTempDir());
  WriteFile(build_temp_dir.GetPath().Append(FILE_PATH_LITERAL("args.gn")), R"(
    import("//build_defines.gni")
    import("params.gni")
  )");
  WriteFile(build_temp_dir.GetPath().Append(FILE_PATH_LITERAL("params.gni")),
            "variable2 = true");

  // Run setup and check that the args.gn imports are in dependency files.
  Setup setup;
  Err err;
  EXPECT_TRUE(setup.DoSetupWithErr(FilePathToUTF8(build_temp_dir.GetPath()),
                                   true, cmdline, &err));

  const auto& dependency_files =
      setup.build_settings().build_args().build_args_dependency_files();
  ASSERT_EQ(2u, dependency_files.size());
  const auto dependency_includes_file = [&](std::string_view file_name) {
    return std::any_of(
        dependency_files.begin(), dependency_files.end(),
        [&](const SourceFile& file) { return file.GetName() == file_name; });
  };
  EXPECT_TRUE(dependency_includes_file("build_defines.gni"));
  EXPECT_TRUE(dependency_includes_file("params.gni"));
}

TEST_F(SetupTest, AbsolutePythonPathInsideRootDir) {
  base::CommandLine cmdline(base::CommandLine::NO_PROGRAM);

  const char kDotfileContents[] = R"(
buildconfig = "//BUILDCONFIG.gn"
script_executable = ""
)";

  // Create a temp directory containing a .gn file and a BUILDCONFIG.gn file,
  // pass it as --root.
  base::ScopedTempDir in_temp_dir;
  ASSERT_TRUE(in_temp_dir.CreateUniqueTempDir());
  base::FilePath in_path = base::MakeAbsoluteFilePath(in_temp_dir.GetPath());
  base::FilePath dot_gn_name = in_path.Append(FILE_PATH_LITERAL(".gn"));
  WriteFile(dot_gn_name, kDotfileContents);

  base::FilePath absolute_bin_dir = in_path.Append(FILE_PATH_LITERAL("bin"));
  base::FilePath script_executable =
      absolute_bin_dir.Append(FILE_PATH_LITERAL("python"));

  WriteFile(in_path.Append(FILE_PATH_LITERAL("BUILDCONFIG.gn")), "");
  cmdline.AppendSwitchPath(switches::kRoot, in_path);
  cmdline.AppendSwitchPath(switches::kScriptExecutable, script_executable);

  base::FilePath build_dir = in_path.Append(FILE_PATH_LITERAL("out"));

  // Run setup and check that python_path_is_relative_to_build_dir is true and
  // python_path is relative to the build directory.
  Setup setup;
  Err err;
  EXPECT_TRUE(
      setup.DoSetupWithErr(FilePathToUTF8(build_dir), true, cmdline, &err));
  EXPECT_TRUE(setup.build_settings().python_path_is_relative_to_build_dir());
  EXPECT_EQ(setup.build_settings().python_path(),
            base::FilePath(FILE_PATH_LITERAL("../bin/python")));
}

TEST_F(SetupTest, AbsolutePythonPathOutsideRootDir) {
  base::CommandLine cmdline(base::CommandLine::NO_PROGRAM);

  const char kDotfileContents[] = R"(
buildconfig = "//BUILDCONFIG.gn"
script_executable = ""
)";

  // Create a temp directory containing a .gn file and a BUILDCONFIG.gn file,
  // pass it as --root.
  base::ScopedTempDir in_temp_dir;
  ASSERT_TRUE(in_temp_dir.CreateUniqueTempDir());
  base::FilePath in_path = base::MakeAbsoluteFilePath(in_temp_dir.GetPath());
  base::FilePath dot_gn_name = in_path.Append(FILE_PATH_LITERAL(".gn"));
  WriteFile(dot_gn_name, kDotfileContents);

  base::ScopedTempDir other_temp_dir;
  ASSERT_TRUE(other_temp_dir.CreateUniqueTempDir());
  base::FilePath absolute_bin_dir =
      other_temp_dir.GetPath().Append(FILE_PATH_LITERAL("bin"));
  base::FilePath script_executable =
      absolute_bin_dir.Append(FILE_PATH_LITERAL("python"));

  WriteFile(in_path.Append(FILE_PATH_LITERAL("BUILDCONFIG.gn")), "");
  cmdline.AppendSwitchPath(switches::kRoot, in_path);
  cmdline.AppendSwitchPath(switches::kScriptExecutable, script_executable);

  base::FilePath build_dir = in_path.Append(FILE_PATH_LITERAL("out"));

  // Run setup and check that python_path_is_relative_to_build_dir is false and
  // python_path is unchanged (apart from normalization).
  Setup setup;
  Err err;
  EXPECT_TRUE(
      setup.DoSetupWithErr(FilePathToUTF8(build_dir), true, cmdline, &err));
  EXPECT_FALSE(setup.build_settings().python_path_is_relative_to_build_dir());
  EXPECT_EQ(setup.build_settings().python_path(),
            script_executable.NormalizePathSeparatorsTo('/'));
}

TEST_F(SetupTest, RelativePythonPath) {
  base::CommandLine cmdline(base::CommandLine::NO_PROGRAM);

  const char kDotfileContents[] = R"(
buildconfig = "//BUILDCONFIG.gn"
script_executable = ""
)";

  // Create a temp directory containing a .gn file and a BUILDCONFIG.gn file,
  // pass it as --root.
  base::ScopedTempDir in_temp_dir;
  ASSERT_TRUE(in_temp_dir.CreateUniqueTempDir());
  base::FilePath in_path = base::MakeAbsoluteFilePath(in_temp_dir.GetPath());
  base::FilePath dot_gn_name = in_path.Append(FILE_PATH_LITERAL(".gn"));
  WriteFile(dot_gn_name, kDotfileContents);

  base::FilePath relative_bin_dir(FILE_PATH_LITERAL("bin"));
  base::FilePath script_executable =
      relative_bin_dir.Append(FILE_PATH_LITERAL("python"));

#if defined(OS_WIN)
  // On Windows, if script executable is a relative path, then it must exist in
  // either the current directory or PATH with a `.exe` or `.bat` extension,
  // otherwise `FindWindowsPython` fails.
  base::FilePath original_cwd;
  base::GetCurrentDirectory(&original_cwd);

  // Switch to another temporary directory for the test.
  base::ScopedTempDir cwd_temp_dir;
  ASSERT_TRUE(cwd_temp_dir.CreateUniqueTempDir());

  base::FilePath absolute_script_executable = cwd_temp_dir.GetPath()
                                                  .Append(script_executable)
                                                  .ReplaceExtension(u".exe");
  ASSERT_TRUE(base::CreateDirectoryAndGetError(
      absolute_script_executable.DirName(), nullptr));
  WriteFile(absolute_script_executable, "");
  base::SetCurrentDirectory(cwd_temp_dir.GetPath());
#endif

  WriteFile(in_path.Append(FILE_PATH_LITERAL("BUILDCONFIG.gn")), "");
  cmdline.AppendSwitchPath(switches::kRoot, in_path);
  cmdline.AppendSwitchPath(switches::kScriptExecutable, script_executable);

  base::FilePath build_dir = in_path.Append(FILE_PATH_LITERAL("out"));

  // Run setup and check that python_path_is_relative_to_build_dir is false and
  // python_path is made absolute on Windows, and unchanged on other platforms.
  Setup setup;
  Err err;
  EXPECT_TRUE(
      setup.DoSetupWithErr(FilePathToUTF8(build_dir), true, cmdline, &err));
  EXPECT_FALSE(setup.build_settings().python_path_is_relative_to_build_dir());

#if defined(OS_WIN)
  EXPECT_EQ(setup.build_settings().python_path(),
            absolute_script_executable.NormalizePathSeparatorsTo('/'));

  // Change back to the original cwd.
  base::SetCurrentDirectory(original_cwd);
#else
  EXPECT_EQ(setup.build_settings().python_path(), script_executable);
#endif
}