diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 92a2f30a..8a248a96 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - name: Shell script static analysis - run: shellcheck bin/fetch-configlet bin/verify-unity-version bin/check-unitybegin bin/run-tests format.sh + run: shellcheck bin/fetch-configlet bin/verify-unity-version bin/check-unitybegin format.sh - name: Check concept exercises formatting run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dc5e39ce..7b627b6c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,9 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - name: Determine number of available hardware threads + run: echo "NUM_THREADS=$(nproc || sysctl -n hw.ncpu)" >> $GITHUB_ENV - name: Test Exercises env: CC: ${{ matrix.compiler }} - run: ./bin/run-tests -a + run: make -j ${{ env.NUM_THREADS }} diff --git a/bin/run-tests b/bin/run-tests deleted file mode 100755 index c133db42..00000000 --- a/bin/run-tests +++ /dev/null @@ -1,150 +0,0 @@ -#!/usr/bin/env bash -set -eu - -usage="run-test - runs exercise tests -Usage: - run-tests [OPTIONS] [-e ] - -Options: - - -c Look in concept exercises, if no exercise specified all concept exercises will be tested - -p Look practice exercises, if no exercise specified all practice exercises will be tested - -e A specific exercise to test - -a Test all concept and practice exercises - -h Print this help" - -help () { - # Display help - echo "${usage}" -} - -execute_test () { - if [ -d "${1}" ]; then - ( - cd "${1}" - # Get the exercise name from the directory - exercise_name=$(echo "$1" | tr '-' '_') - - echo "Running tests for ${exercise_name}"; - - #remove the ignore line - sed -i='' 's/TEST_IGNORE();//' ./test_*.c - - # Copy the examples with the correct name for the exercise - if [ -e ".meta/example.c" ] - then - mv .meta/example.c ./"${exercise_name}".c - fi - - if [ -e ".meta/example.h" ] - then - mv .meta/example.h ./"${exercise_name}".h - fi - - # Make it! - make clean >> /dev/null; - make memcheck - echo "" - ) - fi -} - -copy_exercises () { - # Copies exercises from a given directory in ./exercises to the same - # directory in ./build - if [ $# -gt 0 ] && [ -d "exercises/${1}" ] - then - local directory="${1}" - shift - mkdir -p "build/${directory}" - if [ $# -gt 0 ] - then - echo "Copying ${directory} exercises ${*}" - cd "exercises/${directory}" - cp -r "$@" "../../build/${directory}/" - cd "../.." - else - echo "Copying all ${directory} exercises" - cp -r "exercises/${directory}" "build" - fi - fi -} - -execute_tests () { - # Executes all exercises in the given directory - if [ $# -gt 0 ] && [ -d "${1}" ] - then - pushd "${1}" > /dev/null - for exercise in * - do - execute_test "${exercise}" - done - popd > /dev/null - fi -} - -# Move to the root directory of the repo so you can run this script from anywhere -script_dir="$( cd "$( dirname "$0" )" >/dev/null 2>&1 && pwd )" -cd "${script_dir}"/.. - -# Clear up any previous run -rm -rf build - -concept="false" -practice="false" -exercise=() - -# Handle arguments -while getopts "acpe:h" option -do - case "${option}" in - a) - concept="true" - practice="true" - break;; - c) - concept="true";; - p) - practice="true";; - e) - exercise+=("${OPTARG}");; - h) - help - exit;; - *) - exit 1;; - esac -done - -# Handle a lack of arguments -if [ $OPTIND -eq 1 ] -then - echo "$0: no arguments were passed" - exit 1 -fi - -# Decide which exercises to copy the build dir -# Copying to a new avoids polluting the track exercises -if [ "$concept" == "true" ] -then - if [ "$practice" != "true" ] && [ ${#exercise[@]} -ne 0 ] - then - copy_exercises "concept" "${exercise[@]}" - else - copy_exercises "concept" - fi -fi - -if [ "$practice" == "true" ] -then - if [ "$concept" != "true" ] && [ ${#exercise[@]} -ne 0 ] - then - copy_exercises "practice" "${exercise[@]}" - else - copy_exercises "practice" - fi -fi - -# Execute any copied exercises -execute_tests "build/concept" -execute_tests "build/practice" diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 6dc1ada2..cf189fe9 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -105,7 +105,8 @@ The structure of en exercise directory is as follows (note the differing hyphen These are both skipped by the `exercism` CLI when downloading to the client, so it is imperative that you do not reference the names of the files in your code. If you need to provide a header file example that is necessary to run your tests it should be named `{my_exercise}.h` instead. Please also use [include guards][] in your header files. - The exercise tests can be run using the [`bin/run-tests`][run-tests] script which will rename the `example.{c|h}` files accordingly. + The exercise tests can be run using `make` from the repository root. + The top-level makefile will rename the `example.{c|h}` files accordingly (use `make help` to learn about individual targets). * `makefile` - is the makefile for the exercise as it would build using proper filenames (i.e. `{exercise_name}.c` and `{exercise_name}.h` instead of `example.c` and `example.h` respectively). Makefiles are expected to change very little between exercises so it should be easy to copy one from another exercise. * `README.md` - is the readme that relates to the exercise. @@ -142,7 +143,7 @@ If you would like the [`/format`][format-workflow] automated action to work corr * [Lychee link checker][lychee] action * `configlet.yml` fetches the latest version of configlet from which it then runs the `lint` command on the track * `format-code.yml` checks for the string `/format` within any comment on a PR, if it finds it then `format.sh` is run on the exercises and any resulting changes are committed. A deploy key is required for the commit to be able to re-trigger CI. The deploy key is administered by Exercism directly. -* `build.yml` runs the `./bin/run-tests` tool on all exercises +* `test.yml` runs `make` in the repository root to test all exercises ### The Tools @@ -160,7 +161,6 @@ The work the tools in this directory perform is described as follows: ``` * `fetch-configlet` fetches the `configlet` tool from its [repository][configlet]. -* `run-tests` loops through each exercise, prepares the exercise for building and then builds it using `make`, runs the unit tests and then checks it for memory leaks with AddressSanitizer. ### Run Tools Locally @@ -168,13 +168,13 @@ You can also run individual tools on your own machine before committing. Firstly make sure you have the necessary applications installed (such as `clang-format`, [`git`][git], [`sed`][sed], [`make`][make] and a C compiler), and then run the required tool from the repository root. For example: ```bash -~/git/c$ ./bin/run-tests +~/git/c$ make ``` -If you'd like to run only some of the tests to check your work, you can specify them as arguments to the run-tests script. +If you'd like to run only some of the tests to check your work, you can specify them as targets to make. ```bash -~/git/c$ ./bin/run-tests -p -e acronym -e all-your-base -e allergies +~/git/c$ make acronym all-your-base allergies ``` ## Test Runner @@ -207,7 +207,6 @@ Read more about [test runners]. [versions]: ./VERSIONS.md [test-file-layout]: ./C_STYLE_GUIDE.md#test-file-layout [include guards]: https://en.wikipedia.org/wiki/Include_guard -[run-tests]: ../bin/run-tests [configlet]: https://github.com/exercism/configlet [configlet releases page]: https://github.com/exercism/configlet/releases [hosted runners]: https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners diff --git a/makefile b/makefile new file mode 100644 index 00000000..4405cfb0 --- /dev/null +++ b/makefile @@ -0,0 +1,129 @@ +# This makefile creates a target to build and test each exercise using the provided example +# implementation. The exercise target depends on a list of other targets that +# 1) Copy the main test file and adjust it to include all tests, +# 2) Copy the example implementation so that it is used, +# 3) Copy the makefile and unittest framework, +# 4) Build and test the exercise. +# +# Use `make ` to build and test a specific exercise. Simply running `make` builds and +# tests all available exercises. + + +# Macro to create the rules for one exercise. +# Arguments: +# $(1) - slug +# $(2) - slug with dashes replaced by underscores +# $(3) - type of exercise: 'practice' or 'concept' +# $(4) - name of test implementation: 'example' or 'exemplar' +define setup_exercise + +# Copy the test file and removes TEST_IGNORE +build/exercises/$(3)/$(1)/test_$(2).c: exercises/$(3)/$(1)/test_$(2).c + @mkdir -p $$(dir $$@) + @sed 's#TEST_IGNORE();#// &#' $$< > $$@ + +# Copy example/exemplar implementation +build/exercises/$(3)/$(1)/$(2).c: exercises/$(3)/$(1)/.meta/$(4).c + @mkdir -p $$(dir $$@) + @cp $$< $$@ + +build/exercises/$(3)/$(1)/$(2).h: $$(wildcard exercises/$(3)/$(1)/.meta/$(4).h exercises/$(3)/$(1)/*.h) + @# Copy all .h files in the exercises directory + @cp exercises/$(3)/$(1)/*.h build/exercises/$(3)/$(1) || true + @# If an example.h/exemplar.h file exists in .meta, replace slug.h with that one + @if [ -e exercises/$(3)/$(1)/.meta/$(4).h ]; then \ + cp exercises/$(3)/$(1)/.meta/$(4).h build/exercises/$(3)/$(1)/$(2).h; \ + fi + +# Copy Makefile +build/exercises/$(3)/$(1)/makefile: exercises/$(3)/$(1)/makefile + @mkdir -p $$(dir $$@) + @cp $$< $$@ + +# Copy the test framework +build/exercises/$(3)/$(1)/test-framework: $$(wildcard exercises/$(3)/$(1)/test-framework/*) + @mkdir -p $$@ + @cp exercises/$(3)/$(1)/test-framework/* build/exercises/$(3)/$(1)/test-framework/ + +INPUT_FILE_TARGETS = \ + build/exercises/$(3)/$(1)/test_$(2).c \ + build/exercises/$(3)/$(1)/$(2).c \ + build/exercises/$(3)/$(1)/$(2).h \ + build/exercises/$(3)/$(1)/makefile \ + build/exercises/$(3)/$(1)/test-framework + +# Build the exercise. +build/exercises/$(3)/$(1)/tests.out: $$(INPUT_FILE_TARGETS) + $$(MAKE) -C build/exercises/$(3)/$(1) tests.out + +# Run the exercise's test binary and create a stamp file if all tests pass +build/exercises/$(3)/$(1)/tests-passed.stamp: build/exercises/$(3)/$(1)/tests.out + @rm -f build/exercises/$(3)/$(1)/tests-passed.stamp + @build/exercises/$(3)/$(1)/tests.out && touch build/exercises/$(3)/$(1)/tests-passed.stamp + +# Build and run the memcheck variant +# This is only a single target, since an exercise's makefile builds and runs memcheck as a single target +build/exercises/$(3)/$(1)/memcheck-passed.stamp: $$(INPUT_FILE_TARGETS) + @rm -f build/exercises/$(3)/$(1)/memcheck-passed.stamp + $$(MAKE) -C build/exercises/$(3)/$(1) memcheck && touch build/exercises/$(3)/$(1)/memcheck-passed.stamp + +# Top-level target for an exercise. It depends on the stamp files for the actual tests and the memcheck +# run. Only when both stamp files are present will this target be considered "done", so as long as there +# are some errors, building that target will trigger a re-run of the tests or memcheck binary. +.PHONY: $(1) +$(1): build/exercises/$(3)/$(1)/tests-passed.stamp build/exercises/$(3)/$(1)/memcheck-passed.stamp + +# Remove all artifacts of the exercise +.PHONY: $(1).clean +$(1).clean: $$(INPUT_FILE_TARGETS) + rm -rf build/exercises/$(3)/$(1) + +endef + +PRACTICE_EXERCISES := $(notdir $(wildcard exercises/practice/*)) +CONCEPT_EXERCISES := $(notdir $(wildcard exercises/concept/*)) + +all: practice concept + +.PHONY: practice +practice: $(PRACTICE_EXERCISES) + @if [ -z "$(PRACTICE_EXERCISES)" ]; then \ + echo "No practice exercises found."; \ + fi + +.PHONY: concept +concept: $(CONCEPT_EXERCISES) + @if [ -z "$(CONCEPT_EXERCISES)" ]; then \ + echo "No concept exercises found."; \ + fi + +# Instantiate the macro for each practice exercise to create targets for each exercise. +$(foreach exercise,$(PRACTICE_EXERCISES),$(eval $(call setup_exercise,$(exercise),$(subst -,_,$(exercise)),practice,example))) +$(foreach exercise,$(CONCEPT_EXERCISES),$(eval $(call setup_exercise,$(exercise),$(subst -,_,$(exercise)),concept,exemplar))) + +.PHONY: list-practice +list-practice: + @for exercise in $(PRACTICE_EXERCISES); do \ + echo "$$exercise"; \ + done + +.PHONY: list-concept +list-concept: + @for exercise in $(CONCEPT_EXERCISES); do \ + echo "$$exercise"; \ + done + +.PHONY: clean +clean: + rm -rf build + +.PHONY: help +help: + @echo "Available targets:" + @echo " all - Build and test all practice exercises (default)" + @echo " - Build and test a specific exercise given by its slug" + @echo " clean - Remove all build artifacts" + @echo " .clean - Remove build artifacts of a specific exercise given by its slug" + @echo " list-practice - List all practice exercises" + @echo " list-concept - List all concept exercises" + @echo " help - Show this help message"