Compare commits
10 Commits
coverage-t
...
6dfe7487ae
| Author | SHA1 | Date | |
|---|---|---|---|
|
6dfe7487ae
|
|||
|
c2b16d3c18
|
|||
|
2be9831c0c
|
|||
|
c5a7b6053d
|
|||
|
9b124f2e27
|
|||
|
b014eb2360
|
|||
|
e423f9594e
|
|||
|
772f960865
|
|||
|
28bde4176d
|
|||
|
8d21d5c7dd
|
16
.dir-locals.el
Normal file
16
.dir-locals.el
Normal file
@@ -0,0 +1,16 @@
|
||||
;;; Directory Local Variables -*- no-byte-compile: t -*-
|
||||
;;; See AGENTS.md -> "Coding Style" for the rationale.
|
||||
;;;
|
||||
;;; The canonical style is cc-mode "stroustrup" with tabs enabled: 4 columns per
|
||||
;;; level, tabs 8 columns wide, so depth 1 is four spaces, depth 2 is one tab,
|
||||
;;; depth 3 is a tab plus four spaces. A correctly formatted file is a fixed
|
||||
;;; point of `indent-region' under these settings.
|
||||
;;;
|
||||
;;; Reindent a whole file with C-x h C-M-\, or the tree with
|
||||
;;; `scripts/reindent.sh'.
|
||||
|
||||
((c-mode . ((c-file-style . "stroustrup")
|
||||
(indent-tabs-mode . t)
|
||||
(tab-width . 8)
|
||||
(fill-column . 100)
|
||||
(require-final-newline . t))))
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -1,3 +1,11 @@
|
||||
./build/*
|
||||
# A leading ./ is not a valid gitignore pattern, so "./build/*" matched nothing
|
||||
# and every out-of-tree build tree showed up as untracked. This also covers the
|
||||
# instrumented trees scripts/coverage.py and the AKGL_COVERAGE option create.
|
||||
build*/
|
||||
.aider*
|
||||
*~
|
||||
|
||||
# Generated by configure_file into the build tree. include/ precedes the build
|
||||
# tree on the include path, so a stray copy here would silently shadow the real
|
||||
# one and pin every consumer to whatever version it was generated at.
|
||||
include/akgl/version.h
|
||||
|
||||
259
AGENTS.md
259
AGENTS.md
@@ -2,7 +2,43 @@
|
||||
|
||||
## Project Structure & Module Organization
|
||||
|
||||
This is a C library intended to support the development of video games. Public C headers live in `include/akgl/`; keep declarations there aligned with their implementations in `src/`. Tests are standalone C programs under `tests/`, with JSON, image, and map fixtures in `tests/assets/`. The `util/` directory contains the `charviewer` utility and its sample assets. Third-party and companion libraries are vendored in `deps/`. Treat `build/`, generated `akgl.pc` files, and `include/akgl/SDL_GameControllerDB.h` as build outputs rather than hand-maintained source.
|
||||
This is a C library intended to support the development of video games. Public C headers live in `include/akgl/`; keep declarations there aligned with their implementations in `src/`. Tests are standalone C programs under `tests/`, with JSON, image, and map fixtures in `tests/assets/`. The `util/` directory contains the `charviewer` utility and its sample assets. Third-party and companion libraries are vendored in `deps/`. Treat `build/` and generated `akgl.pc` files as build outputs rather than hand-maintained source; `include/akgl/SDL_GameControllerDB.h` is generated too, but is tracked on purpose — see below.
|
||||
|
||||
## Generated and Vendored Sources
|
||||
|
||||
### `include/akgl/SDL_GameControllerDB.h` is generated, and is tracked deliberately
|
||||
|
||||
`mkcontrollermappings.sh` regenerates this header by fetching the community
|
||||
controller database from `raw.githubusercontent.com`. **It is committed to the
|
||||
repository on purpose**: it is the offline fallback that keeps the library
|
||||
buildable if upstream is renamed, rate-limited, taken down, or simply
|
||||
unreachable from the build machine. Do not delete it, do not add it to
|
||||
`.gitignore`, and do not "clean up" the fact that a generated file is tracked.
|
||||
|
||||
Working rules:
|
||||
|
||||
- **Never hand-edit it.** It is machine-written; changes belong in
|
||||
`mkcontrollermappings.sh`.
|
||||
- **Do not commit incidental regeneration churn.** The build regenerates the
|
||||
header on every run, and the script stamps `$(date)` into a comment, so an
|
||||
ordinary build leaves the file modified with nothing of substance changed.
|
||||
Revert that before committing: `git checkout -- include/akgl/SDL_GameControllerDB.h`.
|
||||
- **Update it as a deliberate, standalone commit** when you actually want newer
|
||||
mappings, so the diff is reviewable and bisectable.
|
||||
- **Never commit a copy with `AKGL_SDL_GAMECONTROLLER_DB_LEN 0`.** That is the
|
||||
signature of a failed fetch, not an empty upstream — see the defect note
|
||||
below. Check the constant before staging this file.
|
||||
- Formatting tooling ignores it: `scripts/reindent.sh` and the pre-commit hook
|
||||
both skip it.
|
||||
|
||||
> **Known defect (tracked in `TODO.md`).** The safety net is currently
|
||||
> self-defeating. `mkcontrollermappings.sh` has no `set -e` and does not check
|
||||
> `curl`'s exit status, so when the fetch fails it writes a header with
|
||||
> `AKGL_SDL_GAMECONTROLLER_DB_LEN 0` and an empty array **over the good tracked
|
||||
> copy, and exits 0**. Because CMake re-runs the generator on every build, an
|
||||
> offline build silently destroys the very fallback the file exists to provide.
|
||||
> Until that is fixed, treat a dirty `SDL_GameControllerDB.h` after a build as
|
||||
> suspect and check the length constant before committing it.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
|
||||
@@ -30,9 +66,226 @@ Run mutation testing with `cmake --build build --target mutation`. For a quick s
|
||||
|
||||
Generate HTML and Cobertura coverage reports with `cmake -S . -B build-coverage -DAKGL_COVERAGE=ON -DCMAKE_BUILD_TYPE=Debug`, then build and run CTest. Reports are written to `build-coverage/coverage/`; this mode requires `gcovr` and GCC or Clang.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
## Coding Style
|
||||
|
||||
Follow the surrounding C style: braces on the next line for function bodies, spaces inside control-flow parentheses, and short, focused functions. Preserve the indentation of the file being edited; older files contain both spaces and tabs. Public symbols use the `akgl_` prefix, public types use `akgl_TypeName`, and constants/macros use `AKGL_UPPER_SNAKE_CASE`. Name matching header/source pairs by feature, such as `include/akgl/sprite.h` and `src/sprite.c`. No repository-wide formatter or linter is configured, so avoid unrelated formatting churn.
|
||||
The canonical style is Emacs `cc-mode` **`stroustrup`**, with tabs enabled. This
|
||||
is not advisory — new and edited code must match what `cc-mode` produces, so that
|
||||
reindenting a region never shows up as a diff.
|
||||
|
||||
### Emacs setup
|
||||
|
||||
`.dir-locals.el` in the repository root already applies the style to every
|
||||
`c-mode` buffer, so nothing needs configuring by hand:
|
||||
|
||||
```elisp
|
||||
((c-mode . ((c-file-style . "stroustrup")
|
||||
(indent-tabs-mode . t)
|
||||
(tab-width . 8)
|
||||
(fill-column . 100)
|
||||
(require-final-newline . t))))
|
||||
```
|
||||
|
||||
Interactively: `C-x h C-M-\` (`mark-whole-buffer` + `indent-region`) reindents
|
||||
the buffer. A correctly formatted file is a fixed point of that operation —
|
||||
reindenting must produce no change.
|
||||
|
||||
### Reindenting from the command line
|
||||
|
||||
```sh
|
||||
scripts/reindent.sh # reindent every tracked C source in place
|
||||
scripts/reindent.sh src/tilemap.c # reindent only the named files
|
||||
scripts/reindent.sh --check # list non-conforming files, change nothing
|
||||
```
|
||||
|
||||
`--check` exits 1 when something needs reindenting and 2 if Emacs is missing, so
|
||||
it is usable from CI. The script drives Emacs in batch mode through
|
||||
`scripts/reindent.el`, which reindents, normalises leading whitespace to the
|
||||
canonical tab/space mix, strips trailing whitespace, and ensures a final
|
||||
newline. `include/akgl/SDL_GameControllerDB.h` is generated and always skipped.
|
||||
|
||||
Note that `reindent.el` deliberately does **not** use Emacs' `tabify`: that
|
||||
function's `tabify-regexp` is `" [ \t]+"`, which is not anchored to the start of
|
||||
a line, so it rewrites runs of spaces anywhere and destroys the hand-aligned
|
||||
value columns in the bit-flag tables in `actor.h` and `iterator.h`. Only leading
|
||||
whitespace is ever rewritten.
|
||||
|
||||
### The pre-commit hook
|
||||
|
||||
`scripts/hooks/pre-commit` reindents staged C sources before the commit is
|
||||
written. Enable it once per clone:
|
||||
|
||||
```sh
|
||||
git config core.hooksPath scripts/hooks
|
||||
```
|
||||
|
||||
It inspects the *staged* content rather than the working tree, so what lands in
|
||||
the commit is what was checked. When a file needs reindenting it is fixed in the
|
||||
working tree and re-staged — but only if the index and working tree agree for
|
||||
that file. If they differ, re-staging would sweep unstaged work into the commit,
|
||||
so the hook stops and tells you to reindent and stage it yourself. It steps
|
||||
aside during a merge, and warns rather than blocking if Emacs is unavailable.
|
||||
`git commit --no-verify` bypasses it.
|
||||
|
||||
For reference, `cc-mode` defines the style as:
|
||||
|
||||
```elisp
|
||||
("stroustrup"
|
||||
(c-basic-offset . 4)
|
||||
(c-comment-only-line-offset . 0)
|
||||
(c-offsets-alist . ((statement-block-intro . +)
|
||||
(substatement-open . 0)
|
||||
(substatement-label . 0)
|
||||
(label . 0)
|
||||
(statement-cont . +))))
|
||||
```
|
||||
|
||||
### Indentation and whitespace
|
||||
|
||||
- **4 columns per level** (`c-basic-offset` 4).
|
||||
- **Tabs are 8 columns wide and are used for indentation** (`indent-tabs-mode`
|
||||
`t`, `tab-width` 8). Emacs emits the largest possible run of tabs and pads the
|
||||
remainder with spaces, which produces this ladder. Editors that expand tabs, or
|
||||
that assume a 4-column tab, will silently corrupt it:
|
||||
|
||||
| Depth | Columns | Bytes |
|
||||
|---|---|---|
|
||||
| 1 | 4 | 4 spaces |
|
||||
| 2 | 8 | `TAB` |
|
||||
| 3 | 12 | `TAB` + 4 spaces |
|
||||
| 4 | 16 | `TAB` `TAB` |
|
||||
| 5 | 20 | `TAB` `TAB` + 4 spaces |
|
||||
|
||||
- No trailing whitespace. Files end with a single newline.
|
||||
- `case` labels sit at the same column as their `switch` (`label . 0`).
|
||||
- Continuation lines of a wrapped expression indent one level (`statement-cont . +`).
|
||||
|
||||
Most of `src/` already conforms. The known non-conforming files are
|
||||
`src/json_helpers.c`, `src/util.c` (from `akgl_rectangle_points` onward),
|
||||
`src/assets.c`, `src/staticstring.c`, parts of `src/actor.c`, and the headers
|
||||
`include/akgl/util.h` and `include/akgl/staticstring.h` — all of which use a
|
||||
2-column offset. Convert a file to the canonical style in its own commit, not
|
||||
mixed into a behavioral change.
|
||||
|
||||
### Braces
|
||||
|
||||
- **Function bodies open on their own line, in column 0.**
|
||||
- **Control statements keep the brace on the same line**, one space before it.
|
||||
- **`else`, `else if`, and `while` of a `do`/`while` stay on the closing-brace
|
||||
line**: `} else {`, not a bare `else` on the next line. Note that this is a
|
||||
house convention rather than something `cc-mode` enforces — the `stroustrup`
|
||||
style governs indentation only and will not move a brace or an `else` for you.
|
||||
- **Always brace, even single-statement bodies.**
|
||||
|
||||
```c
|
||||
akerr_ErrorContext *akgl_actor_update(akgl_Actor *obj)
|
||||
{
|
||||
if ( obj->curSpriteFrameId == 0 ) {
|
||||
obj->curSpriteReversing = false;
|
||||
} else if ( obj->parent != NULL ) {
|
||||
obj->curSpriteFrameId -= 1;
|
||||
} else {
|
||||
obj->curSpriteFrameId += 1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Spacing
|
||||
|
||||
- **Spaces inside control-flow parentheses**: `if ( x == y ) {`, `while ( done == false ) {`,
|
||||
`for ( i = 0; i < len; i++ ) {`. This is the dominant existing convention
|
||||
(140 sites to 7) and `cc-mode` will not add or remove it.
|
||||
- No space between a function name and its argument list, at both call and
|
||||
definition sites, and no padding inside call parentheses: `SDL_Log("x %d", n)`.
|
||||
- Binary operators and assignment are surrounded by single spaces; unary
|
||||
operators are not separated from their operand.
|
||||
- **The pointer `*` binds to the identifier**, not the type: `char *name`,
|
||||
`akgl_Actor **dest`. Never `char* name`.
|
||||
- When a call is too long for one line, put each argument on its own line
|
||||
indented one level past the callee, with the closing `)` on its own line. This
|
||||
is the established shape for the `FAIL_*` and `CATCH` macros:
|
||||
|
||||
```c
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
SDL_SetPointerProperty(AKGL_REGISTRY_ACTOR, name, (void *)obj),
|
||||
AKERR_KEY,
|
||||
"Unable to add actor to registry"
|
||||
);
|
||||
```
|
||||
|
||||
### No repository-wide formatter
|
||||
|
||||
There is no `clang-format` or linter wired into the build, and `cc-mode`'s
|
||||
indentation engine is not exactly reproducible by `clang-format`. Do not
|
||||
introduce one without discussion, and do not reformat code you are not otherwise
|
||||
changing — unrelated whitespace churn makes review harder and is the reason the
|
||||
style drifted in the first place.
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
- **Public functions**: `akgl_<subsystem>_<verb>`, all lower snake_case —
|
||||
`akgl_actor_set_character`, `akgl_heap_next_string`. No camelCase, and never
|
||||
embed a type name (`akgl_Actor_cmhf_left_on` is wrong; it should be
|
||||
`akgl_actor_cmhf_left_on`).
|
||||
- **Public types**: `akgl_TypeName` — `akgl_Actor`, `akgl_SpriteSheet`,
|
||||
`akgl_PhysicsBackend`. Every type exported from a header takes the prefix;
|
||||
bare names like `point` and `RectanglePoints` are defects, not precedent.
|
||||
- **Constants and macros**: `AKGL_UPPER_SNAKE_CASE`. A constant belongs to the
|
||||
subsystem it describes: a character limit is `AKGL_CHARACTER_MAX_*`, not
|
||||
`AKGL_SPRITE_MAX_CHARACTER_*`. Name a constant for what its value *is* — a
|
||||
nanoseconds-per-millisecond scale factor is `AKGL_TIME_ONEMS_NS`.
|
||||
- **Exported globals** take the `akgl_` prefix like any other public symbol. Do
|
||||
not add bare names (`renderer`, `camera`, `window`), leading-underscore names
|
||||
(reserved at file scope), or SCREAMING_SNAKE names for mutable objects —
|
||||
`AKGL_UPPER_SNAKE_CASE` is for constants.
|
||||
- **`static` helpers drop the `akgl_` prefix**, which exists only to avoid
|
||||
external collisions.
|
||||
- **Include guards**: `_AKGL_<FILE>_H_`, matching the file name. Guard names
|
||||
without the project prefix risk colliding with system headers.
|
||||
- **Parameter names must match between the declaration and the definition** —
|
||||
Doxygen publishes the header spelling. Conventional names: `dest` for an
|
||||
output parameter (not `dst`), `self` for a backend receiver, `obj` for the
|
||||
instance being initialized or inspected, `e` for an incoming error context to
|
||||
be inspected.
|
||||
- **The local error context is named `errctx`** (92 sites to 45). New code uses
|
||||
`errctx`; convert `e` when you touch a function for other reasons.
|
||||
- **Header/source pairs are named by feature**: `include/akgl/sprite.h` and
|
||||
`src/sprite.c`.
|
||||
|
||||
## Error-Handling Protocol
|
||||
|
||||
The `akerror` control-flow macros have a shape that must be followed exactly,
|
||||
because the failure mode is silent.
|
||||
|
||||
- Inside an `ATTEMPT` block use the **`_BREAK`** variants (`FAIL_ZERO_BREAK`,
|
||||
`FAIL_NONZERO_BREAK`, `FAIL_BREAK`) and `CATCH`. Outside it use the
|
||||
**`_RETURN`** variants.
|
||||
- **Never use a `*_RETURN` macro inside an `ATTEMPT` block.** It returns past the
|
||||
`CLEANUP` block, so every release, `fclose`, and free in `CLEANUP` is skipped.
|
||||
This has already caused a heap-string leak on the success path of
|
||||
`akgl_get_json_tilemap_property`.
|
||||
- `CLEANUP` must precede `PROCESS`. Transposing them moves the cleanup body into
|
||||
the `PROCESS` switch, where it runs only when an error context exists.
|
||||
- `CATCH` reports failure by `break`ing, which binds to the innermost enclosing
|
||||
loop or `switch`. A `CATCH` written directly inside a `while` exits the loop
|
||||
rather than the function — put the `ATTEMPT` block inside the loop.
|
||||
- Validate every pointer parameter before dereferencing it, including the ones a
|
||||
sibling function happens not to check.
|
||||
|
||||
## API Surface
|
||||
|
||||
Every function with external linkage must be declared in a header, and every
|
||||
declaration must have a definition. A non-`static` function that appears in no
|
||||
header is still in the ABI but is unreachable by callers; a declaration with no
|
||||
definition is a link error for anyone compiling against the header alone. If a
|
||||
function is exposed only so tests can reach it, declare it under the existing
|
||||
"part of the internal API" comment block in the relevant header.
|
||||
|
||||
Headers must be self-contained: include what you use, so that a translation unit
|
||||
including a single `akgl` header compiles without a particular include order.
|
||||
Within the project use the angled form, `#include <akgl/sibling.h>`.
|
||||
|
||||
Declare no-argument functions as `(void)`, not `()`.
|
||||
|
||||
## Rules
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(akgl LANGUAGES C)
|
||||
# The single source of truth for the library version. It drives the generated
|
||||
# include/akgl/version.h, the shared library's VERSION and SOVERSION, and the
|
||||
# Version field in akgl.pc. Bump it here and nowhere else.
|
||||
project(akgl VERSION 0.1.0 LANGUAGES C)
|
||||
|
||||
include(CTest)
|
||||
option(AKGL_COVERAGE "Instrument libakgl and generate coverage reports with CTest" OFF)
|
||||
@@ -39,10 +42,8 @@ add_subdirectory(deps/SDL_image EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(deps/SDL_mixer EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(deps/SDL_ttf EXCLUDE_FROM_ALL)
|
||||
|
||||
# Reserve error-name table entries for libakgl-specific status codes.
|
||||
target_compile_definitions(akerror PUBLIC
|
||||
AKERR_MAX_ERR_VALUE=256
|
||||
)
|
||||
# libakerror 1.0.0 sizes its own status-name registry; consumers no longer do.
|
||||
# libakgl claims its status codes at runtime in akgl_heap_init() instead.
|
||||
|
||||
set(AKGL_SUPPRESS_DEPENDENCY_TESTS FALSE)
|
||||
|
||||
@@ -62,11 +63,19 @@ else()
|
||||
if(NOT TARGET SDL3_ttf::SDL3_ttf)
|
||||
find_package(SDL3_ttf REQUIRED)
|
||||
endif()
|
||||
# No version here: libakerror ships no akerrorConfigVersion.cmake, so asking
|
||||
# for one makes find_package reject every install. The floor is enforced by
|
||||
# the #error in include/akgl/error.h instead, which feature-tests
|
||||
# AKERR_FIRST_CONSUMER_STATUS.
|
||||
if(NOT TARGET akerror::akerror)
|
||||
find_package(akerror REQUIRED)
|
||||
endif()
|
||||
# 0.1 rather than bare: libakstdlib 0.1.0 ships an akstdlibConfigVersion.cmake
|
||||
# with SameMinorVersion compatibility, mirroring its soname, so this accepts
|
||||
# any 0.1.x and refuses 0.2 and 1.0. Unversioned, this path would silently
|
||||
# accept an ABI-incompatible libakstdlib.
|
||||
if(NOT TARGET akstdlib::akstdlib)
|
||||
find_package(akstdlib REQUIRED)
|
||||
find_package(akstdlib 0.1 REQUIRED)
|
||||
endif()
|
||||
if(NOT TARGET jansson::jansson)
|
||||
find_package(jansson)
|
||||
@@ -80,6 +89,8 @@ set(exec_prefix "\${prefix}")
|
||||
set(libdir "\${exec_prefix}/lib")
|
||||
set(includedir "\${prefix}/include")
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/akgl.pc.in ${CMAKE_CURRENT_BINARY_DIR}/akgl.pc @ONLY)
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/include/akgl/version.h.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/include/akgl/version.h @ONLY)
|
||||
|
||||
# Tests use both relative paths and SDL_GetBasePath(), so stage fixtures beside
|
||||
# test executables in every out-of-tree build.
|
||||
@@ -102,6 +113,7 @@ add_library(akgl SHARED
|
||||
src/assets.c
|
||||
src/character.c
|
||||
src/draw.c
|
||||
src/error.c
|
||||
src/game.c
|
||||
src/controller.c
|
||||
src/heap.c
|
||||
@@ -113,21 +125,39 @@ add_library(akgl SHARED
|
||||
src/staticstring.c
|
||||
src/tilemap.c
|
||||
src/util.c
|
||||
src/version.c
|
||||
${GAMECONTROLLERDB_H}
|
||||
)
|
||||
|
||||
# While the major version is 0 the ABI is not stable across minor releases, so
|
||||
# the soname carries major.minor -- libakgl.so.0.1. A plain SOVERSION 0 would
|
||||
# claim 0.1.0 and 0.2.0 are interchangeable, which is exactly the silent
|
||||
# mispairing the soname is here to prevent. At 1.0.0 this becomes the major
|
||||
# alone, matching libakerror.
|
||||
if(PROJECT_VERSION_MAJOR EQUAL 0)
|
||||
set(AKGL_SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}")
|
||||
else()
|
||||
set(AKGL_SOVERSION "${PROJECT_VERSION_MAJOR}")
|
||||
endif()
|
||||
|
||||
set_target_properties(akgl PROPERTIES
|
||||
VERSION ${PROJECT_VERSION}
|
||||
SOVERSION ${AKGL_SOVERSION}
|
||||
)
|
||||
|
||||
add_library(akgl::akgl ALIAS akgl)
|
||||
|
||||
add_executable(charviewer util/charviewer.c)
|
||||
add_executable(test_semver_unit deps/semver/semver_unit.c)
|
||||
add_executable(akgl_test_semver_unit deps/semver/semver_unit.c)
|
||||
|
||||
# Every suite here is a standalone C program named tests/<name>.c, built as
|
||||
# test_<name> and registered with CTest under <name>.
|
||||
# akgl_test_<name> and registered with CTest under <name>.
|
||||
set(AKGL_TEST_SUITES
|
||||
actor
|
||||
bitmasks
|
||||
character
|
||||
controller
|
||||
error
|
||||
game
|
||||
heap
|
||||
json_helpers
|
||||
@@ -137,14 +167,19 @@ set(AKGL_TEST_SUITES
|
||||
staticstring
|
||||
tilemap
|
||||
util
|
||||
version
|
||||
)
|
||||
|
||||
# The executables carry an akgl_ prefix but the CTest names do not: a vendored
|
||||
# dependency is free to ship its own tests/version.c, and libakstdlib now does.
|
||||
# Its target is created by add_subdirectory even though EXCLUDE_FROM_ALL keeps
|
||||
# it from being built, so an unprefixed test_version here is a configure error.
|
||||
foreach(suite IN LISTS AKGL_TEST_SUITES)
|
||||
add_executable(test_${suite} tests/${suite}.c)
|
||||
add_test(NAME ${suite} COMMAND test_${suite})
|
||||
add_executable(akgl_test_${suite} tests/${suite}.c)
|
||||
add_test(NAME ${suite} COMMAND akgl_test_${suite})
|
||||
endforeach()
|
||||
|
||||
add_test(NAME semver_unit COMMAND test_semver_unit)
|
||||
add_test(NAME semver_unit COMMAND akgl_test_semver_unit)
|
||||
|
||||
set_tests_properties(
|
||||
${AKGL_TEST_SUITES} semver_unit
|
||||
@@ -155,6 +190,9 @@ set_tests_properties(
|
||||
target_include_directories(akgl PUBLIC
|
||||
include/
|
||||
deps/semver/
|
||||
# akgl/version.h is generated by configure_file, so it lives in the build tree
|
||||
# rather than beside the headers it is included from.
|
||||
${CMAKE_CURRENT_BINARY_DIR}/include/
|
||||
)
|
||||
|
||||
if(AKGL_COVERAGE)
|
||||
@@ -209,8 +247,8 @@ target_link_libraries(akgl
|
||||
)
|
||||
|
||||
foreach(suite IN LISTS AKGL_TEST_SUITES)
|
||||
target_link_libraries(test_${suite} PRIVATE akstdlib::akstdlib akerror::akerror akgl SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer jansson::jansson -lm)
|
||||
target_include_directories(test_${suite} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/tests")
|
||||
target_link_libraries(akgl_test_${suite} PRIVATE akstdlib::akstdlib akerror::akerror akgl SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer jansson::jansson -lm)
|
||||
target_include_directories(akgl_test_${suite} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/tests")
|
||||
endforeach()
|
||||
|
||||
target_link_libraries(charviewer PRIVATE akstdlib::akstdlib akerror::akerror akgl SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer jansson::jansson -lm)
|
||||
@@ -230,7 +268,7 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||
"$<TARGET_FILE_DIR:akstdlib::akstdlib>"
|
||||
)
|
||||
foreach(suite IN LISTS AKGL_TEST_SUITES)
|
||||
set_target_properties(test_${suite} PROPERTIES BUILD_RPATH "${AKGL_VENDORED_RPATH}")
|
||||
set_target_properties(akgl_test_${suite} PROPERTIES BUILD_RPATH "${AKGL_VENDORED_RPATH}")
|
||||
endforeach()
|
||||
set_target_properties(charviewer akgl PROPERTIES BUILD_RPATH "${AKGL_VENDORED_RPATH}")
|
||||
|
||||
@@ -285,8 +323,8 @@ if(Python3_FOUND)
|
||||
)
|
||||
endif()
|
||||
|
||||
set(main_lib_dest "lib/akgl-${MY_LIBRARY_VERSION}")
|
||||
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/akgl.pc DESTINATION "lib/pkgconfig/")
|
||||
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/include/akgl/version.h DESTINATION "include/akgl/")
|
||||
install(TARGETS akgl DESTINATION "lib/")
|
||||
install(FILES "deps/semver/semver.h" DESTINATION "include/")
|
||||
install(FILES "include/akgl/actor.h" DESTINATION "include/akgl/")
|
||||
|
||||
92
README.md
92
README.md
@@ -270,6 +270,98 @@ PASS(e, akgl_heap_release_string(width));
|
||||
```
|
||||
|
||||
|
||||
## Git hooks
|
||||
|
||||
The repository ships a `pre-commit` hook that keeps committed C sources in the
|
||||
project's canonical format (Emacs `cc-mode` "stroustrup"; see `AGENTS.md` for the
|
||||
full style guide). The hook lives in `scripts/hooks/` and is version controlled,
|
||||
but **Git configuration is not cloned**, so every clone has to be pointed at it
|
||||
once:
|
||||
|
||||
```sh
|
||||
git config core.hooksPath scripts/hooks
|
||||
```
|
||||
|
||||
Confirm it took effect:
|
||||
|
||||
```sh
|
||||
git rev-parse --git-path hooks # should print: scripts/hooks
|
||||
```
|
||||
|
||||
That is the whole installation. The hook is already committed with its
|
||||
executable bit set, so nothing needs `chmod`.
|
||||
|
||||
### What the hook does
|
||||
|
||||
On each commit it looks at the **staged** content of any added, copied, modified,
|
||||
or renamed `.c`/`.h` file under `src/`, `include/`, `tests/`, or `util/`, and
|
||||
reindents it if it does not already match the canonical style. Checking the
|
||||
staged content rather than the working tree means what lands in the commit is
|
||||
what was actually verified.
|
||||
|
||||
When a file needs reindenting, the hook fixes it in the working tree and
|
||||
re-stages it — but only when the index and working tree agree for that file. If
|
||||
they differ, you have staged part of a file with `git add -p`, and re-staging
|
||||
would sweep your unstaged work into the commit. Rather than do that silently the
|
||||
hook stops and prints the commands to run yourself:
|
||||
|
||||
```sh
|
||||
scripts/reindent.sh path/to/file.c
|
||||
git add path/to/file.c
|
||||
```
|
||||
|
||||
`include/akgl/SDL_GameControllerDB.h` is generated and always skipped.
|
||||
|
||||
### Requirements
|
||||
|
||||
The hook drives Emacs in batch mode, because `cc-mode`'s indentation engine is
|
||||
the definition of the style and `clang-format` cannot reproduce it exactly. If
|
||||
`emacs` is not on `PATH` the hook prints a warning and allows the commit — a
|
||||
hook that refuses to run without an optional tool only teaches people to reach
|
||||
for `--no-verify`. Install Emacs to get the check; without it, formatting is on
|
||||
you.
|
||||
|
||||
### Bypassing and uninstalling
|
||||
|
||||
Skip the hook for a single commit:
|
||||
|
||||
```sh
|
||||
git commit --no-verify
|
||||
```
|
||||
|
||||
Remove it entirely:
|
||||
|
||||
```sh
|
||||
git config --unset core.hooksPath
|
||||
```
|
||||
|
||||
### If you already have local hooks
|
||||
|
||||
`core.hooksPath` **replaces** the hooks directory outright — once it is set, Git
|
||||
stops reading `.git/hooks/` altogether, so any hooks you keep there will silently
|
||||
stop firing. If that matters, leave `core.hooksPath` unset and symlink just this
|
||||
one hook instead:
|
||||
|
||||
```sh
|
||||
ln -s ../../scripts/hooks/pre-commit .git/hooks/pre-commit
|
||||
```
|
||||
|
||||
### Formatting without the hook
|
||||
|
||||
The hook is a convenience, not the source of truth. The same check is available
|
||||
directly, and is what you would run in CI:
|
||||
|
||||
```sh
|
||||
scripts/reindent.sh --check # list non-conforming files; exit 1 if any
|
||||
scripts/reindent.sh # reindent every tracked C source in place
|
||||
scripts/reindent.sh src/game.c # reindent specific files
|
||||
```
|
||||
|
||||
`--check` exits `0` when everything conforms, `1` when a file needs reindenting,
|
||||
and `2` if Emacs is missing or the script cannot run — so a CI job that treats
|
||||
any non-zero status as failure will not mistake a broken toolchain for a clean
|
||||
tree.
|
||||
|
||||
## Mutation testing
|
||||
|
||||
The mutation harness makes one deliberate source-code change at a time in a scratch copy, then rebuilds and runs the passing CTest suite to measure whether tests detect the change. The known-failing `character` test is excluded by default.
|
||||
|
||||
420
TODO.md
420
TODO.md
@@ -1,5 +1,369 @@
|
||||
# TODO
|
||||
|
||||
## Internal consistency
|
||||
|
||||
Findings from a sweep of `src/` and `include/`. These are consistency and
|
||||
convention problems, not new functional defects — where one has a functional
|
||||
consequence it is called out. Ordered roughly by blast radius. Items that
|
||||
overlap the existing **Defects** list are cross-referenced rather than repeated.
|
||||
|
||||
### 1. Public naming conventions
|
||||
|
||||
1. **Include guards use three different schemes.** `_AKGL_ACTOR_H_`,
|
||||
`_AKGL_CHARACTER_H_`, `_AKGL_GAME_H_`, `_AKGL_HEAP_H_`, `_AKGL_ITERATOR_H_`,
|
||||
`_AKGL_SPRITE_H_`, and `_AKGL_TYPES_H_` carry the project prefix; `_ASSETS_H_`,
|
||||
`_CONTROLLER_H_`, `_DRAW_H_`, `_ERROR_H_`, `_JSON_HELPERS_H_`, `_PHYSICS_H_`,
|
||||
`_REGISTRY_H_`, `_RENDERER_H_`, `_TEXT_H_`, `_TILEMAP_H_`, and `_UTIL_H_` do
|
||||
not. Pick `_AKGL_<FILE>_H_` everywhere.
|
||||
|
||||
The worst case is `include/akgl/staticstring.h:6`, which guards with
|
||||
`_STRING_H_` — a name several libc implementations use for their own
|
||||
`<string.h>`. The same file then does `#include "string.h"` (line 9) with
|
||||
quotes, which is a relative-first lookup that only reaches the system header
|
||||
by accident. Rename the guard to `_AKGL_STATICSTRING_H_` and use
|
||||
`#include <string.h>`.
|
||||
|
||||
2. **Function names contradict the stated convention.** `AGENTS.md` says public
|
||||
symbols use `akgl_` and types use `akgl_TypeName`. Exceptions:
|
||||
- `akgl_Actor_cmhf_left_on` and its seven siblings (`include/akgl/actor.h:196-252`)
|
||||
embed the *type* name in a *function* name, unlike every other actor entry
|
||||
point (`akgl_actor_*`). Rename to `akgl_actor_cmhf_*`.
|
||||
- `akgl_game_updateFPS` (`include/akgl/game.h:104`) is camelCase; every other
|
||||
function is snake_case.
|
||||
- `akgl_render_init2d` (`include/akgl/renderer.h:83`) puts the `2d` at the end
|
||||
while the six functions it installs are `akgl_render_2d_*`. Make it
|
||||
`akgl_render_2d_init`.
|
||||
- `akgl_sprite_sheet_coords_for_frame` (`include/akgl/sprite.h:86`) spells it
|
||||
`sprite_sheet`; `akgl_spritesheet_initialize` and `akgl_heap_next_spritesheet`
|
||||
spell it `spritesheet`.
|
||||
|
||||
3. **Two public types have no prefix at all.** `point` and `RectanglePoints`
|
||||
(`include/akgl/util.h:13-25`) are unprefixed, and `RectanglePoints` is
|
||||
PascalCase with no namespace. Both are dumped into every translation unit
|
||||
that includes `util.h`. Rename to `akgl_Point` / `akgl_RectanglePoints`.
|
||||
|
||||
4. **Global variables use four different conventions.** `window`, `bgm`, `game`,
|
||||
`gamemap`, `renderer`, `physics`, and `camera` (`src/game.c:26-43`) are
|
||||
unprefixed single common words exported from a shared library — `renderer`
|
||||
and `camera` in particular are very likely to collide with a consuming game.
|
||||
Alongside them the same file exports `akgl_mixer` and `akgl_tracks` (prefixed),
|
||||
`_akgl_renderer` / `_akgl_camera` / `_akgl_physics` / `_akgl_gamemap`
|
||||
(underscore-prefixed, which is reserved at file scope), and elsewhere
|
||||
`HEAP_ACTOR`…`HEAP_STRING` (`src/heap.c:17-21`) and `GAME_ControlMaps`
|
||||
(`src/controller.c:13`) are SCREAMING_SNAKE, which the convention reserves for
|
||||
constants and macros. Settle on `akgl_` for all exported objects.
|
||||
|
||||
5. **`AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH`** (`include/akgl/character.h:13`) is a
|
||||
character constant carrying the `SPRITE` prefix, and lives in `character.h`.
|
||||
Rename to `AKGL_CHARACTER_MAX_NAME_LENGTH`.
|
||||
|
||||
6. **`AKGL_TIME_ONESEC_MS` is misnamed and the error is live.**
|
||||
`include/akgl/game.h:22` defines it as `1000000`. One second in milliseconds
|
||||
is `1000`; `1000000` is the number of nanoseconds in a *millisecond*. The name
|
||||
says "one second" but the value means "one millisecond", so:
|
||||
- `src/character.c:209` and `src/sprite.c:141` use it correctly as a
|
||||
milliseconds-to-nanoseconds scale factor.
|
||||
- `src/game.c:136` uses it as documented — a one-second budget for the state
|
||||
lock — in a loop that advances `totaltime += 100` alongside `SDL_Delay(100)`.
|
||||
The loop therefore runs 10,000 iterations of 100 ms, so `akgl_game_state_lock`
|
||||
blocks for roughly 16 minutes rather than 1 second before reporting failure.
|
||||
|
||||
Rename to `AKGL_TIME_ONEMS_NS` to match `AKGL_TIME_ONESEC_NS`, and give
|
||||
`akgl_game_state_lock` a real one-second budget.
|
||||
|
||||
### 2. Header/implementation surface drift
|
||||
|
||||
7. **Nineteen non-static functions are defined in `src/` but declared in no
|
||||
header.** They have external linkage and public-looking names, so they are
|
||||
part of the ABI whether intended or not, and no consumer can call them:
|
||||
|
||||
`akgl_game_load_objectnamemap`, `akgl_game_load_versioncmp`,
|
||||
`akgl_game_save_actors`, `akgl_game_save_actorname_iterator`,
|
||||
`akgl_game_save_charactername_iterator`, `akgl_game_save_spritename_iterator`,
|
||||
`akgl_game_save_spritesheetname_iterator` (`src/game.c`);
|
||||
`akgl_get_json_properties_double`, `akgl_get_json_properties_float`,
|
||||
`akgl_get_json_properties_number`, `akgl_tilemap_load_layer_image`,
|
||||
`akgl_tilemap_load_layer_object_actor`, `akgl_tilemap_load_physics`
|
||||
(`src/tilemap.c`); `akgl_path_relative_from`, `akgl_path_relative_root`
|
||||
(`src/util.c`); `gamepad_handle_added`, `gamepad_handle_button_down`,
|
||||
`gamepad_handle_button_up`, `gamepad_handle_removed` (`src/controller.c`).
|
||||
|
||||
Each should be either declared in its header or made `static`. Note that
|
||||
`tilemap.h` already has a "part of the internal API, exposed here for unit
|
||||
testing" block — the tilemap entries belong there.
|
||||
|
||||
8. **`akgl_game_init_screen` is declared but never defined**
|
||||
(`include/akgl/game.h:100`). Same failure mode as **Defects → Known and still
|
||||
open #10**, which covers the four `akgl_controller_handle_*` declarations;
|
||||
fold this one into that item.
|
||||
|
||||
9. **Static helpers use three different naming styles.** `actor_visible`
|
||||
(`src/actor.c:185`) is bare; `akgl_character_load_json_inner` and
|
||||
`akgl_character_load_json_state_int_from_strings` (`src/character.c:101,132`)
|
||||
and `akgl_sprite_load_json_spritesheet` (`src/sprite.c:51`) carry the full
|
||||
public prefix; `gamepad_handle_*` (`src/controller.c:121+`) uses a third
|
||||
subsystem word that appears nowhere else. Adopt one rule — the clearest is
|
||||
that `static` helpers drop the `akgl_` prefix, since it exists to avoid
|
||||
external collisions.
|
||||
|
||||
10. **Parameter names disagree between declaration and definition.** Doxygen
|
||||
documents the header spelling, so the generated docs describe names the
|
||||
implementation does not use:
|
||||
|
||||
| Header | Implementation |
|
||||
|---|---|
|
||||
| `character.h:41` `basechar` | `character.c:21` `obj` |
|
||||
| `character.h:69` `props` | `character.c:75` `registry` |
|
||||
| `heap.h:121` `ptr` | `heap.c:143` `basechar` |
|
||||
| `registry.h:97` `value` | `registry.c:163` `src` |
|
||||
| `json_helpers.h:134` `e` | `json_helpers.c:149` `err` |
|
||||
| `tilemap.h:134,143` `dest` | `tilemap.c:646,767` `map` |
|
||||
|
||||
The `tilemap.h` pair is the most misleading: the parameter is the map being
|
||||
*read* and drawn, but it is named `dest` and documented as "Output
|
||||
destination populated by the function".
|
||||
|
||||
11. **Object-pool size macros are defined twice, and the override hook is
|
||||
dead.** `heap.h:15-29` wraps `AKGL_MAX_HEAP_ACTOR`, `_SPRITE`, `_SPRITESHEET`,
|
||||
`_CHARACTER`, and `_STRING` in `#ifndef` guards so a consumer can override
|
||||
them, but `actor.h:65`, `sprite.h:19-20`, and `character.h:14` define the same
|
||||
four unconditionally and are included from `heap.h:9-11`. Whichever header
|
||||
lands first wins and the `#ifndef` never fires, so the override mechanism
|
||||
cannot work. Define each pool size once — `heap.h` is the natural home.
|
||||
|
||||
12. **Headers rely on their includers for types.** `iterator.h` uses `uint32_t`
|
||||
without `<stdint.h>`; `json_helpers.h` uses `json_t` without `<jansson.h>`;
|
||||
`util.h` uses `SDL_FRect` and `bool` without any SDL include; `text.h` uses
|
||||
`SDL_Color` and `akerr_ErrorContext` without including SDL or `akerror.h`;
|
||||
`controller.h` uses `akgl_Actor` without including `actor.h`. Each compiles
|
||||
only because of `.c`-file include ordering. Headers should be self-contained.
|
||||
|
||||
13. **Include spelling is split between quoted and angled forms for the same
|
||||
directory.** `actor.h:10-11`, `character.h:10-11`, `controller.h:11`,
|
||||
`game.h:11-14`, `json_helpers.h:10`, and `staticstring.h:9` use
|
||||
`#include "sibling.h"`; `physics.h:11-13`, `registry.h:9-10`, `renderer.h:13`,
|
||||
`tilemap.h:10-12`, and `util.h:10` use `#include <akgl/sibling.h>`. The
|
||||
angled form is correct for an installed library.
|
||||
|
||||
14. **Empty parameter lists.** `akgl_heap_init()`, `akgl_heap_init_actor()`,
|
||||
`akgl_registry_init*()`, `akgl_game_init()`, `akgl_game_init_screen()`, and
|
||||
`akgl_game_updateFPS()` declare `()` rather than `(void)`, while
|
||||
`akgl_controller_list_keyboards(void)`, `akgl_controller_open_gamepads(void)`,
|
||||
`akgl_game_lowfps(void)`, and `akgl_game_state_lock(void)` use `(void)`.
|
||||
Before C23 the two are not equivalent — `()` suppresses argument checking.
|
||||
`akgl_heap_init_actor` is even declared `()` in `heap.h:57` and defined
|
||||
`(void)` in `heap.c:48`.
|
||||
|
||||
15. **`AKERR_NOIGNORE` is applied inconsistently at definition sites.** Headers
|
||||
use it uniformly (except `akgl_sprite_sheet_coords_for_frame`, `sprite.h:86`,
|
||||
which omits it). Definitions are split even within one file: `registry.c:55,120,163,172`
|
||||
repeat it, `registry.c:27,44,63,71,79,96,104,112` do not. Since the attribute
|
||||
is already on the declaration, drop it from all definitions.
|
||||
|
||||
### 3. Error-handling pattern
|
||||
|
||||
16. **`*_RETURN` macros are used inside `ATTEMPT` blocks, which skips `CLEANUP`.**
|
||||
The established pattern is `FAIL_*_BREAK` / `CATCH` inside `ATTEMPT` and
|
||||
`*_RETURN` outside it. Violations:
|
||||
- `src/tilemap.c:52` — `SUCCEED_RETURN` on the success path of
|
||||
`akgl_get_json_tilemap_property` returns directly from inside `ATTEMPT`,
|
||||
bypassing the `CLEANUP` at line 54 that releases `tmpstr` and `typestr`.
|
||||
Every successful property lookup leaks two heap strings, and the string
|
||||
pool is only 256 entries.
|
||||
- `src/tilemap.c:620` — `FAIL_RETURN` inside `ATTEMPT`.
|
||||
- `src/controller.c:286-289,306` — `FAIL_ZERO_RETURN` / `FAIL_NONZERO_RETURN`
|
||||
inside `ATTEMPT`; `src/controller.c:383` — `SUCCEED_RETURN` inside `ATTEMPT`,
|
||||
which also leaves `akgl_controller_default` with no return statement on the
|
||||
path that falls out of `FINISH`.
|
||||
- `src/game.c:457,462` — `FAIL_NONZERO_RETURN` inside `ATTEMPT`, skipping the
|
||||
`fclose` in `CLEANUP` at line 482.
|
||||
|
||||
17. **NULL-check discipline varies by function.** In `src/json_helpers.c` only
|
||||
`akgl_get_json_string_value` (line 71) and `akgl_get_json_array_index_string`
|
||||
(line 128) validate `key`/`dest`; the other eight accessors validate only the
|
||||
container and then dereference `dest` unconditionally. In `src/physics.c` the
|
||||
arcade backends check `actor` but `akgl_physics_null_gravity`,
|
||||
`_null_collide`, and `_null_move` (lines 15-34) check only `self`. In
|
||||
`src/renderer.c:65,74`, `akgl_render_2d_frame_start` and `_frame_end`
|
||||
dereference `self->sdl_renderer` with no check on `self`, while
|
||||
`akgl_render_2d_draw_texture` (line 82) checks `self` first.
|
||||
|
||||
18. **Error-context variable naming is split between `errctx` and `e`,
|
||||
sometimes within one file.** `src/util.c` uses `e` in the path helpers
|
||||
(lines 32-116) and `errctx` in the geometry helpers (lines 118-259);
|
||||
`src/heap.c` uses `errctx` everywhere except `akgl_heap_init_actor` (line 50).
|
||||
`src/actor.c`, `character.c`, `json_helpers.c`, `registry.c`, `sprite.c`, and
|
||||
`tilemap.c` favor `errctx`; `game.c`, `physics.c`, `renderer.c`, and
|
||||
`controller.c` favor `e`. Pick one.
|
||||
|
||||
### 4. Types and macros
|
||||
|
||||
19. **`float`/`double` are used raw where `types.h` defines aliases.** `types.h`
|
||||
exports `float32_t` and `float64_t`, and the actor and character structs use
|
||||
`float32_t` throughout — but `akgl_get_json_number_value` takes `float *`
|
||||
(`json_helpers.h:55`), `akgl_get_json_double_value` takes `double *` (line 66),
|
||||
`akgl_PhysicsBackend`'s six drag/gravity fields are `double`
|
||||
(`physics.h:22-27`), and `akgl_Tilemap`'s perspective fields are `float`
|
||||
(`tilemap.h:105-108`). Either use the aliases consistently or delete them.
|
||||
|
||||
20. **`AKGL_COLLIDE_RECTANGLES` (`include/akgl/util.h:27`) has unbalanced
|
||||
parentheses** — three opens, two closes — so any use is a syntax error. It has
|
||||
no callers and duplicates `akgl_collide_rectangles`. Delete it.
|
||||
|
||||
21. **Bitmask macros are unparenthesized.** `AKGL_BITMASK_HAS(x, y)` expands to
|
||||
`(x & y) == y` with no outer parens, so `!AKGL_BITMASK_HAS(a, b)` parses as
|
||||
`!(a & b) == b`. No in-tree caller negates it today (`AKGL_BITMASK_HASNOT`
|
||||
exists for that), so this is latent rather than live. `AKGL_BITMASK_CLEAR(x)`
|
||||
(`game.h:86`) also carries a trailing semicolon inside the macro body, so
|
||||
normal use produces an empty statement. Parenthesize all five and drop the
|
||||
semicolon.
|
||||
|
||||
22. **The state and iterator bit macros mix forms.** `AKGL_ITERATOR_OP_UPDATE`
|
||||
(`iterator.h:15`) is written `1` while its 31 siblings are `1 << n`, and none
|
||||
of the `1 << n` values in `iterator.h` or `actor.h:15-49` are parenthesized.
|
||||
The hand-maintained trailing bit-pattern comments in `actor.h:34-49` are also
|
||||
wrong for the high word — they restart at `0000 0000 0000 0001` for bit 16
|
||||
rather than showing the full 32-bit value.
|
||||
|
||||
23. **`akgl_Frame` (`game.h:27-31`) is defined and never used** anywhere in
|
||||
`src/`, `include/`, `tests/`, or `util/`.
|
||||
|
||||
### 5. `AKGL_ACTOR_STATE_STRING_NAMES` disagrees with `actor.h`
|
||||
|
||||
24. **The array bound differs between declaration and definition.**
|
||||
`include/akgl/actor.h:57` declares `[AKGL_ACTOR_MAX_STATES+1]` (33);
|
||||
`src/actor_state_string_names.c:6` defines `[32]`. Any consumer that trusts
|
||||
the declared bound and reads index 32 reads past the object.
|
||||
|
||||
25. **Two entries name the wrong bit.** `actor.h` assigns bit 11 to
|
||||
`AKGL_ACTOR_STATE_MOVING_IN` and bit 12 to `AKGL_ACTOR_STATE_MOVING_OUT`, but
|
||||
`actor_state_string_names.c:18-19` puts `"AKGL_ACTOR_STATE_UNDEFINED_11"` and
|
||||
`"AKGL_ACTOR_STATE_UNDEFINED_12"` at those indices — names for macros that do
|
||||
not exist. Since `akgl_registry_init_actor_state_strings`
|
||||
(`src/registry.c:79-94`) builds `AKGL_REGISTRY_ACTOR_STATE_STRINGS` from this
|
||||
array and `akgl_character_load_json_state_int_from_strings`
|
||||
(`src/character.c:101`) resolves character-JSON state names through it, a
|
||||
character JSON can never bind a sprite to `MOVING_IN` or `MOVING_OUT`.
|
||||
|
||||
26. **The generation comment is stale.** `actor.h:53-55` says the file "is built
|
||||
by a utility script and not kept in git, see the Makefile for
|
||||
lib_src/actor_state_string_names.c". There is no Makefile (the project is
|
||||
CMake), no `lib_src/`, no such script under `scripts/` or `util/`, and the
|
||||
file *is* tracked in git at `src/actor_state_string_names.c`. Either restore
|
||||
the generator — which would fix items 24 and 25 by construction — or delete
|
||||
the comment and maintain the file by hand.
|
||||
|
||||
### 6. Doxygen drift
|
||||
|
||||
27. **Three struct doc comments in `tilemap.h` are rotated by one.**
|
||||
`akgl_TilemapObject` (line 31) is documented as "Stores tileset metadata,
|
||||
texture, and frame offsets" (that is `akgl_Tileset`); `akgl_TilemapLayer`
|
||||
(line 46) as "Represents an object embedded in a tilemap layer" (that is
|
||||
`akgl_TilemapObject`); `akgl_Tileset` (line 61) as "Stores tile, image, or
|
||||
object data for one map layer" (that is `akgl_TilemapLayer`).
|
||||
|
||||
28. **`point` is documented as "Represents a two-dimensional point"**
|
||||
(`util.h:12`) but has `x`, `y`, and `z`.
|
||||
|
||||
29. **Doc comments live on the definition for public functions.** The convention
|
||||
is header-side documentation, but `akgl_path_relative_root` (`util.c:23`),
|
||||
`akgl_path_relative_from` (`util.c:97`), the four `gamepad_handle_*`
|
||||
(`controller.c:114+`), the `akgl_game_save_*` iterators and
|
||||
`akgl_game_load_*` helpers (`game.c:173+`), and the tilemap helpers
|
||||
(`tilemap.c:90+`) are documented only in the `.c`. This is the same set as
|
||||
item 7 — resolving that resolves this.
|
||||
|
||||
### 7. Formatting
|
||||
|
||||
30. **A minority of files use a 2-column offset instead of the canonical
|
||||
4-column one.** The mix of tabs and spaces across most of `src/` is *not*
|
||||
disorder: it is exactly what Emacs `cc-mode` emits for the `stroustrup` style
|
||||
with `indent-tabs-mode t` and `tab-width 8` — depth 1 is four spaces, depth 2
|
||||
is one tab, depth 3 is a tab plus four spaces, and so on. Twelve of the
|
||||
seventeen `.c` files follow that ladder cleanly.
|
||||
|
||||
The genuine outliers indent at 2 columns: `src/json_helpers.c` (82 lines,
|
||||
except `akgl_get_json_with_default` at line 149 which is 4), `src/util.c`
|
||||
(37 lines, from `akgl_rectangle_points` at line 118 onward), `src/assets.c`
|
||||
(9), `src/staticstring.c` (7), `src/actor.c` (8, in `akgl_actor_add_child`
|
||||
at line 272), plus `include/akgl/util.h` (7) and
|
||||
`include/akgl/staticstring.h` (2).
|
||||
|
||||
**Resolved.** `AGENTS.md` now specifies the canonical style and the exact
|
||||
`cc-mode` settings; `.dir-locals.el` applies them in Emacs; and
|
||||
`scripts/reindent.sh` applies them in batch. The whole tree (32 files across
|
||||
`src/`, `include/`, `tests/`, and `util/`) has been reindented and is a fixed
|
||||
point of `scripts/reindent.sh --check`. Verified whitespace-only: apart from
|
||||
three trailing blank lines removed at EOF in `src/heap.c`, `src/registry.c`,
|
||||
and `tests/tilemap.c`, `git diff -w` over the reindent is empty, and the test
|
||||
results are unchanged (13/14, `character` still the intentional failure).
|
||||
`scripts/hooks/pre-commit` keeps it that way — enable with
|
||||
`git config core.hooksPath scripts/hooks`.
|
||||
|
||||
31. **Leftover debug code ships in the library.** `src/controller.c:91-97` logs
|
||||
four lines whenever `event->type == 768 && event->key.which == 11 &&
|
||||
event->key.key == 13` — hardcoded decimal values for a specific keyboard ID
|
||||
on a specific developer's machine, inside the per-event inner loop.
|
||||
|
||||
32. **Large commented-out blocks.** `src/sprite.c:115-120,157,185-198,210`;
|
||||
`src/character.c:192-199,215`; `src/assets.c:18-27,50`;
|
||||
`src/tilemap.c:583,596-598,640`. All are the same abandoned
|
||||
`SDL_GetBasePath()` path-prefixing approach, superseded by
|
||||
`akgl_path_relative`. Delete them.
|
||||
|
||||
33. **Unused locals.** `screenwidth`/`screenheight` (`game.c:53-54`), `curTime`
|
||||
(`game.c:499`), `curTime` and `j` (`renderer.c:114,116` — `j` is also shadowed
|
||||
by the inner loop at line 128), `target` (`character.c:59`), `result`
|
||||
(`util.c:37,73`), `opflags` (`heap.c:146`, declared and cleared but never
|
||||
read).
|
||||
|
||||
34. **`akgl_game_update`'s default flags OR the same bit twice.**
|
||||
`src/game.c:496` reads
|
||||
`(AKGL_ITERATOR_OP_LAYERMASK | AKGL_ITERATOR_OP_LAYERMASK)`. Given the loop
|
||||
that follows walks layers and calls `updatefunc`, the intent was almost
|
||||
certainly `AKGL_ITERATOR_OP_UPDATE | AKGL_ITERATOR_OP_LAYERMASK`.
|
||||
|
||||
35. **`akgl_draw_background` is the only public function outside the error
|
||||
protocol.** `include/akgl/draw.h:14` returns `void` and reports nothing;
|
||||
every other public entry point returns `akerr_ErrorContext *`. It also calls
|
||||
`SDL_SetRenderDrawColor` and `SDL_RenderFillRect` without checking `renderer`
|
||||
or `renderer->sdl_renderer`.
|
||||
|
||||
36. **`akgl_registry_init_actor` is the only registry initializer that destroys
|
||||
an existing registry** before recreating it (`src/registry.c:47-49`). The
|
||||
other seven leak the old `SDL_PropertiesID` on a second call. Either all of
|
||||
them should do it or none should.
|
||||
|
||||
37. **Redundant casts obscure the code.** `(json_t *)json` where `json` is
|
||||
already `json_t *`, `(akgl_Tilemap *)dest`, `(akgl_Actor *)actorobj`,
|
||||
`(char *)&obj->name` on an array that already decays — roughly 180 pointer
|
||||
casts across `src/`, a large majority of them no-ops, densest in
|
||||
`src/tilemap.c:150-624` and `src/character.c:144-172`.
|
||||
They suppress exactly the conversion warnings that would catch a real
|
||||
mismatch.
|
||||
|
||||
38. **`struct`-qualified parameters in two definitions.**
|
||||
`akgl_physics_null_move` (`src/physics.c:29`) and `akgl_physics_arcade_move`
|
||||
(`src/physics.c:80`) are defined with `struct akgl_PhysicsBackend *self`
|
||||
while the header and the other eight physics functions use the typedef.
|
||||
|
||||
39. **`text.c:19` validates the wrong argument.**
|
||||
`FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Null filepath")` checks
|
||||
`name` a second time; `filepath` is never checked and is passed straight to
|
||||
`TTF_OpenFont`. Copy-paste of line 18.
|
||||
|
||||
40. **`akgl_path_relative` and `akgl_path_relative_from` disagree on the output
|
||||
parameter.** `akgl_path_relative` and `akgl_path_relative_root` take
|
||||
`akgl_String *dst`; `akgl_path_relative_from` takes `akgl_String **dst`
|
||||
(`src/util.c:105`). The `**` form matches the rest of the library
|
||||
(`akgl_get_json_string_value`, `akgl_get_property`, `akgl_heap_next_string`),
|
||||
which allocate when `*dest` is NULL. Related: **Defects → Known and still
|
||||
open #4**, which covers the fact that `akgl_path_relative_from` never writes
|
||||
`*dst` at all.
|
||||
|
||||
41. **`dst` vs `dest` for output parameters.** `akgl_string_copy` and the
|
||||
`akgl_path_relative*` family use `dst`; everything else uses `dest`.
|
||||
|
||||
## Coverage status
|
||||
|
||||
Generated with:
|
||||
@@ -195,6 +559,62 @@ Each was found by a test written to assert correct behavior.
|
||||
11. **`akgl_controller_pushmap` and `akgl_controller_default` accept negative map
|
||||
ids.** Both check `controlmapid >= AKGL_MAX_CONTROL_MAPS` but not
|
||||
`controlmapid < 0`, so a negative id indexes before `GAME_ControlMaps`.
|
||||
12. **A failed controller-DB fetch silently destroys the tracked fallback.**
|
||||
`include/akgl/SDL_GameControllerDB.h` is committed deliberately so the
|
||||
library still builds if upstream disappears. But `mkcontrollermappings.sh`
|
||||
has no `set -e` and never checks `curl`'s exit status: on a failed fetch it
|
||||
writes `mappings.txt` empty, then overwrites the good tracked header with
|
||||
`AKGL_SDL_GAMECONTROLLER_DB_LEN 0` and an empty initializer — and exits 0.
|
||||
Verified by pointing the script at an unresolvable host.
|
||||
|
||||
Two compounding problems:
|
||||
|
||||
- `add_custom_command` at `CMakeLists.txt:89-93` declares
|
||||
`OUTPUT include/akgl/SDL_GameControllerDB.h` as a *relative* path, which
|
||||
CMake resolves against the binary directory, while the script writes to
|
||||
the source directory. The declared output never appears, so the command is
|
||||
permanently out of date and re-runs on every build — making every build
|
||||
depend on network access and leaving the file dirty in the working tree
|
||||
each time.
|
||||
- `const char *SDL_GAMECONTROLLER_DB[] = {\n};` is an empty initializer,
|
||||
which is a constraint violation in ISO C and compiles only as a GCC
|
||||
extension.
|
||||
|
||||
Fix: have the script fetch to a temporary file, check `curl`'s status and a
|
||||
plausible minimum line count, and leave the existing header untouched on
|
||||
failure (exiting non-zero). Separately, make regeneration explicit — a
|
||||
dedicated `controllerdb` target, or an `OUTPUT` that matches where the
|
||||
script actually writes — so an ordinary build neither needs the network nor
|
||||
dirties the tree.
|
||||
13. **A stale build tree in the source directory breaks the coverage run.**
|
||||
`CMakeLists.txt:208` and `CMakeLists.txt:223` pass
|
||||
`--root "${CMAKE_CURRENT_SOURCE_DIR}"` to gcovr, and gcovr searches for
|
||||
`.gcda`/`.gcno` files under the root. The `--object-directory` argument on
|
||||
the line below each does *not* narrow that search: per `gcovr --help` it
|
||||
only identifies "the path between gcda files and the directory where the
|
||||
compiler was originally run". So every instrumented build tree left inside
|
||||
the source directory is folded into the report alongside the one actually
|
||||
being measured.
|
||||
|
||||
With a `build-coverage/` from an earlier session still present, a freshly
|
||||
configured `-DAKGL_COVERAGE=ON` tree fails in `coverage_reset`, before any
|
||||
test runs:
|
||||
|
||||
AssertionError: Got function akgl_game_lowfps on multiple lines: 45, 46.
|
||||
|
||||
45 and 46 are that function's line numbers before and after an unrelated
|
||||
`#include` was added to `src/game.c`: gcovr found 18 stale `.gcno` files
|
||||
describing the old layout, merged them with the current ones, and could not
|
||||
reconcile the two. Moving `build-coverage/` aside makes the same tree pass
|
||||
18/18. Verified with gcovr 7.0.
|
||||
|
||||
The `build*/` entry in `.gitignore` hides these trees from `git status`,
|
||||
which makes the state easier to get into and no easier to notice.
|
||||
|
||||
Fix: pass the build tree to gcovr as an explicit search path instead of
|
||||
letting it default to `--root`, so only the tree under measurement is
|
||||
considered. Touches the two `add_test` blocks at `CMakeLists.txt:204-211`
|
||||
and `CMakeLists.txt:220-229`; nothing outside the `AKGL_COVERAGE` branch.
|
||||
|
||||
## Build notes
|
||||
|
||||
|
||||
2
deps/libakerror
vendored
2
deps/libakerror
vendored
Submodule deps/libakerror updated: 0c0d81249f...5ff87908e7
2
deps/libakstdlib
vendored
2
deps/libakstdlib
vendored
Submodule deps/libakstdlib updated: a87cbfb26d...95e5002512
@@ -6,6 +6,27 @@
|
||||
#ifndef _ERROR_H_
|
||||
#define _ERROR_H_
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
/*
|
||||
* libakerror 1.0.0 is the floor. That release moved the status-name table into a
|
||||
* private registry -- AKERR_MAX_ERR_VALUE and __AKERR_ERROR_NAMES are gone, the
|
||||
* registry entry points raise akerr_ErrorContext * instead of returning int, and
|
||||
* the library gained an soname -- so a translation unit that pairs this header
|
||||
* with a pre-1.0.0 akerror.h is an ABI mismatch, not just a compile problem.
|
||||
*
|
||||
* libakerror publishes no version macro, so this feature-tests on
|
||||
* AKERR_FIRST_CONSUMER_STATUS, which that release introduced, rather than on a
|
||||
* version number that does not exist. Without the guard an embedded build is
|
||||
* fine but a stale installed header fails much further in, on the AKGL_ERR_*
|
||||
* codes below and again inside src/heap.c.
|
||||
*
|
||||
* See deps/libakerror/UPGRADING.md.
|
||||
*/
|
||||
#ifndef AKERR_FIRST_CONSUMER_STATUS
|
||||
#error "libakgl requires libakerror >= 1.0.0: the akerror.h on the include path predates the status registry. Rebuild and reinstall libakerror."
|
||||
#endif
|
||||
|
||||
// This macro is used to silence warnings on string concatenation operations that may fail.
|
||||
// e.g., combining two element of PATH_MAX into a string buffer of AKGL_STRING_MAX_LENGTH.
|
||||
// We have to draw a line in the sand somewhere or we will just let our buffers grow forever
|
||||
@@ -17,10 +38,41 @@
|
||||
#define RESTORE_GCC_WARNINGS \
|
||||
_Pragma("GCC diagnostic pop")
|
||||
|
||||
#define AKGL_ERR_SDL (AKERR_LAST_ERRNO_VALUE + 18)
|
||||
#define AKGL_ERR_REGISTRY (AKERR_LAST_ERRNO_VALUE + 19)
|
||||
#define AKGL_ERR_HEAP (AKERR_LAST_ERRNO_VALUE + 20)
|
||||
#define AKGL_ERR_BEHAVIOR (AKERR_LAST_ERRNO_VALUE + 21)
|
||||
#define AKGL_ERR_LOGICINTERRUPT (AKERR_LAST_ERRNO_VALUE + 22)
|
||||
// libakerror reserves statuses 0-255 for the host's errno values and its own
|
||||
// AKERR_* codes; consumers allocate from AKERR_FIRST_CONSUMER_STATUS upward.
|
||||
// These are fixed offsets from that base rather than from AKERR_LAST_ERRNO_VALUE
|
||||
// so that a libc which grows an errno cannot move them out from under us.
|
||||
//
|
||||
// akgl_error_init() reserves this whole band in one call and registers a name
|
||||
// for every code below. Add a code here and you must name it there, or it
|
||||
// prints as "Unknown Error" in every stack trace that carries it.
|
||||
#define AKGL_ERR_OWNER "libakgl"
|
||||
#define AKGL_ERR_BASE AKERR_FIRST_CONSUMER_STATUS
|
||||
|
||||
#define AKGL_ERR_SDL (AKGL_ERR_BASE + 0) /** An SDL call failed; the message carries SDL_GetError() */
|
||||
#define AKGL_ERR_REGISTRY (AKGL_ERR_BASE + 1) /** A registry property or lookup operation failed */
|
||||
#define AKGL_ERR_HEAP (AKGL_ERR_BASE + 2) /** A heap pool has no free object left to hand out */
|
||||
#define AKGL_ERR_BEHAVIOR (AKGL_ERR_BASE + 3) /** A component did not behave the way its contract requires */
|
||||
#define AKGL_ERR_LOGICINTERRUPT (AKGL_ERR_BASE + 4) /** Actor logic is telling the physics simulator to skip it */
|
||||
|
||||
// One past the last libakgl status. The reservation is all-or-nothing -- a
|
||||
// subset or superset of an existing one is refused -- so this must stay one
|
||||
// past the highest code above.
|
||||
#define AKGL_ERR_LIMIT (AKGL_ERR_BASE + 5)
|
||||
#define AKGL_ERR_COUNT (AKGL_ERR_LIMIT - AKGL_ERR_BASE)
|
||||
|
||||
/**
|
||||
* @brief Claim the libakgl status band and register a name for every code in it.
|
||||
*
|
||||
* Call this before anything else in libakgl. Every other subsystem raises
|
||||
* AKGL_ERR_* codes, and a code raised before this runs carries no name into its
|
||||
* stack trace. Repeating the call is a no-op, so a program that cannot order its
|
||||
* initialization precisely may call it more than once.
|
||||
*
|
||||
* @throws AKERR_STATUS_RANGE_OVERLAP When another component already owns part of the band.
|
||||
* @throws AKERR_STATUS_RANGE_FULL When libakerror has no reservation slots left.
|
||||
* @throws AKERR_STATUS_NAME_FULL When libakerror's name registry is full.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_error_init(void);
|
||||
|
||||
#endif // _ERROR_H_
|
||||
|
||||
@@ -12,8 +12,9 @@
|
||||
#include "tilemap.h"
|
||||
#include "renderer.h"
|
||||
#include "physics.h"
|
||||
|
||||
#define AKGL_VERSION "0.1.0"
|
||||
// AKGL_VERSION used to be defined here by hand, which is how akgl.pc came to
|
||||
// ship an empty Version field: nothing tied the two together.
|
||||
#include <akgl/version.h>
|
||||
|
||||
#define AKGL_GAME_AUDIO_TRACK_BGM 1
|
||||
#define AKGL_GAME_AUDIO_MAX_TRACKS 64
|
||||
|
||||
56
include/akgl/version.h.in
Normal file
56
include/akgl/version.h.in
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* @file version.h
|
||||
* @brief Declares the libakgl version, both as compiled against and as linked.
|
||||
*
|
||||
* GENERATED FILE -- edit include/akgl/version.h.in, never the copy in the build
|
||||
* tree. Every value here comes from the project() call in CMakeLists.txt, which
|
||||
* is also what sets the shared library's VERSION and SOVERSION and the Version
|
||||
* field in akgl.pc. One number, one place, so the header, the soname and
|
||||
* pkg-config cannot drift apart.
|
||||
*
|
||||
* AKGL_VERSION is the version you *compiled against*. akgl_version() reports the
|
||||
* version of the libakgl you actually *linked*. They disagree when a stale
|
||||
* shared library is ahead of the new one on the loader path -- the failure the
|
||||
* soname exists to prevent and this pair exists to diagnose.
|
||||
*/
|
||||
|
||||
#ifndef _AKGL_VERSION_H_
|
||||
#define _AKGL_VERSION_H_
|
||||
|
||||
#define AKGL_VERSION "@PROJECT_VERSION@"
|
||||
#define AKGL_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
|
||||
#define AKGL_VERSION_MINOR @PROJECT_VERSION_MINOR@
|
||||
#define AKGL_VERSION_PATCH @PROJECT_VERSION_PATCH@
|
||||
|
||||
/**
|
||||
* @brief True when the headers on the include path are at least the given version.
|
||||
*
|
||||
* For consumers that must build against more than one libakgl release. This is
|
||||
* the test libakstdlib could not write against libakerror, which published no
|
||||
* version macro and had to feature-test on AKERR_FIRST_CONSUMER_STATUS instead.
|
||||
*
|
||||
* @code
|
||||
* #if AKGL_VERSION_AT_LEAST(0, 2, 0)
|
||||
* akgl_something_new();
|
||||
* #endif
|
||||
* @endcode
|
||||
*/
|
||||
#define AKGL_VERSION_AT_LEAST(major, minor, patch) \
|
||||
((AKGL_VERSION_MAJOR > (major)) || \
|
||||
(AKGL_VERSION_MAJOR == (major) && AKGL_VERSION_MINOR > (minor)) || \
|
||||
(AKGL_VERSION_MAJOR == (major) && AKGL_VERSION_MINOR == (minor) && \
|
||||
AKGL_VERSION_PATCH >= (patch)))
|
||||
|
||||
/**
|
||||
* @brief Report the version of the libakgl that is actually linked.
|
||||
*
|
||||
* Returns "major.minor.patch". Compare it against AKGL_VERSION to detect a
|
||||
* stale shared library. Never returns NULL, and the storage is static -- the
|
||||
* caller must not free or modify it.
|
||||
*
|
||||
* This returns a string rather than an akerr_ErrorContext * because it cannot
|
||||
* fail, the same reason akerr_name_for_status() returns a name directly.
|
||||
*/
|
||||
const char *akgl_version(void);
|
||||
|
||||
#endif // _AKGL_VERSION_H_
|
||||
98
scripts/hooks/pre-commit
Executable file
98
scripts/hooks/pre-commit
Executable file
@@ -0,0 +1,98 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Reindent staged C sources to the canonical style before the commit is made.
|
||||
# See AGENTS.md -> "Coding Style".
|
||||
#
|
||||
# Enable with:
|
||||
# git config core.hooksPath scripts/hooks
|
||||
# Bypass a single commit with `git commit --no-verify`.
|
||||
#
|
||||
# The hook checks the *staged* content, not the working tree, so what gets
|
||||
# committed is what was verified. When a file needs reindenting it is fixed in
|
||||
# the working tree and re-staged -- but only when the working tree and the index
|
||||
# agree for that file. If they disagree (a partial `git add -p`), re-staging
|
||||
# would sweep unstaged work into the commit, so the hook stops and asks you to
|
||||
# do it yourself.
|
||||
|
||||
set -eu
|
||||
|
||||
root=$(git rev-parse --show-toplevel)
|
||||
reindent="$root/scripts/reindent.sh"
|
||||
|
||||
# Leave conflict resolution alone.
|
||||
if [ -e "$root/.git/MERGE_HEAD" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -x "$reindent" ]; then
|
||||
echo "pre-commit: $reindent missing or not executable; skipping style check" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Without Emacs the canonical style cannot be applied or even checked. Warn
|
||||
# rather than block: a hook that fails closed on a missing optional tool just
|
||||
# teaches everyone to pass --no-verify.
|
||||
if ! command -v emacs >/dev/null 2>&1; then
|
||||
echo "pre-commit: emacs not found; skipping reindent (see AGENTS.md)" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
staged=$(git diff --cached --name-only --diff-filter=ACMR \
|
||||
| grep -E '^(src|include|tests|util)/.*\.[ch]$' \
|
||||
| grep -v -x -F 'include/akgl/SDL_GameControllerDB.h' || true)
|
||||
|
||||
if [ -z "$staged" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT INT TERM
|
||||
|
||||
# Reindent a copy of each file's staged content and see whether it moves.
|
||||
needs=''
|
||||
for f in $staged; do
|
||||
copy="$tmp/$(printf '%s' "$f" | tr '/' '_')"
|
||||
git show ":$f" >"$copy"
|
||||
cp "$copy" "$copy.orig"
|
||||
if ! "$reindent" "$copy" >/dev/null 2>&1; then
|
||||
echo "pre-commit: reindent failed on $f; commit aborted" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! cmp -s "$copy" "$copy.orig"; then
|
||||
needs="$needs $f"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$needs" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Refuse to touch anything if even one file is partially staged.
|
||||
blocked=''
|
||||
for f in $needs; do
|
||||
if ! git diff --quiet -- "$f"; then
|
||||
blocked="$blocked $f"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$blocked" ]; then
|
||||
echo "pre-commit: these files need reindenting but have unstaged changes," >&2
|
||||
echo "so re-staging them would pull unstaged work into the commit:" >&2
|
||||
for f in $blocked; do echo " $f" >&2; done
|
||||
echo >&2
|
||||
echo "Reindent and stage them yourself, then commit again:" >&2
|
||||
echo " scripts/reindent.sh$(for f in $blocked; do printf ' %s' "$f"; done)" >&2
|
||||
echo " git add$(for f in $blocked; do printf ' %s' "$f"; done)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Safe: for every file needing work, the index and working tree agree.
|
||||
# shellcheck disable=SC2086
|
||||
"$reindent" $needs
|
||||
# shellcheck disable=SC2086
|
||||
git add $needs
|
||||
|
||||
echo "pre-commit: reindented and re-staged:" >&2
|
||||
for f in $needs; do echo " $f" >&2; done
|
||||
|
||||
exit 0
|
||||
87
scripts/reindent.el
Normal file
87
scripts/reindent.el
Normal file
@@ -0,0 +1,87 @@
|
||||
;;; reindent.el --- batch reindent to cc-mode "stroustrup" -*- lexical-binding: t -*-
|
||||
|
||||
;; Reindents each file named on the command line, in place, to the project's
|
||||
;; canonical style: cc-mode "stroustrup", 4 columns per level, tabs 8 columns
|
||||
;; wide. See AGENTS.md -> "Coding Style".
|
||||
;;
|
||||
;; The style is set explicitly here rather than read from .dir-locals.el so that
|
||||
;; the result does not depend on where the file lives -- the pre-commit hook
|
||||
;; runs this over temporary copies outside the project tree.
|
||||
;;
|
||||
;; Usage: emacs --batch -Q -l scripts/reindent.el -- FILE...
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'cc-mode)
|
||||
|
||||
(setq make-backup-files nil
|
||||
create-lockfiles nil
|
||||
auto-save-default nil
|
||||
vc-handled-backends nil
|
||||
inhibit-message t)
|
||||
|
||||
(defun akgl-retab-leading-whitespace ()
|
||||
"Rewrite every line's leading whitespace as tabs-then-spaces at `tab-width'.
|
||||
|
||||
`indent-region' fixes the column a line starts at, but `indent-line-to' leaves
|
||||
a line alone when it is already at the right column even if the bytes are
|
||||
wrong -- eight spaces where the canonical form is one tab. This pass closes
|
||||
that gap.
|
||||
|
||||
Deliberately not `tabify': that function's `tabify-regexp' is \" [ \\t]+\",
|
||||
which is NOT anchored to the line start, so it rewrites runs of spaces
|
||||
anywhere on the line and destroys the hand-aligned columns in the bit-flag
|
||||
tables in actor.h and iterator.h. Only leading whitespace is touched here, and
|
||||
lines beginning inside a string literal are skipped outright."
|
||||
(goto-char (point-min))
|
||||
(while (not (eobp))
|
||||
(let ((bol (point)))
|
||||
(unless (nth 3 (syntax-ppss bol))
|
||||
(skip-chars-forward " \t" (line-end-position))
|
||||
(let ((col (current-column)))
|
||||
(unless (or (zerop col) (eolp))
|
||||
(let ((want (concat (make-string (/ col tab-width) ?\t)
|
||||
(make-string (% col tab-width) ?\s))))
|
||||
(unless (string= want (buffer-substring bol (point)))
|
||||
(delete-region bol (point))
|
||||
(insert want)))))))
|
||||
(forward-line 1)))
|
||||
|
||||
(defun akgl-reindent-file (path)
|
||||
"Reindent PATH in place. Returns t if the file changed on disk."
|
||||
(let ((before (with-temp-buffer
|
||||
(insert-file-contents path)
|
||||
(buffer-string))))
|
||||
(with-current-buffer (find-file-noselect path t)
|
||||
(c-mode)
|
||||
(c-set-style "stroustrup")
|
||||
(setq indent-tabs-mode t
|
||||
tab-width 8
|
||||
c-basic-offset 4
|
||||
require-final-newline t)
|
||||
;; 1. Put every line at its correct column.
|
||||
(indent-region (point-min) (point-max))
|
||||
;; 2. Convert leading whitespace to the canonical tab/space mix.
|
||||
(akgl-retab-leading-whitespace)
|
||||
;; 3. Trailing whitespace and a single final newline.
|
||||
(delete-trailing-whitespace)
|
||||
(goto-char (point-max))
|
||||
(unless (bolp) (insert "\n"))
|
||||
(let ((changed (not (string= before (buffer-string)))))
|
||||
(when changed (save-buffer))
|
||||
(kill-buffer)
|
||||
changed))))
|
||||
|
||||
;; Emacs leaves the "--" separator in `command-line-args-left'; drop it, along
|
||||
;; with any empty argument, so the remainder is exactly the file list.
|
||||
(dolist (path (seq-remove (lambda (a) (or (string= a "--") (string= a "")))
|
||||
command-line-args-left))
|
||||
(when (akgl-reindent-file path)
|
||||
(princ (format "reindented %s\n" path))))
|
||||
|
||||
;; Exit 0 on success whether or not anything changed, so that any non-zero
|
||||
;; status from this script means a real failure. Callers detect "something
|
||||
;; changed" from stdout, or by comparing files themselves.
|
||||
(kill-emacs 0)
|
||||
|
||||
;;; reindent.el ends here
|
||||
101
scripts/reindent.sh
Executable file
101
scripts/reindent.sh
Executable file
@@ -0,0 +1,101 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Reindent C sources to the project's canonical style (cc-mode "stroustrup",
|
||||
# 4-column offset, 8-column tabs). See AGENTS.md -> "Coding Style".
|
||||
#
|
||||
# scripts/reindent.sh reindent every tracked C source in place
|
||||
# scripts/reindent.sh FILE... reindent only the named files
|
||||
# scripts/reindent.sh --check ... report non-conforming files, change nothing
|
||||
#
|
||||
# Exit status: 0 if everything already conforms (or was reindented), 1 if
|
||||
# --check found a file that needs reindenting, 2 on a usage or environment
|
||||
# error.
|
||||
|
||||
set -eu
|
||||
|
||||
root=$(git rev-parse --show-toplevel)
|
||||
elisp="$root/scripts/reindent.el"
|
||||
|
||||
# Directories whose C sources are hand-maintained. Anything outside these is
|
||||
# vendored (deps/) or generated (include/akgl/SDL_GameControllerDB.h) and is
|
||||
# left alone.
|
||||
SCOPE='src include tests util'
|
||||
GENERATED='include/akgl/SDL_GameControllerDB.h'
|
||||
|
||||
check=0
|
||||
if [ "${1:-}" = "--check" ]; then
|
||||
check=1
|
||||
shift
|
||||
fi
|
||||
|
||||
if ! command -v emacs >/dev/null 2>&1; then
|
||||
echo "reindent: emacs not found; cannot verify or apply the canonical style" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [ ! -f "$elisp" ]; then
|
||||
echo "reindent: missing $elisp" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Build the file list: explicit arguments, or every tracked C source in scope.
|
||||
if [ "$#" -gt 0 ]; then
|
||||
files=$(for f in "$@"; do printf '%s\n' "$f"; done)
|
||||
else
|
||||
files=$(cd "$root" && git ls-files $SCOPE | grep -E '\.[ch]$' || true)
|
||||
fi
|
||||
|
||||
# Drop generated files and anything that no longer exists on disk.
|
||||
files=$(printf '%s\n' "$files" | grep -v -x -F "$GENERATED" || true)
|
||||
files=$(cd "$root" && for f in $files; do [ -f "$f" ] && printf '%s\n' "$f"; done)
|
||||
|
||||
if [ -z "$files" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$check" -eq 0 ]; then
|
||||
# Reindent in place. A non-zero status here is a real failure -- never
|
||||
# swallow it, or a broken Emacs would look like "everything already conforms".
|
||||
# shellcheck disable=SC2086
|
||||
(cd "$root" && emacs --batch -Q -l "$elisp" -- $files) || {
|
||||
echo "reindent: emacs failed; no files were reindented" >&2
|
||||
exit 2
|
||||
}
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --check: reindent throwaway copies and report which originals differ. The
|
||||
# copies keep their extension so cc-mode still selects the right major mode.
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT INT TERM
|
||||
|
||||
# Accept both repo-relative and absolute paths: the default file list is
|
||||
# relative, but callers (including the pre-commit hook) pass absolute ones.
|
||||
abspath() {
|
||||
case "$1" in
|
||||
/*) printf '%s' "$1" ;;
|
||||
*) printf '%s/%s' "$root" "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
copies=''
|
||||
for f in $files; do
|
||||
dest="$tmp/$(printf '%s' "$f" | tr '/' '_')"
|
||||
cp "$(abspath "$f")" "$dest"
|
||||
copies="$copies $dest"
|
||||
done
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
emacs --batch -Q -l "$elisp" -- $copies >/dev/null || {
|
||||
echo "reindent: emacs failed; cannot determine whether sources conform" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
status=0
|
||||
for f in $files; do
|
||||
dest="$tmp/$(printf '%s' "$f" | tr '/' '_')"
|
||||
if ! cmp -s "$(abspath "$f")" "$dest"; then
|
||||
echo "$f"
|
||||
status=1
|
||||
fi
|
||||
done
|
||||
exit $status
|
||||
24
src/error.c
Normal file
24
src/error.c
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* @file error.c
|
||||
* @brief Implements the error subsystem: claims and names the libakgl status band.
|
||||
*/
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/error.h>
|
||||
|
||||
akerr_ErrorContext *akgl_error_init(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
// Claim the whole band before naming anything in it: libakerror refuses a
|
||||
// name for a status we do not own. Any collision propagates to the caller
|
||||
// -- another component owning part of our range is an initialization
|
||||
// failure, not a warning.
|
||||
PASS(errctx, akerr_reserve_status_range(AKGL_ERR_BASE, AKGL_ERR_COUNT, AKGL_ERR_OWNER));
|
||||
PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_SDL, "SDL Error"));
|
||||
PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_REGISTRY, "Registry Error"));
|
||||
PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_HEAP, "Heap Error"));
|
||||
PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_BEHAVIOR, "Behavior Error"));
|
||||
PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_LOGICINTERRUPT, "Logic Interrupt"));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <akgl/staticstring.h>
|
||||
#include <akgl/iterator.h>
|
||||
#include <akgl/physics.h>
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/SDL_GameControllerDB.h>
|
||||
|
||||
SDL_Window *window = NULL;
|
||||
@@ -55,6 +56,10 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init()
|
||||
|
||||
int i = 0;
|
||||
PREPARE_ERROR(e);
|
||||
// First, before anything that can raise: everything below reports through
|
||||
// AKGL_ERR_* codes, and a code raised before its name is registered prints
|
||||
// as "Unknown Error" in the stack trace the caller is left holding.
|
||||
PASS(e, akgl_error_init());
|
||||
strncpy((char *)&game.libversion, AKGL_VERSION, 32);
|
||||
game.gameStartTime = SDL_GetTicksNS();
|
||||
game.lastIterTime = game.gameStartTime;
|
||||
|
||||
@@ -24,11 +24,6 @@ akerr_ErrorContext *akgl_heap_init()
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
int i = 0;
|
||||
akerr_name_for_status(AKGL_ERR_SDL, "SDL Error");
|
||||
akerr_name_for_status(AKGL_ERR_REGISTRY, "Registry Error");
|
||||
akerr_name_for_status(AKGL_ERR_HEAP, "Heap Error");
|
||||
akerr_name_for_status(AKGL_ERR_BEHAVIOR, "Behavior Error");
|
||||
akerr_name_for_status(AKGL_ERR_LOGICINTERRUPT, "Logic Interrupt");
|
||||
PASS(errctx, akgl_heap_init_actor());
|
||||
for ( i = 0; i < AKGL_MAX_HEAP_SPRITE; i++) {
|
||||
memset(&HEAP_SPRITE[i], 0x00, sizeof(akgl_Sprite));
|
||||
@@ -201,4 +196,3 @@ akerr_ErrorContext *akgl_heap_release_string(akgl_String *ptr)
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
|
||||
@@ -189,4 +189,3 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_get_property(char *name, akgl_String **d
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
|
||||
15
src/version.c
Normal file
15
src/version.c
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* @file version.c
|
||||
* @brief Implements the runtime half of the version API.
|
||||
*/
|
||||
|
||||
#include <akgl/version.h>
|
||||
|
||||
const char *akgl_version(void)
|
||||
{
|
||||
// AKGL_VERSION is baked in when this translation unit is compiled, so what
|
||||
// comes back is the version of the shared library the caller linked -- not
|
||||
// the version of the header the caller built against. That asymmetry is the
|
||||
// whole point: comparing the two detects a stale libakgl on the loader path.
|
||||
return AKGL_VERSION;
|
||||
}
|
||||
@@ -903,6 +903,7 @@ int main(void)
|
||||
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
CATCH(errctx, akgl_registry_init_actor());
|
||||
CATCH(errctx, akgl_registry_init_sprite());
|
||||
CATCH(errctx, akgl_registry_init_spritesheet());
|
||||
|
||||
@@ -195,6 +195,7 @@ int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
|
||||
|
||||
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) {
|
||||
|
||||
@@ -532,6 +532,7 @@ int main(void)
|
||||
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD),
|
||||
|
||||
102
tests/error.c
Normal file
102
tests/error.c
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* @file error.c
|
||||
* @brief Unit tests for the libakgl status band: reservation, ownership and names.
|
||||
*
|
||||
* The libakerror registry is process-global, so these tests assert against
|
||||
* whatever akgl_error_init() left behind rather than building their own state.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/error.h>
|
||||
|
||||
#include "testutil.h"
|
||||
|
||||
/**
|
||||
* @brief akgl_error_init() must own the libakgl status band and name every code in it.
|
||||
*
|
||||
* A code whose name never registered degrades to "Unknown Error" in every stack
|
||||
* trace that carries it, and a band we never reserved is one another component
|
||||
* can name out from under us. Both stay silent until something has already gone
|
||||
* wrong, so assert them directly rather than waiting to read a useless trace.
|
||||
*/
|
||||
akerr_ErrorContext *test_error_init_owns_the_status_band(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
static const struct {
|
||||
int status;
|
||||
const char *name;
|
||||
} expected[] = {
|
||||
{ AKGL_ERR_SDL, "SDL Error" },
|
||||
{ AKGL_ERR_REGISTRY, "Registry Error" },
|
||||
{ AKGL_ERR_HEAP, "Heap Error" },
|
||||
{ AKGL_ERR_BEHAVIOR, "Behavior Error" },
|
||||
{ AKGL_ERR_LOGICINTERRUPT, "Logic Interrupt" }
|
||||
};
|
||||
bool named = true;
|
||||
int i = 0;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(e, akgl_error_init());
|
||||
|
||||
TEST_ASSERT(e, (int)(sizeof(expected) / sizeof(expected[0])) == AKGL_ERR_COUNT,
|
||||
"the libakgl status band holds %d codes but %d are named here",
|
||||
AKGL_ERR_COUNT, (int)(sizeof(expected) / sizeof(expected[0])));
|
||||
|
||||
for ( i = 0; i < (int)(sizeof(expected) / sizeof(expected[0])); i++ ) {
|
||||
TEST_ASSERT_FLAG(named,
|
||||
strcmp(akerr_name_for_status(expected[i].status, NULL),
|
||||
expected[i].name) == 0);
|
||||
}
|
||||
TEST_ASSERT(e, named,
|
||||
"akgl_error_init did not register the expected name for every AKGL_ERR_* code");
|
||||
|
||||
// The reservation is what makes those names ours. Without it the
|
||||
// registrations above would still succeed for anyone who asked.
|
||||
TEST_EXPECT_STATUS(e, AKERR_STATUS_NAME_FOREIGN,
|
||||
akerr_register_status_name("not-libakgl", AKGL_ERR_HEAP, "Squatter"),
|
||||
"a foreign owner was allowed to rename a libakgl status");
|
||||
TEST_EXPECT_STATUS(e, AKERR_STATUS_RANGE_OVERLAP,
|
||||
akerr_reserve_status_range(AKGL_ERR_BASE, AKGL_ERR_COUNT, "not-libakgl"),
|
||||
"a foreign owner was allowed to reserve the libakgl status band");
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calling akgl_error_init() twice must be a no-op, not a self-collision.
|
||||
*
|
||||
* Nothing in libakgl orders initialization for an embedding program, so a second
|
||||
* call has to be harmless. libakerror only treats an *identical* reservation as
|
||||
* a repeat -- a subset or superset raises -- which makes this a real constraint
|
||||
* on AKGL_ERR_BASE and AKGL_ERR_COUNT, not a triviality.
|
||||
*/
|
||||
akerr_ErrorContext *test_error_init_is_idempotent(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
|
||||
ATTEMPT {
|
||||
TEST_EXPECT_OK(e, akgl_error_init(), "the second akgl_error_init failed");
|
||||
TEST_EXPECT_OK(e, akgl_error_init(), "the third akgl_error_init failed");
|
||||
TEST_ASSERT(e, strcmp(akerr_name_for_status(AKGL_ERR_SDL, NULL), "SDL Error") == 0,
|
||||
"re-running akgl_error_init lost the name for AKGL_ERR_SDL");
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, test_error_init_owns_the_status_band());
|
||||
CATCH(errctx, test_error_init_is_idempotent());
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
}
|
||||
@@ -402,6 +402,7 @@ int main(void)
|
||||
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
CATCH(errctx, akgl_heap_init());
|
||||
CATCH(errctx, akgl_registry_init());
|
||||
|
||||
|
||||
@@ -361,6 +361,7 @@ int main(void)
|
||||
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
CATCH(errctx, akgl_heap_init());
|
||||
CATCH(errctx, akgl_registry_init());
|
||||
|
||||
|
||||
@@ -347,6 +347,7 @@ int main(void)
|
||||
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
CATCH(errctx, akgl_heap_init());
|
||||
CATCH(errctx, akgl_registry_init());
|
||||
CATCH(errctx, load_fixture());
|
||||
|
||||
@@ -722,6 +722,7 @@ int main(void)
|
||||
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
CATCH(errctx, akgl_heap_init());
|
||||
CATCH(errctx, akgl_registry_init());
|
||||
CATCH(errctx, akgl_registry_init_properties());
|
||||
|
||||
@@ -87,6 +87,7 @@ int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
CATCH(errctx, test_akgl_registry_init_creation_failures());
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
|
||||
@@ -187,6 +187,7 @@ int main(void)
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
renderer = &_akgl_renderer;
|
||||
|
||||
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
|
||||
|
||||
@@ -133,6 +133,7 @@ int main(void)
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
printf("test_fresh_heap_gives_string ....\n");
|
||||
test_fresh_heap_gives_strings();
|
||||
reset_string_heap();
|
||||
|
||||
@@ -414,6 +414,7 @@ int main(void)
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
gamemap = &_akgl_gamemap;
|
||||
renderer = &_akgl_renderer;
|
||||
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
|
||||
@@ -447,4 +448,3 @@ int main(void)
|
||||
} FINISH_NORETURN(errctx);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -310,6 +310,7 @@ int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
CATCH(errctx, test_akgl_rectangle_points_nullpointers());
|
||||
CATCH(errctx, test_akgl_rectangle_points_math());
|
||||
CATCH(errctx, test_akgl_collide_point_rectangle_nullpointers());
|
||||
|
||||
108
tests/version.c
Normal file
108
tests/version.c
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* @file version.c
|
||||
* @brief Unit tests for the version macros and the linked-library accessor.
|
||||
*
|
||||
* These assert that the several places the version appears cannot drift: the
|
||||
* string, the numeric components, and what the shared library reports at
|
||||
* runtime all come from one project() call, and this is what proves it.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/version.h>
|
||||
|
||||
#include "testutil.h"
|
||||
|
||||
/**
|
||||
* @brief The version string and the numeric components must describe one version.
|
||||
*
|
||||
* They are separate substitutions in version.h.in, so a mangled template can
|
||||
* leave them disagreeing and nothing else would notice.
|
||||
*/
|
||||
akerr_ErrorContext *test_version_string_matches_components(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
char assembled[64];
|
||||
|
||||
ATTEMPT {
|
||||
snprintf(assembled, sizeof(assembled), "%d.%d.%d",
|
||||
AKGL_VERSION_MAJOR, AKGL_VERSION_MINOR, AKGL_VERSION_PATCH);
|
||||
TEST_ASSERT(e, strcmp(assembled, AKGL_VERSION) == 0,
|
||||
"AKGL_VERSION is \"%s\" but the components assemble to \"%s\"",
|
||||
AKGL_VERSION, assembled);
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The linked library must report the version its headers were generated from.
|
||||
*
|
||||
* In this build tree they are the same tree, so this can only fail if the test
|
||||
* picked up an installed libakgl off LD_LIBRARY_PATH instead of the one just
|
||||
* built -- which is precisely the mispairing the accessor exists to catch.
|
||||
*/
|
||||
akerr_ErrorContext *test_version_linked_matches_compiled(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
|
||||
ATTEMPT {
|
||||
TEST_ASSERT(e, akgl_version() != NULL, "akgl_version returned NULL");
|
||||
TEST_ASSERT(e, strcmp(akgl_version(), AKGL_VERSION) == 0,
|
||||
"linked libakgl reports \"%s\" but the headers say \"%s\"",
|
||||
akgl_version(), AKGL_VERSION);
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief AKGL_VERSION_AT_LEAST must order versions correctly at the boundaries.
|
||||
*
|
||||
* Checked against the current version rather than fixed literals, so the test
|
||||
* does not have to be rewritten every time the version is bumped.
|
||||
*/
|
||||
akerr_ErrorContext *test_version_at_least_boundaries(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
|
||||
ATTEMPT {
|
||||
TEST_ASSERT(e,
|
||||
AKGL_VERSION_AT_LEAST(AKGL_VERSION_MAJOR, AKGL_VERSION_MINOR, AKGL_VERSION_PATCH),
|
||||
"AKGL_VERSION_AT_LEAST rejected the current version");
|
||||
TEST_ASSERT(e,
|
||||
!AKGL_VERSION_AT_LEAST(AKGL_VERSION_MAJOR, AKGL_VERSION_MINOR, AKGL_VERSION_PATCH + 1),
|
||||
"AKGL_VERSION_AT_LEAST accepted a later patch");
|
||||
TEST_ASSERT(e,
|
||||
!AKGL_VERSION_AT_LEAST(AKGL_VERSION_MAJOR, AKGL_VERSION_MINOR + 1, 0),
|
||||
"AKGL_VERSION_AT_LEAST accepted a later minor");
|
||||
TEST_ASSERT(e,
|
||||
!AKGL_VERSION_AT_LEAST(AKGL_VERSION_MAJOR + 1, 0, 0),
|
||||
"AKGL_VERSION_AT_LEAST accepted a later major");
|
||||
TEST_ASSERT(e,
|
||||
AKGL_VERSION_AT_LEAST(AKGL_VERSION_MAJOR, AKGL_VERSION_MINOR, 0),
|
||||
"AKGL_VERSION_AT_LEAST rejected an earlier patch");
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
CATCH(errctx, test_version_string_matches_components());
|
||||
CATCH(errctx, test_version_linked_matches_compiled());
|
||||
CATCH(errctx, test_version_at_least_boundaries());
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
}
|
||||
Reference in New Issue
Block a user