Skip to content

About

Mock Server for testing Analyzer integrations with OpenELIS Global 2

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

129 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Analyzer Mock Server

See AGENTS.md for repository guidance.

This project simulates laboratory analyzers for integration and acceptance testing. It emits deterministic analyzer traffic through the same transport that a real instrument uses.

System Boundary

The supported result path is:

Analyzer mock -> Analyzer Bridge -> normalized result contract -> OpenELIS

The mock never posts analyzer results directly to OpenELIS and never creates or updates OpenELIS analyzer records. Analyzer Bridge owns protocol listeners, connection identity, parsing, FILE watching, and normalized transport. OpenELIS owns lab-facing setup, local catalog bindings, audit, activation, held-result review, and operational quality control.

Supported Analyzer Traffic

Protocol Mock behavior Bridge entry point
ASTM LIS2-A2 Opens an ASTM TCP session and sends framed records Saved ASTM listener
HL7 v2 Opens an MLLP session and sends an ORU message Saved MLLP listener
FILE Writes a result file into the configured watch directory Saved FILE connection

The HTTP API is a test control surface. It tells the mock to emit analyzer traffic; it is not a result transport to OpenELIS.

Profile-Backed Simulation

A mock template may contain an exact Bridge profileRef:

{
  "profileRef": {
    "profileId": "example-profile",
    "revision": 1
  }
}

When profileRef is present, set ANALYZER_BRIDGE_PROFILES_DIR to the Bridge profile catalog. The mock resolves exactly that profile revision and derives profile-owned analyzer identity, protocol, test codes, result types, units, value vocabularies, and FILE layout. It fails closed when the revision is missing, duplicated, or invalid.

The mock template retains only simulation concerns such as deterministic sample values and transport fixtures. It cannot override profile-owned fields.

GeneXpert ASTM and Hain FluoroCycler are the priority cross-process fixtures. Their messages are tested against exact Bridge profile revisions.

Quick Start

Requirements:

  • Python 3
  • uv for the reproducible test environment
  • Docker for dynamic analyzer networks and cross-process tests

Start the control API:

export ANALYZER_BRIDGE_PROFILES_DIR=/path/to/openelis-analyzer-bridge/src/main/resources/analyzer-profiles
python3 server.py --simulate-api-port 8081

This also starts a listener for every port in config/port_templates.json. To start one listener instead, see ACCESS.md.

Check health:

curl http://localhost:8081/health

Generate a message without sending it:

curl http://localhost:8081/simulate/file/hain_fluorocycler

Send ASTM Through Bridge

The GeneXpert sends only the messages Cepheid documents, kept under fixtures/genexpert/<assay>/<outcome>.astm; genexpert_astm replays them and never generates one:

curl -X POST http://localhost:8081/simulate/fixture/genexpert_astm/hivvl/quantified \
  -H 'Content-Type: application/json' \
  -d '{
    "destination": "tcp://127.0.0.1:12001",
    "sample_id": "LAB-2026-00001"
  }'

The destination must use tcp://. HTTP destinations are rejected.

Replay A Manufacturer's Own Message

For GeneXpert, fixtures/genexpert/<assay>/<outcome>.astm holds the ASTM messages Cepheid prints in its LIS guidance (303-0251 section 2.1.1 and 302-7279 section 6), unchanged except for placeholders. Push one as an instrument would send it:

curl -s -X POST http://localhost:8085/simulate/fixture/genexpert_astm/hivvl/below-40 \
  -H 'Content-Type: application/json' \
  -d '{"destination": "tcp://bridge:12001", "sample_id": "ACC-1", "sender_id": "GX-1",
       "patient": {"id": "MRN-9", "name": "Roe^Jane"},
       "instrument_codes": {"HIVVL": "HIVU"}}'

destination and sample_id are required. {patient_id} and {patient_name} fill the P record (empty by default, as in Cepheid's examples), and instrument_codes maps a profile test code to the code this instrument sends for it ({instrument_code:HIVVL} in a fixture). test_fixtures_match_profile.py keeps the fixtures and the Bridge's baseline profile in step: every record and value in a fixture is declared by the profile, and every declared one is in a fixture. Assays today: hivvl, cov-flu-rsv-plus, cov-flu-plus, cov-plus.

For the Mindray BC-5380, fixtures/mindray-bc5380/cbc/ holds messages built on the two Mindray prints in the BC-5380 Operator's Manual, appendix C.3.3. analysis-result is that CBC as a routine run sends it: the automated count with its alarms, histograms and scattergram. manual-count is the same CBC with the microscope exam a technician enters on the analyzer (OBR-4 00002 Manual Count), counted to 100 white cells; Mindray's print sends that exam with every percentage at 0.0. xr-qc is the X-R QC run as printed. {sample_id} fills OBR-3, {patient_id} and {patient_name} PID-3 and PID-5, and sender_id replaces MSH-3. The template mindray_bc5380_hl7 replays them over MLLP:

curl -s -X POST http://localhost:8085/simulate/fixture/mindray_bc5380_hl7/cbc/analysis-result \
  -H 'Content-Type: application/json' \
  -d '{"destination": "mllp://bridge:2575", "sample_id": "ACC-1", "sender_id": "BC-5380"}'

Send HL7 Through Bridge

curl -X POST http://localhost:8081/simulate/hl7/abbott_architect_hl7 \
  -H 'Content-Type: application/json' \
  -d '{
    "destination": "mllp://127.0.0.1:2575",
    "sample_id": "LAB-2026-00001"
  }'

The destination must use mllp://. HTTP destinations are rejected.

Simulate Several Instruments Of One Type

Two optional body fields make one template stand in for distinct instruments, on both the ASTM and HL7 endpoints:

  • source_ip: the local address the push leaves from, so the LIS sees a different peer. The address must be configured on the mock container.
  • sender_id: the instrument's own name, as a GeneXpert PC sends the System Name from its configuration. It replaces component 1 of ASTM H.5 and HL7 MSH-3 and leaves the rest of the message unchanged. Without it the template's token is sent as before.
curl -X POST http://localhost:8081/simulate/fixture/genexpert_astm/hivvl/quantified \
  -H 'Content-Type: application/json' \
  -d '{
    "destination": "tcp://127.0.0.1:12001",
    "sample_id": "LAB-2026-00001",
    "sender_id": "GX-LAB-A"
  }'

Fix The Instrument's Test Times

A real instrument reports when it ran a test, often long before the result reaches the LIS. By default the mock takes both times from the clock. To check that a receiver keeps the instrument's time, send completed_at (ASTM YYYYMMDDHHMMSS) to /simulate/astm: it becomes the completion time (R.13 in a template with astm_config) and the start time (R.12) is set 90 minutes earlier. An unreadable value is rejected with 400. Templates without astm_config carry the time in R.10 instead, and QC messages ("qc": true) ignore completed_at. A replayed fixture keeps the instrument's own times.

curl -X POST http://localhost:8081/simulate/astm/stago_start4 \
  -H 'Content-Type: application/json' \
  -d '{
    "destination": "tcp://127.0.0.1:12001",
    "completed_at": "20251021161230"
  }'

Write FILE Traffic For Bridge

curl -X POST http://localhost:8081/simulate/file/hain_fluorocycler \
  -H 'Content-Type: application/json' \
  -d '{
    "target_dir": "/data/analyzer-imports/fluorocycler",
    "filename": "result.xlsx"
  }'

target_dir must be the directory watched by the saved Bridge FILE connection. Bridge discovers and transports the file through that watched directory.

For a captured XLSX fixture, the optional sample_ids array replaces the patient rows' sample IDs, in file order. Control rows, the ones a template lists in fixture.controlRows, keep their names unless control_ids renames them, also in file order. The response reports the actual IDs and values written. This lets repeated tests use newly created orders while retaining the analyzer's file layout, measured values and control wells.

variant replays another workbook a template lists under fixture.variants:

Template Variant Workbook
quantstudio7, quantstudio5 control-failing the same plate with its NTC well amplifying
hain_fluorocycler with-controls the patient rows plus a positive (PC) and a negative (NC) control
hain_fluorocycler control-failing the same, with the negative control reading Positive

The QuantStudio workbooks follow the real exports described in the openelis-work QuantStudio spec (sections 3 to 8): a metadata block above the Well header row, upper-case tasks, and two rows per well (the VIH-1 target and the IC internal control) under one sample name. The plate carries standards (STD1_E7, STD3_E5, STD5_E3, Task STANDARD), NTC wells named NC and NTC (Task NTC), and a positive control PC (Task UNKNOWN). The FluoroCycler XT workbooks keep the Madagascar site file's columns; no file we have shows the Type value a control row carries, so control rows use the values the Bridge profile recognises. test_fixtures_match_profile.py checks that each template's controlRows are exactly the rows its pinned profile revision recognises as controls.

Dynamic Analyzer Instances

The control API can provision isolated mock analyzer network identities:

curl -X POST http://localhost:8081/analyzers \
  -H 'Content-Type: application/json' \
  -d '{"name":"genexpert-lab-1","template":"genexpert_astm"}'

curl http://localhost:8081/analyzers

Use the instance name in a /simulate/astm/{name} or /simulate/hl7/{name} request when a test must send from that instance's source address; FILE requests take a template name. Network provisioning is test infrastructure only; Bridge remains the durable connection authority.

Set ANALYZER_NETWORK_NAMESPACE and ANALYZER_SUBNET_PREFIX (default 10.42) to keep one stack's analyzer networks apart from another's on the same Docker host. MOCK_CONTAINER_NAME (default: the container hostname) and BRIDGE_CONTAINER_NAME name the containers joined to each analyzer network.

Serial Analyzers

python3 server.py --serial-port /dev/pts/3 --serial-analyzer mindray_ba88a

--serial-port sends ASTM over a serial device, for example one end of a socat virtual pair; --serial-analyzer names the template to send.

Tests

Run the complete mock suite:

uv run --with-requirements requirements.txt python -m pytest

The suite tests the mock on its own: the file templates generate from the copies of their Bridge profiles in test_profiles. Tests that need the Bridge as well are marked needs_bridge and skipped here; OpenELIS runs them, and the Bridge's cross-process result-traffic harness, against its Bridge and mock submodules, which record the versions that go together. To run them locally, set ANALYZER_BRIDGE_PROFILES_DIR to the Bridge's src/main/resources/analyzer-profiles.

Focused tests should assert observable protocol behavior. Do not add filesystem scans or source-text guards for code that should simply be removed.

Repository Layout

  • api.py: HTTP test-control API
  • server.py: analyzer listener and command-line entry point
  • push.py: ASTM TCP and HL7 MLLP senders
  • protocols/: protocol message generation and framing
  • templates/: deterministic simulator inputs
  • profile_adapter.py: exact Bridge profile revision adapter
  • fixtures/: representative analyzer output files
  • test_*.py: behavior and contract tests

Standards

  • ASTM E1381/E1394 and LIS2-A2 where implemented by analyzer fixtures
  • HL7 v2 ORU over MLLP
  • Analyzer-specific FILE formats represented by curated fixtures

This repository simulates analyzer behavior. Bridge profile and runtime contracts remain authoritative.

About

Mock Server for testing Analyzer integrations with OpenELIS Global 2

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages