651fa4d89b
- add target lib|exe <name>: another library or executable in a source directory named after it (P1204R0's rule applied per target); a new executable links the project's library - add test <name>: tests/<name>/driver.cpp for a library, or a run of an executable, with --arg and --expect - add dep <dependency>: link a library of this project (no cycles, no executables) or an installed package's imported target such as fmt::fmt, adding its find_package() - doctor: read-only check of the files against .canon.toml and the P1204R0 layout; errors for missing declared files or broken markers, warnings for unbuilt sources, undeclared tests, a hand-edited managed block, .h/.cc extensions and include/ or src/ directories - every add command takes --target; the CLI rejects options a command does not take - manifest: packages, test args, and validation of target names, shared source directories and dependencies - templates moved to canon/templates.hpp - tests: multi-target and exe-tests scenarios; every scenario ends with canon doctor; CANON_CHECK accepts expressions containing commas; the CLI test no longer reads a destroyed temporary Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
152 lines
6.7 KiB
C++
152 lines
6.7 KiB
C++
#include <canon/doctor.hpp>
|
|
|
|
#include <canon/cmake.hpp>
|
|
#include <canon/details/testing.hpp>
|
|
#include <canon/executor.hpp>
|
|
#include <canon/generate.hpp>
|
|
|
|
#include <algorithm>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
using namespace canon;
|
|
namespace fs = std::filesystem;
|
|
|
|
namespace {
|
|
|
|
project make_project() {
|
|
const manifest m = new_project_manifest(*parse_project_name(project_kind::library, "libhello"));
|
|
return project{
|
|
.root = "/unused",
|
|
.declared = m,
|
|
.manifest_text = to_toml(m),
|
|
.cmakelists_text = render_cmakelists(m),
|
|
};
|
|
}
|
|
|
|
const std::vector<std::string> fresh_files = {
|
|
".canon.toml", ".gitignore", "CMakeLists.txt", "README.md",
|
|
"hello/hello.cpp", "hello/hello.hpp", "hello/hello.test.cpp", "tests/basics/driver.cpp",
|
|
};
|
|
|
|
std::vector<std::string> with(std::vector<std::string> files, std::initializer_list<std::string> extra) {
|
|
files.insert(files.end(), extra);
|
|
return files;
|
|
}
|
|
|
|
std::vector<std::string> without(std::vector<std::string> files, const std::string& gone) {
|
|
std::erase(files, gone);
|
|
return files;
|
|
}
|
|
|
|
bool reports(const std::vector<finding>& findings, severity level, std::string_view path, std::string_view message) {
|
|
const bool found = std::ranges::any_of(findings, [&](const finding& f) {
|
|
return f.level == level && f.path == path && f.message.contains(message);
|
|
});
|
|
if (!found) {
|
|
std::println(stderr, " missing {} {}: {}", to_string(level), path, message);
|
|
for (const finding& f : findings) {
|
|
std::println(stderr, " got {} {}: {}", to_string(f.level), f.path, f.message);
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
const project p = make_project();
|
|
|
|
CANON_CHECK(diagnose(p, fresh_files).empty());
|
|
|
|
// Declared but missing.
|
|
{
|
|
const auto findings = diagnose(p, without(without(without(fresh_files, "hello/hello.cpp"), "hello/hello.hpp"),
|
|
"tests/basics/driver.cpp"));
|
|
CANON_CHECK_EQ(findings.size(), 3u);
|
|
CANON_CHECK(reports(findings, severity::error, "hello/hello.cpp", "source of libhello is missing"));
|
|
CANON_CHECK(reports(findings, severity::error, "hello/hello.hpp", "header of module hello/hello is missing"));
|
|
CANON_CHECK(reports(findings, severity::error, "tests/basics/driver.cpp", "driver of test basics is missing"));
|
|
}
|
|
CANON_CHECK(reports(diagnose(p, without(fresh_files, "hello/hello.test.cpp")), severity::error,
|
|
"hello/hello.test.cpp", "declared in .canon.toml but missing"));
|
|
|
|
// Present but unknown to .canon.toml.
|
|
{
|
|
const auto findings = diagnose(p, with(fresh_files, {"hello/extra.cpp", "hello/orphan.test.cpp",
|
|
"tests/manual/driver.cpp", "hello/old.h",
|
|
"hello/impl.cc", "hello/notes.txt", "docs/x.cpp",
|
|
"include/hello/hello.hpp", "include/hello/x.hpp",
|
|
"src/hello.cpp"}));
|
|
CANON_CHECK(reports(findings, severity::warning, "hello/extra.cpp", "not a source of any target"));
|
|
CANON_CHECK(reports(findings, severity::warning, "hello/orphan.test.cpp", "module that is not in .canon.toml"));
|
|
CANON_CHECK(reports(findings, severity::warning, "tests/manual/driver.cpp", "functional test is not in .canon.toml"));
|
|
CANON_CHECK(reports(findings, severity::warning, "hello/old.h", "headers use .hpp"));
|
|
CANON_CHECK(reports(findings, severity::warning, "hello/impl.cc", "sources use .cpp"));
|
|
CANON_CHECK(reports(findings, severity::warning, "include/", "instead of splitting them into include/ and src/"));
|
|
CANON_CHECK(reports(findings, severity::warning, "src/", "include/ and src/"));
|
|
CANON_CHECK_EQ(findings.size(), 7u); // notes.txt and docs/ are none of canon's business
|
|
}
|
|
|
|
// A unit test on disk for a module declared without one.
|
|
{
|
|
project untested = p;
|
|
untested.declared.modules.at(0).unit_test = false;
|
|
untested.cmakelists_text = render_cmakelists(untested.declared);
|
|
const auto findings = diagnose(untested, fresh_files);
|
|
CANON_CHECK_EQ(findings.size(), 1u);
|
|
CANON_CHECK(reports(findings, severity::warning, "hello/hello.test.cpp", "(canon add unit-test hello)"));
|
|
|
|
untested.declared.modules.at(0).target = "other";
|
|
CANON_CHECK(reports(diagnose(untested, fresh_files), severity::warning, "hello/hello.test.cpp",
|
|
"(canon add unit-test hello --target other)"));
|
|
}
|
|
|
|
// The managed block.
|
|
{
|
|
project edited = p;
|
|
edited.cmakelists_text.replace(edited.cmakelists_text.find("hello/hello.cpp"), 15, "hello/other.cpp");
|
|
const auto findings = diagnose(edited, fresh_files);
|
|
CANON_CHECK_EQ(findings.size(), 1u);
|
|
CANON_CHECK(reports(findings, severity::warning, "CMakeLists.txt", "managed block differs from .canon.toml"));
|
|
|
|
edited.cmakelists_text = "project(x)\n";
|
|
CANON_CHECK(reports(diagnose(edited, fresh_files), severity::error, "CMakeLists.txt", "no '# >>> canon:managed >>>'"));
|
|
}
|
|
|
|
// Errors come before warnings, each sorted by path.
|
|
{
|
|
const auto findings = diagnose(p, with(without(fresh_files, "hello/hello.hpp"), {"hello/a.cpp", "hello/b.h"}));
|
|
CANON_CHECK_EQ(findings.size(), 3u);
|
|
if (findings.size() == 3) {
|
|
CANON_CHECK(findings[0].level == severity::error);
|
|
CANON_CHECK_EQ(findings[1].path, "hello/a.cpp");
|
|
CANON_CHECK_EQ(findings[2].path, "hello/b.h");
|
|
}
|
|
}
|
|
|
|
// Listing a real project skips build output and hidden directories.
|
|
{
|
|
const details::temp_directory tmp;
|
|
CANON_CHECK(apply(generate_new_project(*parse_project_name(project_kind::library, "libhello")), tmp.path()));
|
|
const fs::path root = tmp.path() / "libhello";
|
|
for (const char* dir : {"build", "build-release", "cmake-build-debug", ".git", ".idea", "hello/build"}) {
|
|
fs::create_directories(root / dir);
|
|
std::ofstream(root / dir / "junk.cpp") << "\n";
|
|
}
|
|
|
|
const auto files = list_project_files(root);
|
|
CANON_CHECK(files == with(fresh_files, {"hello/build/junk.cpp"}) ||
|
|
files == [&] {
|
|
auto sorted = with(fresh_files, {"hello/build/junk.cpp"});
|
|
std::ranges::sort(sorted);
|
|
return sorted;
|
|
}());
|
|
CANON_CHECK(std::ranges::is_sorted(files));
|
|
}
|
|
|
|
return details::exit_status();
|
|
}
|