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 72 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
65 changes: 30 additions & 35 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,30 +143,6 @@ jobs:
timeout-minutes: 30
steps:
- uses: actions/checkout@v3

- 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
uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- uses: docker/build-push-action@v5
with:
context: .
file: docker/postgres/Dockerfile
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
- # Temp fix
# https://github.com/docker/build-push-action/issues/252
# https://github.com/moby/buildkit/issues/1896
name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache

- name: Init Python
uses: actions/setup-python@v4
with:
Expand All @@ -187,7 +163,6 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@v3

- name: Init Python
uses: actions/setup-python@v4
with:
Expand All @@ -209,25 +184,21 @@ 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
# pre-build and cache the orthanc-anon image: installing python3 takes a while, doesn't push
- name: Cache Docker layers
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
key: ${{ runner.os }}-buildx-${{ github.ref }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-${{ github.ref }}-
${{ runner.os }}-buildx-
save-always: true
- uses: docker/build-push-action@v5
with:
context: .
file: docker/postgres/Dockerfile
file: docker/orthanc-anon/Dockerfile
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
- # Temp fix
Expand All @@ -244,6 +215,14 @@ jobs:
python-version: 3.10.6
cache: "pip"

- name: Install Python dependencies
# The -e option is required here due to the way the
# code determines the export directory. See issue #318.
run: |
pip install -e pixl_core/
pip install -e cli/[test]
pip install -e pytest-pixl/

- name: Build test services
working-directory: test
run: |
Expand All @@ -257,3 +236,19 @@ jobs:
working-directory: test
run: |
./run-system-test.sh
echo FINISHED SYSTEM TEST SCRIPT

- name: Dump ehr-api docker logs for debugging
if: ${{ failure() }}
run: |
docker logs -t system-test-ehr-api-1 2>&1

- name: Dump orthanc-anon docker logs for debugging
if: ${{ failure() }}
run: |
docker logs -t system-test-orthanc-anon-1 2>&1

- name: Dump imaging-api docker logs for debugging
if: ${{ failure() }}
run: |
docker logs -t system-test-imaging-api-1 2>&1
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
7 changes: 7 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ services:
- ${PWD}/orthanc/orthanc-anon/config:/run/secrets:ro
networks:
- pixl-net
# needed for same reason as ehr-api
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
postgres:
condition: service_healthy
Expand Down Expand Up @@ -253,6 +256,10 @@ services:
retries: 5
networks:
- pixl-net
# needed for testing under GHA (linux), so this container
# can reach the test FTP server running on the docker host
extra_hosts:
jeremyestein marked this conversation as resolved.
Show resolved Hide resolved
- "host.docker.internal:host-gateway"
volumes:
- ${PWD}/exports:/run/exports

Expand Down
33 changes: 33 additions & 0 deletions docker/orthanc-anon/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,39 @@
# limitations under the License.
FROM osimis/orthanc:22.9.0-full-stable
SHELL ["/bin/bash", "-o", "pipefail", "-e", "-u", "-x", "-c"]
# need some packages for building python 3.9.13
RUN export DEBIAN_FRONTEND=noninteractive \
&& apt update \
&& apt install --yes --no-install-recommends \
build-essential \
libbz2-dev \
libffi-dev \
libgdbm-dev \
libncurses5-dev \
libnss3-dev \
libreadline-dev \
libsqlite3-dev \
libssl-dev \
zlib1g-dev \
&& apt-get autoremove \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

# Default for Debian Bullseye (11) is python 3.9.2, which has a bug in ftplib that is
# affecting us.
# See https://bugs.python.org/issue43285 for more details. It only happens in the system
# test, when docker is using NAT to implement host.docker.internal
# Install 3.9.13 alongside the existing version (removing the original python beforehand
# seems to break orthanc)
ADD https://www.python.org/ftp/python/3.9.13/Python-3.9.13.tgz /python3913/
RUN cd /python3913 \
&& tar -xvf Python-3.9.13.tgz \
&& cd Python-3.9.13 \
&& ./configure --enable-optimizations \
&& make -j `nproc` \
&& make install \
&& make clean \
&& rm -fr /python3913

# Install requirements before copying modules
COPY ./pixl_core/pyproject.toml ./pixl_core/pyproject.toml
Expand Down
3 changes: 3 additions & 0 deletions orthanc/orthanc-anon/plugin/pixl.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ def AzureAccessToken():
return response.json()["access_token"]


TIMER = None


def AzureDICOMTokenRefresh():
"""
Refresh Azure DICOM token
Expand Down
4 changes: 3 additions & 1 deletion pixl_core/src/core/exports.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,10 @@ def copy_to_exports(self, input_omop_dir: pathlib.Path) -> str:
"""
public_input = input_omop_dir / "public"

logging.info("Copying public parquet files from %s to %s", public_input, self.public_output)

# Make directory for exports if they don't exist
ParquetExport._mkdir(self.public_output)
logger.info("Copying public parquet files from %s to %s", public_input, self.public_output)

# Copy extract files, overwriting if it exists
shutil.copytree(public_input, self.public_output, dirs_exist_ok=True)
Expand All @@ -96,6 +97,7 @@ def export_radiology(self, export_df: pd.DataFrame) -> pathlib.Path:
"""Export radiology reports to parquet file"""
self._mkdir(self.radiology_output)
parquet_file = self.radiology_output / "radiology.parquet"
logging.info("Exporting radiology to %s", self.radiology_output)
export_df.to_parquet(parquet_file)
# We are not responsible for making the "latest" symlink, see `copy_to_exports`.
# This avoids the confusion caused by EHR API (which calls export_radiology) having a
Expand Down
7 changes: 4 additions & 3 deletions pixl_core/src/core/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,11 @@ 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()
error_msg = "Failed to run STOR command '%s': '%s'"
error_msg = f"Failed to run STOR command : {command}"
raise ConnectionError(error_msg, command, ftp_error) from ftp_error

# Close the FTP connection
Expand Down Expand Up @@ -159,8 +160,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
7 changes: 7 additions & 0 deletions pixl_core/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import pytest
from core.db.models import Base, Extract, Image
from core.exports import ParquetExport
from pytest_pixl.plugin import FtpHostAddress
from sqlalchemy import Engine, create_engine
from sqlalchemy.orm import Session, sessionmaker

Expand Down Expand Up @@ -79,6 +80,12 @@ def ftps_home_dir(ftps_server) -> Generator[Path, None, None]:
return Path(ftps_server.home_dir)


@pytest.fixture(scope="session")
def ftp_host_address():
"""Run FTP on localhost - no docker containers need to access it"""
return FtpHostAddress.LOCALHOST


@pytest.fixture()
def test_zip_content() -> BinaryIO:
"""Directory containing the test data for uploading to the ftp server."""
Expand Down
25 changes: 15 additions & 10 deletions pytest-pixl/src/pytest_pixl/ftpserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""A ligthweight FTPS server supporting implicit SSL for use in PIXL tests."""

import logging
from pathlib import Path

from decouple import config
from pyftpdlib.authorizers import DummyAuthorizer
from pyftpdlib.handlers import TLS_FTPHandler
from pyftpdlib.servers import FTPServer
from pyftpdlib.servers import ThreadedFTPServer

logger = logging.getLogger(__name__)
logger.setLevel("INFO")

# User permission
# from https://pyftpdlib.readthedocs.io/en/latest/api.html#pyftpdlib.authorizers.DummyAuthorizer.add_user
Expand Down Expand Up @@ -73,19 +76,20 @@ class PixlFTPServer:
:type home_dir: str
"""

def __init__(self, home_dir: str) -> None:
def __init__(self, home_root: Path, host_address: str) -> 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 = host_address
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 +107,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 All @@ -119,11 +123,12 @@ def _check_ssl_files(self) -> None:
assert certfile_path.exists(), f"Could not find cerfile at {certfile_path.absolute()}"
assert keyfile_path.exists(), f"Could not find cerfile at {keyfile_path.absolute()}"

def _create_server(self) -> FTPServer:
def _create_server(self) -> ThreadedFTPServer:
"""
Creates the FTPS server and returns it. The server can be started with the `serve_forever`
method.
"""
address = (self.host, self.port)
self.server = FTPServer(address, self.handler)
logger.info("Starting FTP server on %s:%d", self.host, self.port)
self.server = ThreadedFTPServer(address, self.handler)
return self.server
Loading
Loading