diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f11592437a95f518602a344441532fd6d5473802..e197afc9190b5b657f7aae31968f34210fcc4b22 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,53 +1,55 @@ -# Official language image, in version 3.8(.latest) -image: python:3.8 - -# Set up a virtual environment. -before_script: - - python -m venv venv - - source venv/bin/activate - -# Run the test suite using tox. -# This job is called when commits are pushed to the main or a release branch. -test: - script: +# Shared configuration elements between test jobs. +.config: + # Official Python language image, in version 3.8(.latest) + image: python:3.8 + # Set up a virtual environment, with latest pip and tox installed. + before_script: + - python -m venv venv + - source venv/bin/activate + - pip install -U pip - pip install -U tox - - tox -e py38 + # Configure coverage export and collection. artifacts: reports: coverage_report: coverage_format: cobertura path: coverage.xml coverage: /(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/ - rules: - - if: ($CI_PIPELINE_SOURCE == "push") && - (($CI_COMMIT_BRANCH == "develop") || ($CI_COMMIT_BRANCH =~ "/^r\d\.\d+/")) + # Run on a shared Gitlab CI Runner. tags: - ci.inria.fr - - small + - medium -# Run the test suite using tox, with --fulltest option. -# This job is called on creation and pushes to non-Draft MRs. -# It may also be launched manually upon pushes to Draft MRs or main/dev branch. -test-full: +# Basic test suite: skip GPU use and a few extra integration tests scenarios. +# This job is called when commits are pushed to a non-draft MR branch. +# It may also be called manually on commits to draft MR branches. +test-minimal: + extends: + .config script: - - pip install -U tox - - tox -e py38 -- --fulltest - artifacts: - reports: - coverage_report: - coverage_format: cobertura - path: coverage.xml - coverage: /(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/ + - tox -e py38 -- --cpu-only rules: - if: ($CI_PIPELINE_SOURCE == "merge_request_event") && ($CI_MERGE_REQUEST_TITLE !~ /^Draft:.*/) - if: ($CI_PIPELINE_SOURCE == "merge_request_event") && ($CI_MERGE_REQUEST_TITLE =~ /^Draft:.*/) when: manual + allow_failure: true + +# Exhaustive test suite: lint the code, run all tests, use GPU when available. +# This job is called when commits are pushed to a main branch. +# It may also be called manually on commits to MR branches. +test-maximal: + extends: + .config + script: + - tox -e py38 -- --fulltest + rules: - if: ($CI_PIPELINE_SOURCE == "push") && - (($CI_COMMIT_BRANCH == "develop") || ($CI_COMMIT_BRANCH =~ "/^r\d\.\d+/")) + ( + ($CI_COMMIT_BRANCH == "develop") || + ($CI_COMMIT_BRANCH =~ "/^r\d\.\d+/") + ) + - if: ($CI_PIPELINE_SOURCE == "merge_request_event") when: manual - allow_failure: true # do not block the base "test" pipeline - tags: - - ci.inria.fr - - small + allow_failure: true diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh new file mode 100644 index 0000000000000000000000000000000000000000..189986b730d1ee3e75ccd723bc49697598371879 --- /dev/null +++ b/scripts/run_tests.sh @@ -0,0 +1,184 @@ +#!/bin/bash + +# Copyright 2023 Inria (Institut National de Recherche en Informatique +# et Automatique) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +: ' +Bash script to launch test pipelines for declearn. +' + +run_command(){ + : ' + Run a given command, echoing it and being verbose about success/failure. + + Return: + 1 if the command failed, 0 if it was successful. + ' + cmd=$@ + name="\e[34m$(echo $cmd | cut -d' ' -f1)\e[0m" + echo -e "\nRunning command: \e[34m$cmd\e[0m" + if $cmd; then + echo -e "$name command was \e[32msuccessful\e[0m" + return 0 + else + echo -e "$name command \e[31mfailed\e[0m" + return 1 + fi +} + + +run_commands() { + : ' + Verbosely run a set of commands and report whether all were a success. + + Syntax: + run_commands "context string" ("command")+ + + Return: + The number of sub-commands that failed (0 if successful). + ' + context=$1; shift; commands=( "$@" ) + n_cmd=${#commands[@]} + declare -i failed + echo "Running $context ($n_cmd commands)." + for ((i = 0; i < $n_cmd ; i++)); do + run_command ${commands[i]} + if [[ $? -ne 0 ]]; then failed+=1; fi + done + if [[ $failed -eq 0 ]]; then + echo -e "\n\e[32mAll $n_cmd $context commands were successful.\e[0m" + else + echo -e "\n\e[31m$failed/$n_cmd $context commands failed.\e[0m" + fi + return $failed +} + + +lint_declearn_code() { + : ' + Verbosely run linters on the declearn package source code. + + Return: + The number of sub-commands that failed (0 if successful). + ' + commands=( + "pylint declearn" + "mypy --install-types --non-interactive declearn" + "black --check declearn" + ) + run_commands "declearn code static analysis" "${commands[@]}" +} + + +lint_declearn_tests() { + : ' + Verbosely run linters on the declearn test suite source code. + + Return: + The number of sub-commands that failed (0 if successful). + ' + commands=( + "pylint --recursive=y test" + "mypy --install-types --non-interactive --exclude=conftest.py declearn" + "black --check test" + ) + run_commands "declearn test code static analysis" "${commands[@]}" +} + + +run_declearn_tests() { + : ' + Verbosely run the declearn test suite and export coverage results. + + Syntax: + run_tests [optional_pytest_flags] + + Return: + The number of sub-commands that failed (0 if successful). + ' + # Remove any pre-existing coverage file. + if [[ -f .coverage ]]; then rm .coverage; fi + # Run the various sets of tests. + commands=( + "run_unit_tests $@" + "run_integration_tests $@" + ) + run_commands "declearn test suite" "${commands[@]}" + status=$? + # Display and export the cumulated coverage. + coverage report + coverage xml + # Return the success/failure status of tests. + return $status +} + + +run_unit_tests() { + : ' + Verbosely run the declearn unit tests (excluding integration ones). + ' + echo "Running DecLearn unit tests." + command="pytest $@ + --cov --cov-append --cov-report= + --ignore=test/functional/ + test + " + run_command $command +} + + +run_integration_tests() { + : ' + Verbosely run the declearn integration tests (skipping unit ones). + ' + echo "Running DecLearn integration tests." + command="pytest $@ + --cov --cov-append --cov-report= + test/functional/ + " + run_command $command +} + + +main() { + if [[ $# -eq 0 ]]; then + echo "Missing required positional argument." + echo "Usage: {lint_code|lint_tests|run_tests} [PYTEST_FLAGS]*" + exit 1 + fi + action=$1; shift + case $action in + lc | lint_code) + lint_declearn_code + exit $? + ;; + lt | lint_tests) + lint_declearn_tests + exit $? + ;; + rt | run_tests) + run_declearn_tests "$@" + exit $? + ;; + *) + echo "Usage: {lint_code|lint_tests|run_tests} [PYTEST_FLAGS]*" + echo "Aliases for the action parameter values: {lc|lt|rt}." + exit 1 + ;; + esac +} + + +main $@ diff --git a/tox.ini b/tox.ini index 1e962ca49d9252c275ee526d2c4860cb580c25d7..a66fd5864cc019f59c9fe79f4c9dfe1728b7628a 100644 --- a/tox.ini +++ b/tox.ini @@ -2,32 +2,52 @@ envlist = py38 minversion = 3.18.0 -[testenv] -recreate = True + +[testenv:py{38,39,310,311,312}] +description = + "Default job, running all kinds of tests." extras = all,tests allowlist_externals = - openssl + bash, openssl commands= - # run unit tests first - pytest {posargs} \ - --cov --cov-report= \ # reset then accumulate coverage quietly - --ignore=test/functional/ \ - test - # verify code acceptance by pylint - pylint declearn - pylint --recursive=y test - # verify code typing using mypy - mypy --install-types --non-interactive declearn - # verify code formatting - black --check declearn - black --check test - # run functional ~ integration tests (that build on unit ones) - pytest {posargs} \ - --cov --cov-append --cov-report=term \ # acc. and display coverage - test/functional/ - # export the finalized coverage report to xml - coverage xml + bash scripts/run_tests.sh lint_code + bash scripts/run_tests.sh lint_tests + bash scripts/run_tests.sh run_tests {posargs} + + +[testenv:py{38,39,310,311,312}-tests] +description = + "Individualized job to run unit and integration tests." +extras = + all,tests +allowlist_externals = + bash, openssl +commands= + bash scripts/run_tests.sh run_tests {posargs} + + +[testenv:py{38,39,310,311,312}-lint_code] +description = + "Individualized job to run source code static analysis." +extras = + all,tests +allowlist_externals = + bash +commands = + bash scripts/run_tests.sh lint_code + + +[testenv:py{38,39,310,311,312}-lint_tests] +description = + "Individualized job to run test code static analysis." +extras = + all,tests +allowlist_externals = + bash +commands = + bash scripts/run_tests.sh lint_tests + [pytest] addopts = --full-trace