Files
canon/tests/scenarios.cmake
paulhorn 6981f55f28 Add canon sync
canon sync rewrites the managed block of CMakeLists.txt from .canon.toml,
for after either was edited by hand. It prints a unified diff of the
change (also with --dry-run), writes only CMakeLists.txt so comments in
.canon.toml survive, does nothing if the block already matches, and
warns when the build will still fail because .canon.toml declares files
that do not exist. doctor's hand-edited-block warning now points to it.

- diff: a small line-based unified diff, no external tools
- tests: a sync scenario (hand-edited manifest, --dry-run leaves the file
  untouched, sync from a subdirectory, no-op second run, hand edits in
  the block replaced); unit tests for plan_sync and unified_diff

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 19:09:35 +02:00

155 lines
6.9 KiB
CMake

# canon command sequences shared by the golden and end-to-end tests. Each
# scenario builds one project inside <work_dir>/<scenario>/. Requires CANON.
#
# canon runs with XDG_CONFIG_HOME set to <work_dir>/<scenario>-config, so user
# templates in your own ~/.config never leak into the results.
set(CANON_SCENARIOS new-lib new-exe add-lib add-exe multi-target exe-tests templates sync)
# 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 ${CMAKE_COMMAND} -E env XDG_CONFIG_HOME=${config_home} ${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 ${CMAKE_COMMAND} -E env XDG_CONFIG_HOME=${config_home} ${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})
set(config_home ${work_dir}/${scenario}-config) # seen by canon_run
file(REMOVE_RECURSE ${config_home})
file(MAKE_DIRECTORY ${config_home})
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)
elseif(scenario STREQUAL "multi-target")
canon_run(${dir} new lib libhello)
set(project ${dir}/libhello)
canon_run(${project} add target exe hello-cli) # links libhello
canon_run(${project} add module args --target hello-cli)
canon_run(${project} add target lib libextra)
canon_run(${project} add dep libextra) # libhello -> libextra
canon_run(${project} add dep Threads::Threads --target hello-cli)
canon_run(${project} add test edge-cases)
canon_run_fails(${project} add dep libhello --target libextra) # cycle
canon_run_fails(${project} add dep hello-cli) # executable
canon_run_fails(${project} add dep fmt) # neither
canon_run_fails(${project} add target exe hello) # hello/ is taken
elseif(scenario STREQUAL "exe-tests")
canon_run(${dir} new exe hello)
set(project ${dir}/hello)
canon_run(${project} add test greets-paul --arg Paul --expect "^Hello, Paul!")
canon_run(${project} add test spaces --arg "two words" --expect "Hello, two words!")
canon_run_fails(${project} add test greets-paul)
elseif(scenario STREQUAL "templates")
# User templates apply to every new project: a license line on library and module headers.
canon_run(${dir} templates export library.hpp module.hpp --user)
foreach(template library.hpp module.hpp)
set(file ${config_home}/canon/templates/${template})
file(READ ${file} text)
file(WRITE ${file} "// SPDX-License-Identifier: MIT\n${text}")
endforeach()
canon_run(${dir} new lib libhello)
set(project ${dir}/libhello)
# A project template wins over a user template.
canon_run(${project} templates export module.hpp --dry-run) # must write nothing
canon_run(${project} templates export module.hpp)
file(READ ${project}/.canon/templates/module.hpp text)
file(WRITE ${project}/.canon/templates/module.hpp "// Part of {{namespace}}.\n${text}")
canon_run(${project} add module details/core)
canon_run(${project} templates)
canon_run_fails(${project} templates export module.hpp) # never overwrites
# Broken templates are errors, not silently skipped.
file(WRITE ${project}/.canon/templates/module.cpp "{{nmae}}\n")
canon_run_fails(${project} add module broken)
canon_run_fails(${project} templates)
file(REMOVE ${project}/.canon/templates/module.cpp)
file(WRITE ${config_home}/canon/templates/modul.hpp "")
canon_run_fails(${project} add module broken)
file(REMOVE ${config_home}/canon/templates/modul.hpp)
elseif(scenario STREQUAL "sync")
canon_run(${dir} new lib libhello)
set(project ${dir}/libhello)
# Declare a source by editing .canon.toml by hand, then bring CMakeLists.txt along.
file(WRITE ${project}/hello/extra.cpp "namespace hello {\nint extra() { return 42; }\n}\n")
file(READ ${project}/.canon.toml manifest)
string(REPLACE "sources = [\"hello/hello.cpp\"]" "sources = [\"hello/hello.cpp\", \"hello/extra.cpp\"]"
manifest "${manifest}")
file(WRITE ${project}/.canon.toml "# edited by hand\n${manifest}")
file(READ ${project}/CMakeLists.txt before)
canon_run(${project} sync --dry-run)
file(READ ${project}/CMakeLists.txt after)
if(NOT before STREQUAL after)
message(FATAL_ERROR "canon sync --dry-run changed CMakeLists.txt")
endif()
canon_run(${project}/hello sync) # from a subdirectory
canon_run(${project} sync) # already in sync
# Hand edits inside the managed block are replaced; the comment in .canon.toml survives.
file(READ ${project}/CMakeLists.txt cmake)
string(REPLACE "set_target_properties" "add_compile_options(-w)\nset_target_properties" cmake "${cmake}")
file(WRITE ${project}/CMakeLists.txt "${cmake}")
canon_run(${project} sync)
else()
message(FATAL_ERROR "unknown scenario '${scenario}'")
endif()
canon_run(${project} doctor) # every scenario ends healthy
set(${out_var} ${project} PARENT_SCOPE)
endfunction()