diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml new file mode 100644 index 0000000..31711a1 --- /dev/null +++ b/.gitea/workflows/ci.yaml @@ -0,0 +1,161 @@ +name: akbasic CI Build +run-name: ${{ gitea.actor }} akbasic test +on: [push] + +jobs: + cmake_build: + runs-on: ubuntu-latest + steps: + - run: echo "Triggered by ${{ gitea.event_name }} from ${{ gitea.repository }}@${{ gitea.ref }}. Building on ${{ runner.os }}." + - name: Check out repository code + uses: actions/checkout@v4 + with: + # Not recursive, deliberately. The top-level build needs + # deps/libakerror and deps/libakstdlib (via add_subdirectory) and + # deps/basicinterpret (the golden corpus is driven in place from it). + # It does *not* need deps/libakgl, which is guarded behind + # AKBASIC_WITH_AKGL and defaults OFF -- and recursing into it would + # clone SDL, SDL_image, SDL_mixer, SDL_ttf and jansson for a target + # that is never configured. libakstdlib's own nested deps/libakerror is + # skipped for the same reason: our CMakeLists declares akerror::akerror + # first and libakstdlib guards on if(NOT TARGET ...). + submodules: true + - name: dependencies + run: | + sudo apt-get update -y + sudo apt-get install -y cmake gcc + - name: build + run: | + cmake -S . -B build + cmake --build build --parallel + # The suite is 61 cases: 41 golden files byte-compared against the Go + # reference's own corpus, 17 unit tests, 2 embedding examples, and 1 + # known-failing test that asserts the *correct* contract for defects + # carried over from the reference (TODO.md section 6). A green run + # therefore does not mean defect-free -- see AKBASIC_KNOWN_FAILING_TESTS. + # + # --output-junit resolves relative to the test dir, so give an absolute + # path to land the report in the workspace root for the reporter below. + - name: test (JUnit) + run: ctest --test-dir build --output-on-failure --output-junit "$(pwd)/ctest-junit.xml" + # annotate_only: true skips creating a check run via the Checks API, which + # Gitea does not support and 404s on (mikepenz/action-junit-report#23). + # Results surface via the job summary instead. + - name: publish test results + if: always() + uses: mikepenz/action-junit-report@v4 + with: + report_paths: 'ctest-junit.xml' + annotate_only: true + detailed_summary: true + include_passed: true + fail_on_failure: 'true' + - run: echo "๐Ÿ This job's status is ${{ job.status }}." + + docs: + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v4 + # No submodules at all. Doxyfile's INPUT is include/akbasic, src and + # examples, none of which live under deps/, and doxygen does not need to + # resolve to parse a declaration. Verified by running it + # against a tree with no deps/ present. + - name: dependencies + run: | + sudo apt-get update -y + sudo apt-get install -y doxygen graphviz + # This is a gate, not a convenience. The Doxyfile sets + # WARN_AS_ERROR = FAIL_ON_WARNINGS, so a doc block that documents some of a + # function's parameters but not all of them fails the build. All 114 public + # declarations under include/akbasic carry a @brief, a @param per + # parameter, a @return and their @throws; keep it that way. + # + # OUTPUT_DIRECTORY is build/docs and doxygen will not create a missing + # parent, so make it first -- there is no cmake step in this job to do it. + - name: build API documentation + run: | + mkdir -p build + doxygen Doxyfile + - name: upload API documentation + if: always() + uses: actions/upload-artifact@v4 + with: + name: api-documentation + path: build/docs/html/ + if-no-files-found: error + - run: echo "๐Ÿ This job's status is ${{ job.status }}." + + sanitizers: + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v4 + with: + submodules: true + - name: dependencies + run: | + sudo apt-get update -y + sudo apt-get install -y cmake gcc + # The whole suite under ASan and UBSan, golden files included. This is the + # gate libakstdlib's TODO.md section 1 calls its highest-value missing item + # -- worth having here from the start, because this library is all fixed + # pools and manual buffer arithmetic, which is exactly what it catches. + - name: build and test under ASan + UBSan + run: | + cmake -S . -B build-asan \ + -DAKBASIC_SANITIZE=ON \ + -DCMAKE_BUILD_TYPE=Debug + cmake --build build-asan --parallel + ctest --test-dir build-asan --output-on-failure + - run: echo "๐Ÿ This job's status is ${{ job.status }}." + + coverage: + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v4 + with: + submodules: true + - name: dependencies + run: | + sudo apt-get update -y + sudo apt-get install -y cmake gcc gcovr + # The gate is a ratchet, not a target: src/ sits at 92.3% of lines and + # 96.9% of functions, so 90 fails on a real regression (a test deleted, or + # new untested code added) without tripping over rounding. + # + # There is deliberately no branch gate. Branch coverage reads about 17% + # and is not a meaningful number here, for the reason libakgl's and + # libakstdlib's TODO.md both record: the akerror control-flow macros expand + # into large branch trees at every call site, most of them unreachable in + # normal operation. Gating on it would mean writing tests for libakerror's + # macros, which is libakerror's mutation suite's job. + # + # --root . with a src/ filter keeps the dependency trees out of the report. + # Note gcovr searches for .gcda under --root, so a stale instrumented build + # left in the source directory would be folded in -- that is libakgl defect + # #13, and the reason build*/ is gitignored rather than kept around. + - name: coverage + run: | + cmake -S . -B build-coverage \ + -DAKBASIC_COVERAGE=ON \ + -DCMAKE_BUILD_TYPE=Debug + cmake --build build-coverage --parallel + ctest --test-dir build-coverage --output-on-failure + mkdir -p build-coverage/coverage + gcovr --root . --filter 'src/.*' \ + --print-summary \ + --html-details build-coverage/coverage/index.html \ + --xml build-coverage/coverage/coverage.xml \ + --fail-under-line 90 + # Publish even when the threshold gate fails, so the uncovered lines are + # visible -- each one is a missing test. + - name: upload coverage reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: code-coverage + path: build-coverage/coverage/ + if-no-files-found: warn + - run: echo "๐Ÿ This job's status is ${{ job.status }}." diff --git a/README.md b/README.md index 8927a09..37a19f8 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,10 @@ ctest --test-dir build --output-on-failure doxygen Doxyfile ``` +CI is `.gitea/workflows/ci.yaml` and runs all four of those: the suite, the doxygen gate, +ASan+UBSan, and coverage gated at 90% of lines. The generated API documentation and the +coverage report are uploaded as build artifacts (`api-documentation` and `code-coverage`). + The `Doxyfile` is configured the way `libakgl`'s is, including `WARN_AS_ERROR = FAIL_ON_WARNINGS` โ€” a doc block that documents some of a function's parameters but not all of them fails the run, so `doxygen Doxyfile` is a gate rather than a diff --git a/TODO.md b/TODO.md index 43b9283..3692e0b 100644 --- a/TODO.md +++ b/TODO.md @@ -635,10 +635,17 @@ Dependency baseline: | `deps/libakstdlib` | 0.1.0 | soname `libakstdlib.so.0.1`. `AKSL_VERSION_CHECK()` asserted in `tests/version_check.c`. Its `aksl_ato*` family is banned here โ€” see ยง1.9. | | `deps/libakgl` | 0.1.0 | Migrated to the 1.0.0 registry and owns 256โ€“260. Not yet linked: `AKBASIC_WITH_AKGL` defaults OFF and `src/sink_akgl.c` does not exist. | -**This repository has no CI.** `libakgl` and `libakstdlib` both carry -`.gitea/workflows/ci.yaml`; akbasic does not, so every gate here โ€” ctest, the sanitizer build, -coverage, and `doxygen Doxyfile` with `WARN_AS_ERROR` โ€” is currently run by hand. Copying -`deps/libakgl/.gitea/workflows/ci.yaml` and adjusting the option prefixes is the whole job. +CI is `.gitea/workflows/ci.yaml`, in the shape the sibling libraries use: four jobs covering +the suite, the doxygen gate, ASan+UBSan and coverage. The generated API documentation and the +coverage report are both uploaded as artifacts. Two notes on it worth keeping: + +- The checkout is `submodules: true`, **not** `recursive`. The build needs `libakerror`, + `libakstdlib` and `basicinterpret`; it does not need `libakgl`, which is guarded behind + `AKBASIC_WITH_AKGL` and defaults OFF โ€” and recursing into it would clone SDL, SDL_image, + SDL_mixer, SDL_ttf and jansson for a target that is never configured. The `docs` job needs + no submodules at all. +- There is no mutation-testing job, unlike `libakerror`, `libakstdlib` and `libakgl`. This + repository has no `scripts/mutation_test.py`; see the item below. What remains, in priority order: @@ -647,5 +654,11 @@ What remains, in priority order: 2. **ยง4 โ€” the language completion work queue.** Groups A, B, D, F and J need nothing from `libakgl` and can start immediately. Multiple statements per line (the `COLON` token exists and nothing consumes it) should come first, because `DO`/`LOOP` reads badly without it. -3. **ยง6 items 12โ€“16** โ€” the defects the port uncovered. Item 12 is the one that produces a +3. **ยง6 items 12โ€“17** โ€” the defects the port uncovered. Item 12 is the one that produces a *wrong answer* rather than a refused one, and should be fixed first. +4. **Mutation testing.** All three sibling libraries gate on it and this one does not, because + there is no `scripts/mutation_test.py` here to run. It matters more than usual for this + codebase: the akerror macros expand at their call sites, so line coverage cannot see them + and mutation testing is the only thing that checks the `ATTEMPT`/`CATCH`/`PASS` control + flow at all. `deps/libakerror/scripts/mutation_test.py` is the one to port; `src/value.c` + or `src/scanner.c` would be the right first target.