diff --git a/.gitignore b/.gitignore index 73d283f54728d3f913dd73f1653169f8603b98bd..36e1a40a0e9ca088e02560042f57c841ea89ef19 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,7 @@ tools/ess-env.conf .vscode require-ess/require.Makefile require-ess/Db/*.db -require-ess/Db/*.d \ No newline at end of file +require-ess/Db/*.d +cellMods +*.log +__pycache__ \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8bfd42d2ea2a631e741be16f944a020d8bae850b..a54104a0bec649991e26645314caf525b55afd5d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -38,7 +38,6 @@ shfmt: build require: stage: build - when: always before_script: - curl -L -o epics.tar.gz https://artifactory.esss.lu.se/artifactory/e3/epics-base-$E3_CI_BASE_VERSION.tar.gz - tar -zxvf epics.tar.gz @@ -54,10 +53,13 @@ build require: artifacts: paths: - epics + - configure/*.local test require: stage: test before_script: - source $(pwd)/epics/base-*/require/*/bin/setE3Env.bash script: - - echo exit | iocsh.bash \ No newline at end of file + - make test + needs: + - build require diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..bb60d25257a6cfd559bdbfb6f52c1040d9c49dd5 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,19 @@ +repos: + - repo: https://github.com/ambv/black + rev: 21.6b0 + hooks: + - id: black + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.3.0 + hooks: + - id: flake8 + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.0.1 + hooks: + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/timothycrosley/isort + rev: 5.9.1 + hooks: + - id: isort + args: ["--profile", "black"] diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a69614fad21acbb7f90fc2175bd51410f0fb491..e6286331288610624503a2bb6a011314011ebfe1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Removed `iocsh_gdb.bash` and `iocsh_nice.bash`, both of whose functionality can be called via `iocsh.bash -dg` and `iocsh.bash -n`, respectively. * Patch file location has been change from `patch/Site/$VERSION-description.p0.patch` to `patch/Site/$VERSION/description.p0.patch` * Require will automatically build `.template` and `.substitutions` files into the common build directory instead of into the source Db path +* Rudimentary testing has been added: +* * Tests that the correct version is loaded +* * Tests that elementary patching/building works as expected ### Bugfixes * `iocsh.bash --help` (and variants) no longer loads tries to load `env.sh`. @@ -90,7 +93,7 @@ A second major change (mostly via bugfixes) is that the local install command, ` the old -loc is still supported, but will be deprecated in a future release. * Added a `prebuild` target that runs before build so module developers can run specific code before the build process. * A module developer can now install dbd files separate from the module dbd file by using `DBD_INSTALLS += file.dbd`. - + ### Bugfixes * Ensures that lowercase module names are enforced consistently * Vendor libraries are only installed at install time, not at build time diff --git a/configure/CONFIG b/configure/CONFIG index d747a382e60dadbc5f0dbe3563f3333bcc147dba..9756166a880a341f3e99bcc312e84753026cf688 100644 --- a/configure/CONFIG +++ b/configure/CONFIG @@ -21,10 +21,12 @@ endif include $(EPICS_BASE)/configure/CONFIG_BASE_VERSION include $(TOP)/configure/E3/CONFIG_REQUIRE +include $(TOP)/configure/E3/CONFIG_SHELL include $(TOP)/configure/E3/CONFIG_E3_PATH include $(TOP)/configure/E3/CONFIG_E3_MAKEFILE include $(TOP)/configure/E3/CONFIG_EPICS include $(TOP)/configure/E3/CONFIG_EXPORT +include $(TOP)/configure/E3/CONFIG_TEST VARS_EXCLUDES+=EPICS_VERSION diff --git a/configure/E3/CONFIG_SHELL b/configure/E3/CONFIG_SHELL new file mode 100644 index 0000000000000000000000000000000000000000..22770a9bd631115762d404101ec6d8a1390a9909 --- /dev/null +++ b/configure/E3/CONFIG_SHELL @@ -0,0 +1,6 @@ + +ifdef DEBUG_SHELL + SHELL = /bin/sh -x +else + SHELL = /usr/bin/bash +endif diff --git a/configure/E3/CONFIG_TEST b/configure/E3/CONFIG_TEST new file mode 100644 index 0000000000000000000000000000000000000000..4b27c89ce95016ffa5bc114f390b55f8ebaf68db --- /dev/null +++ b/configure/E3/CONFIG_TEST @@ -0,0 +1 @@ +TEST_DIR:=$(TOP)/tests diff --git a/configure/E3/RULES_TEST b/configure/E3/RULES_TEST new file mode 100644 index 0000000000000000000000000000000000000000..5a6e2b8473f663b14bfa544ef6867e3f46f1b39e --- /dev/null +++ b/configure/E3/RULES_TEST @@ -0,0 +1,4 @@ +.PHONY: test + +test: + pytest $(TEST_DIR) -v diff --git a/configure/RULES b/configure/RULES index 02026e3d3d129598e499127cd7faa84db928adc1..49e2bba9ac5185131d91e86a39aee2e2fe70f329 100644 --- a/configure/RULES +++ b/configure/RULES @@ -15,6 +15,7 @@ include $(TOP)/configure/E3/RULES_REQUIRE include $(TOP)/configure/E3/RULES_PATCH include $(TOP)/configure/E3/RULES_DB include $(TOP)/configure/E3/RULES_VARS +include $(TOP)/configure/E3/RULES_TEST ifneq (,$(findstring dev,$(MAKECMDGOALS))) diff --git a/require-ess/tools/driver.makefile b/require-ess/tools/driver.makefile index 7ee92663acd4f0592f49b87922942982d830ed12..bf7574edc55fcdfa6e6d7de8b5d6178367dcffbc 100644 --- a/require-ess/tools/driver.makefile +++ b/require-ess/tools/driver.makefile @@ -44,7 +44,7 @@ # Name of the built module. # If not defined, it is derived from the directory name. # SOURCES -# All source files to compile. +# All source files to compile. # If not defined, default is all *.c *.cc *.cpp *.st *.stt in # the source directory (where you run make). # If you define this, you must list ALL sources. @@ -66,7 +66,7 @@ USERMAKEFILE:=$(lastword $(filter-out $(lastword ${MAKEFILE_LIST}), ${MAKEFILE_L ##---## In E3, We only use ONE EPICS_BASE in order to COMPILE A MODULE -##---## +##---## ##---## In E3, EPICS_LOCATION is the EPICS BASE /testing/epics/base-MAJ.MIN.REV[.PATCH] EPICS_LOCATION = ##---## In E3, we extract BASE_VERSION from EPICS_LOCATION @@ -74,10 +74,10 @@ E3_EPICS_VERSION:=$(patsubst base-%,%,$(notdir $(EPICS_LOCATION))) E3_SITEMODS_PATH = E3_SITEAPPS_PATH = BUILD_EPICS_VERSIONS = $(E3_EPICS_VERSION) -##---## +##---## BUILDCLASSES = Linux -EPICS_MODULES = +EPICS_MODULES = MODULE_LOCATION =${EPICS_MODULES}/$(or ${PRJ},$(error PRJ not defined))/$(or ${LIBVERSION},$(error LIBVERSION not defined)) @@ -276,14 +276,14 @@ endef $(eval $(call INSTALL_UI_RULE,QT,${CONFIGBASE}/qt,qt/*)) else # EPICSVERSION -# EPICSVERSION defined +# EPICSVERSION defined # Second or third run (see T_A branch below) EPICS_BASE=${EPICS_LOCATION} CONFIG=${EPICS_BASE}/configure -# There is no 64 bit support before 3.14.12 +# There is no 64 bit support before 3.14.12 ifneq ($(filter %_64,$(EPICS_HOST_ARCH)),) ifeq ($(wildcard $(EPICS_BASE)/lib/$(EPICS_HOST_ARCH)),) EPICS_HOST_ARCH:=$(patsubst %_64,%,$(EPICS_HOST_ARCH)) @@ -405,7 +405,7 @@ uninstall:: debug:: @echo "EPICS_BASE = ${EPICS_BASE}" - @echo "EPICSVERSION = ${EPICSVERSION}" + @echo "EPICSVERSION = ${EPICSVERSION}" @echo "CROSS_COMPILER_TARGET_ARCHS = ${CROSS_COMPILER_TARGET_ARCHS}" @echo "EXCLUDE_ARCHS = ${EXCLUDE_ARCHS}" @echo "LIBVERSION = ${LIBVERSION}" @@ -480,13 +480,13 @@ export VLIBS export CFG -# These variables are written into a .yaml file in the installed module directory to keep track of +# These variables are written into a .yaml file in the installed module directory to keep track of # metadata for which module was compiled. -${PRJ}_GIT_DESC := $(shell git describe --tags 2> /dev/null || git rev-parse HEAD) +${PRJ}_GIT_DESC := $(shell git describe --tags 2> /dev/null || git rev-parse HEAD 2> /dev/null) export ${PRJ}_GIT_DESC # The formatting here is just to make sure this is properly parseable .yaml data -${PRJ}_GIT_STATUS := [ $(shell git status --porcelain | grep -v "\.Makefile" | sed 's/^/\\\"/' | sed 's/$$/\\\", /')] +${PRJ}_GIT_STATUS := [ $(shell git status --porcelain 2> /dev/null | grep -v "\.Makefile" | sed 's/^/\\\"/' | sed 's/$$/\\\", /')] export ${PRJ}_GIT_STATUS else # in O.* @@ -507,7 +507,7 @@ EPICS_INCLUDES = # Add include directory of foreign modules to include file search path. # # The default behaviour is to start with <module>_VERSION and to select the highest -# available build number, unless a build no. is specified. This is determined with the +# available build number, unless a build no. is specified. This is determined with the # shell script build_number.sh included with require. define ADD_INCLUDES_TEMPLATE @@ -571,7 +571,7 @@ LDFLAGS += ${PROVIDES} ${USR_LDFLAGS_${T_A}} # 3.14.8 uses HDEPENDS to select depends mode # 3.14.12 uses 'HDEPENDSCFLAGS -MMD' (does not catch #include <...>) # 3.15 uses 'HDEPENDS_COMPFLAGS = -MM -MF $@' (does not catch #include <...>) -HDEPENDS = +HDEPENDS = HDEPENDS_METHOD = COMP HDEPENDS_COMPFLAGS = -c MKMF = DO_NOT_USE_MKMF @@ -630,9 +630,9 @@ debug:: @echo "BPTS = ${BPTS}" @echo "DBDINSTALLS = ${DBDINSTALLS}" @echo "HDRS = ${HDRS}" - @echo "SOURCES = ${SOURCES}" - @echo "SOURCES_${OS_CLASS} = ${SOURCES_${OS_CLASS}}" - @echo "SRCS = ${SRCS}" + @echo "SOURCES = ${SOURCES}" + @echo "SOURCES_${OS_CLASS} = ${SOURCES_${OS_CLASS}}" + @echo "SRCS = ${SRCS}" @echo "REQ = ${REQ}" @echo "LIBOBJS = ${LIBOBJS}" @echo "DBDS = ${DBDS}" @@ -655,12 +655,12 @@ INSTALL_LOADABLE_SHRLIBS= # We ony want to include ${BASERULES} from EPICS base if we are /not/ in debug # mode. Including this causes all of the source files to be compiled! ifeq (,$(findstring debug,${MAKECMDGOALS})) -include ${BASERULES} +include ${BASERULES} endif # Fix incompatible release rules. RELEASE_DBDFLAGS = -I ${EPICS_BASE}/dbd -RELEASE_INCLUDES = -I${EPICS_BASE}/include +RELEASE_INCLUDES = -I${EPICS_BASE}/include # For EPICS 3.15: RELEASE_INCLUDES += -I${EPICS_BASE}/include/compiler/${CMPLR_CLASS} RELEASE_INCLUDES += -I${EPICS_BASE}/include/os/${OS_CLASS} @@ -798,7 +798,7 @@ SNCFLAGS += -r # 2) We also need -c option in $(COMPILE.c) in order to compile generated source file properly # 3) SNC (2.1.21) should use -o, because without them, snc returns $(*F).i.c instead of $(*F).c # With the EPICS standard building rule, -o and mv are used. -# +# # Tuesday, November 28 15:59:37 CET 2017, Jeong Han Lee @@ -817,7 +817,7 @@ SNCFLAGS += -r @mv $(*F).c.tmp $(*F).c @echo ">> Compiling $(*F).c" $(RM) $@ - $(COMPILE.c) -c ${SNC_CFLAGS} $(*F).c + $(COMPILE.c) -c ${SNC_CFLAGS} $(*F).c @echo ">> Building $(*F)_snl.dbd" awk -F [\(\)] '/epicsExportRegistrar/ { print "registrar (" $$2 ")"}' $(*F).c > $(*F)_snl.dbd diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..98827e1dfc7f8fb11c219940d3b2332dd7848e1b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,48 @@ +from pathlib import Path + +import pytest +from git import Repo + +from .utils import TEST_MODULE_NAME + + +@pytest.fixture +def wrapper(tmpdir, request): + """ + Sets up a wrapper with the minimal necessary configuration in order to build a module + + Note that a number of necessary variables are expected to be present in the environment + """ + wrapper_dir = Path(tmpdir / "wrapper") + test_dir = wrapper_dir / TEST_MODULE_NAME + test_dir.mkdir(parents=True) + + config_file = """ +TOP:=$(CURDIR) + +include $(REQUIRE_CONFIG)/CONFIG +include $(REQUIRE_CONFIG)/RULES_SITEMODS +""" + + module_makefile = """ +where_am_I := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) +include $(E3_REQUIRE_TOOLS)/driver.makefile + +TEMPLATES += {templates} +SOURCES += {sources} +DBDS += {dbds} +HEADERS += {headers} +""" + + # TODO: This should not be necessary, but it is for patching. + Repo.init(wrapper_dir) + with open(wrapper_dir / "Makefile", "w") as f: + f.write(config_file) + + make_vars = {"templates": "", "sources": "", "dbds": "", "headers": ""} + make_vars.update(**request.param) + with open(wrapper_dir / f"{TEST_MODULE_NAME}.Makefile", "w") as f: + f.write(module_makefile.format(**make_vars)) + with open(wrapper_dir / TEST_MODULE_NAME / "test.dbd", "w") as f: + pass + yield wrapper_dir diff --git a/tests/test_build.py b/tests/test_build.py new file mode 100644 index 0000000000000000000000000000000000000000..cab6b2dfece42af1f79c3caa2551716d970fb8ed --- /dev/null +++ b/tests/test_build.py @@ -0,0 +1,104 @@ +import re + +import pytest + +from .utils import TEST_MODULE_NAME, run_make + +MODULE_VERSION = "0.0.0+0" +MODULE_VERSION_NO_BUILD = "0.0.0" + +RE_MISSING_FILE = "No rule to make target [`']{filename}'" + +DB_FILE = """record(ai, "TEST") { + +} +""" + +PATCH_FILE = """ +diff --git database.db database.db +index 1806ff6..8701832 100644 +--- database.db ++++ database.db +@@ -1,3 +1,3 @@ + record(ai, "TEST") {{ +- ++ field(DESC, "{desc}") + }} +""" + + +def create_patch_file(path, desc): + path.parent.mkdir(parents=True, exist_ok=True) + with open(path, "w") as f: + f.write(PATCH_FILE.format(desc=desc)) + + +@pytest.mark.parametrize( + "wrapper", + [{"templates": "database.db"}], + indirect=True, +) +def test_patch(wrapper): + db_path = wrapper / TEST_MODULE_NAME / "database.db" + with open(db_path, "w") as f: + f.write(DB_FILE) + + patch_dir = wrapper / "patch" / "Site" + create_patch_file(patch_dir / MODULE_VERSION / "apply.p0.patch", "OK") + create_patch_file( + patch_dir / MODULE_VERSION_NO_BUILD / "dont-apply.p0.patch", "Bad" + ) + create_patch_file(patch_dir / (MODULE_VERSION + "-dont-apply.p0.patch"), "Bad") + + rc, outs, _ = run_make( + wrapper, + "init", + E3_MODULE_VERSION=MODULE_VERSION, + ) + assert rc == 0 + assert "You are in the local source mode" in outs.decode("utf-8") + + rc, _, _ = run_make(wrapper, "patch", E3_MODULE_VERSION=MODULE_VERSION) + assert rc == 0 + with open(db_path, "r") as f: + db_contents = f.read() + assert 'field(DESC, "OK")' in db_contents + assert "Bad" not in db_contents + + rc, _, _ = run_make(wrapper, "build", E3_MODULE_VERSION=MODULE_VERSION) + assert rc == 0 + assert any((wrapper / TEST_MODULE_NAME).glob("O.*")) + + rc, _, _ = run_make(wrapper, "cellinstall", E3_MODULE_VERSION=MODULE_VERSION) + assert rc == 0 + assert any((wrapper / "cellMods").glob("**/*.db")) + + +@pytest.mark.parametrize( + "wrapper", + [{"dbds": "nonexistent.dbd"}], + indirect=True, +) +def test_missing_dbd_file(wrapper): + rc, _, errs = run_make(wrapper, "build") + + assert rc == 2 + assert re.search( + RE_MISSING_FILE.format(filename=re.escape("../nonexistent.dbd")), + errs.decode("utf-8"), + ) + + +@pytest.mark.parametrize( + "wrapper", + [{"sources": "nonexistent.c"}], + indirect=True, +) +def test_missing_source_file(wrapper): + rc, _, errs = run_make(wrapper, "build") + + assert rc == 2 + assert re.search( + RE_MISSING_FILE.format(filename=re.escape("nonexistent.o")), + errs.decode("utf-8"), + ) diff --git a/tests/test_versions.py b/tests/test_versions.py new file mode 100644 index 0000000000000000000000000000000000000000..e2285d276ac2bc718719ee0e1ca4e4babadc194a --- /dev/null +++ b/tests/test_versions.py @@ -0,0 +1,61 @@ +import re + +import pytest + +from .utils import TEST_MODULE_NAME, run_ioc_get_output, run_make + +RE_MODULE_LOADED = f"Loaded {TEST_MODULE_NAME} version {{version}}" +RE_MODULE_NOT_LOADED = f"Module {TEST_MODULE_NAME} (not available|version {{required}} not available|version {{required}} not available \\(but other versions are available\\))" + + +@pytest.mark.parametrize( + "wrapper", + [{"templates": "", "sources": "", "dbds": "test.dbd", "headers": ""}], + indirect=True, +) +@pytest.mark.parametrize( + "requested, expected, installed", + [ + # If nothing is installed, nothing should be loaded + ("", "", []), + # Test versions can be loaded + ("test", "test", ["test", "0.0.1"]), + # Numeric versions should be prioritized over test versions + ("", "0.0.1+0", ["test", "0.0.1"]), + ("", "0.0.1+0", ["0.0.1", "test"]), + # Highest build number should be loaded if version is unspecified + ("", "0.0.1+7", ["0.0.1", "0.0.1+7", "0.0.1+3"]), + # Only load the given build number if it is specified + ("0.0.1+0", "", ["0.0.1+1"]), + # If no build number is specified, load the highest build number + ("0.0.1", "0.0.1+4", ["0.1.0", "0.0.1+4", "0.0.1"]), + # Build number 0 means load that version exactly + ("0.0.1+0", "0.0.1+0", ["0.0.1+0"]), + ("0.0.1+0", "0.0.1+0", ["0.0.1", "0.0.1+1", "1.0.0"]), + # 1-test counts as a test version, as does 1.0 + ("", "0.0.1+0", ["0.0.1", "1-test"]), + ("", "0.0.1+0", ["0.0.1", "1.0"]), + # Numeric version should be prioritised over "higher" test version + ("", "0.1.0+0", ["0.1.0", "1.0.0-rc1"]), + ], +) +def test_version(wrapper, requested, expected, installed): + for version in installed: + returncode, _, _ = run_make( + wrapper, + "clean", + "cellinstall", + E3_MODULE_VERSION=version, + ) + assert returncode == 0 + + rc, o, e = run_ioc_get_output(requested, wrapper / "cellMods") + + if expected: + match = re.search(RE_MODULE_LOADED.format(version=re.escape(expected)), o) + assert rc == 0 + assert match + else: + match = re.search(RE_MODULE_NOT_LOADED.format(required=re.escape(requested)), o) + assert match + assert rc != 0 diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..14b691713d17df414ddb7051f85d8bf8475dbb43 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,64 @@ +import os +import subprocess +import time +from pathlib import Path +from random import choice +from string import ascii_lowercase + +from run_iocsh import IOC + +# Random module name +TEST_MODULE_NAME = "test_mod_" + "".join(choice(ascii_lowercase) for _ in range(16)) + +EPICS_BASE = os.environ.get("EPICS_BASE") +E3_REQUIRE_VERSION = os.environ.get("E3_REQUIRE_VERSION") + + +def set_env_vars(env: dict) -> dict: + """ + Set a number of environment variables needed for require to build a module + """ + assert "EPICS_BASE" in env + assert Path(env["EPICS_BASE"]).is_dir + assert "E3_REQUIRE_VERSION" in env + assert env["E3_REQUIRE_VERSION"] + + E3_REQUIRE_LOCATION = f"{EPICS_BASE}/require/{E3_REQUIRE_VERSION}" + assert Path(E3_REQUIRE_LOCATION).is_dir() + env["E3_REQUIRE_LOCATION"] = E3_REQUIRE_LOCATION + + REQUIRE_CONFIG = f"{E3_REQUIRE_LOCATION}/configure" + env["REQUIRE_CONFIG"] = REQUIRE_CONFIG + + env["E3_MODULE_NAME"] = TEST_MODULE_NAME + env["E3_MODULE_SRC_PATH"] = TEST_MODULE_NAME + env["E3_MODULE_MAKEFILE"] = f"{TEST_MODULE_NAME}.Makefile" + + # Add a default version + env["E3_MODULE_VERSION"] = "0.0.0" + + return env + + +def run_make(path, *args, **kwargs): + """ + Attempt to run `make <args>` in the specified path with <kwargs> as environment variables + """ + test_env = set_env_vars(os.environ.copy()) + for kw in kwargs: + test_env[kw] = kwargs[kw] + make_cmd = ["make", "-C", path] + list(args) + p = subprocess.Popen( + make_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=test_env + ) + outs, errs = p.communicate() + return p.returncode, outs, errs + + +def run_ioc_get_output(version, cell_path): + """ + Run an IOC and try to load the test module + """ + with IOC("-r", f"{TEST_MODULE_NAME},{version}", "-l", cell_path) as ioc: + time.sleep(1) + return ioc.proc.returncode, ioc.outs, ioc.errs