From 8a8a8b83447ec488fdc97c2b392abfbb61ca182c Mon Sep 17 00:00:00 2001 From: nmaeder Date: Wed, 8 Oct 2025 17:36:09 +0200 Subject: [PATCH 1/8] add a weekly ci schedule --- .github/workflows/scheduled.yaml | 43 ++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/scheduled.yaml diff --git a/.github/workflows/scheduled.yaml b/.github/workflows/scheduled.yaml new file mode 100644 index 0000000..f59fce6 --- /dev/null +++ b/.github/workflows/scheduled.yaml @@ -0,0 +1,43 @@ +name: scheduled-tests + +on: + schedule: + - cron: "0 2 * * 6" + +jobs: + test: + name: Test on ${{ matrix.os }}, Python ${{ matrix.python-version }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: ['3.10'] + + steps: + - uses: actions/checkout@v4 + + - name: Additional info about the build + shell: bash + run: | + uname -a + df -h + ulimit -a + + - uses: mamba-org/setup-micromamba@v1 + with: + create-args: >- + python=${{ matrix.python-version }} + init-shell: bash + environment-file: dev/conda-env/test_env.yaml + cache-environment: true + + - name: Install package + shell: bash -l {0} + run: | + pip install . --no-deps + conda list + + - name: Run tests + shell: bash -l {0} + run: | + pytest -v --color=yes --cov=serenityff --cov-report=xml --run-slow serenityff/charge/tests/ tests/ From 9cd514b07f5aefe316bccfef4bd06f36e14e9dca Mon Sep 17 00:00:00 2001 From: nmaeder Date: Wed, 8 Oct 2025 17:37:54 +0200 Subject: [PATCH 2/8] no cacheing --- .github/workflows/scheduled.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/scheduled.yaml b/.github/workflows/scheduled.yaml index f59fce6..d8b6102 100644 --- a/.github/workflows/scheduled.yaml +++ b/.github/workflows/scheduled.yaml @@ -29,7 +29,6 @@ jobs: python=${{ matrix.python-version }} init-shell: bash environment-file: dev/conda-env/test_env.yaml - cache-environment: true - name: Install package shell: bash -l {0} From fb47e8333dc235bf38ab21e9eb4e9b11ac89a077 Mon Sep 17 00:00:00 2001 From: nmaeder Date: Wed, 8 Oct 2025 18:09:12 +0200 Subject: [PATCH 3/8] cleanup --- .github/workflows/CI.yaml | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml index d2f84fb..8f0755f 100644 --- a/.github/workflows/CI.yaml +++ b/.github/workflows/CI.yaml @@ -7,11 +7,6 @@ on: pull_request: branches: - "main" - #schedule: - # # Nightly tests run on master by default: - # # Scheduled workflows run on the latest commit on the default or base branch. - # # (from https://help.github.com/en/actions/reference/events-that-trigger-workflows#scheduled-events-schedule) - # - cron: "0 0 * * *" jobs: test: @@ -19,8 +14,8 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest] #[ubuntu-latest, macOS-latest] - python-version: ['3.10'] #[3.7, 3.8, 3.9, 3.10, 3.11, 3.12] + os: [ubuntu-latest] + python-version: ['3.10'] steps: - uses: actions/checkout@v4 @@ -32,10 +27,8 @@ jobs: df -h ulimit -a - - uses: mamba-org/setup-micromamba@v1 with: - #python-version: ${{ matrix.python-version }} create-args: >- python=${{ matrix.python-version }} init-shell: bash @@ -47,11 +40,9 @@ jobs: pip install . --no-deps conda list - - name: Run tests shell: bash -l {0} run: | - pip install . --no-deps pytest -v --color=yes --cov=serenityff --cov-report=xml --run-slow serenityff/charge/tests/ tests/ analyze: From 78cb4999857727ba14b8c2cef446071bbfbbc3bd Mon Sep 17 00:00:00 2001 From: Niels Maeder Date: Mon, 13 Oct 2025 15:00:58 +0200 Subject: [PATCH 4/8] remove trailing whitespace --- .github/workflows/scheduled.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scheduled.yaml b/.github/workflows/scheduled.yaml index d8b6102..49d9a73 100644 --- a/.github/workflows/scheduled.yaml +++ b/.github/workflows/scheduled.yaml @@ -10,7 +10,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest] + os: [ubuntu-latest] python-version: ['3.10'] steps: From 6a6281d885eac26f1c5f87129e4040da01ac7414 Mon Sep 17 00:00:00 2001 From: Niels Maeder Date: Mon, 15 Dec 2025 14:43:10 +0100 Subject: [PATCH 5/8] use the user conda envs for the scheduled ci --- .github/workflows/scheduled.yaml | 55 ++++++++-------- .../charge/tests/test_gnn_extraction.py | 15 +++++ serenityff/charge/tests/test_gnn_training.py | 6 ++ serenityff/charge/tests/test_gnn_utils.py | 18 +++++ serenityff/charge/tests/test_off_plugin.py | 6 ++ tests/conftest.py | 65 +++++++++++-------- .../charge/gnn/utils/test_rdkit_helper.py | 9 ++- .../utils/test_serenityff_charge_handler.py | 7 +- 8 files changed, 126 insertions(+), 55 deletions(-) diff --git a/.github/workflows/scheduled.yaml b/.github/workflows/scheduled.yaml index 49d9a73..2bce2e8 100644 --- a/.github/workflows/scheduled.yaml +++ b/.github/workflows/scheduled.yaml @@ -6,37 +6,38 @@ on: jobs: test: - name: Test on ${{ matrix.os }}, Python ${{ matrix.python-version }} - runs-on: ${{ matrix.os }} + name: Run ${{ matrix.env-name }} environment + runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - os: [ubuntu-latest] - python-version: ['3.10'] + include: + - env-name: "min" + file: "tree_only_env.yml" + mark: "min" + - env-name: "med" + file: "min_environment.yml" + mark: "med" + - env-name: "max" + file: "environment.yml" + mark: "max" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Additional info about the build - shell: bash - run: | - uname -a - df -h - ulimit -a + - name: Setup Conda + uses: conda-incubator/setup-miniconda@v3 + with: + activate-environment: test-env + environment-file: ${{ matrix.file }} - - uses: mamba-org/setup-micromamba@v1 - with: - create-args: >- - python=${{ matrix.python-version }} - init-shell: bash - environment-file: dev/conda-env/test_env.yaml + - name: End-to-End Install + shell: bash -l {0} + run: | + conda install -y pytest pytest-cov + pip install git+https://github.com/rinikerlab/DASH-tree.git@${{ github.sha }}" - - name: Install package - shell: bash -l {0} - run: | - pip install . --no-deps - conda list - - - name: Run tests - shell: bash -l {0} - run: | - pytest -v --color=yes --cov=serenityff --cov-report=xml --run-slow serenityff/charge/tests/ tests/ + - name: Run Tests + shell: bash -l {0} + run: | + pytest -v --color=yes --cov=serenityff --cov-report=xml --run-slow -- "${{ matrix.mark }}" serenityff/charge/tests/ tests/ diff --git a/serenityff/charge/tests/test_gnn_extraction.py b/serenityff/charge/tests/test_gnn_extraction.py index 24438ef..9fc4f69 100644 --- a/serenityff/charge/tests/test_gnn_extraction.py +++ b/serenityff/charge/tests/test_gnn_extraction.py @@ -104,6 +104,7 @@ def graph(cwd) -> CustomData: ) +@pytest.mark.max def test_getter_setter(explainer) -> None: with pytest.raises(TypeError): explainer.gnn_explainer = "asdf" @@ -111,11 +112,13 @@ def test_getter_setter(explainer) -> None: return +@pytest.mark.max def test_load(model, statedict) -> None: assert all(a == b for a, b in zip(model.state_dict(), statedict)) return +@pytest.mark.max def test_explain_atom(explainer, graph) -> None: explainer.gnn_explainer.explain_node( node_idx=0, @@ -150,6 +153,7 @@ def test_explain_atom(explainer, graph) -> None: return +@pytest.mark.max def test_extractor_properties(extractor, model, model_path, statedict_path, explainer) -> None: extractor._set_model(model) assert isinstance(extractor.model, ChargeCorrectedNodeWiseAttentiveFP) @@ -166,6 +170,7 @@ def test_extractor_properties(extractor, model, model_path, statedict_path, expl assert isinstance(extractor.model, NodeWiseAttentiveFP) +@pytest.mark.max def test_split_sdf(cwd, sdf_path) -> None: Extractor._split_sdf( sdf_file=sdf_path, @@ -178,6 +183,7 @@ def test_split_sdf(cwd, sdf_path) -> None: return +@pytest.mark.max def test_job_id(cwd) -> None: with open(f"{cwd}/id.txt", "w") as f: f.write("sdcep ab ein \n sdf <12345> saoeb ") @@ -187,11 +193,13 @@ def test_job_id(cwd) -> None: return +@pytest.mark.max def test_mol_from_sdf(sdf_path): mol = mols_from_sdf(sdf_file=sdf_path)[0] assert mol.GetNumBonds() == 19 +@pytest.mark.max def test_graph_from_mol(mol, num_atoms, num_bonds, formal_charge, smiles) -> None: with pytest.raises(ValueError): get_graph_from_mol(mol=mol, index=0, sdf_property_name=None) @@ -209,6 +217,7 @@ def test_graph_from_mol(mol, num_atoms, num_bonds, formal_charge, smiles) -> Non return +@pytest.mark.max @pytest.mark.parametrize("sdf_prop", [(None), ("MBIScharge")]) def test_graph_from_mol_no_y(mol, num_atoms, num_bonds, formal_charge, smiles, sdf_prop) -> None: graph = get_graph_from_mol(mol=mol, index=0, sdf_property_name=sdf_prop, no_y=True) @@ -223,6 +232,7 @@ def test_graph_from_mol_no_y(mol, num_atoms, num_bonds, formal_charge, smiles, s return +@pytest.mark.max def test_arg_parser(args, sdf_path, statedict_path) -> None: args = Extractor._parse_filenames(args) assert args.sdffile == sdf_path @@ -230,6 +240,7 @@ def test_arg_parser(args, sdf_path, statedict_path) -> None: return +@pytest.mark.max def test_script_writing(cwd) -> None: Extractor._write_worker(directory=cwd) Extractor._write_cleaner(directory=cwd) @@ -239,11 +250,13 @@ def test_script_writing(cwd) -> None: return +@pytest.mark.max def test_explainer_initialization(extractor, model) -> None: extractor._initialize_expaliner(model=model, epochs=1) return +@pytest.mark.max def test_command_to_shell_file(cwd) -> None: command_to_shell_file("echo Hello World", f"{cwd}/test.sh") os.path.isfile(f"{cwd}/test.sh") @@ -253,6 +266,7 @@ def test_command_to_shell_file(cwd) -> None: os.remove(f"{cwd}/test.sh") +@pytest.mark.max def test_csv_handling(cwd, sdf_path, extractor, model): extractor._initialize_expaliner(model=model, epochs=1) outfile = f"{cwd}/sdftest/combined.csv" @@ -268,6 +282,7 @@ def test_csv_handling(cwd, sdf_path, extractor, model): rmtree(f"{cwd}/sdftest") +@pytest.mark.max def test_run_extraction_local(extractor, statedict_path, cwd, sdf_path) -> None: extractor.run_extraction_local( sdf_file=sdf_path, diff --git a/serenityff/charge/tests/test_gnn_training.py b/serenityff/charge/tests/test_gnn_training.py index e25177f..e98292d 100644 --- a/serenityff/charge/tests/test_gnn_training.py +++ b/serenityff/charge/tests/test_gnn_training.py @@ -85,6 +85,7 @@ def trainer(model, optimizer): return trainer +@pytest.mark.max def test_init_and_forward_model(model, graph) -> None: model = model model.train() @@ -99,6 +100,7 @@ def test_init_and_forward_model(model, graph) -> None: return +@pytest.mark.max def test_initialize_trainer(trainer, model, sdf_path, pt_path, statedict_path, model_path, statedict) -> None: # test init assert trainer.device == device("cuda") if is_available() else device("cpu") @@ -147,6 +149,7 @@ def test_initialize_trainer(trainer, model, sdf_path, pt_path, statedict_path, m return +@pytest.mark.max def test_prepare_train_data(trainer, sdf_path): with pytest.warns(Warning): trainer.prepare_training_data() @@ -158,6 +161,7 @@ def test_prepare_train_data(trainer, sdf_path): return +@pytest.mark.max def test_train_model(trainer, sdf_path) -> None: trainer.gen_graphs_from_sdf(sdf_path) trainer.prepare_training_data(train_ratio=0.5) @@ -173,6 +177,7 @@ def test_train_model(trainer, sdf_path) -> None: return +@pytest.mark.max def test_prediction(trainer, graph, molecule) -> None: a = trainer.predict(graph) @@ -198,6 +203,7 @@ def test_prediction(trainer, graph, molecule) -> None: return +@pytest.mark.max def test_on_gpu(trainer) -> None: assert trainer._on_gpu == is_available() diff --git a/serenityff/charge/tests/test_gnn_utils.py b/serenityff/charge/tests/test_gnn_utils.py index 20bfe41..fc5d89b 100644 --- a/serenityff/charge/tests/test_gnn_utils.py +++ b/serenityff/charge/tests/test_gnn_utils.py @@ -126,6 +126,7 @@ def node_features() -> np.ndarray: ) +@pytest.mark.max def test_get_split_numbers() -> None: assert [1, 0] == get_split_numbers(N=1, train_ratio=0.5) assert [1, 1] == get_split_numbers(N=2, train_ratio=0.5) @@ -136,6 +137,7 @@ def test_get_split_numbers() -> None: return +@pytest.mark.max def test_random_split(data) -> None: train, test = split_data_random(data_list=data, train_ratio=0.5) assert len(train) == 6 @@ -143,6 +145,7 @@ def test_random_split(data) -> None: return +@pytest.mark.max def test_kfold_split(data) -> None: train1, test1 = split_data_Kfold(data, n_splits=2, split=0) train2, test2 = split_data_Kfold(data, n_splits=2, split=1) @@ -151,6 +154,7 @@ def test_kfold_split(data) -> None: return +@pytest.mark.max def test_initialization() -> None: featurizer = Featurizer() featurizer = MolecularFeaturizer() @@ -159,6 +163,7 @@ def test_initialization() -> None: return +@pytest.mark.max def test_one_hot_encode(atoms, allowable_set) -> None: assert one_hot_encode(atoms[0].GetSymbol(), allowable_set) == [1.0, 0.0, 0.0] assert one_hot_encode(atoms[0].GetSymbol(), allowable_set, include_unknown_set=True) == [1.0, 0.0, 0.0, 0.0] @@ -166,6 +171,7 @@ def test_one_hot_encode(atoms, allowable_set) -> None: return +@pytest.mark.max def test_hbond_constructor(mol) -> None: factory = _ChemicalFeaturesFactory.get_instance() import os @@ -180,12 +186,14 @@ def test_hbond_constructor(mol) -> None: return +@pytest.mark.max def test_H_bonding(mol, atoms) -> None: hbond_infos = construct_hydrogen_bonding_info(mol) assert get_atom_hydrogen_bonding_one_hot(atoms[11], hbond_infos) == [1.0, 1.0] return +@pytest.mark.max def test_degree(atoms) -> None: assert np.where(get_atom_total_degree_one_hot(atoms[20])) == np.array([[1]]) assert np.where(get_atom_total_degree_one_hot(atoms[11])) == np.array([[2]]) @@ -194,6 +202,7 @@ def test_degree(atoms) -> None: return +@pytest.mark.max def test_atom_feature(mol, atoms, allowable_set) -> None: hbond_infos = construct_hydrogen_bonding_info(mol) feat = _construct_atom_feature( @@ -206,6 +215,7 @@ def test_atom_feature(mol, atoms, allowable_set) -> None: return +@pytest.mark.max def test_bond_feat(bonds) -> None: np.testing.assert_array_equal(np.where(_construct_bond_feature(bonds[0])), np.array([[3, 4, 5, 6]])) np.testing.assert_array_equal(np.where(_construct_bond_feature(bonds[6])), np.array([[0, 4, 6]])) @@ -214,6 +224,7 @@ def test_bond_feat(bonds) -> None: return +@pytest.mark.max def test_feature_vector_generation(smiles, mol, allowable_set, empty_set) -> None: featurizer = MolGraphConvFeaturizer(use_edges=True) @@ -231,6 +242,7 @@ def test_feature_vector_generation(smiles, mol, allowable_set, empty_set) -> Non return +@pytest.mark.max def test_graph_data_basics(node_features, edge_index, edge_features, node_pos_features) -> None: # Trigger all __init__() failures. with pytest.raises(TypeError): @@ -255,6 +267,7 @@ def test_graph_data_basics(node_features, edge_index, edge_features, node_pos_fe return +@pytest.mark.max def test_getters( graph_data, custom_graph_data, @@ -285,6 +298,7 @@ def test_getters( return +@pytest.mark.max def test_to_pyg( custom_graph_data, node_features, @@ -301,6 +315,7 @@ def test_to_pyg( return +@pytest.mark.max def test_custom_data_attributes(custom_data) -> None: data = custom_data assert data.smiles == "abc" @@ -321,6 +336,7 @@ def test_custom_data_attributes(custom_data) -> None: return +@pytest.mark.max def test_base_featurizer(): featurizer = Featurizer() datapoints = [1] @@ -332,6 +348,7 @@ def test_base_featurizer(): assert np.array_equal(features, np.asarray([np.array([])])) +@pytest.mark.max def test_molecular_featurizer(mol, smiles): featurizer = MolecularFeaturizer() featurizer.featurize([mol]) @@ -340,6 +357,7 @@ def test_molecular_featurizer(mol, smiles): featurizer.featurize(smiles) +@pytest.mark.max def test_exceptions(): with pytest.raises(NotInitializedError): raise NotInitializedError("msg") diff --git a/serenityff/charge/tests/test_off_plugin.py b/serenityff/charge/tests/test_off_plugin.py index 4d919a0..0a78b31 100644 --- a/serenityff/charge/tests/test_off_plugin.py +++ b/serenityff/charge/tests/test_off_plugin.py @@ -88,6 +88,7 @@ def charges_amber(): chg_atol = 0.4 +@pytest.mark.med def test_handler_functions(handler): assert handler.check_handler_compatibility(handler) is None assert handler._TAGNAME == "SerenityFFCharge" @@ -95,6 +96,7 @@ def test_handler_functions(handler): assert handler._KWARGS == ["toolkit_registry"] +@pytest.mark.med def test_off_handler_plugins(force_field_with_plugins, keys): keys.append("SerenityFFCharge") for key in keys: @@ -103,6 +105,7 @@ def test_off_handler_plugins(force_field_with_plugins, keys): force_field_with_plugins.get_parameter_handler("faulty") +@pytest.mark.med @pytest.mark.skipif( condition=are_in_CI(), reason="This test is too slow for CI", @@ -122,6 +125,7 @@ def test_off_handler_custom(force_field_custom_offxml, keys): # ------------------------------------------------ +@pytest.mark.med @pytest.mark.skipif( condition=are_in_CI(), reason="This test is too slow for CI", @@ -142,6 +146,7 @@ def test_plugin_charges_get(force_field_with_plugins, molecule, charges_amber, c ) +@pytest.mark.med @pytest.mark.skipif( condition=are_in_CI(), reason="This test is too slow for CI", @@ -162,6 +167,7 @@ def test_plugin_charges_register(force_field_with_plugins, molecule, handler, ch ) +@pytest.mark.med @pytest.mark.skipif( condition=are_in_CI(), reason="This test is too slow for CI", diff --git a/tests/conftest.py b/tests/conftest.py index cc1d77b..80003ed 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,40 +1,53 @@ -# Copyright (C) 2024-2025 ETH Zurich, Niels Maeder and other DASH contributors. - """Pytest configuration file.""" -from collections.abc import Sequence - import pytest +from collections.abc import Sequence def pytest_addoption(parser: pytest.Parser) -> None: - """Add cli option to include slow tests. - - Args: - parser (pytest.Parser): pytest parser. - """ - parser.addoption("--run-slow", action="store_true", default=False, help="Include slow tests.") + parser.addoption( + "--tier", + action="store", + default="min", + choices=("min", "med", "max"), + help="Run tests for a specific environment tier.", + ) + parser.addoption( + "--run-slow", action="store_true", default=False, help="Include slow tests." + ) def pytest_configure(config: pytest.Config) -> None: - """Add slow marker to pytest. + config.addinivalue_line("markers", "min: expected to pass with tree_ony_env.yml") + config.addinivalue_line("markers", "med: expected to pass with min_environment.yml") + config.addinivalue_line("markers", "max: expected to pass with environment.yml") + config.addinivalue_line("markers", "slow: slow running tests") - Args: - config (pytest.Config): pytest configuration. - """ - config.addinivalue_line("markers", "slow: mark test as slow to run") +def pytest_collection_modifyitems( + config: pytest.Config, items: Sequence[pytest.Item] +) -> None: + tier = config.getoption("--tier") + run_slow = config.getoption("--run-slow") -def pytest_collection_modifyitems(config: pytest.Config, items: Sequence[pytest.Item]) -> None: - """Skip slow tests if not included. + allowed_tiers = {"min"} + if tier == "med": + allowed_tiers.update({"med"}) + elif tier == "max": + allowed_tiers.update({"med", "max"}) - Args: - config (pytest.Config): pytest configuration. - items (Sequence[pytest.Item]): pytest items. - """ - if config.getoption("--run-slow"): - return - skip_slow = pytest.mark.skip(reason="need --run-slow option to run.") for item in items: - if "slow" in item.keywords: - item.add_marker(skip_slow) + item_tiers = { + mark.name + for mark in item.iter_markers() + if mark.name in {"min", "med", "max"} + } + + if item_tiers and not (item_tiers & allowed_tiers): + item.add_marker( + pytest.mark.skip(reason=f"Test requires higher tier than '{tier}'") + ) + continue + + if "slow" in item.keywords and not run_slow: + item.add_marker(pytest.mark.skip(reason="Slow test: use --run-slow to run")) diff --git a/tests/serenityff/charge/gnn/utils/test_rdkit_helper.py b/tests/serenityff/charge/gnn/utils/test_rdkit_helper.py index 91ab425..c9ad692 100644 --- a/tests/serenityff/charge/gnn/utils/test_rdkit_helper.py +++ b/tests/serenityff/charge/gnn/utils/test_rdkit_helper.py @@ -33,7 +33,7 @@ def sample_mol_missing_prop(): mol = Chem.MolFromSmiles("CCO") return mol - +@pytest.mark.max def test_get_mol_prop_as_pt_tensor_success(sample_mol_with_prop): """Test successful retrieval of property as a tensor.""" expected = pt.tensor([1.0, 2.5, -3.0], dtype=pt.float) @@ -42,6 +42,7 @@ def test_get_mol_prop_as_pt_tensor_success(sample_mol_with_prop): assert pt.equal(result, expected) +@pytest.mark.max def test_get_mol_prop_as_pt_tensor_raises_value_error_on_none_prop( sample_mol_missing_prop, ): @@ -50,6 +51,7 @@ def test_get_mol_prop_as_pt_tensor_raises_value_error_on_none_prop( get_mol_prop_as_pt_tensor(None, sample_mol_missing_prop) +@pytest.mark.max def test_get_mol_prop_as_pt_tensor_raises_value_error_on_missing_prop( sample_mol_missing_prop, ): @@ -58,6 +60,7 @@ def test_get_mol_prop_as_pt_tensor_raises_value_error_on_missing_prop( get_mol_prop_as_pt_tensor("missing_prop", sample_mol_missing_prop) +@pytest.mark.max def test_get_mol_prop_as_pt_tensor_raises_type_error_on_nan( sample_mol_with_nan_prop, ): @@ -66,6 +69,7 @@ def test_get_mol_prop_as_pt_tensor_raises_type_error_on_nan( get_mol_prop_as_pt_tensor("test_prop_nan", sample_mol_with_nan_prop) +@pytest.mark.max def test_get_mol_prop_as_np_array_success(sample_mol_with_prop): """Test successful retrieval of property as a numpy array.""" expected = np.array([1.0, 2.5, -3.0]) @@ -74,6 +78,7 @@ def test_get_mol_prop_as_np_array_success(sample_mol_with_prop): np.testing.assert_array_equal(result, expected) +@pytest.mark.max def test_get_mol_prop_as_np_array_raises_value_error_on_none_prop( sample_mol_missing_prop, ): @@ -82,6 +87,7 @@ def test_get_mol_prop_as_np_array_raises_value_error_on_none_prop( get_mol_prop_as_np_array(None, sample_mol_missing_prop) +@pytest.mark.max def test_get_mol_prop_as_np_array_raises_value_error_on_missing_prop( sample_mol_missing_prop, ): @@ -90,6 +96,7 @@ def test_get_mol_prop_as_np_array_raises_value_error_on_missing_prop( get_mol_prop_as_np_array("missing_prop", sample_mol_missing_prop) +@pytest.mark.max def test_get_mol_prop_as_np_array_raises_type_error_on_nan( sample_mol_with_nan_prop, ): diff --git a/tests/serenityff/charge/utils/test_serenityff_charge_handler.py b/tests/serenityff/charge/utils/test_serenityff_charge_handler.py index d5d3526..8618dd0 100644 --- a/tests/serenityff/charge/utils/test_serenityff_charge_handler.py +++ b/tests/serenityff/charge/utils/test_serenityff_charge_handler.py @@ -58,7 +58,6 @@ "SerenityFFCharge", ] - @pytest.fixture() def handler() -> SerenityFFChargeHandler: return SerenityFFChargeHandler(version=0.3) @@ -79,6 +78,7 @@ def molecule() -> Molecule: return Molecule.from_smiles("CCO") +@pytest.mark.med def test_handler_init(handler: SerenityFFChargeHandler) -> None: assert handler._TAGNAME == "SerenityFFCharge" assert handler._DEPENDENCIES == [ @@ -100,6 +100,7 @@ def test_handler_init(handler: SerenityFFChargeHandler) -> None: assert handler.attention_threshold == 10 +@pytest.mark.med def test_singleton() -> None: instance1 = SerenityFFChargeHandler(version=0.3) instance2 = SerenityFFChargeHandler(version=0.3) @@ -107,6 +108,7 @@ def test_singleton() -> None: assert instance1 is instance2 +@pytest.mark.med def test_loading_off_handler_plugins( force_field_custom_offxml: ForceField, force_field_with_plugins: ForceField ) -> None: @@ -117,6 +119,7 @@ def test_loading_off_handler_plugins( assert force_field_custom_offxml.get_parameter_handler(key) +@pytest.mark.med def test_plugin_charges_get_parameter_handler( force_field_with_plugins: SerenityFFChargeHandler, molecule, @@ -134,6 +137,7 @@ def test_plugin_charges_get_parameter_handler( ) +@pytest.mark.med def test_plugin_charges_register( force_field_with_plugins, molecule, @@ -152,6 +156,7 @@ def test_plugin_charges_register( ) +@pytest.mark.med def test_custom_force_field_file_charges(force_field_custom_offxml: ForceField, molecule) -> None: assert allclose( force_field_custom_offxml.get_partial_charges(molecule), From a65272dbb47f8eeb330e61e28972d8796936fefd Mon Sep 17 00:00:00 2001 From: Niels Maeder Date: Mon, 15 Dec 2025 14:46:26 +0100 Subject: [PATCH 6/8] run pre-commit --- tests/conftest.py | 18 ++++-------------- .../charge/gnn/utils/test_rdkit_helper.py | 1 + .../utils/test_serenityff_charge_handler.py | 1 + 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 80003ed..e4eca61 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,9 +12,7 @@ def pytest_addoption(parser: pytest.Parser) -> None: choices=("min", "med", "max"), help="Run tests for a specific environment tier.", ) - parser.addoption( - "--run-slow", action="store_true", default=False, help="Include slow tests." - ) + parser.addoption("--run-slow", action="store_true", default=False, help="Include slow tests.") def pytest_configure(config: pytest.Config) -> None: @@ -24,9 +22,7 @@ def pytest_configure(config: pytest.Config) -> None: config.addinivalue_line("markers", "slow: slow running tests") -def pytest_collection_modifyitems( - config: pytest.Config, items: Sequence[pytest.Item] -) -> None: +def pytest_collection_modifyitems(config: pytest.Config, items: Sequence[pytest.Item]) -> None: tier = config.getoption("--tier") run_slow = config.getoption("--run-slow") @@ -37,16 +33,10 @@ def pytest_collection_modifyitems( allowed_tiers.update({"med", "max"}) for item in items: - item_tiers = { - mark.name - for mark in item.iter_markers() - if mark.name in {"min", "med", "max"} - } + item_tiers = {mark.name for mark in item.iter_markers() if mark.name in {"min", "med", "max"}} if item_tiers and not (item_tiers & allowed_tiers): - item.add_marker( - pytest.mark.skip(reason=f"Test requires higher tier than '{tier}'") - ) + item.add_marker(pytest.mark.skip(reason=f"Test requires higher tier than '{tier}'")) continue if "slow" in item.keywords and not run_slow: diff --git a/tests/serenityff/charge/gnn/utils/test_rdkit_helper.py b/tests/serenityff/charge/gnn/utils/test_rdkit_helper.py index c9ad692..ba77f21 100644 --- a/tests/serenityff/charge/gnn/utils/test_rdkit_helper.py +++ b/tests/serenityff/charge/gnn/utils/test_rdkit_helper.py @@ -33,6 +33,7 @@ def sample_mol_missing_prop(): mol = Chem.MolFromSmiles("CCO") return mol + @pytest.mark.max def test_get_mol_prop_as_pt_tensor_success(sample_mol_with_prop): """Test successful retrieval of property as a tensor.""" diff --git a/tests/serenityff/charge/utils/test_serenityff_charge_handler.py b/tests/serenityff/charge/utils/test_serenityff_charge_handler.py index 8618dd0..bc57945 100644 --- a/tests/serenityff/charge/utils/test_serenityff_charge_handler.py +++ b/tests/serenityff/charge/utils/test_serenityff_charge_handler.py @@ -58,6 +58,7 @@ "SerenityFFCharge", ] + @pytest.fixture() def handler() -> SerenityFFChargeHandler: return SerenityFFChargeHandler(version=0.3) From 158bc9278263bea3917894927079974ae43e0935 Mon Sep 17 00:00:00 2001 From: Niels Maeder Date: Mon, 15 Dec 2025 14:52:34 +0100 Subject: [PATCH 7/8] make sure max is run on pull requests --- .github/workflows/CI.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml index 8f0755f..64c6248 100644 --- a/.github/workflows/CI.yaml +++ b/.github/workflows/CI.yaml @@ -43,7 +43,7 @@ jobs: - name: Run tests shell: bash -l {0} run: | - pytest -v --color=yes --cov=serenityff --cov-report=xml --run-slow serenityff/charge/tests/ tests/ + pytest -v --color=yes --cov=serenityff --cov-report=xml --run-slow --tier max serenityff/charge/tests/ tests/ analyze: name: Analyze From 0796a600a3397e124e66622ed2b27fc356201c13 Mon Sep 17 00:00:00 2001 From: Niels Maeder Date: Mon, 15 Dec 2025 14:52:43 +0100 Subject: [PATCH 8/8] typo --- .github/workflows/scheduled.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scheduled.yaml b/.github/workflows/scheduled.yaml index 2bce2e8..f3e85e6 100644 --- a/.github/workflows/scheduled.yaml +++ b/.github/workflows/scheduled.yaml @@ -40,4 +40,4 @@ jobs: - name: Run Tests shell: bash -l {0} run: | - pytest -v --color=yes --cov=serenityff --cov-report=xml --run-slow -- "${{ matrix.mark }}" serenityff/charge/tests/ tests/ + pytest -v --color=yes --cov=serenityff --cov-report=xml --run-slow --tier "${{ matrix.mark }}" serenityff/charge/tests/ tests/