diff options
Diffstat (limited to '.github')
| -rw-r--r-- | .github/workflows/module_qemu.yml | 151 | ||||
| -rw-r--r-- | .github/workflows/r52_fvp.yml | 5 | ||||
| -rw-r--r-- | .github/workflows/regression_template.yml | 106 |
3 files changed, 259 insertions, 3 deletions
diff --git a/.github/workflows/module_qemu.yml b/.github/workflows/module_qemu.yml new file mode 100644 index 00000000..7cc62709 --- /dev/null +++ b/.github/workflows/module_qemu.yml @@ -0,0 +1,151 @@ +name: module_qemu + +# Copyright (c) 2026 Eclipse ThreadX contributors +# SPDX-License-Identifier: MIT +# Portions of this file were generated with AI assistance. +# +# Builds the module execution harness in test/tx/cmake/module_qemu for each +# Cortex-M module port it covers and EXECUTES every image on QEMU's MPS2 +# board for that core, asserting each image's self-reported result. +# +# Why this exists: until this workflow landed, no Cortex-M module port ran in +# CI. gcc_check compiles the module manager sources once per port and links +# nothing that loads a module; the host tests under test/tx/module_manager +# compile a handful of portable module manager sources against stubs. A +# module's start-up, its kernel calls through the supervisor call, the MPU +# programming on every context switch and the fault path that terminates a +# misbehaving module are only exercised by executing them, and r52_fvp does +# that for the Cortex-R52 alone. +# +# What it covers, per port: +# +# - cortex_m3 on mps2-an385 and cortex_m4 on mps2-an386, the latter built +# for its FPU with the hard-float ABI; +# - a protected module loaded, started, exercising the kernel, stopped and +# unloaded twice, with its memory given back; +# - a protected module faulting on a read of kernel memory, reported to the +# application and terminated -- which is also the proof that QEMU's MPU is +# enforcing at all; +# - the same module built privileged and unprotected, whose identical read +# succeeds. +# +# QEMU comes from the Ubuntu archive. The images report through semihosting +# and end the simulation themselves, so a hang shows up as the runner's +# timeout rather than as a pass. + +on: + # push as well as pull_request, for the reason gcc_check.yml states at + # length: dev needs its own baseline so a bad squash-merge is caught then + # rather than by whoever opens the next pull request. + push: + branches: [ master, dev ] + paths: + - ".github/workflows/module_qemu.yml" + - "common/**" + - "common_modules/**" + - "ports_module/cortex_m3/**" + - "ports_module/cortex_m4/**" + - "test/tx/cmake/module_qemu/**" + - "scripts/tx_ci_common.sh" + pull_request: + branches: [ master, dev ] + # Duplicated rather than shared through a YAML anchor, as in r52_fvp.yml. + # **Edit both.** + paths: + - ".github/workflows/module_qemu.yml" + - "common/**" + - "common_modules/**" + - "ports_module/cortex_m3/**" + - "ports_module/cortex_m4/**" + - "test/tx/cmake/module_qemu/**" + - "scripts/tx_ci_common.sh" + +permissions: + contents: read + +jobs: + module_qemu: + runs-on: ubuntu-24.04 + timeout-minutes: 30 + + env: + # Pinned deliberately and shared with gcc_check.yml, ci_cortex_m.yml and + # r52_fvp.yml through one cache entry; change them together. + GCC_VERSION: 14.3.rel1 + + steps: + - name: Check out the repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + # Path and key match gcc_check.yml's AArch32 entry exactly, so the + # workflows share one cache entry. + - name: Cache the AArch32 Arm GNU toolchain + id: cache-arm32 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: toolchain + key: arm-gnu-toolchain-${{ env.GCC_VERSION }}-x86_64-arm-none-eabi + + # The checksum suffix is .sha256asc and not .sha256; see gcc_check.yml. + - name: Install the AArch32 Arm GNU toolchain + if: steps.cache-arm32.outputs.cache-hit != 'true' + run: | + set -eu + base="https://developer.arm.com/-/media/Files/downloads/gnu/${GCC_VERSION}/binrel" + archive="arm-gnu-toolchain-${GCC_VERSION}-x86_64-arm-none-eabi.tar.xz" + mkdir -p toolchain && cd toolchain + curl -fsSLO "$base/$archive" + curl -fsSLO "$base/$archive.sha256asc" + sha256sum -c "$archive.sha256asc" + tar xf "$archive" + rm -f "$archive" + + # Through the bounded, retried apt helpers in tx_ci_common.sh, for the + # reasons that file records: on this runner pool a bare apt call can stall + # for hours. Ninja is installed only if the image stopped shipping it. + - name: Install QEMU + run: | + set -eu + . scripts/tx_ci_common.sh + packages="qemu-system-arm" + command -v ninja >/dev/null 2>&1 || packages="$packages ninja-build" + retry sudo "${TIMEOUT[@]}" apt-get "${APT_OPTIONS[@]}" update || true + # shellcheck disable=SC2086 + retry sudo "${TIMEOUT[@]}" apt-get "${APT_OPTIONS[@]}" install -y --no-install-recommends $packages + qemu-system-arm --version | head -1 + + - name: Build and run the module harness + run: | + set -eu + export PATH="$GITHUB_WORKSPACE/toolchain/arm-gnu-toolchain-${GCC_VERSION}-x86_64-arm-none-eabi/bin:$PATH" + arm-none-eabi-gcc --version | head -1 + + status=0 + for port in cortex_m3 cortex_m4; do + build="build/module_qemu_$port" + echo "" + echo "==============================================================" + echo "== $port" + echo "==============================================================" + + if ! cmake -S test/tx/cmake/module_qemu -B "$build" -G Ninja \ + -DCMAKE_TOOLCHAIN_FILE="$PWD/test/tx/cmake/module_qemu/arm-none-eabi-qemu.cmake" \ + -DMODULE_QEMU_PORT="$port"; then + echo "::error::$port: CMake configure failed" + status=1 + continue + fi + if ! cmake --build "$build"; then + echo "::error::$port: the images did not build" + status=1 + continue + fi + # --no-tests=error: the tests are registered only when QEMU and + # Python are found, and ctest's default would pass on none. + if ! ctest --test-dir "$build" --output-on-failure --no-tests=error; then + echo "::error::$port: one or more images failed on QEMU, or no tests were registered" + status=1 + fi + done + + exit $status diff --git a/.github/workflows/r52_fvp.yml b/.github/workflows/r52_fvp.yml index 2293e1bd..b7184abd 100644 --- a/.github/workflows/r52_fvp.yml +++ b/.github/workflows/r52_fvp.yml @@ -22,7 +22,8 @@ name: r52_fvp # # - the default configuration -- the images gcc_check's CMake stage builds, # but run rather than merely linked, plus the FVP module manager example, -# whose two images are the module isolation tests; +# whose two images are the module isolation tests. They link every portable +# module manager source, so common_modules/ is among the trigger paths; # - the feature configuration -- the same images with VFP and the hard float # ABI, FIQ, IRQ nesting and FIQ nesting. Those four options gate whole # assembly blocks (the VMRS/VSTMDB/VLDMIA sequences, the FIQ context paths, @@ -44,6 +45,7 @@ on: - "CMakeLists.txt" - "cmake/cortex_r52*.cmake" - "common/**" + - "common_modules/**" - "ports/cortex_r52/**" - "ports_module/cortex_r52/**" pull_request: @@ -58,6 +60,7 @@ on: - "CMakeLists.txt" - "cmake/cortex_r52*.cmake" - "common/**" + - "common_modules/**" - "ports/cortex_r52/**" - "ports_module/cortex_r52/**" diff --git a/.github/workflows/regression_template.yml b/.github/workflows/regression_template.yml index 392f6a96..decfadd4 100644 --- a/.github/workflows/regression_template.yml +++ b/.github/workflows/regression_template.yml @@ -41,6 +41,21 @@ on: default: 15 required: false type: number + # Minutes for the test step. Sixty covers every ThreadX suite, whose + # longest observed run is 37 minutes. NetX Duo runs all 43 configurations + # of its full suite in this one step, which took 201 minutes on 25 Oct + # 2026, so it passes a larger value. + test_timeout_minutes: + default: 60 + required: false + type: number + # Keep the .deb files apt downloads in the install step in the Actions + # cache, so a later run installs them without fetching them again. See the + # apt cache steps below. + apt_cache: + default: true + required: false + type: boolean skip_test: default: false required: false @@ -123,6 +138,59 @@ jobs: with: submodules: true + # The apt cache. The install scripts retry and bound every fetch, but a + # mirror can be slow without being down: on 23 Oct 2026 it served packages + # at 50 to 250 KB/s while indexes still came at 12 MB/s, and the ThreadX and + # RISC-V installs timed out on all three attempts inside an 11 MB and a 58 MB + # file. With the .deb files restored from the Actions cache, apt fetches + # only the indexes and whatever changed since the cache was saved. + # + # It is done here rather than in the install scripts so that every caller's + # script gets it unchanged: an apt.conf.d entry points apt-get's archive + # directory at the restored one. A restored file cannot alter what is + # installed, because apt checks every .deb against the hash in the signed + # index before using it, and fetches it again if the two differ. + # + # The key carries the runner image, whose package versions the cache + # matches, and the install script, whose package list it holds. A miss on + # either still restores the closest older cache, so a new image or one + # added package costs only the packages that changed. + - name: Prepare the apt cache key + id: apt_key + if: ${{ inputs.apt_cache }} + env: + INSTALL_SCRIPT: ${{ inputs.install_script }} + run: | + echo "dir=${RUNNER_TEMP}/apt-archives" >> "$GITHUB_OUTPUT" + echo "image=${ImageOS}-${ImageVersion}" >> "$GITHUB_OUTPUT" + echo "script=$( (sha256sum "$INSTALL_SCRIPT" 2> /dev/null || echo none) | cut -c1-16)" >> "$GITHUB_OUTPUT" + + - name: Restore the apt cache + id: apt_restore + if: ${{ inputs.apt_cache }} + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ steps.apt_key.outputs.dir }} + key: apt-${{ steps.apt_key.outputs.image }}-${{ steps.apt_key.outputs.script }} + restore-keys: | + apt-${{ steps.apt_key.outputs.image }}- + apt- + + # apt downloads as the unprivileged _apt user, which must own partial/. + # Keep-Downloaded-Packages is set because the apt front end deletes what it + # installed by default; apt-get keeps it, but a caller's script may use + # either. + - name: Point apt at the cache + if: ${{ inputs.apt_cache }} + env: + APT_DIR: ${{ steps.apt_key.outputs.dir }} + run: | + sudo mkdir -p "$APT_DIR/partial" + sudo chown _apt:root "$APT_DIR/partial" + printf 'Dir::Cache::Archives "%s/";\nAPT::Keep-Downloaded-Packages "true";\nBinary::apt::APT::Keep-Downloaded-Packages "true";\n' "$APT_DIR" \ + | sudo tee /etc/apt/apt.conf.d/99-ci-archive-cache > /dev/null + echo "apt cache: $(find "$APT_DIR" -maxdepth 1 -name '*.deb' | wc -l) packages restored" + # apt and PyPI have stalled this step twice, for 55 minutes and for over two # hours, against a normal 27 to 152 seconds. Nothing here had a timeout, so a # stall ran until the six hour job limit and cost a whole run rather than @@ -131,6 +199,29 @@ jobs: timeout-minutes: ${{ inputs.install_timeout_minutes }} run: ${{ inputs.install_script }} + # Saved straight after the install rather than at the end of the job, so a + # run whose tests fail still leaves the packages for the next one. autoclean + # drops .deb files the indexes no longer list, so the cache does not grow + # with every superseded version. apt's lock file and partial/ are root-only + # and are removed so the cache action, which runs unprivileged, can read the + # rest. + - name: Save the apt cache + if: ${{ inputs.apt_cache && steps.apt_restore.outputs.cache-hit != 'true' }} + env: + APT_DIR: ${{ steps.apt_key.outputs.dir }} + run: | + sudo apt-get autoclean -qq + sudo rm -rf "$APT_DIR/partial" "$APT_DIR/lock" + sudo chmod -R a+rX "$APT_DIR" + echo "apt cache: $(find "$APT_DIR" -maxdepth 1 -name '*.deb' | wc -l) packages, $(du -sh "$APT_DIR" | cut -f1)" + + - name: Upload the apt cache + if: ${{ inputs.apt_cache && steps.apt_restore.outputs.cache-hit != 'true' }} + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ steps.apt_key.outputs.dir }} + key: apt-${{ steps.apt_key.outputs.image }}-${{ steps.apt_key.outputs.script }} + # TX_COVERAGE instruments every build configuration rather than only the one # whose name ends in _coverage. It has to be set for the build as well as the # test: the build is where -fprofile-arcs is decided, and the test run is @@ -142,7 +233,7 @@ jobs: run: ${{ inputs.build_script }} - name: Test - timeout-minutes: 60 + timeout-minutes: ${{ inputs.test_timeout_minutes }} env: TX_COVERAGE: ${{ inputs.skip_coverage && 'OFF' || 'ON' }} run: ${{ inputs.test_script }} @@ -276,9 +367,20 @@ jobs: # run_tests that is cancelled -- which is what a job that never gets a runner # ends up as -- leaves both false and lets the deploy proceed to download an # artifact the cancelled job never uploaded. + # + # The one exception is a caller passing skip_test, such as the deploy jobs + # of ThreadX and NetX Duo: run_tests is skipped by design, and the steps below + # download the coverage the caller's other jobs uploaded. The caller's needs + # gates those jobs. !cancelled() is required because an if without a status + # function implies success(), which is false when run_tests is skipped. deploy_code_coverage: runs-on: ubuntu-24.04 - if: ((github.event_name == 'push') || (github.event_name == 'workflow_dispatch')) && !inputs.skip_coverage && !inputs.skip_deploy && needs.run_tests.result == 'success' + if: >- + !cancelled() && + ((github.event_name == 'push') || (github.event_name == 'workflow_dispatch')) && + !inputs.skip_coverage && !inputs.skip_deploy && + (needs.run_tests.result == 'success' || + (inputs.skip_test && needs.run_tests.result == 'skipped')) needs: run_tests environment: name: github-pages |
