| // Copyright (c) 2013 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 <functional> |
| #include <map> |
| #include <memory> |
| #include <utility> |
| #include <vector> |
| |
| #include "gn/build_settings.h" |
| #include "gn/err.h" |
| #include "gn/loader.h" |
| #include "gn/parse_tree.h" |
| #include "gn/parser.h" |
| #include "gn/scheduler.h" |
| #include "gn/test_with_scheduler.h" |
| #include "gn/tokenizer.h" |
| #include "util/msg_loop.h" |
| #include "util/test/test.h" |
| |
| namespace { |
| |
| bool ItemContainsBuildDependencyFile(const Item* item, |
| const SourceFile& source_file) { |
| const auto& build_dependency_files = item->build_dependency_files(); |
| return build_dependency_files.end() != |
| build_dependency_files.find(source_file); |
| } |
| |
| class MockBuilder { |
| public: |
| void OnItemDefined(std::unique_ptr<Item> item); |
| std::vector<const Item*> GetAllItems() const; |
| |
| private: |
| std::vector<std::unique_ptr<Item>> items_; |
| }; |
| |
| void MockBuilder::OnItemDefined(std::unique_ptr<Item> item) { |
| items_.push_back(std::move(item)); |
| } |
| |
| std::vector<const Item*> MockBuilder::GetAllItems() const { |
| std::vector<const Item*> result; |
| for (const auto& item : items_) { |
| result.push_back(item.get()); |
| } |
| |
| return result; |
| } |
| |
| class MockInputFileManager { |
| public: |
| using Callback = std::function<void(const ParseNode*)>; |
| |
| MockInputFileManager(); |
| ~MockInputFileManager(); |
| |
| LoaderImpl::AsyncLoadFileCallback GetAsyncCallback(); |
| |
| // Sets a given response for a given source file. |
| void AddCannedResponse(const SourceFile& source_file, |
| const std::string& source); |
| |
| // Returns true if there is/are pending load(s) matching the given file(s). |
| bool HasOnePending(const SourceFile& f) const; |
| bool HasTwoPending(const SourceFile& f1, const SourceFile& f2) const; |
| |
| void IssueAllPending(); |
| |
| private: |
| struct CannedResult { |
| std::unique_ptr<InputFile> input_file; |
| std::vector<Token> tokens; |
| std::unique_ptr<ParseNode> root; |
| }; |
| |
| InputFileManager::SyncLoadFileCallback GetSyncCallback(); |
| |
| bool AsyncLoadFile(const LocationRange& origin, |
| const BuildSettings* build_settings, |
| const SourceFile& file_name, |
| const Callback& callback, |
| Err* err) { |
| pending_.push_back(std::make_pair(file_name, callback)); |
| return true; |
| } |
| |
| using CannedResponseMap = std::map<SourceFile, std::unique_ptr<CannedResult>>; |
| CannedResponseMap canned_responses_; |
| |
| std::vector<std::pair<SourceFile, Callback>> pending_; |
| }; |
| |
| MockInputFileManager::MockInputFileManager() { |
| g_scheduler->input_file_manager()->set_load_file_callback(GetSyncCallback()); |
| } |
| |
| MockInputFileManager::~MockInputFileManager() { |
| g_scheduler->input_file_manager()->set_load_file_callback(nullptr); |
| } |
| |
| LoaderImpl::AsyncLoadFileCallback MockInputFileManager::GetAsyncCallback() { |
| return |
| [this](const LocationRange& origin, const BuildSettings* build_settings, |
| const SourceFile& file_name, const Callback& callback, Err* err) { |
| return AsyncLoadFile(origin, build_settings, file_name, callback, err); |
| }; |
| } |
| |
| InputFileManager::SyncLoadFileCallback MockInputFileManager::GetSyncCallback() { |
| return |
| [this](const SourceFile& file_name, InputFile* file) { |
| CannedResponseMap::const_iterator found = canned_responses_.find(file_name); |
| if (found == canned_responses_.end()) |
| return false; |
| file->SetContents(found->second->input_file->contents()); |
| return true; |
| }; |
| } |
| |
| // Sets a given response for a given source file. |
| void MockInputFileManager::AddCannedResponse(const SourceFile& source_file, |
| const std::string& source) { |
| std::unique_ptr<CannedResult> canned = std::make_unique<CannedResult>(); |
| canned->input_file = std::make_unique<InputFile>(source_file); |
| canned->input_file->SetContents(source); |
| |
| // Tokenize. |
| Err err; |
| canned->tokens = Tokenizer::Tokenize(canned->input_file.get(), &err); |
| EXPECT_FALSE(err.has_error()); |
| |
| // Parse. |
| canned->root = Parser::Parse(canned->tokens, &err); |
| if (err.has_error()) |
| err.PrintToStdout(); |
| EXPECT_FALSE(err.has_error()); |
| |
| canned_responses_[source_file] = std::move(canned); |
| } |
| |
| bool MockInputFileManager::HasOnePending(const SourceFile& f) const { |
| return pending_.size() == 1u && pending_[0].first == f; |
| } |
| |
| bool MockInputFileManager::HasTwoPending(const SourceFile& f1, |
| const SourceFile& f2) const { |
| if (pending_.size() != 2u) |
| return false; |
| return pending_[0].first == f1 && pending_[1].first == f2; |
| } |
| |
| void MockInputFileManager::IssueAllPending() { |
| BlockNode block(BlockNode::DISCARDS_RESULT); // Default response. |
| |
| for (const auto& cur : pending_) { |
| CannedResponseMap::const_iterator found = canned_responses_.find(cur.first); |
| if (found == canned_responses_.end()) |
| cur.second(&block); |
| else |
| cur.second(found->second->root.get()); |
| } |
| pending_.clear(); |
| } |
| |
| // LoaderTest ------------------------------------------------------------------ |
| |
| class LoaderTest : public TestWithScheduler { |
| public: |
| LoaderTest() { build_settings_.SetBuildDir(SourceDir("//out/Debug/")); } |
| |
| protected: |
| BuildSettings build_settings_; |
| MockBuilder mock_builder_; |
| MockInputFileManager mock_ifm_; |
| }; |
| |
| } // namespace |
| |
| // ----------------------------------------------------------------------------- |
| |
| TEST_F(LoaderTest, Foo) { |
| SourceFile build_config("//build/config/BUILDCONFIG.gn"); |
| build_settings_.set_build_config_file(build_config); |
| |
| scoped_refptr<LoaderImpl> loader(new LoaderImpl(&build_settings_)); |
| |
| // The default toolchain needs to be set by the build config file. |
| mock_ifm_.AddCannedResponse(build_config, |
| "set_default_toolchain(\"//tc:tc\")"); |
| |
| loader->set_async_load_file(mock_ifm_.GetAsyncCallback()); |
| |
| // Request the root build file be loaded. This should kick off the default |
| // build config loading. |
| SourceFile root_build("//BUILD.gn"); |
| loader->Load(root_build, LocationRange(), Label()); |
| EXPECT_TRUE(mock_ifm_.HasOnePending(build_config)); |
| |
| // Completing the build config load should kick off the root build file load. |
| mock_ifm_.IssueAllPending(); |
| MsgLoop::Current()->RunUntilIdleForTesting(); |
| EXPECT_TRUE(mock_ifm_.HasOnePending(root_build)); |
| |
| // Load the root build file. |
| mock_ifm_.IssueAllPending(); |
| MsgLoop::Current()->RunUntilIdleForTesting(); |
| |
| // Schedule some other file to load in another toolchain. |
| Label second_tc(SourceDir("//tc2/"), "tc2"); |
| SourceFile second_file("//foo/BUILD.gn"); |
| loader->Load(second_file, LocationRange(), second_tc); |
| EXPECT_TRUE(mock_ifm_.HasOnePending(SourceFile("//tc2/BUILD.gn"))); |
| |
| // Running the toolchain file should schedule the build config file to load |
| // for that toolchain. |
| mock_ifm_.IssueAllPending(); |
| MsgLoop::Current()->RunUntilIdleForTesting(); |
| |
| // We have to tell it we have a toolchain definition now (normally the |
| // builder would do this). |
| const Settings* default_settings = loader->GetToolchainSettings(Label()); |
| Toolchain second_tc_object(default_settings, second_tc); |
| loader->ToolchainLoaded(&second_tc_object); |
| EXPECT_TRUE(mock_ifm_.HasOnePending(build_config)); |
| |
| // Scheduling a second file to load in that toolchain should not make it |
| // pending yet (it's waiting for the build config). |
| SourceFile third_file("//bar/BUILD.gn"); |
| loader->Load(third_file, LocationRange(), second_tc); |
| EXPECT_TRUE(mock_ifm_.HasOnePending(build_config)); |
| |
| // Running the build config file should make our third file pending. |
| mock_ifm_.IssueAllPending(); |
| MsgLoop::Current()->RunUntilIdleForTesting(); |
| EXPECT_TRUE(mock_ifm_.HasTwoPending(second_file, third_file)); |
| |
| EXPECT_FALSE(scheduler().is_failed()); |
| } |
| |
| TEST_F(LoaderTest, BuildDependencyFilesAreCollected) { |
| SourceFile build_config("//build/config/BUILDCONFIG.gn"); |
| SourceFile root_build("//BUILD.gn"); |
| build_settings_.set_build_config_file(build_config); |
| build_settings_.set_item_defined_callback( |
| [builder = &mock_builder_](std::unique_ptr<Item> item) { |
| builder->OnItemDefined(std::move(item)); |
| }); |
| |
| scoped_refptr<LoaderImpl> loader(new LoaderImpl(&build_settings_)); |
| mock_ifm_.AddCannedResponse(build_config, |
| "set_default_toolchain(\"//tc:tc\")"); |
| std::string root_build_content = |
| "executable(\"a\") { sources = [ \"a.cc\" ] }\n" |
| "config(\"b\") { configs = [\"//t:t\"] }\n" |
| "toolchain(\"c\") {}\n" |
| "pool(\"d\") { depth = 1 }"; |
| mock_ifm_.AddCannedResponse(root_build, root_build_content); |
| |
| loader->set_async_load_file(mock_ifm_.GetAsyncCallback()); |
| |
| // Request the root build file be loaded. This should kick off the default |
| // build config loading. |
| loader->Load(root_build, LocationRange(), Label()); |
| EXPECT_TRUE(mock_ifm_.HasOnePending(build_config)); |
| |
| // Completing the build config load should kick off the root build file load. |
| mock_ifm_.IssueAllPending(); |
| MsgLoop::Current()->RunUntilIdleForTesting(); |
| EXPECT_TRUE(mock_ifm_.HasOnePending(root_build)); |
| |
| // Completing the root build file should define a target which must have |
| // set of source files hashes. |
| mock_ifm_.IssueAllPending(); |
| MsgLoop::Current()->RunUntilIdleForTesting(); |
| |
| std::vector<const Item*> items = mock_builder_.GetAllItems(); |
| EXPECT_TRUE(items[0]->AsTarget()); |
| EXPECT_TRUE(ItemContainsBuildDependencyFile(items[0], root_build)); |
| EXPECT_TRUE(items[1]->AsConfig()); |
| EXPECT_TRUE(ItemContainsBuildDependencyFile(items[1], root_build)); |
| EXPECT_TRUE(items[2]->AsToolchain()); |
| EXPECT_TRUE(ItemContainsBuildDependencyFile(items[2], root_build)); |
| EXPECT_TRUE(items[3]->AsPool()); |
| EXPECT_TRUE(ItemContainsBuildDependencyFile(items[3], root_build)); |
| |
| EXPECT_FALSE(scheduler().is_failed()); |
| } |
| |
| TEST_F(LoaderTest, TemplateBuildDependencyFilesAreCollected) { |
| SourceFile build_config("//build/config/BUILDCONFIG.gn"); |
| SourceFile root_build("//BUILD.gn"); |
| build_settings_.set_build_config_file(build_config); |
| build_settings_.set_item_defined_callback( |
| [builder = &mock_builder_](std::unique_ptr<Item> item) { |
| builder->OnItemDefined(std::move(item)); |
| }); |
| |
| scoped_refptr<LoaderImpl> loader(new LoaderImpl(&build_settings_)); |
| mock_ifm_.AddCannedResponse(build_config, |
| "set_default_toolchain(\"//tc:tc\")"); |
| mock_ifm_.AddCannedResponse( |
| SourceFile("//test.gni"), |
| "template(\"tmpl\") {\n" |
| " executable(target_name) { sources = invoker.sources }\n" |
| "}\n"); |
| mock_ifm_.AddCannedResponse(root_build, |
| "import(\"//test.gni\")\n" |
| "tmpl(\"a\") {sources = [ \"a.cc\" ]}\n"); |
| |
| loader->set_async_load_file(mock_ifm_.GetAsyncCallback()); |
| |
| // Request the root build file be loaded. This should kick off the default |
| // build config loading. |
| loader->Load(root_build, LocationRange(), Label()); |
| EXPECT_TRUE(mock_ifm_.HasOnePending(build_config)); |
| |
| // Completing the build config load should kick off the root build file load. |
| mock_ifm_.IssueAllPending(); |
| MsgLoop::Current()->RunUntilIdleForTesting(); |
| EXPECT_TRUE(mock_ifm_.HasOnePending(root_build)); |
| |
| // Completing the root build file should define a target which must have |
| // set of source files hashes. |
| mock_ifm_.IssueAllPending(); |
| MsgLoop::Current()->RunUntilIdleForTesting(); |
| |
| std::vector<const Item*> items = mock_builder_.GetAllItems(); |
| EXPECT_TRUE(items[0]->AsTarget()); |
| // Ensure the target as a dep on BUILD.gn even though it was defined via |
| // a template. |
| EXPECT_TRUE(ItemContainsBuildDependencyFile(items[0], root_build)); |
| |
| EXPECT_FALSE(scheduler().is_failed()); |
| } |
| |
| TEST_F(LoaderTest, NonDefaultBuildFileName) { |
| std::string new_name = "BUILD.more.gn"; |
| |
| SourceFile build_config("//build/config/BUILDCONFIG.gn"); |
| build_settings_.set_build_config_file(build_config); |
| |
| scoped_refptr<LoaderImpl> loader(new LoaderImpl(&build_settings_)); |
| loader->set_build_file_extension("more"); |
| |
| // The default toolchain needs to be set by the build config file. |
| mock_ifm_.AddCannedResponse(build_config, |
| "set_default_toolchain(\"//tc:tc\")"); |
| |
| loader->set_async_load_file(mock_ifm_.GetAsyncCallback()); |
| |
| // Request the root build file be loaded. This should kick off the default |
| // build config loading. |
| SourceFile root_build("//" + new_name); |
| loader->Load(root_build, LocationRange(), Label()); |
| EXPECT_TRUE(mock_ifm_.HasOnePending(build_config)); |
| |
| // Completing the build config load should kick off the root build file load. |
| mock_ifm_.IssueAllPending(); |
| MsgLoop::Current()->RunUntilIdleForTesting(); |
| EXPECT_TRUE(mock_ifm_.HasOnePending(root_build)); |
| |
| // Load the root build file. |
| mock_ifm_.IssueAllPending(); |
| MsgLoop::Current()->RunUntilIdleForTesting(); |
| |
| // Schedule some other file to load in another toolchain. |
| Label second_tc(SourceDir("//tc2/"), "tc2"); |
| SourceFile second_file("//foo/" + new_name); |
| loader->Load(second_file, LocationRange(), second_tc); |
| EXPECT_TRUE(mock_ifm_.HasOnePending(SourceFile("//tc2/" + new_name))); |
| |
| // Running the toolchain file should schedule the build config file to load |
| // for that toolchain. |
| mock_ifm_.IssueAllPending(); |
| MsgLoop::Current()->RunUntilIdleForTesting(); |
| |
| // We have to tell it we have a toolchain definition now (normally the |
| // builder would do this). |
| const Settings* default_settings = loader->GetToolchainSettings(Label()); |
| Toolchain second_tc_object(default_settings, second_tc); |
| loader->ToolchainLoaded(&second_tc_object); |
| EXPECT_TRUE(mock_ifm_.HasOnePending(build_config)); |
| |
| // Running the build config file should make our second file pending. |
| mock_ifm_.IssueAllPending(); |
| MsgLoop::Current()->RunUntilIdleForTesting(); |
| EXPECT_TRUE(mock_ifm_.HasOnePending(second_file)); |
| |
| EXPECT_FALSE(scheduler().is_failed()); |
| } |