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

Refactor system tests to use the new FTP server and to run in pytest #301

Merged
merged 76 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
c2f5555
Remove the old FTP server that was giving us problems
jeremyestein Feb 14, 2024
fe1d5bb
Reconfigure the FTP server for being accessed from inside a container,
jeremyestein Feb 14, 2024
e79bc06
Convert FTPS upload check script into pytest tests (DICOM not passing
jeremyestein Feb 14, 2024
8382b9f
Convert ehr_anon test to pytest
jeremyestein Feb 14, 2024
8f49652
Merge branch 'main' into jeremy/refactor-system-test
jeremyestein Feb 14, 2024
40f8aae
Allow unset parameter
jeremyestein Feb 14, 2024
f7f2bbe
linting fix
jeremyestein Feb 14, 2024
5616717
Remove unneeded GHA step; add python deps which the test script used to
jeremyestein Feb 14, 2024
bbf47dd
Not sure why pip install was failing
jeremyestein Feb 14, 2024
7c607d5
test not tests
jeremyestein Feb 14, 2024
3ba073d
I'll get there in the end
jeremyestein Feb 14, 2024
0917d19
Try again
jeremyestein Feb 14, 2024
7376fd5
Check radiology parquet file
jeremyestein Feb 14, 2024
cccfcd5
pip fix
jeremyestein Feb 14, 2024
522df15
pip fix2
jeremyestein Feb 14, 2024
e2fbda6
Dump docker logs on failure
jeremyestein Feb 15, 2024
545792b
Predict we'll need this to work on linux docker
jeremyestein Feb 14, 2024
176006f
Is always() better?
jeremyestein Feb 15, 2024
3c0e16c
gha syntax fail
jeremyestein Feb 15, 2024
68e88ae
Is this step causing the hanging problem?
jeremyestein Feb 15, 2024
11a3e53
See if we have more luck doing it my own way
jeremyestein Feb 15, 2024
8f14159
More docker logs; not that this works at all...
jeremyestein Feb 15, 2024
01eacd1
Maybe this step was not as unneeded as I thought?
jeremyestein Feb 15, 2024
fcf948c
Increase pytest verbosity
jeremyestein Feb 15, 2024
b524dd9
Add some debugging statements so we can see how far things are getting
jeremyestein Feb 15, 2024
6dc4cda
Put in more debugging and set +e so we can still run stuff after the
jeremyestein Feb 15, 2024
6f33f38
pytest itself appears to be hanging - run it in the background and see
jeremyestein Feb 15, 2024
071d83b
Set FTP_HOST dynamically depending on docker host OS
jeremyestein Feb 15, 2024
11dbf1f
Speculative fix for FTP server hanging issue
jeremyestein Feb 15, 2024
71a9051
Fix type annotation
jeremyestein Feb 15, 2024
a09b0ea
Remove a load of speculative/debugging changes so I can get back to
jeremyestein Feb 15, 2024
3697fac
Fix warning related to no quotes
jeremyestein Feb 15, 2024
cc43d27
I think the simpler way of finding the docker host will work now
jeremyestein Feb 15, 2024
7151ea7
Make sure stderr is captured too
jeremyestein Feb 15, 2024
dde9e4b
Merge branch 'main' into jeremy/refactor-system-test
jeremyestein Feb 16, 2024
43acb90
Merge branch 'main' into jeremy/refactor-system-test
jeremyestein Feb 19, 2024
fd0bb98
Quite large workaround for small, annoying bug in ftplib that was
jeremyestein Feb 16, 2024
badc8d6
Remove temp prints, sleeps and other temp/debugging code
jeremyestein Feb 19, 2024
1d4252d
Also log orthanc-anon to see why that's failing
jeremyestein Feb 20, 2024
103374c
Works for me, but let's try some temporary sleeps to see if the problem
jeremyestein Feb 20, 2024
1faf506
Switch docker build cache to orthanc-anon since that's now the slow one
jeremyestein Feb 20, 2024
9882fa3
Merge branch 'main' into jeremy/refactor-system-test
jeremyestein Feb 20, 2024
c17abfc
See if this is needed on GHA
jeremyestein Feb 20, 2024
39b9c93
Bind to the correct interface on linux (possibly)
jeremyestein Feb 20, 2024
30a922b
Which host to bind the test FTP server to also depends on the particular
jeremyestein Feb 20, 2024
d8d0b04
Fix docs and linting
jeremyestein Feb 20, 2024
5dc18fa
More debugging
jeremyestein Feb 20, 2024
4d33f13
Fix enum oops
jeremyestein Feb 20, 2024
a04be7f
Try build cache on passing imaging api tests (#313)
stefpiatek Feb 21, 2024
2a759b6
Test only fails on GHA - add some debugging to find out why
jeremyestein Feb 21, 2024
b7c4783
Yet more debugging - are directories empty to start with?
jeremyestein Feb 21, 2024
f6522f2
More debugging - how are files in wrong dirs getting creating?
jeremyestein Feb 21, 2024
bae521d
Logging in successful subcommands was being missed
jeremyestein Feb 21, 2024
2a86e16
Attempt to fix caching keys for orthanc image build
jeremyestein Feb 21, 2024
678ae4c
I think the exports directory is being determined to be the installed…
jeremyestein Feb 21, 2024
6dbe775
Remove excessive logging, add some other
jeremyestein Feb 21, 2024
8c30d4a
Add more generic restore key
jeremyestein Feb 21, 2024
a3621bf
Remove debugging
jeremyestein Feb 21, 2024
5fd9064
Merge branch 'main' into jeremy/refactor-system-test
jeremyestein Feb 22, 2024
b30a7d1
Fix linting
jeremyestein Feb 22, 2024
3775c73
Move helper method to the pytest-pixl package, as per review comment
jeremyestein Feb 22, 2024
3b6a5d9
Move test script to its own pytest test without much modification so far
jeremyestein Feb 22, 2024
0b148ba
minor linting fixes
jeremyestein Feb 22, 2024
1bc5082
Correct dependency?
jeremyestein Feb 22, 2024
dc19bd0
Add warning that system tests don't quite tidy up after themselves
jeremyestein Feb 22, 2024
b1b5491
Further linting fixes
jeremyestein Feb 22, 2024
e1b556c
Merge branch 'main' into jeremy/refactor-system-test
jeremyestein Feb 22, 2024
204e17e
Reference issue
jeremyestein Feb 22, 2024
c1e8ff3
Delete tests as per review comment
jeremyestein Feb 23, 2024
6547385
Abstract out the "wait for condition for N seconds" pattern
jeremyestein Feb 23, 2024
bd0ebbb
Inline method
jeremyestein Feb 23, 2024
ca8ecb9
Remove TODO
jeremyestein Feb 23, 2024
807e561
Merge branch 'main' into jeremy/refactor-system-test
jeremyestein Feb 26, 2024
bfc24a5
Make fuller use of run_subprocess wrapper
jeremyestein Feb 26, 2024
5d17973
Add pytest-pixl as test dependency to pixl ehr
jeremyestein Feb 26, 2024
a754b97
Apparently it needs to be installed before being referenced in
jeremyestein Feb 26, 2024
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
16 changes: 10 additions & 6 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -212,12 +212,6 @@ jobs:
timeout-minutes: 30
steps:
- uses: actions/checkout@v3
- name: Prune docker volumes
# seems like we're getting an error from ftp-server exiting with zero status code
# this is a workaround that resolves it locally 🤞
run: |
docker volume prune --force

- uses: docker/setup-buildx-action@v3
# pre-build and cache the postgres container as installing python3 takes a while, doesn't push
- name: Cache Docker layers
Expand Down Expand Up @@ -247,6 +241,12 @@ jobs:
python-version: 3.10.6
cache: "pip"

- name: Install Python dependencies
run: |
pip install pixl_core/
pip install cli/[test]
pip install pytest-pixl/

- name: Build test services
working-directory: test
run: |
Expand All @@ -260,3 +260,7 @@ jobs:
working-directory: test
run: |
./run-system-test.sh

- name: Dump docker logs on failure
if: failure()
uses: jwalton/gh-docker-logs@v2
5 changes: 4 additions & 1 deletion cli/src/pixl_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,10 @@ def extract_radiology_reports(parquet_dir: Path) -> None:

success_code = 200
if response.status_code != success_code:
msg = f"Failed to run export-patient-data due to: {response.text}"
msg = (
f"Failed to run export-patient-data due to: "
f"error code {response.status_code} {response.text}"
)
raise RuntimeError(msg)


Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,8 @@ services:
retries: 5
networks:
- pixl-net
extra_hosts: # needed for linux
- "host.docker.internal:host-gateway"
volumes:
- ${PWD}/exports:/run/exports

Expand Down
5 changes: 3 additions & 2 deletions pixl_core/src/core/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ def upload_dicom_image(zip_content: BinaryIO, pseudo_anon_id: str) -> None:

# Store the file using a binary handler
try:
logger.info("Running command %s", command)
ftp.storbinary(command, zip_content)
except ftplib.all_errors as ftp_error:
ftp.quit()
Expand Down Expand Up @@ -140,8 +141,8 @@ def _connect_to_ftp() -> FTP_TLS:
ftp.login(ftp_user, ftp_password)
ftp.prot_p()
except ftplib.all_errors as ftp_error:
error_msg = "Failed to connect to FTPS server: '%s'"
raise ConnectionError(error_msg, ftp_error) from ftp_error
error_msg = f"Failed to connect to FTPS server: {ftp_user}@{ftp_host}:{ftp_port}"
jeremyestein marked this conversation as resolved.
Show resolved Hide resolved
raise ConnectionError(error_msg) from ftp_error
return ftp


Expand Down
13 changes: 7 additions & 6 deletions pytest-pixl/src/pytest_pixl/ftpserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,19 +73,20 @@ class PixlFTPServer:
:type home_dir: str
"""

def __init__(self, home_dir: str) -> None:
def __init__(self, home_root: Path) -> None:
"""
Initialise the FTPS server. Sets the hostname, port, username and password
from the corresponding environment variables.
:param home_dir: The home directory for the FTP server. This is the directory where the
user will be placed after login.
:param home_root: The directory where the user's home directory will be created.
The home dir is the directory where the user will be placed after login.
"""
self.host = config("FTP_HOST", default="127.0.0.1")
stefpiatek marked this conversation as resolved.
Show resolved Hide resolved
self.host = "127.0.0.1"
self.port = int(config("FTP_PORT", default=20021))

self.user_name = config("FTP_USER_NAME", default="pixl_user")
self.user_password = config("FTP_USER_PASSWORD", default="longpassword")
self.home_dir = home_dir
self.home_dir: Path = home_root / self.user_name
self.home_dir.mkdir()

self.certfile = Path(__file__).parents[1] / "resources" / "ssl" / "localhost.crt"
self.keyfile = Path(__file__).parents[1] / "resources" / "ssl" / "localhost.key"
Expand All @@ -103,7 +104,7 @@ def _add_user(self) -> None:
will be a directory on the local filesystem!
"""
self.authorizer.add_user(
self.user_name, self.user_password, self.home_dir, perm=USER_PERMISSIONS
self.user_name, self.user_password, str(self.home_dir), perm=USER_PERMISSIONS
)

def _setup_TLS_handler(self) -> None:
Expand Down
8 changes: 3 additions & 5 deletions pytest-pixl/src/pytest_pixl/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
# limitations under the License.
"""Pytest fixtures."""

import os
import threading
from collections.abc import Generator

Expand All @@ -22,15 +21,14 @@
from pytest_pixl.ftpserver import PixlFTPServer


@pytest.fixture(scope="module")
@pytest.fixture(scope="session")
def ftps_server(tmp_path_factory) -> Generator[PixlFTPServer, None, None]:
"""
Spins up an FTPS server in a separate process for testing. Configuration is controlled by the
FTP_* environment variables.
"""
tmp_home_dir = tmp_path_factory.mktemp("ftps_server") / os.environ["FTP_USER_NAME"]
tmp_home_dir.mkdir()
ftps_server = PixlFTPServer(home_dir=str(tmp_home_dir))
tmp_home_dir_root = tmp_path_factory.mktemp("ftps_server")
ftps_server = PixlFTPServer(home_root=tmp_home_dir_root)
thread = threading.Thread(target=ftps_server.server.serve_forever)
thread.start()
yield ftps_server
Expand Down
2 changes: 1 addition & 1 deletion ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,5 @@ mccabe.max-complexity = 18
exclude=["scripts"]

[extend-per-file-ignores]
"**/tests/*" = ["PLR2004"] # Magic value used in comparison
"**/test*/*" = ["PLR2004"] # Magic value used in comparison
"hasher/tests/*" = ["ARG001"] # unused function argument
6 changes: 3 additions & 3 deletions test/.env
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ PIXL_EHR_API_PORT=7006
PIXL_IMAGING_API_PORT=7007
RABBITMQ_PORT=7008
RABBITMQ_ADMIN_PORT=7009
FTP_PORT=21
FTP_PORT=20021

# PIXL Orthanc raw instance
ORTHANC_RAW_USERNAME=orthanc_raw_username
Expand Down Expand Up @@ -62,6 +62,6 @@ RABBITMQ_USERNAME=rabbitmq_username
RABBITMQ_PASSWORD=rabbitmq_password

# FTP server
FTP_HOST=ftp-server
FTP_USER_NAME=ftp_username
FTP_HOST=host.docker.internal
FTP_USER_NAME=pixl_user
FTP_USER_PASSWORD=longpassword
14 changes: 14 additions & 0 deletions test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@ You can run the system test with:
./run-system-test.sh
```

Or to do all the setup but not run any tests:
```bash
./run-system-test.sh setup
```

You can then develop and run tests repeatedly with `pytest` or through your IDE.
But you are responsible for knowing
when to re-run the setup if something it depends on has changed.

Run the following to teardown:
```bash
./run-system-test.sh teardown
```

## File organisation

### PIXL configuration
Expand Down
7 changes: 1 addition & 6 deletions test/scripts/check_entry_in_pixl_anon.sh → test/check_utils.py
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#!/bin/bash
# Copyright (c) University College London Hospitals NHS Foundation Trust
#
# Licensed under the Apache License, Version 2.0 (the "License");
Expand All @@ -12,9 +11,5 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -euxo pipefail

_sql_command="select * from emap_data.ehr_anon"
_result=$(docker exec system-test-postgres-1 /bin/bash -c \
"PGPASSWORD=pixl_db_password psql -U pixl_db_username -d pixl -c \"$_sql_command\"")
echo "$_result" | grep -q "2 row"
"""Utilities for the system test"""
89 changes: 89 additions & 0 deletions test/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Copyright (c) University College London Hospitals NHS Foundation Trust
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""System/E2E test setup"""
import logging
import subprocess
from pathlib import Path

import pytest

pytest_plugins = "pytest_pixl"


@pytest.fixture()
def host_export_root_dir():
"""Intermediate export dir as seen from the host"""
return Path(__file__).parents[1] / "exports"


def run_subprocess(
jeremyestein marked this conversation as resolved.
Show resolved Hide resolved
cmd: list[str], working_dir: Path, *, shell=False, timeout=360
) -> subprocess.CompletedProcess[bytes]:
"""
Run a command but capture the stderr and stdout better than the CalledProcessError
string representation does
"""
try:
logging.info("Running command %s", cmd)
return subprocess.run(
cmd,
check=True,
cwd=working_dir,
shell=shell, # noqa: S603 input is trusted
timeout=timeout,
capture_output=True,
)
except subprocess.CalledProcessError as exception:
logging.error("*** exception occurred running: '%s'", cmd) # noqa: TRY400 will raise anyway
logging.error("*** stdout:\n%s", exception.stdout.decode()) # noqa: TRY400
logging.error("*** stderr:\n%s", exception.stderr.decode()) # noqa: TRY400
peshence marked this conversation as resolved.
Show resolved Hide resolved
raise


TEST_DIR = Path(__file__).parent
RESOURCES_DIR = TEST_DIR / "resources"
RESOURCES_OMOP_DIR = RESOURCES_DIR / "omop"


@pytest.fixture(scope="session")
def _setup_pixl_cli(ftps_server) -> None:
"""Run pixl populate/start. Cleanup intermediate export dir on exit."""
# CLI calls need to have CWD = test dir so they can find the pixl_config.yml file
run_subprocess(["pixl", "populate", str(RESOURCES_OMOP_DIR.absolute())], TEST_DIR)
run_subprocess(["pixl", "start"], TEST_DIR)
# poll here for two minutes to check for imaging to be processed, printing progress
yield
run_subprocess(
[
"docker",
"exec",
"system-test-ehr-api-1",
"rm",
"-r",
"/run/exports/test-extract-uclh-omop-cdm/",
],
TEST_DIR,
)


@pytest.fixture(scope="session")
def _extract_radiology_reports(_setup_pixl_cli) -> None:
"""
run pixl extract-radiology-reports.
TODO: should then poll for two minutes to ensure that the database is populated,
jeremyestein marked this conversation as resolved.
Show resolved Hide resolved
as per ./scripts/check_entry_in_pixl_anon.sh
"""
run_subprocess(
["pixl", "extract-radiology-reports", str(RESOURCES_OMOP_DIR.absolute())], TEST_DIR
)
23 changes: 0 additions & 23 deletions test/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,26 +74,3 @@ services:
retries: 5
networks:
pixl-net:

ftp-server:
container_name: system-test-ftp-server
build:
context: ./dummy-services/ftp-server
ports:
- "127.0.0.1:20021:21"
- "21000-21010:21000-21010"
volumes:
# Mount for uploaded data
- "./dummy-services/ftp-server/mounts/data/:/home/${FTP_USER_NAME}/"
environment:
ADDRESS: ${FTP_HOST}
USERS: ${FTP_USER_NAME}|${FTP_USER_PASSWORD}|/home/${FTP_USER_NAME}
TLS_KEY: /etc/ssl/private/localhost.key
TLS_CERT: /etc/ssl/private/localhost.crt
healthcheck:
test: ping localhost:21 -w 2
interval: 10s
timeout: 5s
retries: 5
networks:
pixl-net:
28 changes: 0 additions & 28 deletions test/dummy-services/ftp-server/Dockerfile

This file was deleted.

19 changes: 0 additions & 19 deletions test/dummy-services/ftp-server/ssl/localhost.crt

This file was deleted.

28 changes: 0 additions & 28 deletions test/dummy-services/ftp-server/ssl/localhost.key

This file was deleted.

Loading
Loading