Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

System tests on k8s #793

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/developer/hyperion/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Documentation specific for the Hyperion module within MX-Bluesky
reference/param-hierarchy
reference/readme
deploying-hyperion
system-tests

+++

Expand Down
20 changes: 20 additions & 0 deletions docs/developer/hyperion/system-tests.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
System Tests
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there are any system tests which we permanently want to leave in the option to locally run them, we should explain how to run them here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think long term the plan would be that rather than run locally directly against the ispyb dev database + expeye staging, we would run them locally against the test instances running in podman. Then we could get rid of the various ST_xxx config options.

This is what I'm currently doing, however there are a few minor wrinkles with podman that I have run into.
It's somewhat complicated:

  • There are 3 or 4 different ways of running compose files:
    • podman with podman-compose (what I'm currently using)
    • podman with docker-compose plugin (I think technically this is now the more actively developed solution)
    • docker with docker-compose standalone (Now deprecated, I didn't try this)
    • docker with docker-compose plugin (current docker solution)
    • I don't know if podman with docker-compose standalone works, I didn't try

However I seem to run into slightly different problems depending which of the above I'm using. Note that a lot of these issues are also dependent on how you are running podman/docker and whether they are running with root privileges. So it's doubly complicated as if I ran them on my laptop or at home instead of my workstation I'd probably get different results.

  • podman with podman-compose - I get caching issues with images not getting always getting pulled from gcr.io, you can work around by deleting them but this is annoying. However with the compose file I have (using the default bridging network), it doesn't expose the services to the host so you can't run the tests locally in the debugger. Unfortunately if you try host networking, then you run into other problems in that the containers can't talk to each other bc no hostnames.
  • podman with docker-cli - I ran into docker strangeness as it seems to use the docker local repository and also authentication. But also ran into similar but slightly different problems with networking. I think a lot of this is possibly due to not having root therefore it doesn't set up routing properly.
  • docker I didn't try but assume similar problems w/o root.

Bottom line is, for ideal local dev scenario, the test containers need to be routable from the outside so that you can run individual python test modules locally against the test service instances. The services also need to be routable to each other.
If you want to run the test container locally as well it needs to be able to reach the outside in order to pull from GH/pypi

The solution I have at the moment is to run in podman with the test instance via the bridging network but able to mount a local RO git repo, this is reasonable but to use it you have to shell in to the container.

It is definitely possible to get what I want with docker as I have pretty much exactly this setup on my home server, but there I have root and also the server is configured as a router. </end of rant>

============

The system tests are run by a project hosted in a GitLab private repository, `Hyperion System Testing`_.

This contains a ``.gitlab-ci`` pipeline that defines a job that runs the tests.

System Test Images
------------------

The system tests require a number of container images to run:

* The test runner image
* An ISPyB MariaDB database server image
* An ExpEye image

It is expected that these images should need to change only infrequently and builds are triggered
manually as needed - see the Hyperion System Testing project for how to rebuild them and how to trigger the tests.

.. _`Hyperion System Testing`: https://gitlab.diamond.ac.uk/MX-GDA/hyperion-system-testing
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ markers = [
"dlstbx: marks tests as requiring dlstbx (deselect with '-m \"not dlstbx\"')",
"skip_log_setup: marks tests so that loggers are not setup before the test.",
"skip_in_pycharm: marks test as not working in pycharm testrunner",
"system_test: marks tests as a system test"
]
addopts = """
--tb=native -vv --doctest-modules --doctest-glob="*.rst" --durations=10
Expand Down Expand Up @@ -165,7 +166,7 @@ legacy_tox_ini = """
[tox]
skipsdist=True

[testenv:{pre-commit,type-checking,tests,docs}]
[testenv:{pre-commit,type-checking,tests,docs,systemtests}]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: We've been calling it system-test in other places, not really bothered but would be nice to be consistent

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, dashes have a special meaning within tox:

https://tox.wiki/en/latest/user_guide.html

Test environment names can consist of alphanumeric characters and dashes; for example: py311-django42. The name will be split on dashes into multiple factors, meaning py311-django42 will be split into two factors: py311 and django42.

Copy link

@callumforrester callumforrester Feb 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, should we also change type-checking and pre-commit then?

# Don't create a virtualenv for the command, requires tox-direct plugin
direct = True
passenv = *
Expand All @@ -180,6 +181,7 @@ commands =
type-checking: pyright src tests {posargs}
tests: pytest --cov=mx_bluesky --cov-report term --cov-report xml:cov.xml {posargs}
docs: sphinx-{posargs:build -EW --keep-going} -T docs build/html
systemtests: pytest --junit-xml=systemtests_output.xml -m system_test tests/system_tests
commands_pre =
docs: /usr/bin/bash -c "{toxinidir}/utility_scripts/generate_plantuml.py > \
docs/developer/hyperion/reference/param_hierarchy.puml"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from dataclasses import dataclass
from enum import StrEnum

from requests import patch, post
from requests import JSONDecodeError, patch, post
from requests.auth import AuthBase

from mx_bluesky.common.external_interaction.ispyb.ispyb_utils import (
Expand Down Expand Up @@ -34,7 +34,11 @@ def _get_base_url_and_token() -> tuple[str, str]:
def _send_and_get_response(auth, url, data, send_func) -> dict:
response = send_func(url, auth=auth, json=data)
if not response.ok:
raise ISPyBDepositionNotMade(f"Could not write {data} to {url}: {response}")
try:
resp_txt = str(response.json())
except JSONDecodeError:
resp_txt = str(response)
raise ISPyBDepositionNotMade(f"Could not write {data} to {url}: {resp_txt}")
return response.json()


Expand Down
7 changes: 7 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1509,3 +1509,10 @@ def mock_ispyb_conn_multiscan(base_ispyb_conn):
list(range(12, 24)),
list(range(56, 68)),
)


@pytest.fixture(scope="function", autouse=True)
def clear_device_factory_caches_after_every_test(active_device_factories):
yield None
for f in active_device_factories:
f.cache_clear() # type: ignore
12 changes: 12 additions & 0 deletions tests/system_tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import re
from decimal import Decimal
from unittest.mock import AsyncMock, patch
Expand All @@ -8,6 +9,8 @@
from dodal.devices.oav.oav_parameters import OAVConfig
from ophyd_async.testing import set_mock_value

from mx_bluesky.hyperion.parameters.constants import CONST

# Map all the case-sensitive column names from their normalised versions
DATA_COLLECTION_COLUMN_MAP = {
s.lower(): s
Expand Down Expand Up @@ -119,6 +122,15 @@
}


@pytest.fixture(autouse=True)
def use_dev_ispyb_unless_overridden_by_environment():
ispyb_config_path = os.environ.get(
"ISPYB_CONFIG_PATH", CONST.SIM.DEV_ISPYB_DATABASE_CFG
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is ISPYB_CONFIG_PATH set? I can't find a corresponding name in the Dockerfiles

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't override it for the containerised system tests - the test credentials are mounted to the default place.

)
with patch.dict(os.environ, {"ISPYB_CONFIG_PATH": ispyb_config_path}):
yield


@pytest.fixture
def undulator_for_system_test(undulator):
set_mock_value(undulator.current_gap, 1.11)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,8 @@ def event_monitor(monitor: zmq.Socket, connection_active_lock: threading.Lock) -
@pytest.fixture
def RE_with_external_callbacks():
RE = RunEngine()
old_ispyb_config = os.environ.get("ISPYB_CONFIG_PATH")

process_env = os.environ.copy()
process_env["ISPYB_CONFIG_PATH"] = CONST.SIM.DEV_ISPYB_DATABASE_CFG

external_callbacks_process = subprocess.Popen(
[
Expand Down Expand Up @@ -119,8 +117,6 @@ def RE_with_external_callbacks():
external_callbacks_process.kill()
external_callbacks_process.wait(10)
t.join()
if old_ispyb_config:
os.environ["ISPYB_CONFIG_PATH"] = old_ispyb_config


@pytest.mark.s03
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import os
from time import sleep
from unittest.mock import patch

import pytest
from requests import get
Expand All @@ -9,20 +7,11 @@
BLSampleStatus,
ExpeyeInteraction,
)
from mx_bluesky.hyperion.parameters.constants import CONST

CONTAINER_ID = 288588
SAMPLE_ID = 5289780


@pytest.fixture(autouse=True)
def use_dev_ispyb():
with patch.dict(
os.environ, {"ISPYB_CONFIG_PATH": CONST.SIM.DEV_ISPYB_DATABASE_CFG}
):
yield


@pytest.mark.s03
@pytest.mark.parametrize(
"message, expected_message",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from __future__ import annotations

import os
from collections.abc import Callable, Sequence
from copy import deepcopy
from typing import Any, Literal
Expand Down Expand Up @@ -332,7 +331,6 @@ def test_ispyb_deposition_in_gridscan(
fetch_datacollection_grid_attribute: Callable[..., Any],
fetch_datacollection_position_attribute: Callable[..., Any],
):
os.environ["ISPYB_CONFIG_PATH"] = CONST.SIM.DEV_ISPYB_DATABASE_CFG
set_mock_value(
grid_detect_then_xray_centre_composite.s4_slit_gaps.xgap.user_readback, 0.1
)
Expand Down Expand Up @@ -474,7 +472,6 @@ def test_ispyb_deposition_in_rotation_plan(
fetch_datacollection_position_attribute: Callable[..., Any],
feature_flags_update_with_omega_flip,
):
os.environ["ISPYB_CONFIG_PATH"] = CONST.SIM.DEV_ISPYB_DATABASE_CFG
ispyb_cb = RotationISPyBCallback()
RE.subscribe(ispyb_cb)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
from ophyd_async.core import AsyncStatus
from ophyd_async.testing import set_mock_value

from mx_bluesky.common.external_interaction.callbacks.common.ispyb_mapping import (
get_proposal_and_session_from_visit_string,
)
from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import (
GridscanISPyBCallback,
)
Expand Down Expand Up @@ -47,12 +50,16 @@
)
from .conftest import raw_params_from_file

SAMPLE_ID = int(os.environ.get("ST_SAMPLE_ID", 5461074))


@pytest.fixture
def load_centre_collect_params():
json_dict = raw_params_from_file(
"tests/test_data/parameter_json_files/example_load_centre_collect_params.json"
)
json_dict["visit"] = os.environ.get("ST_VISIT", "cm37235-4")
json_dict["sample_id"] = SAMPLE_ID
return LoadCentreCollect(**json_dict)


Expand Down Expand Up @@ -105,7 +112,7 @@ def load_centre_collect_composite(


GRID_DC_1_EXPECTED_VALUES = {
"BLSAMPLEID": 5461074,
"BLSAMPLEID": SAMPLE_ID,
"detectorid": 78,
"axisstart": 0.0,
"axisrange": 0,
Expand Down Expand Up @@ -186,8 +193,6 @@ def load_centre_collect_composite(
"6}_oav_snapshot_270\\.png",
}

SAMPLE_ID = 5461074


@pytest.fixture
def composite_with_no_diffraction(
Expand All @@ -203,15 +208,7 @@ async def mock_zocalo_complete():
yield load_centre_collect_composite


@pytest.fixture(autouse=True)
def use_dev_ispyb():
with patch.dict(
os.environ, values={"ISPYB_CONFIG_PATH": CONST.SIM.DEV_ISPYB_DATABASE_CFG}
):
yield


@pytest.mark.s03
@pytest.mark.system_test
def test_execute_load_centre_collect_full(
load_centre_collect_composite: LoadCentreCollectComposite,
load_centre_collect_params: LoadCentreCollect,
Expand Down Expand Up @@ -245,7 +242,13 @@ def test_execute_load_centre_collect_full(
)
)

robot_load_cb.expeye.start_load.assert_called_once_with("cm37235", 4, 5461074, 2, 6)
expected_proposal, expected_visit = get_proposal_and_session_from_visit_string(
load_centre_collect_params.visit
)
expected_sample_id = load_centre_collect_params.sample_id
robot_load_cb.expeye.start_load.assert_called_once_with(
expected_proposal, expected_visit, expected_sample_id, 2, 6
)
# TODO re-enable this https://github.com/DiamondLightSource/mx-bluesky/issues/690
# robot_load_cb.expeye.update_barcode_and_snapshots.assert_called_once_with(
# 1234,
Expand All @@ -258,7 +261,7 @@ def test_execute_load_centre_collect_full(
# Compare gridscan collection
compare_actual_and_expected(
ispyb_gridscan_cb.ispyb_ids.data_collection_group_id,
{"experimentType": "Mesh3D", "blSampleId": 5461074},
{"experimentType": "Mesh3D", "blSampleId": expected_sample_id},
fetch_datacollectiongroup_attribute,
)
compare_actual_and_expected(
Expand Down Expand Up @@ -293,7 +296,7 @@ def test_execute_load_centre_collect_full(
rotation_dc_ids = fetch_datacollection_ids_for_group_id(rotation_dcg_id)
compare_actual_and_expected(
rotation_dcg_id,
{"experimentType": "SAD", "blSampleId": 5461074},
{"experimentType": "SAD", "blSampleId": expected_sample_id},
fetch_datacollectiongroup_attribute,
)
compare_actual_and_expected(
Expand All @@ -312,10 +315,10 @@ def test_execute_load_centre_collect_full(
ispyb_rotation_cb.ispyb_ids.data_collection_ids[0],
"Sample position (µm): (-2309, -591, 341) Hyperion Rotation Scan - Aperture: Small. ",
)
assert fetch_blsample(SAMPLE_ID).blSampleStatus == "LOADED" # type: ignore
assert fetch_blsample(expected_sample_id).blSampleStatus == "LOADED" # type: ignore


@pytest.mark.s03
@pytest.mark.system_test
def test_load_centre_collect_updates_bl_sample_status_robot_load_fail(
load_centre_collect_composite: LoadCentreCollectComposite,
load_centre_collect_params: LoadCentreCollect,
Expand Down Expand Up @@ -343,10 +346,13 @@ def test_load_centre_collect_updates_bl_sample_status_robot_load_fail(
)
)

assert fetch_blsample(SAMPLE_ID).blSampleStatus == "ERROR - beamline"
assert (
fetch_blsample(load_centre_collect_params.sample_id).blSampleStatus
== "ERROR - beamline"
)


@pytest.mark.s03
@pytest.mark.system_test
def test_load_centre_collect_updates_bl_sample_status_pin_tip_detection_fail(
load_centre_collect_composite: LoadCentreCollectComposite,
load_centre_collect_params: LoadCentreCollect,
Expand Down Expand Up @@ -375,10 +381,13 @@ def test_load_centre_collect_updates_bl_sample_status_pin_tip_detection_fail(
)
)

assert fetch_blsample(SAMPLE_ID).blSampleStatus == "ERROR - sample"
assert (
fetch_blsample(load_centre_collect_params.sample_id).blSampleStatus
== "ERROR - sample"
)


@pytest.mark.s03
@pytest.mark.system_test
def test_load_centre_collect_updates_bl_sample_status_no_beamstop(
load_centre_collect_composite: LoadCentreCollectComposite,
load_centre_collect_params: LoadCentreCollect,
Expand All @@ -399,10 +408,13 @@ def test_load_centre_collect_updates_bl_sample_status_no_beamstop(
)
)

assert fetch_blsample(SAMPLE_ID).blSampleStatus == "ERROR - beamline"
assert (
fetch_blsample(load_centre_collect_params.sample_id).blSampleStatus
== "ERROR - beamline"
)


@pytest.mark.s03
@pytest.mark.system_test
def test_load_centre_collect_updates_bl_sample_status_grid_detection_fail_tip_not_found(
load_centre_collect_composite: LoadCentreCollectComposite,
load_centre_collect_params: LoadCentreCollect,
Expand Down Expand Up @@ -449,10 +461,13 @@ def wait_for_first_oav_grid(name: str, doc: dict):
)
)

assert fetch_blsample(SAMPLE_ID).blSampleStatus == "ERROR - sample"
assert (
fetch_blsample(load_centre_collect_params.sample_id).blSampleStatus
== "ERROR - sample"
)


@pytest.mark.s03
@pytest.mark.system_test
def test_load_centre_collect_updates_bl_sample_status_gridscan_no_diffraction(
composite_with_no_diffraction: LoadCentreCollectComposite,
load_centre_collect_params: LoadCentreCollect,
Expand All @@ -478,10 +493,13 @@ def test_load_centre_collect_updates_bl_sample_status_gridscan_no_diffraction(
)
)

assert fetch_blsample(SAMPLE_ID).blSampleStatus == "ERROR - sample"
assert (
fetch_blsample(load_centre_collect_params.sample_id).blSampleStatus
== "ERROR - sample"
)


@pytest.mark.s03
@pytest.mark.system_test
def test_load_centre_collect_updates_bl_sample_status_rotation_failure(
load_centre_collect_composite: LoadCentreCollectComposite,
load_centre_collect_params: LoadCentreCollect,
Expand Down Expand Up @@ -513,4 +531,7 @@ def test_load_centre_collect_updates_bl_sample_status_rotation_failure(
)
)

assert fetch_blsample(SAMPLE_ID).blSampleStatus == "ERROR - beamline"
assert (
fetch_blsample(load_centre_collect_params.sample_id).blSampleStatus
== "ERROR - beamline"
)
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ def run_zocalo_with_dev_ispyb(
async def inner(sample_name="", fallback=np.array([0, 0, 0])):
dummy_params.file_name = sample_name
_, ispyb_callback = create_gridscan_callbacks()
ispyb_callback.ispyb_config = dummy_ispyb_3d.ISPYB_CONFIG_PATH
RE.subscribe(ispyb_callback)

@bpp.set_run_key_decorator("testing123")
Expand Down
Loading
Loading