Skip to content

Test coverage hardening - #303

Open
Okanmercan99 wants to merge 7 commits into
ignifyr-rename-wholefrom
test-coverage-hardening
Open

Test coverage hardening#303
Okanmercan99 wants to merge 7 commits into
ignifyr-rename-wholefrom
test-coverage-hardening

Conversation

@Okanmercan99

Copy link
Copy Markdown
Collaborator

Summary

Closes the gaps left by a coverage review of the whole reactor. The short tier
goes from ~161 to 373 tests, all Docker-free and network-free.

Writing those tests exposed eight defects, each fixed here alongside the test
that catches it. Two were proven by reverting the fix and watching the new test
go red.

Why

The review compared every main source against what the suites actually assert.
Three patterns came out:

  • Features with no coverage at all — the batching strategy, env-var
    resolution, the job stop/deschedule endpoints, the cst: FHIRPath library.
  • Written contracts with no guardExtensionRegistry's three fail-fast
    rules, the batch future's archiving guarantee, FhirRepositoryWriter's
    server-specific error handling. All documented in CLAUDE.md as "don't break
    this"; none asserted anywhere.
  • Green tests that prove nothingStreamingSinkHandlerTest had no
    expectation; ignifyr-common, ignifyr-server-common and ignifyr-rxnorm
    ran zero tests under Maven (rxnorm's two suites compiled and were never
    executed).

Defects fixed

Where Defect
engine/FhirMappingTask Batch parameter substitution rewrote the prefix of a longer name: with year and yearEnd defined, $yearEnd became 2020End
engine/FileStreamInputArchiver listFiles() on a not-yet-created checkpoint dir threw an NPE inside the shared archiving TimerTask, killing the timer and stopping archiving for every job in the process
engine/SparkUtil getLastCommitOffset threw on an empty/absent commit directory
engine/CommandLineInterface A trailing valueless flag overwrote the command, so run --job reported "unknown command --job"
server/CsvUtil writeCsvHeaders returned a Future that completed before the write, so the header endpoint could answer OK over stale content (this is what the 5s sleep in MappingContextEndpointTest was papering over)
testkit + server fixtures Two mapping fixtures still emitted R4-shaped FHIR that the R5 test server rejects — the care-site mapping wrote nothing at all, other-observation lost 3 of 14 resources
connector-sql tests The "write to FHIR repo successfully" tests asserted only that the cleanup delete returned 200, which is how the two fixture defects above stayed invisible

On the R4→R5 fixture migration

some-folder-1's copy of the same mapping was already migrated, so this is a
half-finished migration rather than a design choice. onFHIR reports only the
first unrecognised element per resource, so it surfaced one layer at a time over
three long-tier runs:
MedicationAdministration.effectiveDateTime -> occurenceDateTime
MedicationAdministration.context -> encounter
MedicationAdministration.medicationCodeableConcept -> medication.concept
MedicationAdministration.category -> array (0..* in R5)
Procedure.performedDateTime -> occurrenceDateTime
Organization.address -> contact[].address

Every replacement was verified by POSTing candidate shapes to an srdc/onfhir:r5
container, not from the spec by memory — note R5 spells
MedicationAdministration.occurence[x] with one r and
Procedure.occurrence[x] with two.

Reviewing commit by commit

The branch is split so each commit builds and passes on its own, and each fix
ships with its guard.

# Commit What to look at
1 ci: fail the build when a module's test sources never run New tier-gate invariant; rxnorm suites moved to the short tier behind a local stub
2 fix(engine): four defects the new unit tests exposed The four one-to-few-line fixes and their tests
3 fix(server): complete the CSV header write before answering mapflatMap
4 fix(testkit): finish the R4-to-R5 migration of the mapping fixtures The six element renames + the strengthened SqlSourceTest assertions
5 test(engine): cover the registry guards, job serialisation and sink routing Also adds ignifyr-common's first test source set
6 test(server): cover execution control, startup and error handling Five previously uncovered routes
7 test(plugins): cover the lookup tables and sub-registry guards The widest untested surfaces: REDCap's 35-branch type table, Kafka coercion, Firely batch attribution

Two structural changes worth a second look:

  • Six helpers widened from private to package-private, purely so a fail-fast
    contract could be asserted without standing up a second classloader.
  • KafkaSourceReader's coercion moved to the companion object. This is not
    cosmetic: as an instance method the UDF closure would capture the reader and
    therefore the SparkSession, breaking executor serialisation. Logic is
    byte-identical.

Deliberately not changed — your call

Three behaviours are pinned by tests rather than corrected, because changing them
is a product decision, not a cleanup:

  • SchemaConverter's asymmetric round-trip. instant reads as
    TimestampType and writes back as dateTime; unsignedIntLongType
    integer64. Changing either table rewrites the StructureDefinitions the server
    exports for existing projects.
  • RedCapUtil.recordIdField defaults to "", and the CLI path takes that
    default, so CLI-extracted schemas get a record-id field with an empty id and
    a trailing-dot path. Schemas already on disk carry that shape.
  • The scheduler fails rather than reseeding when its sync file exists but
    holds no parsable instant (a crash mid-write leaves exactly such a file).
    Falling back to initialTime could re-import a large window.

Also left alone: procedure-occurrence-mapping.json has an unbalanced paren in
its occurrenceDateTime expression. ANTLR recovers, so it evaluates — but it is
pre-existing and unrelated to this PR.

Verification

mvn -B verify                                                    # 373 tests, 0 failures
bash test-flow/check-test-tiers.sh                               # tiers intact
mvn -B verify -pl ignifyr-connector-sql -DskipITs=false          # 12/12  (Docker)
mvn -B verify -pl ignifyr-connector-file -DskipITs=false         # 16/16  (Docker)
mvn -B verify -pl ignifyr-server -DskipITs=false                 # 40/40  (Docker)

check-test-tiers.sh gains a fourth invariant. ignifyr-rxnorm declared no
scalatest-maven-plugin, so its two suites compiled and ran nowhere; they now
run in the short tier against a locally bound RxNorm stub instead of the live
API, which also reaches the not-found and non-200 branches.
* substituteBatchParameters rewrote the prefix of a longer parameter name:
  with `year` and `yearEnd` defined, `$yearEnd` became `2020End`.
* FileStreamInputArchiver called listFiles() on a checkpoint directory that
  does not exist until Spark's first commit. The NPE escaped into the shared
  archiving TimerTask and stopped archiving for every job in the process.
* getLastCommitOffset threw on an empty or absent commit directory.
* nextArg let a trailing valueless flag overwrite the command, so `run --job`
  was reported as "unknown command --job".
writeCsvHeaders mapped over the write instead of flatMapping, so the returned
Future completed while the file was still being written and the mapping-context
header endpoint could answer OK over stale content. This is what the 5s sleep in
MappingContextEndpointTest was working around.
Two fixtures still emitted R4-shaped FHIR that the srdc/onfhir:r5 test server
rejects, while their sibling under some-folder-1 was already migrated. The
care-site mapping wrote nothing at all and other-observation lost 3 of its 14
resources -- both invisible, because the tests only checked that the cleanup
delete answered 200. They now read the written resources back.

  MedicationAdministration.effectiveDateTime         -> occurenceDateTime
  MedicationAdministration.context                   -> encounter
  MedicationAdministration.medicationCodeableConcept -> medication.concept
  MedicationAdministration.category                  -> array (0..* in R5)
  Procedure.performedDateTime                        -> occurrenceDateTime
  Organization.address                               -> contact[].address

Every replacement was verified against the R5 container. Note the spec's own
inconsistency: MedicationAdministration.occurence[x] has one r,
Procedure.occurrence[x] has two.
… routing

Adds the ExtensionRegistry fail-fast guards (duplicate key, second
single-capability provider, conflicting spark-conf key), the batch-completion
contract registerBatchJob documents, env-var resolution, the Spark<->FHIR type
table, SinkHandler's result classification and mapping-job (de)serialisation.

ignifyr-common gets its first test source set: it had none, so nothing there
was covered -- including the cst: FHIRPath library the server activates.

Four helpers widened from private to package-private so a fail-fast contract
can be asserted without a second classloader.
The five execution-control routes (status, list, stop, deschedule, stop
mapping) had no coverage at all, and the 404-vs-empty-list distinction the web
UI relies on was unasserted. Also adds FolderDBInitializer's two startup paths
including its refusal to start on a dangling reference, the error handler's
exception path (as opposed to the already-covered rejection path), and
MetadataService's one-second bound on the redcap lookup.
The widest untested surfaces were plain lookup tables where a wrong entry is
silent: REDCap's 35-branch data type table, Kafka's schema-driven value
coercion, and Firely's HTTP-400 batch attribution. Also adds the two
sub-registry duplicate-content-type guards, the file reader's distinct option
and hdfs:// path resolution, the scheduler's incremental-sync file, and the
MapMarker-to-LogstashMarker rewrite that feeds the Kibana dashboard.

StreamingSinkHandlerTest had no expectation at all -- it now verifies the
writer was handed a further chunk after one threw.

The Kafka coercion moved to the companion object so the UDF carries no
reference to the SparkSession when it is serialised to the executors.
@Okanmercan99
Okanmercan99 requested a review from KeremHmd August 7, 2026 12:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant