diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 00000000..73e6acd1 --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,76 @@ +name: Unit tests + +on: + pull_request: + workflow_dispatch: + +concurrency: + group: unit-tests-${{ github.ref }} + cancel-in-progress: true + +jobs: + unit-tests: + strategy: + fail-fast: false + matrix: + include: + - label: ubuntu-x64 + os: ubuntu-latest + arch: x64 + make_cmd: make + - label: macos-arm64 + os: macos-14 + arch: arm64 + make_cmd: gmake + runs-on: ${{ matrix.os }} + timeout-minutes: 45 + name: ${{ matrix.label }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive + + - name: Set up Miniconda + uses: conda-incubator/setup-miniconda@v3 + with: + activate-environment: test-env + auto-activate-base: false + python-version: "3.13" + + - name: Install build dependencies (Ubuntu) + if: startsWith(matrix.os, 'ubuntu-') + shell: bash -el {0} + run: | + conda install -y -c conda-forge scons compilers boost + + - name: Install build dependencies (macOS) + if: startsWith(matrix.os, 'macos-') + shell: bash -el {0} + run: | + brew install make + conda install -y -c conda-forge scons compilers boost + + - name: Build libobjcryst + shell: bash -el {0} + run: | + sed -i.bak '/if gfb != afb:/,+1d' site_scons/libobjcrystbuildutils.py + PREFIX="$CONDA_PREFIX" python -m SCons -Q lib + + - name: Locate built library + shell: bash -el {0} + run: | + libpath="$(find "$PWD/build" -type f \( -name 'libObjCryst.so' -o -name 'libObjCryst.dylib' \) | head -n 1)" + if [ -z "$libpath" ]; then + echo "No built libObjCryst library found under build/" + exit 1 + fi + echo "LIBOBJCRYST_PATH=$libpath" >> "$GITHUB_ENV" + echo "Using LIBOBJCRYST_PATH=$libpath" + + - name: Run standalone unit tests + shell: bash -el {0} + run: | + CONDA_PREFIX="$CONDA_PREFIX" ${{ matrix.make_cmd }} -C test LIBOBJCRYST="$LIBOBJCRYST_PATH" diff --git a/.gitignore b/.gitignore index 7a752a96..c69058ab 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,7 @@ tags # source distribution tarball libobjcryst-*.tar.gz + +# standalone unit-test artifacts +test/bin/ +test/obj/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d0a05e8..9b23a971 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Release notes +## Version 2026.2.0, - 2026-08-07 + +### Changed +- sync objcryst submodule to upstream `vincefn/objcryst` commit `4091cd9`, including: + - CIF parser robustness fix for truncated input + - fix for single-crystal simulation crashes + - refinement guards for missing data/phase and empty MonteCarloObj +- libobjcryst standalone workflow to build and run the upstream non-GUI unit-test suite + ## Version 2026.1, - 2026-02-05 ### Changed diff --git a/site_scons/fallback_version.py b/site_scons/fallback_version.py index d8dbb6b0..6f20786d 100644 --- a/site_scons/fallback_version.py +++ b/site_scons/fallback_version.py @@ -7,4 +7,4 @@ Update FALLBACK_VERSION when tagging a new release. ''' -FALLBACK_VERSION = '2026.1.post0' +FALLBACK_VERSION = '2026.2.0.post0' diff --git a/src/objcryst b/src/objcryst index 057a5853..90a3afca 160000 --- a/src/objcryst +++ b/src/objcryst @@ -1 +1 @@ -Subproject commit 057a5853de325aa2bb5b87e4358b9b5a3d6b5ae3 +Subproject commit 90a3afca1eaa08078e4fdfad2b7968e05e21a48f diff --git a/test/Makefile b/test/Makefile new file mode 100644 index 00000000..314ad324 --- /dev/null +++ b/test/Makefile @@ -0,0 +1,90 @@ +ROOT_DIR := $(abspath $(CURDIR)/..) +OBJCRYST_TEST_DIR := $(ROOT_DIR)/src/objcryst/test +UNIT_SRC_DIR := $(OBJCRYST_TEST_DIR)/unit +BUILD ?= fast +ARCH ?= $(shell uname -m) +BUILD_DIR ?= $(ROOT_DIR)/build/$(BUILD)-$(ARCH) +LIBOBJCRYST ?= $(firstword $(wildcard $(BUILD_DIR)/src/libObjCryst*.so) \ + $(wildcard $(BUILD_DIR)/src/libObjCryst*.dylib) \ + $(wildcard $(BUILD_DIR)/src/libObjCryst*.dll) \ + $(wildcard $(BUILD_DIR)/src/libObjCryst*.a) \ + $(wildcard $(BUILD_DIR)/libObjCryst*.so) \ + $(wildcard $(BUILD_DIR)/libObjCryst*.dylib) \ + $(wildcard $(BUILD_DIR)/libObjCryst*.dll) \ + $(wildcard $(BUILD_DIR)/libObjCryst*.a)) +BIN_DIR := $(CURDIR)/bin/$(BUILD)-$(ARCH) +OBJ_DIR := $(CURDIR)/obj/$(BUILD)-$(ARCH) +RUNNER := $(CURDIR)/run_unit_tests.sh +TEST_RUNNER := $(CURDIR)/test_runner.sh + +RM ?= rm -f +MKDIR_P ?= mkdir -p + +CXX ?= c++ +CXXFLAGS ?= -O2 -std=c++14 +CPPFLAGS += -DREAL=double \ + -I$(ROOT_DIR)/src \ + -I$(ROOT_DIR)/src/objcryst \ + -I$(ROOT_DIR)/src/objcryst/test/unit \ + -I$(BUILD_DIR)/src \ + -I$(BUILD_DIR)/src/objcryst \ + -I$(BUILD_DIR)/src/version \ + -I$(ROOT_DIR)/src/objcryst/ObjCryst \ + -I$(ROOT_DIR)/src/objcryst/cctbx/include +LDFLAGS += -L$(abspath $(dir $(LIBOBJCRYST))) +LDLIBS += $(abspath $(LIBOBJCRYST)) +ifeq ($(origin CONDA_PREFIX), undefined) +CONDA_PREFIX := +endif + +ifeq ($(shell uname -s),Darwin) +RPATH_FLAG = -Wl,-rpath,$(dir $(LIBOBJCRYST)) +ifneq ($(strip $(CONDA_PREFIX)),) +RPATH_FLAG += -Wl,-rpath,$(CONDA_PREFIX)/lib +endif +else +RPATH_FLAG = -Wl,-rpath,$(dir $(LIBOBJCRYST)) +endif + +UNIT_TESTS := unit_cell_smoke crystallography_workflow \ + api_spacegroup api_crystal api_molecule api_scattering \ + api_powderpattern api_cif api_optimization api_indexing \ + ground_truth + +UNIT_BINS := $(addprefix $(BIN_DIR)/,$(UNIT_TESTS)) +UNIT_OBJS := $(addprefix $(OBJ_DIR)/,$(addsuffix .o,$(UNIT_TESTS))) + +.SECONDARY: $(UNIT_OBJS) + +.PHONY: all build run clean tidy check-layout + +all: build run + +build: check-layout $(UNIT_BINS) $(RUNNER) + +run: build $(RUNNER) + @cd $(UNIT_SRC_DIR) && TEST_TIMEOUT="$${FOX_TEST_TIMEOUT:-30s}" BUILD="$(BUILD)" "$(RUNNER)" + +check-layout: + @test -d "$(UNIT_SRC_DIR)" || { echo "Missing upstream unit test sources at $(UNIT_SRC_DIR)"; exit 1; } + @test -f "$(TEST_RUNNER)" || { echo "Missing shared test runner at $(TEST_RUNNER)"; exit 1; } + @test -n "$(LIBOBJCRYST)" || { echo "No built libObjCryst found under $(BUILD_DIR). Build libobjcryst first."; exit 1; } + @test -f "$(ROOT_DIR)/src/objcryst/test/data/cif/PbSO4-COD-1528837.cif" || { echo "Missing required test data in src/objcryst/test/data"; exit 1; } + +$(BIN_DIR) $(OBJ_DIR): + @$(MKDIR_P) "$@" + +$(OBJ_DIR)/%.o: $(UNIT_SRC_DIR)/%.cpp | $(OBJ_DIR) + $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@ + +$(BIN_DIR)/%: $(OBJ_DIR)/%.o | $(BIN_DIR) + $(CXX) $(CXXFLAGS) $(LDFLAGS) $(RPATH_FLAG) $< -o $@ $(LDLIBS) + +$(RUNNER): + @chmod +x "$(RUNNER)" + +tidy: + @$(RM) $(UNIT_OBJS) + +clean: tidy + @$(RM) $(UNIT_BINS) "$(RUNNER)" diff --git a/test/README.md b/test/README.md new file mode 100644 index 00000000..453823de --- /dev/null +++ b/test/README.md @@ -0,0 +1,17 @@ +This directory provides an independent unit-test harness for running the +upstream ObjCryst non-GUI unit tests against the libobjcryst library built in +this repository. + +Usage: + +1. Build libobjcryst in-tree with SCons, for example: + `PREFIX=/path/to/env python -m SCons -Q lib` +2. Build and run the unit tests: + `CONDA_PREFIX=/path/to/env make -C test` + +The makefile expects the library under `build/-/src/` and uses +`build=fast` by default. Override `BUILD_DIR`, `BUILD`, `ARCH`, or `LIBOBJCRYST` +when using a different in-tree layout. + +On macOS with a conda toolchain, pass `CONDA_PREFIX` so the test binaries add +the environment runtime library directory to their rpath. diff --git a/test/run_unit_tests.sh b/test/run_unit_tests.sh new file mode 100755 index 00000000..d2f32510 --- /dev/null +++ b/test/run_unit_tests.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +set -eu + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +BIN_SUBDIR="${BUILD:-fast}-$(uname -m)" + +# shellcheck source=/dev/null +. "$SCRIPT_DIR/test_runner.sh" + +TEST_TIMEOUT="${FOX_TEST_TIMEOUT:-30s}" + +run_test "unit::unit_cell_smoke" "$SCRIPT_DIR/bin/$BIN_SUBDIR/unit_cell_smoke" +run_test "unit::crystallography_workflow" "$SCRIPT_DIR/bin/$BIN_SUBDIR/crystallography_workflow" + +run_test "unit::spacegroup" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_spacegroup" "spacegroup" +run_test "unit::spacegroup-alternate-settings" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_spacegroup" "spacegroup-alternate-settings" +run_test "unit::spacegroup-reflection-properties" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_spacegroup" "spacegroup-reflection-properties" +run_test "unit::spacegroup-symmetry-operations" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_spacegroup" "spacegroup-symmetry-operations" +run_test "unit::spacegroup-asymmetric-unit" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_spacegroup" "spacegroup-asymmetric-unit" + +run_test "unit::scattering-power-atom" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_crystal" "scattering-power-atom" +run_test "unit::crystal-atom" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_crystal" "crystal-atom" +run_test "unit::crystal-scatterer-management" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_crystal" "crystal-scatterer-management" +run_test "unit::unitcell-geometry" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_crystal" "unitcell-geometry" + +run_test "unit::molecule-atoms-bonds" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_molecule" "molecule-atoms-bonds" +run_test "unit::molecule-angles-dihedrals" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_molecule" "molecule-angles-dihedrals" +run_test "unit::molecule-formula-loglikelihood" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_molecule" "molecule-formula-loglikelihood" + +run_test "unit::scatteringdata-singlecrystal" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_scattering" "scatteringdata-singlecrystal" +run_test "unit::scatteringdata-radiation-types" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_scattering" "scatteringdata-radiation-types" +run_test "unit::diffractiondata-observed" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_scattering" "diffractiondata-observed" +run_test "unit::singlecrystal-groundtruth-xray" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_scattering" "singlecrystal-groundtruth-xray" +run_test "unit::singlecrystal-groundtruth-neutron" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_scattering" "singlecrystal-groundtruth-neutron" +run_test "unit::singlecrystal-simulate-ungrouped" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_scattering" "singlecrystal-simulate-ungrouped" +run_test "unit::singlecrystal-simulate-grouped-equal" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_scattering" "singlecrystal-simulate-grouped-equal" +run_test "unit::singlecrystal-simulate-grouped-user" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_scattering" "singlecrystal-simulate-grouped-user" + +run_test "unit::cif-import" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_cif" "cif-import" +run_test "unit::cif-data-fields" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_cif" "cif-data-fields" +run_test "unit::cif-coordinate-conversion" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_cif" "cif-coordinate-conversion" +run_test "unit::cif-truncated-values" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_cif" "cif-truncated-values" + +run_test "unit::refinablepar" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_optimization" "refinablepar" +run_test "unit::refinableobj" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_optimization" "refinableobj" +run_test "unit::optimizationobj" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_optimization" "optimizationobj" +run_test "unit::optimizationobj-limits-options" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_optimization" "optimizationobj-limits-options" +run_test "unit::lsqnumobj" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_optimization" "lsqnumobj" +run_test "unit::lsqnumobj-residual-statistics" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_optimization" "lsqnumobj-residual-statistics" + +run_test "unit::peaklist-simulate-volume" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_indexing" "peaklist-simulate-volume" +run_test "unit::peaklist-add-remove" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_indexing" "peaklist-add-remove" +run_test "unit::cellexplorer" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_indexing" "cellexplorer" +run_test "unit::cellexplorer-configuration" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_indexing" "cellexplorer-configuration" +run_test "unit::cellexplorer-dicvol-tetragonal" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_indexing" "cellexplorer-dicvol-tetragonal" +run_test "unit::cellexplorer-dicvol-monoclinic" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_indexing" "cellexplorer-dicvol-monoclinic" + +run_test "unit::powderpattern-background" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_powderpattern" "powderpattern-background" +run_test "unit::powderpattern-diffraction" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_powderpattern" "powderpattern-diffraction" +run_test "unit::powderpattern-diffraction-mur" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_powderpattern" "powderpattern-diffraction-mur" +run_test "unit::powderpattern-diffraction-lebail-fhklobs" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_powderpattern" "powderpattern-diffraction-lebail-fhklobs" +run_test "unit::powderpattern-import" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_powderpattern" "powderpattern-import" +run_test "unit::scatteringcorr-subclasses" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_powderpattern" "scatteringcorr-subclasses" +run_test "unit::reflectionprofile-pseudo-voigt" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_powderpattern" "reflectionprofile-pseudo-voigt" +run_test "unit::reflectionprofile-double-exponential-pv" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_powderpattern" "reflectionprofile-double-exponential-pv" + +run_test "unit::powder-groundtruth-xray-pv-gaussian" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_powderpattern" "powder-groundtruth-xray-pv-gaussian" +run_test "unit::powder-groundtruth-xray-pv-lorentzian" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_powderpattern" "powder-groundtruth-xray-pv-lorentzian" +run_test "unit::reflectionprofile-pv-anisotropic-direct" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_powderpattern" "reflectionprofile-pv-anisotropic-direct" +run_test "unit::powder-groundtruth-xray-anisotropic" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_powderpattern" "powder-groundtruth-xray-anisotropic" +run_test "unit::powder-groundtruth-neutron-pv-gaussian" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_powderpattern" "powder-groundtruth-neutron-pv-gaussian" +run_test "unit::powder-groundtruth-neutron-pv-lorentzian" "$SCRIPT_DIR/bin/$BIN_SUBDIR/api_powderpattern" "powder-groundtruth-neutron-pv-lorentzian" diff --git a/test/test_runner.sh b/test/test_runner.sh new file mode 100755 index 00000000..1723852f --- /dev/null +++ b/test/test_runner.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -eu + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +# shellcheck source=/dev/null +. "$ROOT_DIR/src/objcryst/test/scripts/test_runner.sh"