Skip to content

Commit

Permalink
ci: move files from tock pr 4205
Browse files Browse the repository at this point in the history
  • Loading branch information
charles37 committed Oct 21, 2024
1 parent 44df31a commit dab965e
Show file tree
Hide file tree
Showing 20 changed files with 862 additions and 0 deletions.
274 changes: 274 additions & 0 deletions .github/workflows/treadmill-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
# Licensed under the Apache License, Version 2.0 or the MIT License.
# SPDX-License-Identifier: Apache-2.0 OR MIT
# Copyright Tock Contributors 2024.

# This workflow contains all Treadmill-based hardware CI jobs.
#
# Treadmill is a distributed hardware testbed developed within the Tock OS
# project. For more information on Treadmill, have a look at its documentation
# [1] or repository [2].
#
# This workflow is based on the Treadmill GitHub Actions integration guide [3].
# In addition, it features the ability to run multiple Treadmill jobs and
# test-execute stages through GitHub Action's job matrices, and uses a GitHub
# environment to allow deployments with access to secrets for select PRs.
#
# [1]: https://book.treadmill.ci/
# [2]: https://github.com/treadmill-tb/treadmill
# [3]: https://book.treadmill.ci/user-guide/github-actions-integration.html

name: treadmill-ci
env:
TERM: xterm # Makes tput work in actions output

# Controls when the action will run. Triggers the workflow on pull request and
# merge group checks:
#
# KEEP IN SYNC WITH `environment:` ATTRIBUTE BELOW:
on:
push:
branches:
- master
# Pull requests from forks will not have access to the required GitHub API
# secrets below, even if they are using an appropriate deployment environment
# and the workflow runs have been approved according to this environment's
# rules. We don't know whether this is a bug on GitHub's end or deliberate.
# Either way, for now we disable this workflow to run on PRs until we have
# an API proxy that securely performs these GitHub API calls (adding runners
# and starting Treadmill jobs with those runner registration tokens), which
# allows this workflow to run without access to repository secrets.
#pull_request:
merge_group: # Run CI for the GitHub merge queue

permissions:
contents: read

jobs:
test-prepare:
runs-on: ubuntu-latest

# Do not run job on forks
if: github.repository == 'tock/tock'

# This provides access to the secrets required below:
# - for `treadmill-ci`: after approval by certain persons or GH teams
# - for `treadmill-ci-merged`: without approval, on merge queue branches
# and the master branch
#
# KEEP IN SYNC WITH `on:` EVENTS ABOVE:
environment: ${{ github.event_name == 'pull_request' && 'treadmill-ci' || 'treadmill-ci-merged' }}

outputs:
tml-job-ids: ${{ steps.treadmill-job-launch.outputs.tml-job-ids }}
tml-jobs: ${{ steps.treadmill-job-launch.outputs.tml-jobs }}

steps:
- uses: actions-rust-lang/setup-rust-toolchain@v1

- name: Checkout Treadmill repository
uses: actions/checkout@v4
with:
repository: treadmill-tb/treadmill
# treadmill-tb/treadmill main as of Oct 1, 2024, 3:05 PM EDT
ref: 'c82f4d7ebddd17f8275ba52139e64e04623f30cb'
path: treadmill

- name: Cache Treadmill CLI compilation artifacts
id: cache-tml-cli
uses: actions/cache@v4
with:
path: treadmill/target
key: ${{ runner.os }}-tml-cli

- name: Compile the Treadmill CLI binary
run: |
pushd treadmill
cargo build --package tml-cli
popd
echo "$PWD/treadmill/target/debug" >> "$GITHUB_PATH"
# - uses: actions/checkout@v4
# with:
# path: tock

# - name: Analyze changes and determine types of tests to run
# run: |
# echo "TODO: implement this!"

- name: Generate a token to register new just-in-time runners
id: generate-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ vars.TREADMILL_GH_APP_CLIENT_ID }}
private-key: ${{ secrets.TREADMILL_GH_APP_PRIVATE_KEY }}

- name: Create GitHub just-in-time runners and enqueue Treadmill jobs
id: treadmill-job-launch
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
TML_API_TOKEN: ${{ secrets.TREADMILL_API_TOKEN }}

# Currently, all tests run only on hosts attached to an nRF52840DK
DUT_BOARD: nrf52840dk

# A Raspberry Pi OS netboot (NBD) image with a GitHub Actions
# self-hosted runner pre-configured.
#
# For the available images see
# https://book.treadmill.ci/treadmillci-deployment/images.html
IMAGE_ID: 1b6900eff30f37b6d012240f63aa77a22e20934e7f6ebf38e25310552dc08378

# Limit the supervisors to hosts that are compatible with this
# image. This is a hack until we introduce "image sets" which define
# multiple images for various supervisor hosts, but otherwise behave
# identically:
HOST_TYPE: nbd-netboot
HOST_ARCH: arm64
run: |
# When we eventually launch tests on multiple hardware platforms in
# parallel, we need to supply different SUB_TEST_IDs here:
SUB_TEST_ID="0"
# This runner ID uniquely identifies the GitHub Actions runner we're
# registering and allows us to launch test-execute jobs on this exact
# runner (connected to the exact board we want to run tests on).
RUNNER_ID="tml-gh-actions-runner-${GITHUB_REPOSITORY_ID}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${SUB_TEST_ID}"
# Obtain a new just-in-time runner registration token:
RUNNER_CONFIG_JSON="$(gh api \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
/repos/${{ github.repository }}/actions/runners/generate-jitconfig \
-f "name=$RUNNER_ID" \
-F "runner_group_id=1" \
-f "labels[]=$RUNNER_ID" \
-f "work_folder=_work")"
echo "Generated configuration for runner $(echo "$RUNNER_CONFIG_JSON" | jq -r '.runner.name')"
# Generate a set of job paramters that includes the GitHub runner
# registration token and a script that shuts down the host once the
# runner has run through successfully (and created a file indicating
# successful job completion, /run/github-actions-shutdown):
TML_JOB_PARAMETERS="{\
\"gh-actions-runner-encoded-jit-config\": {\
\"secret\": true, \
\"value\": \"$(echo "$RUNNER_CONFIG_JSON" | jq -r '.encoded_jit_config')\" \
}, \
\"gh-actions-runner-exec-stop-post-sh\": {\
\"secret\": false, \
\"value\": \"if [ \\\"\$SERVICE_RESULT\\\" = \\\"success\\\" ] && [ -f /run/github-actions-shutdown ]; then tml-puppet job terminate; fi\" \
}\
}"
echo "Enqueueing treadmill job:"
TML_JOB_ID_JSON="$(tml job enqueue \
"$IMAGE_ID" \
--tag-config "board:$DUT_BOARD;host-type:$HOST_TYPE;host-arch:$HOST_ARCH" \
--parameters "$TML_JOB_PARAMETERS" \
)"
TML_JOB_ID="$(echo "$TML_JOB_ID_JSON" | jq -r .job_id)"
echo "Enqueued Treadmill job with ID $TML_JOB_ID"
# Pass the job IDs and other configuration data into the outputs of
# this step, such that we can run test-execute job instances for each
# Treadmill job we've started:
echo "tml-job-ids=[ \
\"$TML_JOB_ID\" \
]" >> "$GITHUB_OUTPUT"
echo "tml-jobs={ \
\"$TML_JOB_ID\": { \
\"runner-id\": \"$RUNNER_ID\", \
} \
}" >> "$GITHUB_OUTPUT"
test-execute:
needs: test-prepare

strategy:
matrix:
tml-job-id: ${{ fromJSON(needs.test-prepare.outputs.tml-job-ids) }}

runs-on: ${{ fromJSON(needs.test-prepare.outputs.tml-jobs)[matrix.tml-job-id].runner-id }}

steps:
- name: Print Treadmill Job Context and Debug Information
run: |
echo "Treadmill job id: ${{ matrix.tml-job-id }}"
echo "GitHub Actions Runner ID: ${{ fromJSON(needs.test-prepare.outputs.tml-jobs)[matrix.tml-job-id] }}"
echo "===== Parameters: ====="
ls /run/tml/parameters
echo "===== User & group configuration: ====="
echo "whoami: $(whoami)"
echo "groups: $(groups)"
echo "===== Network configration: ====="
ip address
echo "===== Attached USB & serial console devices: ====="
lsusb
ls -lh /dev/ttyAMA* 2>/dev/null || true
ls -lh /dev/ttyACM* 2>/dev/null || true
ls -lh /dev/ttyUSB* 2>/dev/null || true
ls -lh /dev/bus/usb/*/* 2>/dev/null || true
- name: Disable wget progress output
run: |
echo "verbose = off" >> $HOME/.wgetrc
- uses: actions/checkout@v4

- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
# Avoid overwriting the RUSTFLAGS environment variable
rustflags: ""

- name: Install required system packages
run: |
# TODO: currently, the Netboot NBD targets have no access to their
# boot parition (e.g., mounted on /boot/firmware) on a Raspberry Pi OS
# host. This causes certain hooks in response to dpkg / apt commands
# to fail. Thus we ignore errors in these steps until we figure this
# part out.
sudo DEBIAN_FRONTEND=noninteractive apt update || true
sudo DEBIAN_FRONTEND=noninteractive apt install -y \
git cargo openocd python3 python3-pip python3-serial \
python3-pexpect gcc-arm-none-eabi libnewlib-arm-none-eabi \
pkg-config libudev-dev cmake libusb-1.0-0-dev udev make \
gdb-multiarch gcc-arm-none-eabi build-essential || true
# Install probe-rs:
curl --proto '=https' --tlsv1.2 -LsSf \
https://github.com/probe-rs/probe-rs/releases/latest/download/probe-rs-tools-installer.sh \
| sh
- name: Create Python virtual environment and install required dependencies
run: |
python3 -m venv ./hwcienv
source ./hwcienv/bin/activate
pip install -r tools/hwci/requirements.txt -c tools/hwci/requirements-frozen.txt
- name: Run tests
run: |
source ./hwcienv/bin/activate
cd ./tools/hwci
export PYTHONPATH="$PWD:$PYTHONPATH"
python3 core/main.py --board boards/nrf52dk.py --test tests/c_hello.py
- name: Request shutdown after successful job completion
run: |
sudo touch /run/github-actions-shutdown
- name: Provide connection information on job failure
if: failure()
run: |
echo "This CI job has failed, we avoid terminating the Treadmill job"
echo "immediately. It will be active until it reaches its timeout."
echo ""
echo "If you added SSH keys to the `job enqueue` command, you can"
echo "open an interactive session to this host. Connection"
echo "information is available here:"
echo "https://book.treadmill.ci/treadmillci-deployment/sites.html"
echo ""
echo "TODO: print host / supervisor ID as part of workflow"
echo "TODO: determine public SSH endpoint automatically and print"
echo "TODO: allow adding SSH keys to running Treadmill jobs"
13 changes: 13 additions & 0 deletions hwci/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Licensed under the Apache License, Version 2.0 or the MIT License.
# SPDX-License-Identifier: Apache-2.0 OR MIT
# Copyright Tock Contributors 2024.

from .core import main, BoardHarness, TestHarness
from .boards import TockloaderBoard, Nrf52dk, MockBoard
from .tests import (
OneshotTest,
AnalyzeConsoleTest,
WaitForConsoleMessageTest,
c_hello_test,
)
from .utils import SerialPort, MockSerialPort
7 changes: 7 additions & 0 deletions hwci/boards/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Licensed under the Apache License, Version 2.0 or the MIT License.
# SPDX-License-Identifier: Apache-2.0 OR MIT
# Copyright Tock Contributors 2024.

from .tockloader_board import TockloaderBoard
from .nrf52dk import Nrf52dk
from .mock_board import MockBoard
90 changes: 90 additions & 0 deletions hwci/boards/mock_board.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Licensed under the Apache License, Version 2.0 or the MIT License.
# SPDX-License-Identifier: Apache-2.0 OR MIT
# Copyright Tock Contributors 2024.

import logging
import threading
import time
from core.board_harness import BoardHarness
from utils.serial_port import MockSerialPort


class MockBoard(BoardHarness):
def __init__(self):
super().__init__()
self.arch = "cortex-m4"
self.kernel_board_path = "tock/boards/nordic/nrf52840dk"
self.uart_port = self.get_uart_port()
self.uart_baudrate = self.get_uart_baudrate()
self.serial = self.get_serial_port()
self.serial_output_thread = None
self.running = False

def get_uart_port(self):
# Return a mock serial port identifier
return "MOCK_SERIAL_PORT"

def get_uart_baudrate(self):
return 115200 # Same as the actual board

def get_serial_port(self):
return MockSerialPort() # Initialize the mock serial port

def erase_board(self):
logging.info("Mock erase of the board")

def flash_kernel(self):
logging.info("Mock flashing of the Tock OS kernel")

def flash_app(self, app):
logging.info(f"Mock flashing of app: {app}")
# Depending on the app, set up simulated output
if app == "c_hello":
self.simulate_output("Hello World!\r\n")
else:
logging.warning(f"No mock output configured for app: {app}")

def simulate_output(self, message):
# Start a thread to simulate serial output
def output_thread():
self.running = True
logging.info("Starting mock serial output thread")
time.sleep(1)
self.serial.write(message.encode())
self.running = False
logging.info("Mock serial output thread finished")

self.serial_output_thread = threading.Thread(target=output_thread)
self.serial_output_thread.start()
time.sleep(1)

def simulate_multi_alarm_output(self):
def output_thread():
self.running = True
logging.info("Starting mock multi-alarm serial output thread")
start_time = int(time.time())
while self.running:
current_time = int(time.time()) - start_time
# Simulate alarm 1 firing every 2 seconds
if current_time % 2 == 0:
line = f"1 {current_time} {current_time + 2}\r\n"
self.serial.write(line.encode())
# Simulate alarm 2 firing every 4 seconds
if current_time % 4 == 0:
line = f"2 {current_time} {current_time + 4}\r\n"
self.serial.write(line.encode())
time.sleep(1)
if current_time > 10: # Timeout after 10 seconds
self.running = False
logging.info("Mock multi-alarm serial output thread finished")

self.serial_output_thread = threading.Thread(target=output_thread)
self.serial_output_thread.start()

def stop(self):
self.running = False
if self.serial_output_thread and self.serial_output_thread.is_alive():
self.serial_output_thread.join()


board = MockBoard()
Loading

0 comments on commit dab965e

Please sign in to comment.