`spr_collisions()` had no test. `tests/sprite_verbs.c` drives the collision path end to end but through a mock backend, so the real overlap arithmetic in `src/sprite_akgl.c` could have changed what `BUMP(1)` reports for every program in existence and the suite would still have printed 110/110. That gap is closed here, before anything touches the arithmetic, so that there is a *before* to compare an *after* against. Six cases against the real akgl backend: nothing defined, two sprites overlapping, edge-to-edge, a hidden sprite, and the x-expand bit doubling the box that collides rather than only the one that draws. Edge-to-edge earns its place -- the test is a strict `<`, a tile-aligned program puts sprites there constantly, and a replacement answering "touching" instead of "overlapping" would change every one of them silently. **The seventh is the cross-shaped overlap**, and it is the one to watch. A tall thin sprite crossing a short wide one overlaps without either rectangle holding a corner of the other; `akgl_collide_rectangles()` is documented as answering "no" there, which is why `src/sprite_akgl.c` does the four comparisons itself rather than calling it. Two further assertions stop that test passing by accident: each sprite is moved clear along the axis it is supposed to be short on, so a sprite that came out the wrong size fails rather than quietly reporting an ordinary overlap. `tests/collision_perf.c` answers the question nobody had measured. The service runs at the top of every interpreter *step* and the frontend takes 256 steps per rendered frame, so a busy program scans up to 256 times a frame over sprites that have not moved. At RelWithDebInfo, scale 10, best of 5: the scan is 96.3 ns at eight overlapping sprites and 19.4 ns at none, against a rendered frame of 1.17 ms. **256 scans is 24.7 us, or 2.1% of a frame, in the pathological case, and 0.42% for a program with no sprites.** So the per-step cadence stays. It is what makes a collision report describe where the sprites have just been moved to rather than where they were, and 2% of a frame in a case no real program reaches is not worth changing when a handler fires for every program that already works. The numbers and that conclusion are in `MAINTENANCE.md` so it does not get re-argued. The benchmark borrows libakgl's `benchutil.h` by include path rather than copying it, the way the fixture font is already borrowed, and is labelled `perf` so `ctest -LE perf` can leave it out. It runs at scale 1 in the ordinary suite -- 1.2 seconds -- because a benchmark nothing ever builds is a benchmark that rots. Both suites green: 111 with akgl, 110 without. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL
653 lines
28 KiB
CMake
653 lines
28 KiB
CMake
cmake_minimum_required(VERSION 3.10)
|
|
# The single source of truth for the version, in the shape libakstdlib and
|
|
# libakgl both use. It flows into the library SOVERSION and nothing else spells
|
|
# a version number.
|
|
#
|
|
# 0.x on purpose: TODO.md section 12 records eleven defects carried over from the
|
|
# Go reference that are deliberately reproduced and not yet fixed, so the
|
|
# language surface is not being promised yet.
|
|
project(akbasic VERSION 0.1.0 LANGUAGES C)
|
|
|
|
# Pre-1.0 the ABI may break on a minor bump, so the soname carries MAJOR.MINOR.
|
|
# At 1.0 this becomes ${PROJECT_VERSION_MAJOR} alone.
|
|
if(PROJECT_VERSION_MAJOR EQUAL 0)
|
|
set(AKBASIC_SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}")
|
|
else()
|
|
set(AKBASIC_SOVERSION "${PROJECT_VERSION_MAJOR}")
|
|
endif()
|
|
|
|
include(CTest)
|
|
include(GNUInstallDirs)
|
|
|
|
option(AKBASIC_WITH_AKGL "Build the libakgl-backed text sink and link SDL3" OFF)
|
|
option(AKBASIC_BUILD_EXAMPLES "Build the embedding example in examples/" ON)
|
|
option(AKBASIC_COVERAGE "Instrument the build with gcov coverage counters" OFF)
|
|
option(AKBASIC_SANITIZE "Build with ASan + UBSan" OFF)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Dependencies
|
|
#
|
|
# libakgl vendors its own copies of libakerror and libakstdlib and guards every
|
|
# dependency with if(NOT TARGET ...), so akerror::akerror and akstdlib::akstdlib
|
|
# must exist *before* add_subdirectory(deps/libakgl) or the targets are declared
|
|
# twice.
|
|
#
|
|
# All three dependencies register their CTest tests unconditionally. Pulled in
|
|
# EXCLUDE_FROM_ALL their test binaries are never built, so each one would land in
|
|
# our suite as "Not Run" and fail. CMake has no way to un-register a test and
|
|
# set_tests_properties cannot reach across directory scopes, so add_test() and
|
|
# set_tests_properties() are shadowed for the duration of the add_subdirectory()
|
|
# calls. Note libakstdlib carries the same shadow but only arms it when *it* is
|
|
# top-level, so it does nothing for us -- this one has to wrap all three.
|
|
#
|
|
# libakerror additionally namespaces its `mutation` target when embedded but not
|
|
# its `coverage` target (deps/libakerror/CMakeLists.txt:194 vs :172), so a
|
|
# coverage build collides on the `coverage` target and fails to configure at all.
|
|
# Rename the dependency's on the way past. Remove this once libakerror applies
|
|
# the same CMAKE_SOURCE_DIR test to `coverage` that it already applies to
|
|
# `mutation` -- filed in deps/libakstdlib/TODO.md section 2.3.
|
|
#
|
|
# **Only one project in a tree may shadow add_test(), and this is that project.**
|
|
# CMake exposes an overridden command as `_name` and chains exactly one level: a
|
|
# second override rebinds `_add_test` to the first override and the builtin
|
|
# becomes unreachable to everyone, so our own registrations recurse until CMake
|
|
# stops at "Maximum recursion depth of 1000 exceeded". libakgl 0.3.0 briefly
|
|
# shadowed it unconditionally and this is exactly what happened; the top-level
|
|
# guard it used to have is back, and libakstdlib has always had one.
|
|
# ---------------------------------------------------------------------------
|
|
set(AKBASIC_SUPPRESS_ADD_TEST TRUE)
|
|
|
|
function(add_test)
|
|
if(NOT AKBASIC_SUPPRESS_ADD_TEST)
|
|
_add_test(${ARGV})
|
|
endif()
|
|
endfunction()
|
|
|
|
function(set_tests_properties)
|
|
if(NOT AKBASIC_SUPPRESS_ADD_TEST)
|
|
_set_tests_properties(${ARGV})
|
|
endif()
|
|
endfunction()
|
|
|
|
function(add_custom_target _name)
|
|
if(AKBASIC_SUPPRESS_ADD_TEST AND _name STREQUAL "coverage")
|
|
_add_custom_target(akerror_coverage ${ARGN})
|
|
else()
|
|
_add_custom_target(${ARGV})
|
|
endif()
|
|
endfunction()
|
|
|
|
add_subdirectory(deps/libakerror EXCLUDE_FROM_ALL)
|
|
add_subdirectory(deps/libakstdlib EXCLUDE_FROM_ALL)
|
|
if(AKBASIC_WITH_AKGL)
|
|
# libakgl 0.3.0 adds its own vendored SDL, SDL_image, SDL_mixer, SDL_ttf and
|
|
# jansson whether or not it is top-level, so there is nothing to do here but
|
|
# add it. Before that it went down a find_package path when embedded and
|
|
# required all five installed on the system, and this block declared them by
|
|
# hand first -- filed as libakgl API-gap item 5 and resolved there.
|
|
#
|
|
# akerror::akerror and akstdlib::akstdlib still have to exist first: libakgl
|
|
# guards every dependency with if(NOT TARGET ...), which is what stops its
|
|
# vendored copies of those two being declared a second time.
|
|
add_subdirectory(deps/libakgl EXCLUDE_FROM_ALL)
|
|
endif()
|
|
|
|
set(AKBASIC_SUPPRESS_ADD_TEST FALSE)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Instrumentation, applied per target so it never leaks into an exported
|
|
# interface.
|
|
# ---------------------------------------------------------------------------
|
|
function(akbasic_instrument _target)
|
|
if(AKBASIC_COVERAGE)
|
|
target_compile_options(${_target} PRIVATE --coverage -O0 -g)
|
|
target_link_options(${_target} PRIVATE --coverage)
|
|
endif()
|
|
if(AKBASIC_SANITIZE)
|
|
target_compile_options(${_target} PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer -g)
|
|
target_link_options(${_target} PRIVATE -fsanitize=address,undefined)
|
|
endif()
|
|
endfunction()
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# The interpreter library. Everything here must be free of SDL, free of any
|
|
# process-terminating call, and buildable with no libakgl present.
|
|
# ---------------------------------------------------------------------------
|
|
set(AKBASIC_SOURCES
|
|
src/args.c
|
|
src/audio_tables.c
|
|
src/data.c
|
|
src/environment.c
|
|
src/error.c
|
|
src/grammar.c
|
|
src/format.c
|
|
src/graphics_tables.c
|
|
src/parser.c
|
|
src/parser_commands.c
|
|
src/play.c
|
|
src/runtime.c
|
|
src/runtime_audio.c
|
|
src/runtime_commands.c
|
|
src/runtime_console.c
|
|
src/runtime_disk.c
|
|
src/runtime_format.c
|
|
src/runtime_functions.c
|
|
src/runtime_graphics.c
|
|
src/runtime_housekeeping.c
|
|
src/runtime_machine.c
|
|
src/renumber.c
|
|
src/runtime_input.c
|
|
src/runtime_sprite.c
|
|
src/runtime_struct.c
|
|
src/runtime_structure.c
|
|
src/runtime_trap.c
|
|
src/scanner.c
|
|
src/sink_stdio.c
|
|
src/sink_tee.c
|
|
src/sprite_tables.c
|
|
src/host.c
|
|
src/structtype.c
|
|
src/symtab.c
|
|
src/value.c
|
|
src/variable.c
|
|
src/verbs.c
|
|
)
|
|
|
|
add_library(akbasic ${AKBASIC_SOURCES})
|
|
add_library(akbasic::akbasic ALIAS akbasic)
|
|
|
|
target_include_directories(akbasic PUBLIC
|
|
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
|
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/>
|
|
)
|
|
|
|
target_compile_options(akbasic PRIVATE -Wall -Wextra)
|
|
target_link_libraries(akbasic PUBLIC akstdlib::akstdlib akerror::akerror)
|
|
target_link_libraries(akbasic PRIVATE m)
|
|
|
|
set_target_properties(akbasic PROPERTIES
|
|
VERSION ${PROJECT_VERSION}
|
|
SOVERSION ${AKBASIC_SOVERSION}
|
|
)
|
|
|
|
akbasic_instrument(akbasic)
|
|
|
|
# The libakgl-backed text sink, kept in its own target so the core library and
|
|
# the whole golden suite build with no SDL present.
|
|
if(AKBASIC_WITH_AKGL)
|
|
add_library(akbasic_akgl
|
|
src/audio_akgl.c
|
|
src/graphics_akgl.c
|
|
src/input_akgl.c
|
|
src/sink_akgl.c
|
|
src/sprite_akgl.c
|
|
)
|
|
target_compile_options(akbasic_akgl PRIVATE -Wall -Wextra)
|
|
target_link_libraries(akbasic_akgl PUBLIC akbasic akgl)
|
|
akbasic_instrument(akbasic_akgl)
|
|
|
|
# The SDL host, in its own target so the separation goal 3 rests on is visible
|
|
# in the build graph and not only in a comment: akbasic is the interpreter,
|
|
# akbasic_akgl is the adaptors that draw through somebody else's renderer, and
|
|
# this is the one thing in the repository that creates a window. A game
|
|
# embedding the interpreter links the first two and not this.
|
|
add_library(akbasic_frontend src/frontend_akgl.c)
|
|
target_compile_options(akbasic_frontend PRIVATE -Wall -Wextra)
|
|
target_link_libraries(akbasic_frontend PUBLIC akbasic_akgl)
|
|
akbasic_instrument(akbasic_frontend)
|
|
|
|
# The documentation's figure generator: a second, much smaller host that draws
|
|
# one program onto an offscreen target and writes a PNG. Not instrumented and
|
|
# not a test -- it produces a build artifact rather than an answer, and folding
|
|
# it into the coverage figure would credit the interpreter for lines only a
|
|
# documentation build runs.
|
|
add_executable(akbasic_screenshot tools/screenshot.c)
|
|
target_compile_options(akbasic_screenshot PRIVATE -Wall -Wextra)
|
|
target_link_libraries(akbasic_screenshot PRIVATE akbasic_akgl SDL3_image::SDL3_image)
|
|
|
|
# Regenerating every figure in docs/ is a deliberate act, never part of a
|
|
# build: the PNGs are checked in, and a rebuild that silently rewrote them
|
|
# would put a binary diff in front of anybody who happened to run `make`.
|
|
add_custom_target(docs_screenshots
|
|
COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tools/docs_screenshots.sh"
|
|
--root "${CMAKE_CURRENT_SOURCE_DIR}"
|
|
--tool "$<TARGET_FILE:akbasic_screenshot>"
|
|
DEPENDS akbasic_screenshot
|
|
COMMENT "Regenerating the documentation figures in docs/images"
|
|
VERBATIM)
|
|
endif()
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# The standalone driver. This is the only place FINISH_NORETURN may appear.
|
|
# ---------------------------------------------------------------------------
|
|
add_executable(basic src/main.c)
|
|
target_compile_options(basic PRIVATE -Wall -Wextra)
|
|
target_link_libraries(basic PRIVATE akbasic)
|
|
if(AKBASIC_WITH_AKGL)
|
|
target_link_libraries(basic PRIVATE akbasic_frontend)
|
|
# The reference's own font, at the reference's own size. Its main.go opens
|
|
# "./fonts/C64_Pro_Mono-STYLE.ttf", which only ever resolved from its own
|
|
# source directory; compiled in, it resolves from anywhere, and AKBASIC_FONT
|
|
# overrides it at runtime for an installed copy. Vendored into assets/fonts/
|
|
# rather than reached for in the submodule -- see assets/fonts/PROVENANCE.md,
|
|
# which also carries the licence question that came with it.
|
|
target_compile_definitions(basic PRIVATE
|
|
AKBASIC_HAVE_AKGL=1
|
|
AKBASIC_FONT_PATH="${CMAKE_CURRENT_SOURCE_DIR}/assets/fonts/C64_Pro_Mono-STYLE.ttf")
|
|
endif()
|
|
akbasic_instrument(basic)
|
|
|
|
# The embedding example. It is built by default and registered as a test so the
|
|
# code README.md quotes cannot rot: a signature change breaks the build.
|
|
if(AKBASIC_BUILD_EXAMPLES)
|
|
foreach(_example IN ITEMS embed hostvars hoststruct)
|
|
add_executable(akbasic_example_${_example} examples/${_example}.c)
|
|
target_compile_options(akbasic_example_${_example} PRIVATE -Wall -Wextra)
|
|
target_link_libraries(akbasic_example_${_example} PRIVATE akbasic)
|
|
akbasic_instrument(akbasic_example_${_example})
|
|
_add_test(NAME example_${_example} COMMAND akbasic_example_${_example})
|
|
endforeach()
|
|
endif()
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests.
|
|
#
|
|
# Three lists, and two of them invert the meaning of "Passed", exactly as
|
|
# libakstdlib does:
|
|
# AKBASIC_TESTS must exit 0
|
|
# AKBASIC_WILL_FAIL_TESTS abort by design
|
|
# AKBASIC_KNOWN_FAILING_TESTS assert the *correct* contract for a defect
|
|
# recorded in TODO.md and are expected to fail.
|
|
# When one starts passing CTest reports
|
|
# "unexpectedly passed" -- that is the cue to move
|
|
# it into AKBASIC_TESTS along with the fix.
|
|
#
|
|
# Test executables are named akbasic_test_<name>, never test_<name>: a
|
|
# dependency's targets are created even under EXCLUDE_FROM_ALL, and libakstdlib
|
|
# already ships test_version. The CTest name stays bare, so `ctest -R value`
|
|
# still selects it.
|
|
# ---------------------------------------------------------------------------
|
|
set(AKBASIC_TESTS
|
|
audio_verbs
|
|
console_verbs
|
|
devices
|
|
disk_verbs
|
|
environment_scope
|
|
error_codes
|
|
for_next
|
|
format_verbs
|
|
grammar_leaves
|
|
graphics_verbs
|
|
hoststruct
|
|
hostvars
|
|
housekeeping_verbs
|
|
input_verbs
|
|
interrupts
|
|
machine_verbs
|
|
numeric_contract
|
|
parser_commands
|
|
parser_expressions
|
|
read_data
|
|
renumber
|
|
runtime_evaluate
|
|
runtime_verbs
|
|
scanner_tokens
|
|
sink_stdio
|
|
sink_tee
|
|
sprite_verbs
|
|
structure_verbs
|
|
struct_pointers
|
|
struct_types
|
|
trap_verbs
|
|
symtab
|
|
unnumbered
|
|
user_functions
|
|
value_arithmetic
|
|
value_bitwise
|
|
value_compare
|
|
value_pool
|
|
variable_subscript
|
|
verbs_table
|
|
version_check
|
|
)
|
|
|
|
set(AKBASIC_WILL_FAIL_TESTS
|
|
)
|
|
|
|
# Empty, and worth keeping declared rather than deleted -- libakstdlib left its
|
|
# AKSL_KNOWN_FAILING_TESTS in place for the same reason when 0.2.0 cleared it.
|
|
# The next defect that has to be reproduced before it can be fixed goes here, and
|
|
# the machinery to do that should not have to be reinvented.
|
|
set(AKBASIC_KNOWN_FAILING_TESTS
|
|
for_semantics
|
|
)
|
|
|
|
foreach(_test IN LISTS AKBASIC_TESTS AKBASIC_WILL_FAIL_TESTS AKBASIC_KNOWN_FAILING_TESTS)
|
|
add_executable(akbasic_test_${_test} tests/${_test}.c)
|
|
target_compile_options(akbasic_test_${_test} PRIVATE -Wall -Wextra)
|
|
target_link_libraries(akbasic_test_${_test} PRIVATE akbasic)
|
|
target_include_directories(akbasic_test_${_test} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/tests")
|
|
akbasic_instrument(akbasic_test_${_test})
|
|
_add_test(NAME ${_test} COMMAND akbasic_test_${_test})
|
|
endforeach()
|
|
|
|
# The akgl-backed suite. Its own list because it is the only test here that links
|
|
# SDL, and because it can only exist when AKBASIC_WITH_AKGL is on -- the whole
|
|
# point of the backend records is that everything else builds without it.
|
|
#
|
|
# It runs under the dummy video and audio drivers with a software renderer,
|
|
# following deps/libakgl/tests/draw.c: a 128x128 target read back with
|
|
# SDL_RenderReadPixels, so it needs no display and no offscreen harness.
|
|
if(AKBASIC_WITH_AKGL)
|
|
add_executable(akbasic_test_akgl_backends tests/akgl_backends.c)
|
|
target_compile_options(akbasic_test_akgl_backends PRIVATE -Wall -Wextra)
|
|
target_link_libraries(akbasic_test_akgl_backends PRIVATE akbasic_akgl)
|
|
target_include_directories(akbasic_test_akgl_backends PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/tests")
|
|
# libakgl's monospaced fixture font, borrowed rather than copied: a second copy
|
|
# of a 10 KB binary is a second thing to keep in step for no benefit.
|
|
target_compile_definitions(akbasic_test_akgl_backends PRIVATE
|
|
AKBASIC_TEST_FONT="${CMAKE_CURRENT_SOURCE_DIR}/deps/libakgl/tests/assets/akgl_test_mono.ttf")
|
|
akbasic_instrument(akbasic_test_akgl_backends)
|
|
_add_test(NAME akgl_backends COMMAND akbasic_test_akgl_backends)
|
|
_set_tests_properties(akgl_backends PROPERTIES
|
|
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/tests"
|
|
TIMEOUT 60
|
|
ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_AUDIODRIVER=dummy")
|
|
|
|
# What the collision scan costs against what a frame costs. Labelled `perf` so
|
|
# `ctest -LE perf` leaves it out of the ordinary run: it is the only test here
|
|
# that deliberately spends wall-clock time, and the numbers are meaningless in
|
|
# the Debug trees anyway -- see the file's own header.
|
|
add_executable(akbasic_test_collision_perf tests/collision_perf.c)
|
|
target_compile_options(akbasic_test_collision_perf PRIVATE -Wall -Wextra)
|
|
target_link_libraries(akbasic_test_collision_perf PRIVATE akbasic_akgl)
|
|
# libakgl's benchmark harness, borrowed by include path rather than copied, for
|
|
# the same reason the fixture font is: a second copy is a fork. SYSTEM because
|
|
# benchutil.h is a header of statics and -Wunused-function would fire on every
|
|
# helper this suite happens not to call.
|
|
target_include_directories(akbasic_test_collision_perf SYSTEM PRIVATE
|
|
"${CMAKE_CURRENT_SOURCE_DIR}/deps/libakgl/tests")
|
|
target_compile_definitions(akbasic_test_collision_perf PRIVATE
|
|
AKBASIC_TEST_FONT="${CMAKE_CURRENT_SOURCE_DIR}/deps/libakgl/tests/assets/akgl_test_mono.ttf")
|
|
_add_test(NAME collision_perf COMMAND akbasic_test_collision_perf)
|
|
_set_tests_properties(collision_perf PROPERTIES
|
|
LABELS perf
|
|
TIMEOUT 300
|
|
ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_RENDER_DRIVER=software")
|
|
|
|
# The frontend suite. Separate from the one above because it tests the *host*
|
|
# rather than the adaptors -- it is the only test that links akbasic_frontend,
|
|
# and the only one that opens a window and pumps events. It uses the
|
|
# reference's own Commodore font, because proving the drawn text is in that
|
|
# font is part of what TODO.md section 3 asks for.
|
|
add_executable(akbasic_test_akgl_frontend tests/akgl_frontend.c)
|
|
target_compile_options(akbasic_test_akgl_frontend PRIVATE -Wall -Wextra)
|
|
target_link_libraries(akbasic_test_akgl_frontend PRIVATE akbasic_frontend)
|
|
target_include_directories(akbasic_test_akgl_frontend PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/tests")
|
|
target_compile_definitions(akbasic_test_akgl_frontend PRIVATE
|
|
AKBASIC_TEST_C64_FONT="${CMAKE_CURRENT_SOURCE_DIR}/assets/fonts/C64_Pro_Mono-STYLE.ttf")
|
|
akbasic_instrument(akbasic_test_akgl_frontend)
|
|
_add_test(NAME akgl_frontend COMMAND akbasic_test_akgl_frontend)
|
|
_set_tests_properties(akgl_frontend PROPERTIES
|
|
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/tests"
|
|
TIMEOUT 60
|
|
ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_AUDIODRIVER=dummy;SDL_RENDER_DRIVER=software")
|
|
|
|
# The keyboard test, and the only one in the repository that uses a real X
|
|
# server rather than the dummy driver. Note the deliberate absence of an
|
|
# ENVIRONMENT line: forcing the dummy driver here would defeat the whole point.
|
|
#
|
|
# Everything else synthesises SDL events, which tests the code *downstream* of
|
|
# SDL and cannot test the code upstream of it. That gap shipped a bug -- the
|
|
# frontend never called SDL_StartTextInput(), so SDL emitted no text-input
|
|
# events, the line editor dropped every keystroke, and the suite stayed green
|
|
# because it was pushing those events itself. This one drives xdotool at a
|
|
# focused window, so X11, SDL's composition and the window manager are all in
|
|
# the path.
|
|
#
|
|
# Needs xdotool and script(1), and it **steals keyboard focus** for a few
|
|
# seconds. Skipped rather than failed when it cannot run -- headless, no
|
|
# xdotool, no window manager -- because none of those means the answer is no.
|
|
# AKBASIC_SKIP_INTERACTIVE=1 skips it while you are using the machine.
|
|
find_program(XDOTOOL_EXECUTABLE xdotool)
|
|
if(NOT XDOTOOL_EXECUTABLE)
|
|
message(STATUS "xdotool not found: the akgl_typing test will skip. "
|
|
"Install it to cover the real keyboard path.")
|
|
endif()
|
|
_add_test(NAME akgl_typing
|
|
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tests/akgl_typing.sh $<TARGET_FILE:basic>)
|
|
_set_tests_properties(akgl_typing PROPERTIES
|
|
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
|
|
TIMEOUT 180
|
|
SKIP_RETURN_CODE 77)
|
|
|
|
# Every figure in docs/ re-rendered and byte-compared against the checked-in
|
|
# copy. docs_examples only asks whether the file *exists*, which catches a
|
|
# figure that was never generated and not one that stopped being of the code
|
|
# beside it -- and that second failure is the one this whole arrangement is
|
|
# against.
|
|
#
|
|
# **A byte comparison of a rendered PNG is a deliberate bet**, the same bet
|
|
# tests/reference/ already makes about golden output: that the dummy video
|
|
# driver and the software renderer are reproducible. They are, run to run and
|
|
# build to build. What is untested is an SDL upgrade that shifts one pixel of
|
|
# a diagonal, and the answer to that is to regenerate the figures in the same
|
|
# commit as the bump -- not to weaken this to a size check, which would pass
|
|
# for every wrong picture that happened to be 320x200.
|
|
_add_test(NAME docs_screenshots
|
|
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tools/docs_screenshots.sh
|
|
--root "${CMAKE_CURRENT_SOURCE_DIR}"
|
|
--tool $<TARGET_FILE:akbasic_screenshot>
|
|
--check)
|
|
_set_tests_properties(docs_screenshots PROPERTIES
|
|
TIMEOUT 120
|
|
ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_AUDIODRIVER=dummy;SDL_RENDER_DRIVER=software")
|
|
endif()
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# The documentation's own examples.
|
|
#
|
|
# docs/ and README.md are full of programs, transcripts, C snippets and shell
|
|
# commands, and every one of them was checked by hand exactly once -- when it
|
|
# was written. Three were already wrong the day this was added, one of them a
|
|
# struct that had grown two members hours earlier. Documentation goes stale
|
|
# because the *code* moved, so this is an ordinary test case rather than
|
|
# something a docs-only CI job runs: it fails when the interpreter changes under
|
|
# the chapter, which is the case a path filter would miss.
|
|
#
|
|
# tests/docs_examples.sh reads the fence info strings; MAINTENANCE.md documents
|
|
# them.
|
|
#
|
|
# The C snippets compile against the real include path, which is transitive
|
|
# through akerror, akstdlib and -- in this configuration -- akgl. Writing it out
|
|
# for the script to read keeps that one source of truth: a hardcoded -I list
|
|
# here would rot exactly the way the documentation does.
|
|
set(AKBASIC_DOCS_CFLAGS_FILE "${CMAKE_CURRENT_BINARY_DIR}/docs_cflags.txt")
|
|
if(AKBASIC_WITH_AKGL)
|
|
set(AKBASIC_DOCS_CFLAGS_TARGET akbasic_akgl)
|
|
else()
|
|
set(AKBASIC_DOCS_CFLAGS_TARGET akbasic)
|
|
endif()
|
|
file(GENERATE
|
|
OUTPUT "${AKBASIC_DOCS_CFLAGS_FILE}"
|
|
CONTENT "-I$<JOIN:$<TARGET_PROPERTY:${AKBASIC_DOCS_CFLAGS_TARGET},INCLUDE_DIRECTORIES>,\n-I>\n"
|
|
)
|
|
|
|
# Spelled out with if() rather than $<$<BOOL:...>:--akgl>, because a
|
|
# generator expression that evaluates to nothing still contributes an *empty
|
|
# argument*. The script read that empty string as the first filename, checked
|
|
# no documents at all, and passed -- caught only because it reports what it ran.
|
|
set(AKBASIC_DOCS_ARGS
|
|
--root "${CMAKE_CURRENT_SOURCE_DIR}"
|
|
--basic $<TARGET_FILE:basic>
|
|
--cflags-file "${AKBASIC_DOCS_CFLAGS_FILE}")
|
|
if(AKBASIC_WITH_AKGL)
|
|
list(APPEND AKBASIC_DOCS_ARGS --akgl)
|
|
endif()
|
|
|
|
_add_test(NAME docs_examples
|
|
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tests/docs_examples.sh ${AKBASIC_DOCS_ARGS})
|
|
_set_tests_properties(docs_examples PROPERTIES
|
|
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
|
|
TIMEOUT 300)
|
|
if(AKBASIC_WITH_AKGL)
|
|
# Same reason as the golden cases: an AKGL `basic` opens a window, and the
|
|
# chapters on graphics, sound and sprites are most of what runs here.
|
|
_set_tests_properties(docs_examples PROPERTIES
|
|
ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_AUDIODRIVER=dummy;SDL_RENDER_DRIVER=software")
|
|
endif()
|
|
|
|
if(AKBASIC_TESTS OR AKBASIC_WILL_FAIL_TESTS OR AKBASIC_KNOWN_FAILING_TESTS)
|
|
_set_tests_properties(
|
|
${AKBASIC_TESTS} ${AKBASIC_WILL_FAIL_TESTS} ${AKBASIC_KNOWN_FAILING_TESTS}
|
|
PROPERTIES WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/tests" TIMEOUT 30
|
|
)
|
|
endif()
|
|
|
|
if(AKBASIC_WILL_FAIL_TESTS OR AKBASIC_KNOWN_FAILING_TESTS)
|
|
_set_tests_properties(
|
|
${AKBASIC_WILL_FAIL_TESTS} ${AKBASIC_KNOWN_FAILING_TESTS}
|
|
PROPERTIES WILL_FAIL TRUE
|
|
)
|
|
endif()
|
|
|
|
# The reference's own corpus, byte-compared against the sibling .txt. One CTest
|
|
# case per .bas so a failure names the file.
|
|
#
|
|
# It used to be driven in place out of deps/basicinterpret, on the reasoning that
|
|
# copying a submodule's corpus guarantees drift. That reasoning was sound and it
|
|
# has been overruled deliberately: the Go dependency is being deprecated, and a
|
|
# build that cannot run its acceptance suite without cloning the implementation
|
|
# it replaced is not finished. The copy is byte-identical to
|
|
# basicinterpreter@d76162c and tests/reference/README.md records
|
|
# where it came from and what the drift now costs.
|
|
file(GLOB_RECURSE AKBASIC_GOLDEN_CASES
|
|
RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/tests/reference"
|
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/reference/*.bas"
|
|
)
|
|
|
|
foreach(_case IN LISTS AKBASIC_GOLDEN_CASES)
|
|
string(REGEX REPLACE "^tests/" "" _name "${_case}")
|
|
string(REGEX REPLACE "\\.bas$" "" _name "${_name}")
|
|
string(REPLACE "/" "_" _name "${_name}")
|
|
_add_test(
|
|
NAME golden_${_name}
|
|
COMMAND ${CMAKE_COMMAND}
|
|
-DBASIC=$<TARGET_FILE:basic>
|
|
-DCASE=${CMAKE_CURRENT_SOURCE_DIR}/tests/reference/${_case}
|
|
-P ${CMAKE_CURRENT_SOURCE_DIR}/tests/golden.cmake
|
|
)
|
|
endforeach()
|
|
|
|
if(AKBASIC_GOLDEN_CASES)
|
|
set(AKBASIC_GOLDEN_NAMES)
|
|
foreach(_case IN LISTS AKBASIC_GOLDEN_CASES)
|
|
string(REGEX REPLACE "^tests/" "" _name "${_case}")
|
|
string(REGEX REPLACE "\\.bas$" "" _name "${_name}")
|
|
string(REPLACE "/" "_" _name "${_name}")
|
|
list(APPEND AKBASIC_GOLDEN_NAMES golden_${_name})
|
|
endforeach()
|
|
_set_tests_properties(${AKBASIC_GOLDEN_NAMES} PROPERTIES TIMEOUT 30)
|
|
# An AKGL build of `basic` opens a window, and forty-one of them is not what
|
|
# anybody running the suite wanted. The dummy driver produces the same stdout,
|
|
# which is the only thing a golden case compares.
|
|
if(AKBASIC_WITH_AKGL)
|
|
_set_tests_properties(${AKBASIC_GOLDEN_NAMES} PROPERTIES
|
|
ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_AUDIODRIVER=dummy;SDL_RENDER_DRIVER=software")
|
|
endif()
|
|
endif()
|
|
|
|
# The local golden corpus, for verbs the reference never implemented.
|
|
#
|
|
# Still separate now that the reference's corpus lives in this repository too,
|
|
# and the reason changed rather than went away: tests/reference/ is a *record* of
|
|
# what the Go implementation did and nothing in it should ever be edited to suit
|
|
# this one, while tests/language/ is ours to change. Registered under local_ so a
|
|
# failure says at a glance which of the two it came from -- and so a diff that
|
|
# touches tests/reference/ stands out as the thing it is.
|
|
#
|
|
# Note what this can and cannot cover. The graphics and sound verbs draw and play
|
|
# rather than print, so what a golden file sees of them is their *refusals* and
|
|
# whatever a program can PRINT about the state they changed. The behaviour that
|
|
# reaches a device is asserted against tests/mockdevice.h instead.
|
|
file(GLOB_RECURSE AKBASIC_LOCAL_CASES
|
|
RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/tests"
|
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/language/*.bas"
|
|
)
|
|
|
|
#
|
|
# One family of local cases is driver-specific and has to be. A case named
|
|
# no_device.bas asserts that a verb needing a device refuses politely when it was
|
|
# given none, which is true of the stdio driver and deliberately false of the
|
|
# AKGL one -- attaching the three device backends is the whole point of
|
|
# AKBASIC_WITH_AKGL, and TODO.md section 8 item 1 asks for exactly that change in
|
|
# observable behaviour. Skipping them there is not a coverage hole: the refusal
|
|
# path is asserted directly against the backend records in tests/devices.c, which
|
|
# both configurations build.
|
|
#
|
|
set(AKBASIC_LOCAL_NAMES)
|
|
foreach(_case IN LISTS AKBASIC_LOCAL_CASES)
|
|
if(AKBASIC_WITH_AKGL AND _case MATCHES "no_device\\.bas$")
|
|
continue()
|
|
endif()
|
|
string(REGEX REPLACE "^language/" "" _name "${_case}")
|
|
string(REGEX REPLACE "\\.bas$" "" _name "${_name}")
|
|
string(REPLACE "/" "_" _name "${_name}")
|
|
_add_test(
|
|
NAME local_${_name}
|
|
COMMAND ${CMAKE_COMMAND}
|
|
-DBASIC=$<TARGET_FILE:basic>
|
|
-DCASE=${CMAKE_CURRENT_SOURCE_DIR}/tests/${_case}
|
|
-P ${CMAKE_CURRENT_SOURCE_DIR}/tests/golden.cmake
|
|
)
|
|
list(APPEND AKBASIC_LOCAL_NAMES local_${_name})
|
|
endforeach()
|
|
|
|
if(AKBASIC_LOCAL_NAMES)
|
|
_set_tests_properties(${AKBASIC_LOCAL_NAMES} PROPERTIES TIMEOUT 30)
|
|
if(AKBASIC_WITH_AKGL)
|
|
_set_tests_properties(${AKBASIC_LOCAL_NAMES} PROPERTIES
|
|
ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_AUDIODRIVER=dummy;SDL_RENDER_DRIVER=software")
|
|
endif()
|
|
endif()
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Mutation testing.
|
|
#
|
|
# Coverage says which lines ran; mutation testing says whether anything would
|
|
# have noticed if they were wrong. That distinction matters more here than in an
|
|
# ordinary C library: the akerror control-flow macros expand at their call sites,
|
|
# so gcov attributes ATTEMPT/CATCH/PASS to the caller and cannot really see them.
|
|
# Mutation testing is the only thing that checks them at all.
|
|
#
|
|
# This target runs the whole akbasic-owned src/ tree and is slow -- upwards of an
|
|
# hour. CI runs a narrower, faster set with a --threshold gate; see
|
|
# .gitea/workflows/ci.yaml.
|
|
#
|
|
# Namespaced when embedded in another project, for the same reason the coverage
|
|
# targets in the dependencies are: a sibling may well ship a `mutation` target.
|
|
find_package(Python3 COMPONENTS Interpreter)
|
|
if(Python3_FOUND)
|
|
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
|
set(AKBASIC_MUTATION_TARGET mutation)
|
|
else()
|
|
set(AKBASIC_MUTATION_TARGET akbasic_mutation)
|
|
endif()
|
|
add_custom_target(${AKBASIC_MUTATION_TARGET}
|
|
COMMAND ${Python3_EXECUTABLE}
|
|
${CMAKE_CURRENT_SOURCE_DIR}/scripts/mutation_test.py
|
|
--source-root ${CMAKE_CURRENT_SOURCE_DIR}
|
|
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
|
USES_TERMINAL
|
|
COMMENT "Running mutation tests (breaks the library, expects tests to fail)"
|
|
)
|
|
endif()
|
|
|
|
install(TARGETS akbasic basic
|
|
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
|
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
|
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
|
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
|
|
)
|
|
install(DIRECTORY include/akbasic DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|