diff --git a/CMakeLists.txt b/CMakeLists.txt index 5d8355d..f1a0e23 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.17) -project(cppship VERSION 0.8.1) +project(cppship VERSION 0.8.2) # cpp std set(CMAKE_CXX_STANDARD 20) diff --git a/cppship.toml b/cppship.toml index c446e39..03f86f2 100644 --- a/cppship.toml +++ b/cppship.toml @@ -1,6 +1,6 @@ [package] name = "cppship" -version = "0.8.1" +version = "0.8.2" authors = [] std = 20 diff --git a/include/cppship/cmake/naming.h b/include/cppship/cmake/naming.h index 7f47e62..fbbaf17 100644 --- a/include/cppship/cmake/naming.h +++ b/include/cppship/cmake/naming.h @@ -7,9 +7,18 @@ namespace cppship::cmake { class NameTargetMapper { public: + explicit NameTargetMapper(std::string_view package) + : mPackage(package) + { + } + + std::string binary(std::string_view name); std::string test(std::string_view name); std::string example(std::string_view name); std::string bench(std::string_view name); + +private: + std::string mPackage; }; -} \ No newline at end of file +} diff --git a/include/cppship/cmd/bench.h b/include/cppship/cmd/bench.h index a59e2ca..33abf7f 100644 --- a/include/cppship/cmd/bench.h +++ b/include/cppship/cmd/bench.h @@ -9,7 +9,7 @@ namespace cppship::cmd { struct BenchOptions { Profile profile = Profile::release; - std::optional target; + std::optional name; std::optional package; }; diff --git a/include/cppship/cmd/test.h b/include/cppship/cmd/test.h index 1a2bb55..9483c7c 100644 --- a/include/cppship/cmd/test.h +++ b/include/cppship/cmd/test.h @@ -9,7 +9,7 @@ namespace cppship::cmd { struct TestOptions { Profile profile = Profile::debug; - std::optional target; + std::optional name; std::optional package; std::optional name_regex; bool rerun_failed = false; diff --git a/include/cppship/core/layout.h b/include/cppship/core/layout.h index a53e1c2..09bce1e 100644 --- a/include/cppship/core/layout.h +++ b/include/cppship/core/layout.h @@ -1,11 +1,12 @@ #pragma once -#include "cppship/util/fs.h" #include #include #include #include +#include "cppship/util/fs.h" + namespace cppship { struct Target { @@ -18,6 +19,8 @@ class Layout { public: Layout(const fs::path& root, std::string_view name); + std::string_view package() const { return mName; } + std::set all_files() const; std::optional lib() const; @@ -56,4 +59,4 @@ class Layout { std::map> mTests; }; -} \ No newline at end of file +} diff --git a/include/cppship/core/workspace.h b/include/cppship/core/workspace.h index a4b3bc0..1b47459 100644 --- a/include/cppship/core/workspace.h +++ b/include/cppship/core/workspace.h @@ -29,6 +29,8 @@ class Workspace { auto layouts() const { return ranges::views::values(packages_); } + const Layout* layout(std::string_view package) const; + private: fs::path root_; @@ -36,7 +38,6 @@ class Workspace { std::map packages_; }; -// Workspace is not full supported now, use this to enforce default layout. const Layout& enforce_default_package(const Workspace& workspace); } diff --git a/lib/cmake/generator.cpp b/lib/cmake/generator.cpp index 0b76ddd..00b91c5 100644 --- a/lib/cmake/generator.cpp +++ b/lib/cmake/generator.cpp @@ -197,9 +197,12 @@ void CmakeGenerator::add_app_sources_() boost::replace_all_copy(boost::to_upper_copy(std::string { mName }), "-", "_")), }; + NameTargetMapper mapper(mName); for (const auto& bin : mLayout->binaries()) { + const auto target = mapper.binary(bin.name); cmake::CmakeBin gen({ - .name = bin.name, + .name = target, + .name_alias = bin.name, .sources = bin.sources, .lib = mLib, .deps = mDeps, @@ -208,7 +211,7 @@ void CmakeGenerator::add_app_sources_() }); gen.build(mOut); - mBinaryTargets.emplace(bin.name); + mBinaryTargets.emplace(target); } } @@ -250,7 +253,7 @@ void CmakeGenerator::add_benches_() find_package(benchmark REQUIRED) )"; - NameTargetMapper mapper; + NameTargetMapper mapper(mName); auto deps = mDevDeps; deps.push_back({ .cmake_package = "benchmark", @@ -264,6 +267,7 @@ find_package(benchmark REQUIRED) .sources = bin.sources, .lib = mLib, .deps = deps, + .runtime_dir = "benches", }); gen.build(mOut); @@ -278,13 +282,12 @@ void CmakeGenerator::add_examples_() return; } - NameTargetMapper mapper; + NameTargetMapper mapper(mName); for (const auto& bin : examples) { const auto target = mapper.example(bin.name); cmake::CmakeBin gen({ .name = target, - .name_alias = bin.name, .sources = bin.sources, .lib = mLib, .deps = mDevDeps, @@ -308,13 +311,17 @@ void CmakeGenerator::add_test_sources_() find_package(GTest REQUIRED) )"; - NameTargetMapper mapper; + NameTargetMapper mapper(mName); for (const auto& test : tests) { const auto target = mapper.test(test.name); mOut << '\n' << fmt::format("add_executable({} {})\n", target, test.sources.begin()->generic_string()) - << fmt::format("target_link_libraries({} PRIVATE GTest::gtest_main)\n", target); + << fmt::format("target_link_libraries({} PRIVATE GTest::gtest_main)\n", target) + << fmt::format( + R"(set_target_properties({} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${{CMAKE_BINARY_DIR}}/tests"))", + target) + << '\n'; if (mLib) { mOut << fmt::format("target_link_libraries({} PRIVATE {})\n", target, *mLib); @@ -323,7 +330,7 @@ find_package(GTest REQUIRED) mOut << fmt::format("target_link_libraries({} PRIVATE {})\n", target, boost::join(dep.cmake_targets, " ")); } - mOut << fmt::format("add_test({} {})\n", target, target); + mOut << fmt::format("add_test(NAME {} COMMAND {})\n", target, target); mOut << fmt::format("set_tests_properties({} PROPERTIES LABELS {})\n", target, mName); mTestTargets.emplace(target); diff --git a/lib/cmake/naming.cpp b/lib/cmake/naming.cpp index 4bde8d7..7a0e32a 100644 --- a/lib/cmake/naming.cpp +++ b/lib/cmake/naming.cpp @@ -4,8 +4,10 @@ using namespace cppship::cmake; -std::string NameTargetMapper::test(std::string_view name) { return fmt::format("{}_test", name); } +std::string NameTargetMapper::binary(std::string_view name) { return fmt::format("{}_bin", name); } -std::string NameTargetMapper::example(std::string_view name) { return fmt::format("{}_example", name); } +std::string NameTargetMapper::test(std::string_view name) { return fmt::format("{}_{}_test", mPackage, name); } -std::string NameTargetMapper::bench(std::string_view name) { return fmt::format("{}_bench", name); } \ No newline at end of file +std::string NameTargetMapper::example(std::string_view name) { return fmt::format("{}_{}_example", mPackage, name); } + +std::string NameTargetMapper::bench(std::string_view name) { return fmt::format("{}_{}_bench", mPackage, name); } diff --git a/lib/cmd/bench.cpp b/lib/cmd/bench.cpp index 3614031..6ab5276 100644 --- a/lib/cmd/bench.cpp +++ b/lib/cmd/bench.cpp @@ -4,9 +4,8 @@ #include #include -#include #include -#include +#include #include #include "cppship/cmake/msvc.h" @@ -24,7 +23,7 @@ namespace { int run_one_bench(const cmd::BuildContext& ctx, const std::string_view bench) { - const auto bin = ctx.profile_dir / msvc::fix_bin_path(ctx, bench); + const auto bin = ctx.profile_dir / kBenchesPath / msvc::fix_bin_path(ctx, bench); const auto cmd = bin.string(); status("bench", "{}", cmd); return boost::process::system(cmd); @@ -34,17 +33,23 @@ int run_one_bench(const cmd::BuildContext& ctx, const std::string_view bench) int cmd::run_bench(const BenchOptions& options) { - NameTargetMapper mapper; - BuildContext ctx(options.profile); BuildOptions build_options { .profile = options.profile }; - if (options.target) { - if (ranges::all_of( - ctx.workspace.layouts(), [&](const auto& layout) { return !layout.bench(*options.target); })) { - throw Error { fmt::format("bench `{}` not found", *options.target) }; + if (options.name) { + const auto layouts = ctx.workspace.layouts() + | filter([&](const Layout& layout) { return layout.bench(*options.name).has_value(); }) + | ranges::to(); + if (layouts.empty()) { + throw Error { fmt::format("bench `{}` not found", *options.name) }; + } + if (layouts.size() > 1) { + throw Error { fmt::format("too many benches with name {}, eg. {}, {}", + *options.name, + layouts.front().package(), + layouts.back().package()) }; } - build_options.cmake_target = mapper.bench(*options.target); + build_options.cmake_target = NameTargetMapper(layouts.front().package()).bench(*options.name); } else { build_options.package = options.package; build_options.groups.insert(BuildGroup::benches); @@ -61,6 +66,8 @@ int cmd::run_bench(const BenchOptions& options) for (const auto& layout : ctx.workspace.layouts()) { for (const auto& bench : layout.benches()) { + NameTargetMapper mapper(layout.package()); + const int res = run_one_bench(ctx, mapper.bench(bench.name)); if (res != 0) { result = res; diff --git a/lib/cmd/build.cpp b/lib/cmd/build.cpp index a158c19..c140ce7 100644 --- a/lib/cmd/build.cpp +++ b/lib/cmd/build.cpp @@ -65,6 +65,9 @@ bool cmd::BuildContext::is_expired(const fs::path& path) const std::optional cmd::BuildContext::get_active_package() const { if (package_root == root) { + if (const auto* p = manifest.get_if_package()) { + return std::make_optional(p->name()); + } return std::nullopt; } diff --git a/lib/cmd/install.cpp b/lib/cmd/install.cpp index 01198c5..6f3a2f6 100644 --- a/lib/cmd/install.cpp +++ b/lib/cmd/install.cpp @@ -8,6 +8,7 @@ #include #include +#include "cppship/cmake/naming.h" #include "cppship/cmd/build.h" #include "cppship/core/layout.h" #include "cppship/core/manifest.h" @@ -54,7 +55,11 @@ int cmd::run_install([[maybe_unused]] const InstallOptions& options) throw InvalidCmdOption { "--bin", fmt::format("specified binary {} is not found", *options.binary) }; } - const int result = run_build({ .profile = options.profile, .cmake_target = options.binary }); + cmake::NameTargetMapper mapper(layout.package()); + const int result = run_build({ + .profile = options.profile, + .cmake_target = options.binary.has_value() ? std::make_optional(mapper.binary(*options.binary)) : std::nullopt, + }); if (result != 0) { return EXIT_FAILURE; } diff --git a/lib/cmd/run.cpp b/lib/cmd/run.cpp index 1543330..e939c4e 100644 --- a/lib/cmd/run.cpp +++ b/lib/cmd/run.cpp @@ -1,10 +1,15 @@ #include "cppship/cmd/run.h" #include +#include +#include #include #include #include +#include +#include +#include #include #include "cppship/cmake/msvc.h" @@ -25,7 +30,7 @@ namespace { void validate_options(const cmd::BuildContext& ctx, const cmd::RunOptions& options) { if (options.bin && options.example) { - throw Error { "should not specify --bin and --example in the same time" }; + throw Error { "should not specify --bin and --example at the same time" }; } if (options.package) { if (ctx.manifest.get(*options.package) == nullptr) { @@ -34,59 +39,82 @@ void validate_options(const cmd::BuildContext& ctx, const cmd::RunOptions& optio } } -void validate_target(const Workspace& workspace, const cmd::RunOptions& opt) +std::string choose_target(const cmd::BuildContext& ctx, const cmd::RunOptions& options) { - const auto layouts = workspace.layouts(); - if (const auto& bin = opt.bin) { - if (ranges::none_of(layouts, [&](const Layout& layout) { return layout.binary(*bin).has_value(); })) { - throw Error { fmt::format("binary `{}` not found", *bin) }; - } - } else if (const auto& example = opt.example) { - if (ranges::none_of(layouts, [&](const Layout& layout) { return layout.example(*example).has_value(); })) { - throw Error { fmt::format("example `{}` not found", *example) }; + if (!options.bin && !options.example) { + const std::string package = std::invoke([&] { + if (options.package) { + return *options.package; + } + if (const auto& p = ctx.get_active_package()) { + return *p; + } + + throw Error { "no target selected" }; + }); + + const auto* layout = ctx.workspace.layout(package); + auto target = layout->binary(package); + if (!target) { + throw Error { fmt::format("package {} has no default binary", package) }; } - } -} -std::optional choose_binary(const cmd::RunOptions& options, const PackageManifest* manifest) -{ - if (options.example) { - return options.example; + return cmake::NameTargetMapper(package).binary(target->name); } - if (options.bin) { - return options.bin; - } - if (options.package) { - return options.package; + + std::vector layouts = std::invoke([&]() -> std::vector { + if (options.package) { + return { ctx.workspace.layout(*options.package) }; + } + + return ctx.workspace.layouts() | ranges::views::transform([](const Layout& l) { return &l; }) + | ranges::to(); + }); + + const auto candidates = layouts | ranges::views::filter([&](const Layout* layout) { + if (options.bin) { + return layout->binary(*options.bin).has_value(); + } + if (options.example) { + return layout->example(*options.example).has_value(); + } + return false; + }) | ranges::to(); + + if (candidates.empty()) { + throw Error { options.bin ? fmt::format("binary `{}` not found", *options.bin) + : fmt::format("example `{}` not found", *options.example) }; } - if (manifest != nullptr) { - return std::make_optional(manifest->name()); + if (candidates.size() > 1) { + throw Error { "too many targets selected" }; } - return std::nullopt; + const auto* layout = candidates.front(); + cmake::NameTargetMapper mapper(layout->package()); + return options.bin ? mapper.binary(*options.bin) : mapper.example(*options.example); +} + +inline std::string_view binary_target_to_name(std::string_view target) +{ + target.remove_suffix(4); + return target; } + } int cmd::run_run(const RunOptions& options) { BuildContext ctx(options.profile); validate_options(ctx, options); - validate_target(ctx.workspace, options); - - cmake::NameTargetMapper mapper; - const auto bin = choose_binary(options, ctx.manifest.get_if_package()); - if (!bin) { - warn("no target selected"); - return EXIT_SUCCESS; - } - const auto target = options.example ? mapper.example(*bin) : *bin; + const auto target = choose_target(ctx, options); const int result = run_build({ .profile = options.profile, .cmake_target = target }); if (result != 0) { return EXIT_FAILURE; } - const fs::path fixed_bin = msvc::fix_bin_path(ctx, *bin); + const fs::path fixed_bin + = options.example ? msvc::fix_bin_path(ctx, target) : msvc::fix_bin_path(ctx, binary_target_to_name(target)); const auto bin_file = options.example ? ctx.profile_dir / kExamplesPath / fixed_bin : ctx.profile_dir / fixed_bin; if (!has_cmd(bin_file.string())) { error("no binary to run: {}", bin_file.string()); diff --git a/lib/cmd/test.cpp b/lib/cmd/test.cpp index 70954f3..f4863ed 100644 --- a/lib/cmd/test.cpp +++ b/lib/cmd/test.cpp @@ -4,7 +4,8 @@ #include #include -#include +#include +#include #include #include "cppship/cmake/naming.h" @@ -19,7 +20,7 @@ namespace { void valid_options(const cmd::TestOptions& options) { - if (options.target && options.name_regex) { + if (options.name && options.name_regex) { throw Error { "testname and -R should not be specified both" }; } } @@ -30,17 +31,23 @@ int cmd::run_test(const TestOptions& options) { valid_options(options); - cmake::NameTargetMapper mapper; - BuildContext ctx(options.profile); BuildOptions build_opts { .profile = options.profile }; - if (options.target && !options.rerun_failed) { - if (ranges::all_of( - ctx.workspace.layouts(), [&](const auto& layout) { return !layout.test(*options.target); })) { - throw Error { fmt::format("test `{}` not found", *options.target) }; + if (options.name && !options.rerun_failed) { + const auto layouts = ctx.workspace.layouts() + | ranges::views::filter([&](const Layout& layout) { return layout.test(*options.name).has_value(); }) + | ranges::to(); + if (layouts.empty()) { + throw Error { fmt::format("test `{}` not found", *options.name) }; + } + if (layouts.size() > 1) { + throw Error { fmt::format("too many tests with name {}, eg. {}, {}", + *options.name, + layouts.front().package(), + layouts.back().package()) }; } - build_opts.cmake_target = mapper.test(*options.target); + build_opts.cmake_target = cmake::NameTargetMapper(layouts.front().package()).test(*options.name); } else { build_opts.package = options.package; build_opts.groups.insert(BuildGroup::tests); diff --git a/lib/core/workspace.cpp b/lib/core/workspace.cpp index e224f11..e335f5f 100644 --- a/lib/core/workspace.cpp +++ b/lib/core/workspace.cpp @@ -1,8 +1,10 @@ #include "cppship/core/workspace.h" +#include #include #include +#include #include #include #include @@ -16,6 +18,27 @@ using namespace ranges::views; namespace cppship { +namespace { + +template +// NOLINTNEXTLINE(cppcoreguidelines-missing-std-forward) +void check_duplicate_target(std::string_view kind, const Layouts& layouts, Proj&& proj) +{ + std::map target2package; + + for (const auto& layout : layouts) { + for (const auto& target : std::invoke(proj, layout)) { + auto [it, inserted] = target2package.emplace(target.name, layout.package()); + if (!inserted) { + throw Error { fmt::format( + "duplicate {} {} from package {} and {}", kind, target.name, it->second, layout.package()) }; + } + } + } +} + +} + Workspace::Workspace(const fs::path& project_root, const Manifest& manifest) : root_(project_root) { @@ -30,6 +53,8 @@ Workspace::Workspace(const fs::path& project_root, const Manifest& manifest) std::forward_as_tuple(path), std::forward_as_tuple(root_ / path, package_manifest.name())); } + + check_duplicate_target("binary", layouts(), &Layout::binaries); } std::set Workspace::list_files() const @@ -38,6 +63,14 @@ std::set Workspace::list_files() const | ranges::to(); } +const Layout* Workspace::layout(std::string_view package) const +{ + const auto l = layouts(); + auto it = ranges::find_if(l, [package](const Layout& layout) { return layout.package() == package; }); + + return it == l.end() ? nullptr : &*it; +} + const Layout& enforce_default_package(const Workspace& workspace) { const auto* l = workspace.get_default(); diff --git a/src/main.cpp b/src/main.cpp index 51c80bb..cbb3171 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -201,7 +201,7 @@ std::list build_commands(const ArgumentParser& common) auto& test = commands.emplace_back("test", common, [](const ArgumentParser& cmd) { return cmd::run_test({ .profile = get_profile(cmd), - .target = cmd.present("testname"), + .name = cmd.present("testname"), .package = cmd.present("package"), .name_regex = cmd.present("-R"), .rerun_failed = cmd.get("--rerun-failed"), @@ -226,7 +226,7 @@ std::list build_commands(const ArgumentParser& common) auto& bench = commands.emplace_back("bench", common, [](const ArgumentParser& cmd) { return cmd::run_bench({ .profile = parse_profile(cmd.get("--profile")), - .target = cmd.present("benchname"), + .name = cmd.present("benchname"), .package = cmd.present("package"), }); }); diff --git a/tests/cmake/generator.cpp b/tests/cmake/generator.cpp index ae3cd3e..d138be5 100644 --- a/tests/cmake/generator.cpp +++ b/tests/cmake/generator.cpp @@ -39,5 +39,5 @@ version = "0.1.0" )"); CmakeGenerator gen(&layout, meta.get_if_package(), {}); const auto content = std::move(gen).build(); - EXPECT_TRUE(boost::contains(content, "target_compile_definitions(tmp PRIVATE ABC_ABC_ABC_VERSION=")); + EXPECT_TRUE(boost::contains(content, "target_compile_definitions(tmp_bin PRIVATE ABC_ABC_ABC_VERSION=")); } diff --git a/tests/core/workspace.cpp b/tests/core/workspace.cpp new file mode 100644 index 0000000..12fefca --- /dev/null +++ b/tests/core/workspace.cpp @@ -0,0 +1,197 @@ +#include "cppship/core/workspace.h" + +#include +#include +#include +#include + +#include +#include + +#include "cppship/core/manifest.h" +#include "cppship/exception.h" +#include "cppship/util/io.h" + +using namespace cppship; +using namespace cppship::core; + +namespace { + +class DirTree { +public: + explicit DirTree(const std::set& trees) + { + fs::create_directory(mRoot); + fs::current_path(mRoot); + + for (const auto& path : trees) { + fs::path fs_path { path }; + + if (path.ends_with('/')) { + fs::create_directories(fs_path); + } else { + if (fs_path.has_parent_path()) { + fs::create_directories(fs_path.parent_path()); + } + std::ofstream ofs(fs_path); + } + } + } + + ~DirTree() + { + fs::current_path(mOriPath); + fs::remove_all(mRoot); + } + + const fs::path& root() const { return mRoot; } + + std::string relative(const fs::path& path) + { + // use generic_string to deal with windows + return path.lexically_relative(mRoot).generic_string(); + } + +private: + fs::path mRoot = fs::temp_directory_path() / "cppship.test"; + fs::path mOriPath = fs::current_path(); +}; + +} + +TEST(workspace, DuplicateBinary) +{ + DirTree tree({ + "cppship.toml", + "app_1/cppship.toml", + "app_1/src/main.cpp", + "app_2/cppship.toml", + "app_2/src/bin/app_1.cpp", + }); + + write("cppship.toml", R"([workspace] +members = ["app_1", "app_2"])"); + write("app_1/cppship.toml", R"([package] +version = "1.0.0" +name = "app_1")"); + write("app_2/cppship.toml", R"([package] +version = "1.0.0" +name = "app_2")"); + + try { + Manifest manifest(tree.root() / "cppship.toml"); + Workspace workspace(tree.root(), manifest); + FAIL(); + } catch (const Error& e) { + ASSERT_EQ(std::string(e.what()), "duplicate binary app_1 from package app_1 and app_2"); + } +} + +TEST(workspace, DuplicateExamples) +{ + DirTree tree({ + "cppship.toml", + "app_1/cppship.toml", + "app_1/examples/app_1.cpp", + "app_2/cppship.toml", + "app_2/examples/app_1.cpp", + }); + + write("cppship.toml", R"([workspace] +members = ["app_1", "app_2"])"); + write("app_1/cppship.toml", R"([package] +version = "1.0.0" +name = "app_1")"); + write("app_2/cppship.toml", R"([package] +version = "1.0.0" +name = "app_2")"); + + Manifest manifest(tree.root() / "cppship.toml"); + Workspace workspace(tree.root(), manifest); + for (const auto& layout : workspace.layouts()) { + ASSERT_TRUE(layout.example("app_1").has_value()); + } +} + +TEST(workspace, DuplicateBenches) +{ + DirTree tree({ + "cppship.toml", + "app_1/cppship.toml", + "app_1/benches/app_1.cpp", + "app_2/cppship.toml", + "app_2/benches/app_1.cpp", + }); + + write("cppship.toml", R"([workspace] +members = ["app_1", "app_2"])"); + write("app_1/cppship.toml", R"([package] +version = "1.0.0" +name = "app_1")"); + write("app_2/cppship.toml", R"([package] +version = "1.0.0" +name = "app_2")"); + + Manifest manifest(tree.root() / "cppship.toml"); + Workspace workspace(tree.root(), manifest); + + for (const auto& layout : workspace.layouts()) { + ASSERT_TRUE(layout.bench("app_1").has_value()); + } +} + +TEST(workspace, DuplicateTests) +{ + DirTree tree({ + "cppship.toml", + "app_1/cppship.toml", + "app_1/tests/app_1.cpp", + "app_2/cppship.toml", + "app_2/tests/app_1.cpp", + }); + + write("cppship.toml", R"([workspace] +members = ["app_1", "app_2"])"); + write("app_1/cppship.toml", R"([package] +version = "1.0.0" +name = "app_1")"); + write("app_2/cppship.toml", R"([package] +version = "1.0.0" +name = "app_2")"); + + Manifest manifest(tree.root() / "cppship.toml"); + Workspace workspace(tree.root(), manifest); + + for (const auto& layout : workspace.layouts()) { + ASSERT_TRUE(layout.test("app_1").has_value()); + } +} + +TEST(workspace, DuplicateInDifferentKind) +{ + DirTree tree({ + "cppship.toml", + "app_1/cppship.toml", + "app_1/examples/key0.cpp", + "app_1/examples/key1.cpp", + "app_1/src/bin/key0.cpp", + "app_1/src/bin/key1.cpp", + "app_2/cppship.toml", + "app_2/tests/key0.cpp", + "app_2/tests/key1.cpp", + "app_2/benches/key0.cpp", + "app_2/benches/key1.cpp", + }); + + write("cppship.toml", R"([workspace] +members = ["app_1", "app_2"])"); + write("app_1/cppship.toml", R"([package] +version = "1.0.0" +name = "app_1")"); + write("app_2/cppship.toml", R"([package] +version = "1.0.0" +name = "app_2")"); + + Manifest manifest(tree.root() / "cppship.toml"); + Workspace workspace(tree.root(), manifest); +}