Add canon add module and canon add unit-test
canon add module <path> creates <stem>/<path>.hpp, .cpp and .test.cpp (--no-test skips the test), records the module in .canon.toml, adds the source to the project's target and re-renders the managed block of CMakeLists.txt. canon add unit-test adds the test later. Both work from any directory inside the project. - toml: a reader for the TOML subset canon writes, with line numbers in errors, so canon stays dependency-free - manifest: strict parsing (unknown keys, wrong types, bad references) - project: finds .canon.toml by walking up from the current directory - plan/executor: update_file refuses to write if the file changed after it was read, and the report skips directories that already exist - tests: shared scenarios drive both the golden and end-to-end tests; goldens now live in tests/golden/<scenario>/ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+8
-13
@@ -1,6 +1,6 @@
|
||||
# Scaffolds each project kind, then configures, builds and tests the result
|
||||
# with the given compiler. Proves generated projects work, not just that they
|
||||
# look right.
|
||||
# Runs each scenario from ../scenarios.cmake, then configures, builds and tests
|
||||
# the resulting project with the given compiler. Proves generated projects
|
||||
# work, not just that they look right.
|
||||
#
|
||||
# cmake -DCANON=<canon executable> -DWORK_DIR=<scratch dir> -DCXX=<compiler> -P check.cmake
|
||||
|
||||
@@ -12,8 +12,8 @@ foreach(var CANON WORK_DIR CXX)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
file(REMOVE_RECURSE ${WORK_DIR})
|
||||
file(MAKE_DIRECTORY ${WORK_DIR})
|
||||
get_filename_component(WORK_DIR "${WORK_DIR}" ABSOLUTE)
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/../scenarios.cmake)
|
||||
|
||||
function(run)
|
||||
execute_process(COMMAND ${ARGN}
|
||||
@@ -26,14 +26,9 @@ function(run)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
foreach(case lib:libhello exe:hello)
|
||||
string(REPLACE ":" ";" parts ${case})
|
||||
list(GET parts 0 kind)
|
||||
list(GET parts 1 name)
|
||||
|
||||
set(project ${WORK_DIR}/${name})
|
||||
message(STATUS "e2e: canon new ${kind} ${name}")
|
||||
run(${CANON} new ${kind} ${name} --dir ${WORK_DIR})
|
||||
foreach(scenario ${CANON_SCENARIOS})
|
||||
message(STATUS "e2e: ${scenario}")
|
||||
canon_scenario(${scenario} ${WORK_DIR} project)
|
||||
run(${CMAKE_COMMAND} -S ${project} -B ${project}/build
|
||||
-DCMAKE_CXX_COMPILER=${CXX} -DCMAKE_BUILD_TYPE=Debug)
|
||||
run(${CMAKE_COMMAND} --build ${project}/build)
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# canon project manifest: what canon declared for this project.
|
||||
# The managed block in CMakeLists.txt is rendered from this file.
|
||||
|
||||
[project]
|
||||
name = "hello"
|
||||
kind = "exe"
|
||||
stem = "hello"
|
||||
spec = "P1204R0"
|
||||
|
||||
[build]
|
||||
backend = "cmake"
|
||||
cmake-minimum = "3.20"
|
||||
cxx-standard = 23
|
||||
|
||||
[[targets]]
|
||||
name = "hello"
|
||||
type = "executable"
|
||||
sources = ["hello/hello.cpp", "hello/greeting.cpp"]
|
||||
|
||||
[[modules]]
|
||||
path = "hello/greeting"
|
||||
target = "hello"
|
||||
unit-test = true
|
||||
|
||||
[[tests]]
|
||||
name = "basics"
|
||||
kind = "run"
|
||||
target = "hello"
|
||||
expect = "Hello, World!"
|
||||
@@ -0,0 +1,33 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(hello VERSION 0.1.0 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 23)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
option(BUILD_TESTING "Build hello's tests" ON)
|
||||
if(BUILD_TESTING)
|
||||
enable_testing()
|
||||
endif()
|
||||
|
||||
# Add your own CMake outside the managed block below.
|
||||
|
||||
# >>> canon:managed >>>
|
||||
# Rendered from .canon.toml; canon rewrites everything between these markers.
|
||||
|
||||
add_executable(hello
|
||||
hello/hello.cpp
|
||||
hello/greeting.cpp
|
||||
)
|
||||
target_include_directories(hello PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_executable(hello.greeting.test hello/greeting.test.cpp hello/greeting.cpp)
|
||||
target_include_directories(hello.greeting.test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
add_test(NAME hello/greeting.test COMMAND hello.greeting.test)
|
||||
|
||||
add_test(NAME basics COMMAND hello)
|
||||
set_tests_properties(basics PROPERTIES PASS_REGULAR_EXPRESSION "Hello, World!")
|
||||
endif()
|
||||
|
||||
# <<< canon:managed <<<
|
||||
@@ -0,0 +1,5 @@
|
||||
#include <hello/greeting.hpp>
|
||||
|
||||
namespace hello {
|
||||
|
||||
} // namespace hello
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
namespace hello {
|
||||
|
||||
} // namespace hello
|
||||
@@ -0,0 +1,6 @@
|
||||
// Unit tests for <hello/greeting.hpp>. Return non-zero from main() to fail.
|
||||
|
||||
#include <hello/greeting.hpp>
|
||||
|
||||
int main() {
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
# canon project manifest: what canon declared for this project.
|
||||
# The managed block in CMakeLists.txt is rendered from this file.
|
||||
|
||||
[project]
|
||||
name = "libhello"
|
||||
kind = "lib"
|
||||
stem = "hello"
|
||||
spec = "P1204R0"
|
||||
|
||||
[build]
|
||||
backend = "cmake"
|
||||
cmake-minimum = "3.20"
|
||||
cxx-standard = 23
|
||||
|
||||
[[targets]]
|
||||
name = "libhello"
|
||||
type = "library"
|
||||
sources = ["hello/hello.cpp", "hello/core.cpp", "hello/details/utility.cpp"]
|
||||
|
||||
[[modules]]
|
||||
path = "hello/hello"
|
||||
target = "libhello"
|
||||
unit-test = true
|
||||
|
||||
[[modules]]
|
||||
path = "hello/core"
|
||||
target = "libhello"
|
||||
unit-test = true
|
||||
|
||||
[[modules]]
|
||||
path = "hello/details/utility"
|
||||
target = "libhello"
|
||||
unit-test = true
|
||||
|
||||
[[tests]]
|
||||
name = "basics"
|
||||
kind = "driver"
|
||||
path = "tests/basics"
|
||||
target = "libhello"
|
||||
@@ -0,0 +1,45 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(libhello VERSION 0.1.0 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 23)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
option(BUILD_TESTING "Build libhello's tests" ON)
|
||||
if(BUILD_TESTING)
|
||||
enable_testing()
|
||||
endif()
|
||||
|
||||
# Add your own CMake outside the managed block below.
|
||||
|
||||
# >>> canon:managed >>>
|
||||
# Rendered from .canon.toml; canon rewrites everything between these markers.
|
||||
|
||||
add_library(libhello
|
||||
hello/hello.cpp
|
||||
hello/core.cpp
|
||||
hello/details/utility.cpp
|
||||
)
|
||||
set_target_properties(libhello PROPERTIES PREFIX "")
|
||||
target_include_directories(libhello PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_compile_features(libhello PUBLIC cxx_std_23)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_executable(hello.hello.test hello/hello.test.cpp)
|
||||
target_link_libraries(hello.hello.test PRIVATE libhello)
|
||||
add_test(NAME hello/hello.test COMMAND hello.hello.test)
|
||||
|
||||
add_executable(hello.core.test hello/core.test.cpp)
|
||||
target_link_libraries(hello.core.test PRIVATE libhello)
|
||||
add_test(NAME hello/core.test COMMAND hello.core.test)
|
||||
|
||||
add_executable(hello.details.utility.test hello/details/utility.test.cpp)
|
||||
target_link_libraries(hello.details.utility.test PRIVATE libhello)
|
||||
add_test(NAME hello/details/utility.test COMMAND hello.details.utility.test)
|
||||
|
||||
add_executable(tests.basics tests/basics/driver.cpp)
|
||||
target_link_libraries(tests.basics PRIVATE libhello)
|
||||
add_test(NAME basics COMMAND tests.basics)
|
||||
endif()
|
||||
|
||||
# <<< canon:managed <<<
|
||||
@@ -0,0 +1,5 @@
|
||||
#include <hello/core.hpp>
|
||||
|
||||
namespace hello {
|
||||
|
||||
} // namespace hello
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
namespace hello {
|
||||
|
||||
} // namespace hello
|
||||
@@ -0,0 +1,6 @@
|
||||
// Unit tests for <hello/core.hpp>. Return non-zero from main() to fail.
|
||||
|
||||
#include <hello/core.hpp>
|
||||
|
||||
int main() {
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#include <hello/details/utility.hpp>
|
||||
|
||||
namespace hello::details {
|
||||
|
||||
} // namespace hello::details
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
namespace hello::details {
|
||||
|
||||
} // namespace hello::details
|
||||
@@ -0,0 +1,6 @@
|
||||
// Unit tests for <hello/details/utility.hpp>. Return non-zero from main() to fail.
|
||||
|
||||
#include <hello/details/utility.hpp>
|
||||
|
||||
int main() {
|
||||
}
|
||||
+14
-35
@@ -1,5 +1,5 @@
|
||||
# Scaffolds each project kind and compares the result byte for byte with the
|
||||
# expected trees next to this script.
|
||||
# Runs each scenario from ../scenarios.cmake and compares the resulting project
|
||||
# byte for byte with the expected tree in <scenario>/ next to this script.
|
||||
#
|
||||
# cmake -DCANON=<canon executable> -DWORK_DIR=<scratch dir> [-DUPDATE=ON] -P check.cmake
|
||||
#
|
||||
@@ -14,42 +14,20 @@ foreach(var CANON WORK_DIR)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
set(golden_dir ${CMAKE_CURRENT_LIST_DIR})
|
||||
file(REMOVE_RECURSE ${WORK_DIR})
|
||||
file(MAKE_DIRECTORY ${WORK_DIR})
|
||||
get_filename_component(WORK_DIR "${WORK_DIR}" ABSOLUTE)
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/../scenarios.cmake)
|
||||
|
||||
set(golden_dir ${CMAKE_CURRENT_LIST_DIR})
|
||||
set(failed FALSE)
|
||||
|
||||
foreach(case lib:libhello exe:hello)
|
||||
string(REPLACE ":" ";" parts ${case})
|
||||
list(GET parts 0 kind)
|
||||
list(GET parts 1 name)
|
||||
|
||||
execute_process(
|
||||
COMMAND ${CANON} new ${kind} ${name} --dir ${WORK_DIR}
|
||||
RESULT_VARIABLE status
|
||||
OUTPUT_VARIABLE output
|
||||
ERROR_VARIABLE errors)
|
||||
if(NOT status EQUAL 0)
|
||||
message(FATAL_ERROR "canon new ${kind} ${name} failed (${status}):\n${output}${errors}")
|
||||
endif()
|
||||
|
||||
# A second run must refuse to touch the existing project.
|
||||
execute_process(
|
||||
COMMAND ${CANON} new ${kind} ${name} --dir ${WORK_DIR}
|
||||
RESULT_VARIABLE status
|
||||
OUTPUT_QUIET ERROR_QUIET)
|
||||
if(status EQUAL 0)
|
||||
message(SEND_ERROR "canon new ${kind} ${name} succeeded over an existing project")
|
||||
set(failed TRUE)
|
||||
endif()
|
||||
|
||||
set(actual ${WORK_DIR}/${name})
|
||||
set(expected ${golden_dir}/${name})
|
||||
foreach(scenario ${CANON_SCENARIOS})
|
||||
canon_scenario(${scenario} ${WORK_DIR} actual)
|
||||
get_filename_component(name ${actual} NAME)
|
||||
set(expected ${golden_dir}/${scenario}/${name})
|
||||
|
||||
if(UPDATE)
|
||||
file(REMOVE_RECURSE ${expected})
|
||||
file(COPY ${actual} DESTINATION ${golden_dir})
|
||||
file(REMOVE_RECURSE ${golden_dir}/${scenario})
|
||||
file(COPY ${actual} DESTINATION ${golden_dir}/${scenario})
|
||||
message(STATUS "Updated ${expected}")
|
||||
continue()
|
||||
endif()
|
||||
@@ -60,7 +38,7 @@ foreach(case lib:libhello exe:hello)
|
||||
list(SORT expected_files)
|
||||
|
||||
if(NOT actual_files STREQUAL expected_files)
|
||||
message(SEND_ERROR "${name}: files differ from golden\n"
|
||||
message(SEND_ERROR "${scenario}: files differ from golden\n"
|
||||
" actual: ${actual_files}\n"
|
||||
" expected: ${expected_files}")
|
||||
set(failed TRUE)
|
||||
@@ -72,7 +50,8 @@ foreach(case lib:libhello exe:hello)
|
||||
COMMAND ${CMAKE_COMMAND} -E compare_files ${actual}/${file} ${expected}/${file}
|
||||
RESULT_VARIABLE different)
|
||||
if(different)
|
||||
message(SEND_ERROR "${name}/${file} differs from golden (diff ${actual}/${file} ${expected}/${file})")
|
||||
message(SEND_ERROR "${scenario}: ${name}/${file} differs from golden "
|
||||
"(diff ${actual}/${file} ${expected}/${file})")
|
||||
set(failed TRUE)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/build/
|
||||
@@ -0,0 +1,12 @@
|
||||
# hello
|
||||
|
||||
Created by canon. The layout follows [P1204R0](https://wg21.link/p1204r0)
|
||||
(Canonical Project Structure).
|
||||
|
||||
## Build and test
|
||||
|
||||
```bash
|
||||
cmake -S . -B build
|
||||
cmake --build build
|
||||
ctest --test-dir build --output-on-failure
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
#include <print>
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
std::println("Hello, {}!", argc > 1 ? argv[1] : "World");
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/build/
|
||||
@@ -0,0 +1,12 @@
|
||||
# libhello
|
||||
|
||||
Created by canon. The layout follows [P1204R0](https://wg21.link/p1204r0)
|
||||
(Canonical Project Structure).
|
||||
|
||||
## Build and test
|
||||
|
||||
```bash
|
||||
cmake -S . -B build
|
||||
cmake --build build
|
||||
ctest --test-dir build --output-on-failure
|
||||
```
|
||||
@@ -0,0 +1,11 @@
|
||||
#include <hello/hello.hpp>
|
||||
|
||||
#include <format>
|
||||
|
||||
namespace hello {
|
||||
|
||||
std::string greeting(std::string_view name) {
|
||||
return std::format("Hello, {}!", name);
|
||||
}
|
||||
|
||||
} // namespace hello
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace hello {
|
||||
|
||||
// Returns a greeting for name, e.g. "Hello, World!".
|
||||
[[nodiscard]] std::string greeting(std::string_view name);
|
||||
|
||||
} // namespace hello
|
||||
@@ -0,0 +1,13 @@
|
||||
#include <hello/hello.hpp>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <print>
|
||||
|
||||
int main() {
|
||||
const auto actual = hello::greeting("World");
|
||||
if (actual != "Hello, World!") {
|
||||
std::println(stderr, "greeting(\"World\") returned \"{}\"", actual);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Functional test: uses libhello only through its public headers, as a consumer would.
|
||||
|
||||
#include <hello/hello.hpp>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <print>
|
||||
|
||||
int main() {
|
||||
const auto greeting = hello::greeting("libhello");
|
||||
std::println("{}", greeting);
|
||||
return greeting.starts_with("Hello") ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
# canon command sequences shared by the golden and end-to-end tests. Each
|
||||
# scenario builds one project inside <work_dir>/<scenario>/. Requires CANON.
|
||||
|
||||
set(CANON_SCENARIOS new-lib new-exe add-lib add-exe)
|
||||
|
||||
# Scenarios run canon from other directories, so a relative path would break.
|
||||
get_filename_component(CANON "${CANON}" ABSOLUTE)
|
||||
|
||||
# Runs canon in working_dir and fails the script if it fails.
|
||||
function(canon_run working_dir)
|
||||
execute_process(
|
||||
COMMAND ${CANON} ${ARGN}
|
||||
WORKING_DIRECTORY ${working_dir}
|
||||
RESULT_VARIABLE status
|
||||
OUTPUT_VARIABLE output
|
||||
ERROR_VARIABLE errors)
|
||||
if(NOT status EQUAL 0)
|
||||
string(JOIN " " args ${ARGN})
|
||||
message(FATAL_ERROR "canon ${args} failed (${status}) in ${working_dir}:\n${output}${errors}")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# Runs canon in working_dir and fails the script if it succeeds.
|
||||
function(canon_run_fails working_dir)
|
||||
execute_process(
|
||||
COMMAND ${CANON} ${ARGN}
|
||||
WORKING_DIRECTORY ${working_dir}
|
||||
RESULT_VARIABLE status
|
||||
OUTPUT_QUIET ERROR_QUIET)
|
||||
if(status EQUAL 0)
|
||||
string(JOIN " " args ${ARGN})
|
||||
message(FATAL_ERROR "canon ${args} succeeded in ${working_dir} but should have failed")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# Runs the scenario and sets out_var to the project directory it produced.
|
||||
function(canon_scenario scenario work_dir out_var)
|
||||
set(dir ${work_dir}/${scenario})
|
||||
file(REMOVE_RECURSE ${dir})
|
||||
file(MAKE_DIRECTORY ${dir})
|
||||
|
||||
if(scenario STREQUAL "new-lib")
|
||||
canon_run(${dir} new lib libhello)
|
||||
canon_run_fails(${dir} new lib libhello) # never overwrites
|
||||
set(project ${dir}/libhello)
|
||||
|
||||
elseif(scenario STREQUAL "new-exe")
|
||||
canon_run(${dir} new exe hello)
|
||||
set(project ${dir}/hello)
|
||||
|
||||
elseif(scenario STREQUAL "add-lib")
|
||||
canon_run(${dir} new lib libhello)
|
||||
set(project ${dir}/libhello)
|
||||
canon_run(${project} add module core)
|
||||
canon_run(${project}/hello add module details/utility --no-test) # from a subdirectory
|
||||
canon_run(${project} add unit-test details/utility)
|
||||
canon_run(${project} add module parser --dry-run) # must write nothing
|
||||
canon_run_fails(${project} add module core) # already declared
|
||||
canon_run_fails(${project} add unit-test core) # already has one
|
||||
canon_run_fails(${project} add module Bad) # invalid path
|
||||
|
||||
elseif(scenario STREQUAL "add-exe")
|
||||
canon_run(${dir} new exe hello)
|
||||
set(project ${dir}/hello)
|
||||
canon_run(${project} add module greeting)
|
||||
|
||||
else()
|
||||
message(FATAL_ERROR "unknown scenario '${scenario}'")
|
||||
endif()
|
||||
|
||||
set(${out_var} ${project} PARENT_SCOPE)
|
||||
endfunction()
|
||||
Reference in New Issue
Block a user