diff --git a/CHANGELOG.md b/CHANGELOG.md index 7337029..d385894 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,3 +12,10 @@ All notable changes to cargoxx will be documented in this file. builds an empty `cargoxx` binary. - `.clang-format` (LLVM, 100-column) and `.gitignore`. - `SPEC.md`, `TECH_SPEC.md`. +- M1 foundation in `cargoxx.util`: `ErrorCode` (numbers per `TECH_SPEC.md` §4), + `Error`, `Result = std::expected`, and `format(Error)` rendering + `error[Ennnn]: ...` with optional `--> path:line:col` and `hint:` lines. + `format` lives in `src/util/error.cpp` as a module implementation unit. +- `Catch2 v3` wired through `flake.nix` (libc++-built override) and registered + in CMake; `tests/util_error.cpp` covers six format cases via + `catch_discover_tests`. diff --git a/CMakeLists.txt b/CMakeLists.txt index 9724378..9764ea8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,7 @@ project(cargoxx LANGUAGES CXX VERSION 0.1.0) set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_CXX_EXTENSIONS ON) set(CMAKE_CXX_SCAN_FOR_MODULES ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) @@ -29,9 +29,11 @@ if(CARGOXX_WERROR) add_compile_options(-Werror) endif() -# ----- cargoxx library: all module units ----- +# ----- cargoxx library: module units + implementation units ----- add_library(cargoxx STATIC) target_sources(cargoxx + PRIVATE + src/util/error.cpp PUBLIC FILE_SET CXX_MODULES FILES src/lib.cppm @@ -50,3 +52,10 @@ target_sources(cargoxx add_executable(cargoxx_bin src/main.cpp) set_target_properties(cargoxx_bin PROPERTIES OUTPUT_NAME cargoxx) target_link_libraries(cargoxx_bin PRIVATE cargoxx) + +# ----- tests ----- +option(CARGOXX_BUILD_TESTS "Build cargoxx tests" ON) +if(CARGOXX_BUILD_TESTS) + enable_testing() + add_subdirectory(tests) +endif() diff --git a/flake.nix b/flake.nix index 4b46002..590c4db 100644 --- a/flake.nix +++ b/flake.nix @@ -11,6 +11,11 @@ let pkgs = import nixpkgs { inherit system; }; llvmPkgs = pkgs.llvmPackages; + libcxxPkgs = { + catch2_3 = pkgs.catch2_3.override { + stdenv = llvmPkgs.libcxxStdenv; + }; + }; in { devShells.default = llvmPkgs.libcxxStdenv.mkDerivation { name = "cargoxx-dev"; @@ -25,6 +30,7 @@ buildInputs = [ pkgs.sqlite pkgs.reproc + libcxxPkgs.catch2_3 ]; env.NIX_CFLAGS_COMPILE = toString [ "-stdlib=libc++" diff --git a/src/main.cpp b/src/main.cpp index bddc361..a2c68dc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,7 @@ import cargoxx; +import std; int main(int /*argc*/, char** /*argv*/) { + std::println("Hello, cargoxx!"); return 0; } diff --git a/src/util/error.cpp b/src/util/error.cpp new file mode 100644 index 0000000..8ea24ba --- /dev/null +++ b/src/util/error.cpp @@ -0,0 +1,29 @@ +module cargoxx.util; + +import std; + +namespace cargoxx::util { + +auto format(const Error& e) -> std::string { + std::string out; + out += std::format("error[E{:04}]: {}\n", static_cast(e.code), e.message); + + if (e.location) { + if (e.line_col) { + out += std::format(" --> {}:{}:{}\n", + e.location->string(), + e.line_col->first, + e.line_col->second); + } else { + out += std::format(" --> {}\n", e.location->string()); + } + } + + if (!e.hint.empty()) { + out += std::format(" hint: {}\n", e.hint); + } + + return out; +} + +} // namespace cargoxx::util diff --git a/src/util/util.cppm b/src/util/util.cppm index 4f82f7a..c3a5a5f 100644 --- a/src/util/util.cppm +++ b/src/util/util.cppm @@ -1 +1,50 @@ export module cargoxx.util; + +import std; + +export namespace cargoxx::util { + +// Error code numbering follows TECH_SPEC.md §4. +enum class ErrorCode { + ManifestNotFound = 1, + ManifestParseError, + ManifestInvalidField, + ManifestUnknownField, + ManifestVersionInvalid, + + LayoutNoTarget = 20, + LayoutAmbiguousLib, + LayoutInvalidName, + + ResolutionUnknownPackage = 40, + ResolutionNetworkError, + ResolutionUnsatisfiable, + ResolutionVersionNotFound, + + LinkdbUnknownPackage = 60, + LinkdbCorrupt, + LinkdbComponentNotSupported, + + ExecCommandFailed = 80, + ExecToolNotFound, + BuildCmakeFailed, + BuildNixFailed, + + Internal = 100, + NotImplemented, +}; + +struct Error { + ErrorCode code; + std::string message; + std::string hint; + std::optional location; + std::optional> line_col; +}; + +template +using Result = std::expected; + +auto format(const Error& e) -> std::string; + +} // namespace cargoxx::util diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..e83c1e2 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,10 @@ +find_package(Catch2 3 REQUIRED CONFIG) +include(Catch) + +function(cargoxx_add_test name) + add_executable(${name} ${name}.cpp) + target_link_libraries(${name} PRIVATE cargoxx Catch2::Catch2WithMain) + catch_discover_tests(${name}) +endfunction() + +cargoxx_add_test(util_error) diff --git a/tests/util_error.cpp b/tests/util_error.cpp new file mode 100644 index 0000000..fc70690 --- /dev/null +++ b/tests/util_error.cpp @@ -0,0 +1,69 @@ +#include + +import cargoxx.util; +import std; + +using cargoxx::util::Error; +using cargoxx::util::ErrorCode; +using cargoxx::util::format; + +TEST_CASE("format renders code, message, and trailing newline", "[util][format]") { + Error e{ErrorCode::ManifestNotFound, "manifest not found", "", std::nullopt, std::nullopt}; + REQUIRE(format(e) == "error[E0001]: manifest not found\n"); +} + +TEST_CASE("format pads error codes to four digits", "[util][format]") { + Error e{ErrorCode::Internal, "boom", "", std::nullopt, std::nullopt}; + REQUIRE(format(e) == "error[E0100]: boom\n"); +} + +TEST_CASE("format includes location with line:col when present", "[util][format]") { + Error e{ + ErrorCode::ManifestParseError, + "syntax error", + "", + std::filesystem::path{"Cargoxx.toml"}, + std::pair{7, 1}, + }; + REQUIRE(format(e) == "error[E0002]: syntax error\n" + " --> Cargoxx.toml:7:1\n"); +} + +TEST_CASE("format includes location without line:col when only path is set", "[util][format]") { + Error e{ + ErrorCode::LayoutNoTarget, + "no target found", + "", + std::filesystem::path{"./"}, + std::nullopt, + }; + REQUIRE(format(e) == "error[E0020]: no target found\n" + " --> ./\n"); +} + +TEST_CASE("format appends a hint line when hint is non-empty", "[util][format]") { + Error e{ + ErrorCode::LayoutNoTarget, + "no target found", + "run `cargoxx new --lib ` to create a library project", + std::nullopt, + std::nullopt, + }; + REQUIRE(format(e) == + "error[E0020]: no target found\n" + " hint: run `cargoxx new --lib ` to create a library project\n"); +} + +TEST_CASE("format combines location and hint", "[util][format]") { + Error e{ + ErrorCode::LinkdbUnknownPackage, + "package not in link database", + "file an issue or supply a manual recipe", + std::filesystem::path{"Cargoxx.toml"}, + std::pair{7, 1}, + }; + REQUIRE(format(e) == + "error[E0060]: package not in link database\n" + " --> Cargoxx.toml:7:1\n" + " hint: file an issue or supply a manual recipe\n"); +}