6981f55f28
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>
155 lines
6.9 KiB
CMake
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()
|