diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..e53b44de6a2e107bb4e42cb0ff64abcd4051beb5 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,115 @@ +stages: + - configure + - trigger + - downstream modules + +variables: + IMAGE_REGISTRY_URL: $CI_REGISTRY/dumux-repositories/dumux-docker-ci + +# Cases in which to create a pipeline. The `select-pipeline` job further +# specifies the situations in which they must be started manually. Currently, +# we only have automatic pipeline triggers for scheduled pipelines. +workflow: + rules: + - if: $CI_PIPELINE_SOURCE == "schedule" + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + + +################################################################################ +# Stage 1: configure the test pipeline. # +# This creates the .yml to be used for the test pipeline trigger stage. Within # +# merge request, we create a .yml file that adds a test selection stage before # +# the build stage to identify the tests affected by changes introduced in the # +# merge request. In all other cases, we use the default which runs all tests. # +################################################################################ +select-pipeline: + image: $IMAGE_REGISTRY_URL/full:dune-2.7-gcc-ubuntu-20.04 + stage: configure + script: + - | + if [ $CI_PIPELINE_SOURCE == "merge_request_event" ]; then + cp .gitlab-ci/affectedtestsonly.yml pipeline-config.yml + else + cp .gitlab-ci/default.yml pipeline-config.yml + fi + artifacts: + paths: + - pipeline-config.yml + expire_in: 3 hours + rules: + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH + when: manual + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + when: manual + + +################################################################################### +# Stage 2: trigger the Dumux test pipelines # +# In this stage, we trigger the test pipeline with different configurations, i.e. # +# different Dune versions, compilers, etc. Within merge requests, we create three # +# test pipelines including two different compilers and a full and minimal setup # +# of dependencies. In all other situations, additional test jobs are created. # +################################################################################### + +# basic trigger job to start the test pipeline +.base-trigger: + stage: trigger + needs: + - select-pipeline + trigger: + include: + - artifact: pipeline-config.yml + job: select-pipeline + strategy: depend + +# trigger for jobs that should not be created in merge requests +.non-mr-trigger: + extends: .base-trigger + rules: + - if: $CI_PIPELINE_SOURCE != "merge_request_event" + +############################################# +# pipelines to be created in merge requests # +full-dune-2.7-gcc: + extends: .base-trigger + variables: + IMAGE: $IMAGE_REGISTRY_URL/full:dune-2.7-gcc-ubuntu-20.04 + +minimal-dune-2.7-gcc: + extends: .base-trigger + variables: + IMAGE: $IMAGE_REGISTRY_URL/minimal:dune-2.7-gcc-ubuntu-20.04 + +full-dune-2.7-clang: + extends: .base-trigger + variables: + IMAGE: $IMAGE_REGISTRY_URL/full:dune-2.7-clang-ubuntu-20.04 + +################################## +# additional scheduled pipelines # +full-dune-master-gcc: + extends: .non-mr-trigger + variables: + IMAGE: $IMAGE_REGISTRY_URL/full:dune-master-gcc-ubuntu-20.04 + +full-dune-master-clang: + extends: .non-mr-trigger + variables: + IMAGE: $IMAGE_REGISTRY_URL/full:dune-master-clang-ubuntu-20.04 + + +######################################################### +# Stage 3: trigger test pipelines of downstream modules # +######################################################### + +# trigger lecture test +trigger lecture: + stage: downstream modules + trigger: + project: dumux-repositories/dumux-lecture + # TODO: use master when lecture pipeline is set up + branch: feature/test-dumux-trigger + strategy: depend + variables: + DUMUX_MERGE_REQUEST_BRANCH: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME diff --git a/.gitlab-ci/affectedtestsonly.yml b/.gitlab-ci/affectedtestsonly.yml new file mode 100644 index 0000000000000000000000000000000000000000..7387f722f4c223ff448769b8416c28b2f92e51d2 --- /dev/null +++ b/.gitlab-ci/affectedtestsonly.yml @@ -0,0 +1,54 @@ +default: + image: $IMAGE + +stages: + - configure + - build + - test + +workflow: + rules: + - if: $CI_PIPELINE_SOURCE=="parent_pipeline" + +select tests: + stage: configure + script: + - dunecontrol --opts=$DUNE_OPTS_FILE --current all + - | + pushd build-cmake + python3 ../bin/testing/findtests.py -f affectedtests.json -t origin/master + popd + artifacts: + paths: + - build-cmake + expire_in: 3 hours + +build dumux: + stage: build + script: + - | + pushd build-cmake + make clean + python3 ../bin/testing/runselectedtests.py -c affectedtests.json -b + popd + artifacts: + paths: + - build-cmake + expire_in: 3 hours + needs: + - job: select tests + artifacts: true + +test dumux: + stage: test + script: + - | + pushd build-cmake + python3 ../bin/testing/runselectedtests.py -c affectedtests.json -t + popd + needs: + - job: build dumux + artifacts: true + artifacts: + reports: + junit: junit/dumux-cmake.xml diff --git a/.gitlab-ci/default.yml b/.gitlab-ci/default.yml new file mode 100644 index 0000000000000000000000000000000000000000..2293458b024c62ae335be46a4f5d4e57166e35ab --- /dev/null +++ b/.gitlab-ci/default.yml @@ -0,0 +1,31 @@ +default: + image: $IMAGE + +stages: + - build + - test + +workflow: + rules: + - if: $CI_PIPELINE_SOURCE=="parent_pipeline" + +build dumux: + stage: build + script: + - dunecontrol --opts=$DUNE_OPTS_FILE --current all + - dunecontrol --opts=$DUNE_OPTS_FILE --current bexec make -k -j4 build_tests + artifacts: + paths: + - build-cmake + expire_in: 3 hours + +test dumux: + stage: test + script: + - dunecontrol --opts=$DUNE_OPTS_FILE --current bexec dune-ctest -j4 --output-on-failure + needs: + - job: build dumux + artifacts: true + artifacts: + reports: + junit: junit/dumux-cmake.xml diff --git a/bin/testing/findtests.py b/bin/testing/findtests.py new file mode 100755 index 0000000000000000000000000000000000000000..f92322c665944fe3240f016e8e870f22f948852a --- /dev/null +++ b/bin/testing/findtests.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 + +""" +Find those tests that are affected by changes +Run this in the build directory +Warning: This runs 'make clean' on the build directory +""" + +import json +import subprocess +from argparse import ArgumentParser +from glob import glob +from subprocess import PIPE +import os + + +# Check if the set a contains a member of list b +def hasCommonMember(myset, mylist): + return not myset.isdisjoint(mylist) + + +# make dry run and return the compilation command +def getCompileCommand(testConfig): + lines = subprocess.check_output(["make", "--dry-run", + testConfig["target"]], + encoding='ascii').splitlines() + commands = list(filter(lambda comm: 'g++' in comm, lines)) + assert len(commands) <= 1 + return commands[0] if commands else None + + +# get the command and folder to compile the given test +def buildCommandAndDir(testConfig, cache): + compCommand = getCompileCommand(testConfig) + if compCommand is None: + with open(cache) as c: + data = json.load(c) + return data["command"], data["dir"] + else: + (_, dir), command = [comm.split() for comm in compCommand.split("&&")] + with open(cache, "w") as c: + json.dump({"command": command, "dir": dir}, c) + return command, dir + + +# check if a test is affected by changes in the given files +def isAffectedTest(testConfigFile, changed_files): + with open(testConfigFile) as configFile: + testConfig = json.load(configFile) + + cacheFile = "TestTargets/" + testConfig["target"] + ".json" + command, dir = buildCommandAndDir(testConfig, cacheFile) + mainFile = command[-1] + + # detect headers included in this test + # -MM skips headers from system directories + # -H prints the name(+path) of each used header + # for some reason g++ writes to stderr + headers = subprocess.run(command + ["-MM", "-H"], + stderr=PIPE, stdout=PIPE, cwd=dir, + encoding='ascii').stderr.splitlines() + + # filter only headers from this project and turn them into relative paths + projectDir = os.path.abspath(os.getcwd().rstrip("build-cmake")) + + def isProjectHeader(headerPath): + return projectDir in headerPath + + test_files = [os.path.relpath(mainFile.lstrip(". "), projectDir)] + test_files.extend([os.path.relpath(header.lstrip(". "), projectDir) + for header in filter(isProjectHeader, headers)]) + test_files = set(test_files) + + if hasCommonMember(changed_files, test_files): + return True, testConfig["name"], testConfig["target"] + + return False, testConfig["name"], testConfig["target"] + + +if __name__ == '__main__': + + # parse input arguments + parser = ArgumentParser(description='Find tests affected by changes') + parser.add_argument('-s', '--source', required=False, default='HEAD', + help='The source tree (default: `HEAD`)') + parser.add_argument('-t', '--target', required=False, default='master', + help='The tree to compare against (default: `master`)') + parser.add_argument('-f', '--outfile', required=False, + default='affectedtests.json', + help='The file in which to write the affected tests') + args = vars(parser.parse_args()) + + # find the changes files + changed_files = subprocess.check_output(["git", "diff-tree", + "-r", "--name-only", + args['source'], args['target']], + encoding='ascii').splitlines() + changed_files = set(changed_files) + + # clean build directory + subprocess.run(["make", "clean"]) + subprocess.run(["make"]) + + # create cache folder + os.makedirs("TestTargets", exist_ok=True) + + # detect affected tests + print("Detecting affected tests:") + count = 0 + affectedTests = {} + for test in glob("TestMetaData/*json"): + affected, name, target = isAffectedTest(test, changed_files) + if affected: + print("\t- {}".format(name)) + affectedTests[name] = {'target': target} + count += 1 + print("Detected {} affected tests".format(count)) + + with open(args['outfile'], 'w') as jsonFile: + json.dump(affectedTests, jsonFile) diff --git a/bin/testing/runselectedtests.py b/bin/testing/runselectedtests.py new file mode 100755 index 0000000000000000000000000000000000000000..75cb94f2d83cf46f219eb0f406f7cbf3979d8dd7 --- /dev/null +++ b/bin/testing/runselectedtests.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 + +""" +Build and/or run (using `dune-ctest`) a selection of tests. +Run this in the top level of the build tree. +""" + +import sys +import json +import subprocess +from argparse import ArgumentParser + +# require Python 3 +if sys.version_info.major < 3: + sys.exit('Python 3 required') + + +def buildTests(config, flags=['j4']): + + if not config: + print('No tests to be built') + return + + # The MakeFile generated by cmake contains the .NOTPARALLEL statement, as + # it only allows one call to `CMakeFiles/Makefile2` at a time. Parallelism + # is taken care of within that latter Makefile. Therefore, we create a + # small custom Makefile here on top of `Makefile2`, where we define a new + # target, composed of affected tests, that can be built in parallel + with open('TestMakeFile', 'w') as makeFile: + # include make file generated by cmake + makeFile.write('include CMakeFiles/Makefile2\n') + + # define a new target composed of the test targets + makeFile.write('testselection: ') + makeFile.write(' '.join([tc['target'] for tc in config.values()])) + + subprocess.run(['make', '-f', 'TestMakeFile'] + flags + ['testselection'], + check=True) + + +def runTests(config, script='', flags=['-j4', '--output-on-failure']): + + tests = list(config.keys()) + if not tests: + print('No tests to be run. Letting dune-ctest produce empty report.') + tests = ['NOOP'] + + # if not given, try system-wide call to dune-ctest + call = ['dune-ctest'] if not script else ['./' + script.lstrip('./')] + call.extend(flags) + call.extend(['-R'] + tests) + subprocess.run(call, check=True) + + +if __name__ == '__main__': + parser = ArgumentParser(description='Build or run a selection of tests') + parser.add_argument('-a', '--all', + required=False, + action='store_true', + help='use this flag to build/run all tests') + parser.add_argument('-c', '--config', + required=False, + help='json file with configuration of tests to be run') + parser.add_argument('-s', '--script', + required=False, + default='', + help='provide the path to the dune-ctest script') + parser.add_argument('-b', '--build', + required=False, + action='store_true', + help='use this flag to build the tests') + parser.add_argument('-t', '--test', + required=False, + action='store_true', + help='use this flag to run the tests') + parser.add_argument('-bf', '--buildflags', + required=False, + default='-j4', + help='set the flags passed to make') + parser.add_argument('-tf', '--testflags', + required=False, + default='-j4 --output-on-failure', + help='set the flags passed to ctest') + args = vars(parser.parse_args()) + + if not args['build'] and not args['test']: + sys.exit('Neither `build` not `test` flag was set. Exiting.') + + if args['config'] and args['all']: + sys.exit('Error: both `config` and `all` specified. ' + 'Please set only one of these arguments.') + + # prepare build and test flags + buildFlags = args['buildflags'].split(' ') + testFlags = args['testflags'].split(' ') + + # use target `all` + if args['all']: + if args['build']: + print('Building all tests') + subprocess.run(['make'] + buildFlags + ['build_tests'], check=True) + if args['test']: + print('Running all tests') + subprocess.run(['ctest'] + testFlags, check=True) + + # use target selection + else: + with open(args['config']) as configFile: + config = json.load(configFile) + numTests = len(config) + print('{} tests found in the configuration file'.format(numTests)) + + if args['build']: + buildTests(config, buildFlags) + if args['test']: + runTests(config, args['script'], testFlags) diff --git a/cmake/modules/DumuxTestMacros.cmake b/cmake/modules/DumuxTestMacros.cmake index a892127279fe953c57d35918d94e24be0e22e11d..ba9fa59f352c4f560c0364ef294d61fd016dac1a 100644 --- a/cmake/modules/DumuxTestMacros.cmake +++ b/cmake/modules/DumuxTestMacros.cmake @@ -204,6 +204,61 @@ # future Dune features with older Dune versions supported by Dumux function(dumux_add_test) dune_add_test(${ARGV}) + + include(CMakeParseArguments) + set(OPTIONS EXPECT_COMPILE_FAIL EXPECT_FAIL SKIP_ON_77 COMPILE_ONLY) + set(SINGLEARGS NAME TARGET TIMEOUT) + set(MULTIARGS SOURCES COMPILE_DEFINITIONS COMPILE_FLAGS LINK_LIBRARIES CMD_ARGS MPI_RANKS COMMAND CMAKE_GUARD LABELS) + cmake_parse_arguments(ADDTEST "${OPTIONS}" "${SINGLEARGS}" "${MULTIARGS}" ${ARGN}) + + if(NOT ADDTEST_NAME) + # try deducing the test name from the executable name + if(ADDTEST_TARGET) + set(ADDTEST_NAME ${ADDTEST_TARGET}) + endif() + # try deducing the test name form the source name + if(ADDTEST_SOURCES) + # deducing a name is only possible with a single source argument + list(LENGTH ADDTEST_SOURCES len) + if(NOT len STREQUAL "1") + message(FATAL_ERROR "Cannot deduce test name from multiple sources!") + endif() + # strip file extension + get_filename_component(ADDTEST_NAME ${ADDTEST_SOURCES} NAME_WE) + endif() + endif() + if(NOT ADDTEST_COMMAND) + set(ADDTEST_COMMAND ${ADDTEST_NAME}) + endif() + + # Find out whether this test should be a dummy + set(SHOULD_SKIP_TEST FALSE) + set(FAILED_CONDITION_PRINTING "") + foreach(condition ${ADDTEST_CMAKE_GUARD}) + separate_arguments(condition) + if(NOT (${condition})) + set(SHOULD_SKIP_TEST TRUE) + set(FAILED_CONDITION_PRINTING "${FAILED_CONDITION_PRINTING}std::cout << \" ${condition}\" << std::endl;\n") + endif() + endforeach() + + # If we do nothing, switch the sources for a dummy source + if(SHOULD_SKIP_TEST) + dune_module_path(MODULE dune-common RESULT scriptdir SCRIPT_DIR) + set(ADDTEST_TARGET) + set(dummymain ${CMAKE_CURRENT_BINARY_DIR}/main77_${ADDTEST_NAME}.cc) + configure_file(${scriptdir}/main77.cc.in ${dummymain}) + set(ADDTEST_SOURCES ${dummymain}) + endif() + + # Add the executable if it is not already present + if(ADDTEST_SOURCES) + set(ADDTEST_TARGET ${ADDTEST_NAME}) + endif() + + file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/TestMetaData") + file(WRITE "${CMAKE_BINARY_DIR}/TestMetaData/${ADDTEST_NAME}.json" + "\{\n \"name\": \"${ADDTEST_NAME}\",\n \"target\": \"${ADDTEST_TARGET}\"\n\}\n") endfunction() # Evaluate test guards like dune_add_test internally does