From ef798f7640232aeed8f3aa77b54e6879b483a040 Mon Sep 17 00:00:00 2001 From: Adrian Pop Date: Thu, 27 Aug 2026 11:48:41 +0200 Subject: [PATCH] Run every library testing job in one docker image (#295) Only the wasm-jit target ran in a container; every other job ran on the node itself and needed the compilers, the python environment, omc, FMPy and the packages of the tested libraries installed there by hand - which is what #295 asks to be rid of. The image the wasm-jit target already used becomes the image of all of them, under .CI/testing rather than .CI/wasm-jit, and `dockerfile` defaults to it, so a target opts out of the container instead of into it. A node then needs docker, git and an ssh key, and a job moved to another machine tests what it tested before. What the image gained for the targets that did not have one: - autoconf, automake, libtool and omniORB, which the targets that do not ask for a cmake omc need: those still go through `autoreconf && ./configure --with-omniORB`, and the base image carries what the cmake build needs. - FMPy, the simulator of the -fmi-fmpy targets. The job used to `pip install` it per build, which in a container would be thrown away with the container; the version is the image's now, and the build only checks that the interpreter test.py reaches has it. Moved into the container along with the omc build and test.py, because they run the binaries the container will run rather than the node's: - the OMSimulator build. A binary linked against the node's libraries need not load inside the image, and it is test.py, inside, that runs it. - the conversion script, which runs the omc that was just built. Installing the libraries and unpacking the reference files stay on the node: they use the node's own omc, and /mnt/ReferenceFiles is bound into the container under its own path, so the reference results the maintenance job installs are the same files on both sides. The image is tagged after the Dockerfile rather than after the job, so a node keeps one of them instead of one per job it has ever run. --- .CI/Jenkinsfile | 44 ++++++++++++++++------------ .CI/{wasm-jit => testing}/Dockerfile | 20 +++++++++++-- .gitignore | 2 +- README.md | 29 ++++++++++++++++++ 4 files changed, 73 insertions(+), 22 deletions(-) rename .CI/{wasm-jit => testing}/Dockerfile (59%) diff --git a/.CI/Jenkinsfile b/.CI/Jenkinsfile index c7d2092..3323fb3 100644 --- a/.CI/Jenkinsfile +++ b/.CI/Jenkinsfile @@ -385,8 +385,7 @@ pipeline { } steps { runRegressiontest('master', 'wasm-jit', 'setCommandLineOptions("--simCodeTarget=wasm-jit")', '', false, '', '--wasmjitrunner=sim,me,cs', false, false, 0, 'configs/conf.json', - '-DOM_OMC_ENABLE_RUST=ON -DRUST_OMC_CI=ON -DRUST_OMC_THREADS=4 -DCMAKE_C_COMPILER_LAUNCHER=sccache -DCMAKE_CXX_COMPILER_LAUNCHER=sccache', - '.CI/wasm-jit') + '-DOM_OMC_ENABLE_RUST=ON -DRUST_OMC_CI=ON -DRUST_OMC_THREADS=4 -DCMAKE_C_COMPILER_LAUNCHER=sccache -DCMAKE_CXX_COMPILER_LAUNCHER=sccache') } } stage('generateSymbolicJacobian') { @@ -702,7 +701,12 @@ def numLogicalCPU() { def omsimulatorHash() { return 'master' } -def installLibraries(boolean removePackageOrder, boolean conversionScript, name, String omhomeTestedOMC) { +/** + * Installs the libraries a run tests, with the node's own omc, and returns the home directory + * holding them. `runSh` is how the conversion script is run: that one uses the omc that was just + * built, which is a binary of the image when the job has one. + */ +def installLibraries(boolean removePackageOrder, boolean conversionScript, name, String omhomeTestedOMC, Closure runSh) { sh "rm -rf '${env.HOME}/saved_omc/libraries/.openmodelica/libraries'" sh "mkdir -p '${env.HOME}/saved_omc/libraries/'" sh "HOME='${env.HOME}/saved_omc/libraries/' /usr/bin/omc OpenModelicaLibraryTesting/.CI/installLibraries.mos" @@ -714,11 +718,11 @@ def installLibraries(boolean removePackageOrder, boolean conversionScript, name, sh "cp -ai /mnt/ReferenceFiles/ExtraLibs/packaged/* '${env.HOME}/saved_omc/libraries/'" echo "installLibraries removePackageOrder: ${removePackageOrder} conversionScript: ${conversionScript} name: ${name}" if (conversionScript) { - sh """ + runSh(""" cd '${WORKSPACE}/OpenModelicaLibraryTesting' OPENMODELICAHOME="${omhomeTestedOMC}" ./conversionscript.py --diff --allowErrorsInDiff '${env.HOME}/saved_omc/libraries/.openmodelica/libraries' scp converted-libraries/.openmodelica/libraries/*.diff 'libraries.openmodelica.org:/var/www/libraries.openmodelica.org/branches/${name}' - """ + """) return "${WORKSPACE}/OpenModelicaLibraryTesting/converted-libraries" } else { return "${env.HOME}/saved_omc/libraries" @@ -808,9 +812,10 @@ def sccachePreamble() { * This file specifies which libraries to test and what options to use for them. * @param cmakeFlags: Target-specific cmake flags, e.g. `-DOM_OMC_ENABLE_RUST=ON`. If non-empty, omc is * built with cmake instead of autotools; the shared release flags are added here. - * @param dockerfile: Directory with a Dockerfile, relative to the testing repository, e.g. - * `.CI/wasm-jit`. If non-empty, the omc build and test.py run in that image - * instead of on the node; everything using the node's own omc stays outside. + * @param dockerfile: Directory with a Dockerfile, relative to the testing repository. Defaults to + * `.CI/testing`, the image every job runs in: the omc build, the OMSimulator + * build and test.py happen inside it, and only the steps using the node's own + * omc stay outside. Passing `''` runs the job on the node itself. */ /* The FMI simulators a job runs, from the parameters that used to start one job each. * Both ticked is one job that builds every FMU once and simulates it with both, @@ -822,7 +827,7 @@ def fmiSimulators(boolean omsimulator, boolean fmpy) { return simulators } -def runRegressiontest(branch, name, extraFlags, omsHash, omcompiler, extrasimflags, testFlags, boolean removePackageOrder, boolean conversionScript, int jobs=0, libs_config_file = 'configs/conf.json', cmakeFlags = '', dockerfile = '', fmiSimulators = null) { +def runRegressiontest(branch, name, extraFlags, omsHash, omcompiler, extrasimflags, testFlags, boolean removePackageOrder, boolean conversionScript, int jobs=0, libs_config_file = 'configs/conf.json', cmakeFlags = '', dockerfile = '.CI/testing', fmiSimulators = null) { sh ''' find /tmp -name "*openmodelica.hudson*" -exec rm {} ";" || true @@ -846,7 +851,10 @@ def runRegressiontest(branch, name, extraFlags, omsHash, omcompiler, extrasimfla if (dockerfile) { // Build context is the Dockerfile directory; the workspace has a directory per tested model. sh "cp OpenModelicaLibraryTesting/requirements.txt OpenModelicaLibraryTesting/${dockerfile}/" - image = docker.build("openmodelica-library-testing:${name}", "--pull OpenModelicaLibraryTesting/${dockerfile}") + // Tagged after the Dockerfile rather than after the job, so that jobs sharing an image share + // its tag as well and a node keeps one of them instead of one per job it has ever run. + image = docker.build("openmodelica-library-testing:${dockerfile.tokenize('/').last()}", + "--pull OpenModelicaLibraryTesting/${dockerfile}") } // --init reaps the omc processes test.py orphans. ssh refuses to run for a uid it cannot look // up, so the node's passwd entry is needed to publish the results. The home holds the cached omc @@ -885,7 +893,9 @@ def runRegressiontest(branch, name, extraFlags, omsHash, omcompiler, extrasimfla } FMI_TESTING_FLAG = "" if (simulators.contains('OMSimulator') && omsHash) { - sh """ + // In the image rather than on the node: test.py runs this binary from inside it, and one built + // against the node's libraries need not load there. + runSh(""" if ! test -d OMSimulator; then git clone --recursive https://openmodelica.org/git-readonly/OMSimulator.git || exit 1 fi @@ -916,16 +926,14 @@ def runRegressiontest(branch, name, extraFlags, omsHash, omcompiler, extrasimfla fi echo OMSimulator version: ${env.HOME}/saved_omc/OMSimulator/install/bin/OMSimulator --version - """ + """) FMI_TESTING_FLAG = " --fmisimulator=${env.HOME}/saved_omc/OMSimulator/install/bin/OMSimulator" } if (simulators.contains('fmpy')) { - sh """ - # update fmpy - pip install FMPy || true - python3 -m fmpy -h || exit 1 - """ + // The version is the image's; check it in the interpreter test.py will reach rather than + // installing one over it, which in a container would be thrown away with the container. + runSh('python3 -m fmpy -h > /dev/null || exit 1') FMI_TESTING_FLAG += " --fmisimulator='python3 -m fmpy'" } @@ -1117,7 +1125,7 @@ def runRegressiontest(branch, name, extraFlags, omsHash, omcompiler, extrasimfla mkdir -p "/var/www/libraries.openmodelica.org/branches/${name}/" """ - def libraryPath = installLibraries(removePackageOrder, conversionScript, name, "${WORKSPACE}/OpenModelica/${OMCPATH}/build") + def libraryPath = installLibraries(removePackageOrder, conversionScript, name, "${WORKSPACE}/OpenModelica/${OMCPATH}/build", runSh) sh "test -d '${libraryPath}/.openmodelica/libraries/Modelica trunk'" diff --git a/.CI/wasm-jit/Dockerfile b/.CI/testing/Dockerfile similarity index 59% rename from .CI/wasm-jit/Dockerfile rename to .CI/testing/Dockerfile index 30ab938..95dff69 100644 --- a/.CI/wasm-jit/Dockerfile +++ b/.CI/testing/Dockerfile @@ -1,17 +1,28 @@ -# Image for the targets needing the Rust omc (wasm-jit); the nodes run an Ubuntu -# too old to build it. Built by .CI/Jenkinsfile, with this directory as the -# context (the job copies requirements.txt into it). +# The image every library testing job runs in: it builds omc, OMSimulator and runs test.py. +# Built by .CI/Jenkinsfile, with this directory as the context (the job copies requirements.txt +# into it). +# +# One image for all of them rather than one per target: what a node then needs is docker, git and +# an ssh key instead of a set of packages kept up to date by hand, and a job moved to another +# machine tests what it tested before. The base is the one the wasm-jit target needs - the OMDev +# image the OpenModelica repository builds against, in the variant carrying the Rust toolchain, +# since a Rust omc cannot be built on the Ubuntu the nodes run, and its sccache keys only hit for +# the toolchain that job used. FROM docker.openmodelica.org/build-deps:ubuntu-26.04-rust # rsync/ssh publish the results; time and killall are used around test.py. # libcomedi-dev libx11-dev is used by Modelica_DeviceDrivers # Python 3.8 and 3.12 are used by Buildings +# autoconf/automake/libtool and omniORB build the targets that do not ask for a cmake omc: those +# still go through `autoreconf && ./configure --with-omniORB`. The base image carries what the +# cmake build needs, which is not quite the same set. RUN export DEBIAN_FRONTEND=noninteractive \ && apt-get update \ && apt-get install -qy software-properties-common \ && add-apt-repository -y ppa:deadsnakes/ppa \ && apt-get install -qy --no-install-recommends openssh-client psmisc rsync time \ && apt-get install -qy --no-install-recommends libcomedi-dev libx11-dev \ + && apt-get install -qy --no-install-recommends autoconf automake libtool omniidl libomniorb4-dev \ && apt-get install -qy python3.8 python3.12 libpython3.8-dev libpython3.12-dev \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* @@ -23,6 +34,9 @@ RUN python3 -m venv /opt/libtest-venv \ && /opt/libtest-venv/bin/pip install --no-cache-dir -r /tmp/requirements.txt \ # Python dependencies of library install scripts (e.g. Buildings EnergyPlus) && /opt/libtest-venv/bin/pip install --no-cache-dir jinja2 \ + # The simulator of the -fmi-fmpy targets, which test.py runs as `python3 -m fmpy`. Its version + # is the image's: a job cannot pip install one over it that outlives the container. + && /opt/libtest-venv/bin/pip install --no-cache-dir FMPy \ && rm /tmp/requirements.txt ENV PATH=/opt/libtest-venv/bin:$PATH diff --git a/.gitignore b/.gitignore index 409402b..388d96d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ __pycache__ -/.CI/wasm-jit/requirements.txt +/.CI/testing/requirements.txt !HelloWorld.mo !HelloWorld.mos /.venv/ diff --git a/README.md b/README.md index a2997d6..fd5284b 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,35 @@ OpenModelica [issue tracker](https://github.com/OpenModelica/OpenModelica/issues/new/choose) and ask us to do it for you. +### The image the OSMC jobs run in + +Every job of [.CI/Jenkinsfile](.CI/Jenkinsfile) runs in one docker image, built +from [.CI/testing/Dockerfile](.CI/testing/Dockerfile) at the start of the job +and with `--pull`, so a rebuilt base is picked up on its own. What a node needs +is then docker, git and an ssh key to publish the results with; the compilers, +the python environment, the `omc` that generates the mos files, FMPy and the +packages the tested libraries need are the image's, not the machine's, and a job +moved to another machine tests what it tested before. + +The omc build, the OMSimulator build and `test.py` run inside the image; the +steps that use the node's own `omc` - installing the libraries and preparing the +reference files - stay outside it. The node's home directory is mounted, so the +cached omc build in `~/saved_omc`, the installed libraries and the ssh key are +the files they always were, and so is `/mnt/ReferenceFiles`, where the +maintenance job installs the reference results: the container reads the same +directory the node does, and writes the hash `test.py` keeps next to a reference +file back into it. The container is capped at 85% of the node's memory, which a +job running on the node itself was not. + +A dependency a library needs is therefore a line in the Dockerfile rather than +an `apt-get install` repeated on every machine: + +```bash +cp requirements.txt .CI/testing/ # the build context has to carry it +docker build -t openmodelica-library-testing:testing .CI/testing +docker run --rm -it openmodelica-library-testing:testing bash +``` + ## Running the library testing infrastructure on your own server The scripts from this repository can be used to run regression tests for public,