diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 0000000..b7b30d7 --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,123 @@ +name: run-tests + +on: + pull_request: + branches: + - 'main' + +# The cancel-in-progress concurrency option can only be specified at the workflow or job level, and job-level concurrency is ineffective when dealing with queued workflows. +# Given that we don't want to cancel the workflow while fetching mbed-os libraries or flashing, the cancel-in-progress option is set to false to prevent canceling the +# workflow at these times. However, this still prevents multiple identical queued workflows from piling up. +concurrency: + group: ${{ github.event_name }}.${{ github.head_ref }}.${{ github.workflow }} + cancel-in-progress: false + +jobs: + setup: + runs-on: + - self-hosted + + outputs: + target: ${{ steps.get-dev-info.outputs.device }} + ser_port: ${{ steps.get-dev-info.outputs.serial }} + flash_mntpt: ${{ steps.get-dev-info.outputs.mountpoint }} + + steps: + - name: Set Environment Variables + run: | + echo "MBED_OS_REF_LOCATION=$HOME/${{ github.repository }}/mbed-os-ref" >> "$GITHUB_ENV" + + - name: Get Repo + uses: actions/checkout@v3 + with: +# Don't run `git clean -ffdx` and `git reset --hard HEAD` in this step to avoid removing `mbed-os/`. +# These will be run post checkout in the following step + clean: false + +# Clean up any changes in the working index, including untracked files aside from mbed-os/ + - name: Post Checkout Cleanup + run: | + git clean -ffdxe mbed-os + git reset --hard HEAD + +# Checks if the current reference in mbed-os.lib is the same as what is stored on the runner from the +# previous workflow. If not, update the stored reference and fetch the new libraries +# TODO This only handles mbed-os.lib. To be able to handle other .lib files, it would be worth creating +# a script or custom action to check/update all library references + - name: Check if mbed-os Libraries Need to be Fetched + id: check-mbed-os-libs + run: | + echo ${{ github.event.before }} ${{ github.event.after }} + git reflog + # Get the commit hashes of the two most recent states of the repo on the runner + # If mbed-os.lib changed between the two states, fetch the mbed libraries in the next step + hashes=$(git reflog | head -n 1 | grep -Po "(?<=moving from )\S+|(?<= to )\S+$") + echo "$hashes" + git diff --name-only $hashes + echo "SEPARATOR" + git diff $hashes + if [[ -f "$MBED_OS_REF_LOCATION/mbed-os.lib" ]]; then + if [[ -n $(diff "$MBED_OS_REF_LOCATION/mbed-os.lib" mbed-os.lib) ]]; then + cp mbed-os.lib "$MBED_OS_REF_LOCATION" + echo "fetch_libs=y" >> "$GITHUB_OUTPUT" + echo "Needed to fetch mbed-os libraries because they were not up-to-date on the runner" >> $GITHUB_STEP_SUMMARY + else + echo "fetch_libs=$([[ -d mbed-os ]] && echo n || echo y)" >> "$GITHUB_OUTPUT" + [[ -d mbed-os ]] && \ + echo "mbed-os libraries are up-to-date :relieved:" >> $GITHUB_STEP_SUMMARY || \ + echo "Needed to fetch mbed-os libraries because mbed-os did not exist on the runner, but the stored library reference was up-to-date :finnadie:" >> $GITHUB_STEP_SUMMARY + fi + else + mkdir -p "$MBED_OS_REF_LOCATION" + cp mbed-os.lib "$MBED_OS_REF_LOCATION" + echo "fetch_libs=$([[ -d mbed-os ]] && echo n || echo y)" >> "$GITHUB_OUTPUT" + echo "Needed to fetch mbed-os libraries because neither mbed-os nor the stored reference was present on the runner :point_right::point_left:" >> $GITHUB_STEP_SUMMARY + fi + + - name: Fetch Mbed Libraries + if: steps.check-mbed-os-libs.outputs.fetch_libs == 'y' + run: | + if [[ -d mbed-os ]]; then + echo "Removing mbed-os before fetching libraries" + rm -rf mbed-os + fi + mbed-tools deploy + +# Gets device info using `mbed-tools detect` and sets output needed for compiling and flashing +# NOTE: Expecting '/mnt/$USER/' to be the base path for the mount point, as specified in the runner's ldm (https://github.com/LemonBoy/ldm) configuration, +# despite /media/[$USER/] typically being the base path for removable media + - name: Get Device Info + id: get-dev-info + run: | + device=$(python3 parseConfigs.py) + detect_out=$(mbed-tools detect | grep "$device") + #device=$(echo "$detect_out" | grep -oE "NUCLEO_\\S*") + serial=$(echo "$detect_out" | grep -oE "/dev/tty\\S*") + mountpoint=$(echo "$detect_out" | grep -oE "/mnt/$USER/\\S*") + if [[ -z "$serial" || -z "$mountpoint" ]]; then + echo "Failed to detect serial port and/or mount point matching '/dev/tty*' and '/mnt/$USER/*', respectively, for target '$device' :hurtrealbad:" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "device=$device" >> "$GITHUB_OUTPUT" + echo "serial=$serial" >> "$GITHUB_OUTPUT" + echo "mountpoint=$mountpoint" >> "$GITHUB_OUTPUT" + echo "Found target '$device' connected to serial port '$serial' with storage mounted to '$mountpoint' :relieved:" >> $GITHUB_STEP_SUMMARY + + compile-and-flash: + needs: setup + env: + BUILD_PROFILE: develop + TOOLCHAIN: GCC_ARM + runs-on: + - self-hosted + + steps: + - name: Compile and Flash + if: false # TODO Remove + run: | + mbed-tools configure -t ${{ env.TOOLCHAIN }} -m ${{ needs.setup.outputs.target }} + cmake -S . -B cmake_build/${{ needs.setup.outputs.target }}/${{ env.BUILD_PROFILE }}/${{ env.TOOLCHAIN }} -GNinja + cmake --build cmake_build/${{ needs.setup.outputs.target }}/${{ env.BUILD_PROFILE }}/${{ env.TOOLCHAIN }} + cp cmake_build/${{ needs.setup.outputs.target }}/${{ env.BUILD_PROFILE }}/${{ env.TOOLCHAIN }}/embedded-mbed.bin ${{ needs.setup.outputs.flash_mntpt }} + echo "Successfully flashed to ${{ needs.setup.outputs.target }} :v:" >> $GITHUB_STEP_SUMMARY + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0e79ef1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.build +.mbed +BUILD +cmake_build +mbed-os diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..5ccd950 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,33 @@ +cmake_minimum_required(VERSION 3.19.0 FATAL_ERROR) + +set(MBED_PATH ${CMAKE_CURRENT_SOURCE_DIR}/mbed-os CACHE INTERNAL "") +set(MBED_CONFIG_PATH ${CMAKE_CURRENT_BINARY_DIR} CACHE INTERNAL "") +set(APP_TARGET embedded-mbed) + +# --- START MACROS --- +# ---- END MACROS ---- + +include(${MBED_PATH}/tools/cmake/app.cmake) + +project(${APP_TARGET}) + +add_subdirectory(${MBED_PATH}) + +add_executable(${APP_TARGET} testRunner.cpp) + +target_sources(${APP_TARGET} + PRIVATE + testRunner.cpp +) + +target_link_libraries(${APP_TARGET} + PRIVATE + mbed-os +) + +mbed_set_post_build(${APP_TARGET}) + +option(VERBOSE_BUILD "Have a verbose build process") +if(VERBOSE_BUILD) + set(CMAKE_VERBOSE_MAKEFILE ON) +endif() diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0b148ef --- /dev/null +++ b/Makefile @@ -0,0 +1,58 @@ +# Allow the user to specify different values for 'PROFILE' and 'TOOLCHAIN' +PROFILE?=develop +TOOLCHAIN?=GCC_ARM + +# Useless change + +# Parse setup.yml to determine the build target +TARGET := $(shell grep -Po '(^\s+target:\s+)\K.+' setup.yml) +# Check that a value for 'target' was found in setup.yml +ifndef TARGET +$(error Did not find a value for 'target' in setup.yml) +endif +BUILD_DIR := cmake_build/$(TARGET)/$(PROFILE)/$(TOOLCHAIN) +BINARY := embedded-mbed.bin +START_MACROS := ^\# --- START MACROS ---$$ +END_MACROS := ^\# ---- END MACROS ----$$ +# Search for files matching *.{cpp,h}, but don't search mbed-os/ or cmake_build/ +SOURCES := $(shell find . -type d \( -path ./mbed-os -o -path ./cmake_build \) -prune -o -type f -regex '.*\.\(cpp\|h\)' -print) + + +.PHONY: flash +flash: mountpoint compile + cp $(BUILD_DIR)/$(BINARY) $(MOUNTPOINT) + +.PHONY: compile +compile: $(BUILD_DIR)/$(BINARY) + +$(BUILD_DIR)/$(BINARY): $(BUILD_DIR)/mbed_config.cmake setup.yml tests.yml CMakeLists.txt parseConfigs.py $(SOURCES) + ./parseConfigs.py + cmake -S . -B $(BUILD_DIR) -GNinja + cmake --build $(BUILD_DIR) + +.PHONY: configure +configure: $(BUILD_DIR)/mbed_config.cmake + +$(BUILD_DIR)/mbed_config.cmake: mbed-os mbed_app.json + mbed-tools configure -t $(TOOLCHAIN) -m $(TARGET) + +.PHONY: deploy +deploy: mbed-os + +mbed-os: mbed-os.lib + mbed-tools deploy + +# Check that MOUNTPOINT was defined as a CLA +.PHONY: mountpoint +mountpoint: +ifndef MOUNTPOINT + $(error variable 'MOUNTPOINT' not set) +endif + +.PHONY: clean +clean: clean-cmakelists + rm -rf cmake_build/ mbed-os/ + +.PHONY: clean-cmakelists +clean-cmakelists: + sed -i '/$(START_MACROS)/,/$(END_MACROS)/{/$(START_MACROS)/!{/$(END_MACROS)/!d}}' CMakeLists.txt diff --git a/install-pre-commit.sh b/install-pre-commit.sh new file mode 100644 index 0000000..e3540ed --- /dev/null +++ b/install-pre-commit.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +dir=$(dirname "${BASH_SOURCE[0]}") + +chmod +x $dir/pre-commit +cp $dir/pre-commit $dir/.git/hooks/ + +echo "Copied $dir/pre-commit to $dir/.git/hooks/" diff --git a/mbed-os.lib b/mbed-os.lib new file mode 100644 index 0000000..144619b --- /dev/null +++ b/mbed-os.lib @@ -0,0 +1 @@ +https://github.com/ARMmbed/mbed-os/#17dc3dc2e6e2817a8bd3df62f38583319f0e4fed diff --git a/mbed_app.json b/mbed_app.json new file mode 100644 index 0000000..14ca336 --- /dev/null +++ b/mbed_app.json @@ -0,0 +1,8 @@ +{ + "target_overrides": { + "*": { + "target.printf_lib": "std", + "target.c_lib": "small" + } + } +} diff --git a/parseConfigs.py b/parseConfigs.py new file mode 100755 index 0000000..fdea854 --- /dev/null +++ b/parseConfigs.py @@ -0,0 +1,68 @@ +#!/bin/python3 + +import yaml + + +# Get necessary information from configuration files and CMakeLists.txt +# Additionally, clear existing macros between start and end markers in CMakeLists.txt +with open('tests.yml', 'r') as testFile, open('setup.yml', 'r') as setupFile, open('CMakeLists.txt', 'r') as cmakeFile: + testCfg = yaml.safe_load(testFile) + setupCfg = yaml.safe_load(setupFile) + initCmake = cmakeFile.readlines() + + # Iterate through CMakeLists.txt lines and + # - Add all lines to cmake, except for those between the start and end macros markers + # - Store the line number of the start macros marker + cmake = [] + keep = True + for lnum, line in enumerate(initCmake): + if keep: + cmake.append(line) + if line == '# --- START MACROS ---\n': + lineNum = lnum + 1 + keep = False + elif line == '# ---- END MACROS ----\n': + cmake.append(line) + keep = True + + +# Get target and ensure that it is consistent across the configuration files +target = setupCfg['setup']['target'] +assert target == list(testCfg.keys())[0], "Target in setup.yml ({}) does not match the root key in tests.yml ({})".format(target, list(testCfg.keys())[0]) + + +# Insert definitions for devices and drivers macros +# These macros will be used throughout the project whenever test setup-related info is needed +for setupCat in ['devices', 'drivers']: + if isinstance(setupCfg['setup'][setupCat], dict): + cmake.insert(lineNum, '\n# Define macros for {}\n'.format(setupCat)) + lineNum += 1 + for setup, setupInfo in setupCfg['setup'][setupCat].items(): + for attr, val in setupInfo.items(): + # If val is a list, make it a string and reformat it as a C array (i.e. {...} instead of [...]) + if isinstance(val, list): + val = '{' + str(val)[1:-1] + '}' + cmake.insert(lineNum, 'ADD_DEFINITIONS(-D{}_{}={})\n'.format(setup.upper(), attr.upper(), val)) + lineNum += 1 + +# Insert definitions for test macros +# These macros will be used in testRunner.cpp to determine what tests should be run +if isinstance(testCfg[target], dict): + cmake.insert(lineNum, '\n# Define macros for tests\n') + lineNum += 1 + for testCat, tests in testCfg[target].items(): + for test, testType in tests.items(): + if testType != 'disabled': + cmake.insert(lineNum, 'ADD_DEFINITIONS(-D{}_{})\n'.format(testCat.upper(), test.upper())) + lineNum += 1 + +# Add empty line before end macros marker for styling purposes +cmake.insert(lineNum, '\n') + +# Replace the contents of CMakeLists.txt with the modified lines +with open('CMakeLists.txt', 'w') as cmakeFile: + cmakeFile.writelines(cmake) + + +# Output the target so that it can be used to check target availability in the workflow +print(target) diff --git a/pre-commit b/pre-commit new file mode 100644 index 0000000..b72095d --- /dev/null +++ b/pre-commit @@ -0,0 +1,20 @@ +#!/bin/bash + +# If there are no changes to CMakeLists.txt being committed, then there's no need to check this commit's diff +if [[ -z $(git diff --cached --name-only CMakeLists.txt) ]]; then + exit 0 +fi + +# Store a diff with every line of CMakeLists.txt in the context to determine if there were any changes between the markers +diff=$(git diff --cached -U$(wc -l CMakeLists.txt) CMakeLists.txt) +grep -Pzo " # --- START MACROS ---\n # ---- END MACROS ----\n" <<< "$diff" 2>/dev/null + +# Strict check to ensure that each marker is still in the proper location and only appears once +if [ $(grep -E "^.# --- START MACROS ---$" <<< "$diff" 2>/dev/null | wc -l) != 1 ] || \ + [ $(grep -E "^.# ---- END MACROS ----$" <<< "$diff" 2>/dev/null | wc -l) != 1 ] || \ + [ $(grep -Pzo " # --- START MACROS ---\n # ---- END MACROS ----\n" <<< "$diff" 2>/dev/null | wc -l) != 2 ] +then + echo -e "\033[1;31mIllegal Modification:\n \033[0;31mAttempted to change lines related to start and end macro definition markers in CMakeLists.txt\033[0m\n\n" + git diff --cached CMakeLists.txt + exit 1 +fi diff --git a/setup.yml b/setup.yml new file mode 100644 index 0000000..93afbff --- /dev/null +++ b/setup.yml @@ -0,0 +1,32 @@ +# --- Structure --- +# setup: +# target: +# devices: +# : +# : +# drivers: +# : +# : +# +# NOTE: If is an array, format it as a Python list (i.e. [...]) + +setup: + target: NUCLEO_F767ZI + devices: + i2c_generic: + address: '0x10' + ina219: + address: '0x24' + tcp_server: + address: '"192.168.1.16"' + port: 4001 + tcp_client: + address: '"192.168.1.15"' + port: 4001 + drivers: + i2c: + data: PF_0 + clock: PF_1 + ethernet: + netmask: '"255.255.255.0"' + gateway: '"0.0.0.0"' diff --git a/testRunner.cpp b/testRunner.cpp new file mode 100644 index 0000000..a67ccc5 --- /dev/null +++ b/testRunner.cpp @@ -0,0 +1,8 @@ +#include "mbed.h" + +int main() +{ + printf("Hello World!\n"); + + while(1) {} +} diff --git a/tests.yml b/tests.yml new file mode 100644 index 0000000..5b665aa --- /dev/null +++ b/tests.yml @@ -0,0 +1,17 @@ +# --- Structure --- +# : +# : +# : +# +# --- Test Options --- +# mock: Raspberry Pi emulates the device through software +# hardware: Hardware setup connected to the UUT. The Raspberry Pi does not mock the device +# disabled: Test will not be run + +NUCLEO_F767ZI: + i2c: + generic: mock + ina219: mock + ethernet: + ping_tcp_server: mock + ping_tcp_client: mock