Scenarios¶
A rule without a scenario is an assertion. A scenario is a small dataset plus the verdict you expect, and it is the only thing that will tell you — or the next person — that the rule still does what it says.
Scenarios are .cdt files: the data written out where a reviewer can read it, rather than built
up in code.
The shape¶
#!RuleTest
#test CDISC-CG0270 expect=noViolation domain=TS
#note "Row 1 a valid period; row 2 the explicitly allowed empty TSVAL; row 3 a non-duration value under a different parameter, so outside this rule's condition."
#library standard=sdtmig version=3-4
dataset TS label="Trial Summary"
col STUDYID type=Char label="Study Identifier"
col DOMAIN type=Char label="Domain Abbreviation"
col TSPARMCD type=Char label="Trial Summary Parameter Short Name"
col TSVAL type=Char label="Parameter Value"
---
CDISCPILOT01 | TS | AGEMAX | P80Y
CDISCPILOT01 | TS | AGEMAX |
CDISCPILOT01 | TS | AGEMIN | 18Y
#!RuleTest first, then the prelude directives, then the dataset and its columns, then ---, then
pipe-delimited rows in column order. Comments start with # and blank lines are allowed.
Files live beside the rule they test, one directory per rule:
src/test/resources/…/ruletestsuites/cdisc/CDISC-CG0270/
CDISC-CG0270-valid-TS.cdt
CDISC-CG0270-invalid-TS.cdt
The name says what the scenario is for — -valid-, -invalid-, or the shape it exercises.
The four verdicts¶
expect= asserts the rule's execution state and its violations together:
expect= |
the rule must | and |
|---|---|---|
violation |
have executed | fired at least once |
noViolation |
have executed | not fired |
skipped |
not have executed | — |
executionError |
have failed | — |
⛔
skippedandexecutionErrorexist becausenoViolationasserted nothing for them. A skipped rule reports zero violations and is indistinguishable from one that ran and found nothing; so is a rule that errored. A scenario written asnoViolationfor either case passes while exercising nothing — and keeps passing after you tighten a requirement or trim the column it needed.
violation and noViolation now fail with the engine's own skip reason when the rule does not
run, and with its error message when it errored. So a scenario that quietly stops running tells
you.
skipped scenarios are built by withholding¶
A skipped scenario proves a requirement is real. What you withhold depends on the facet:
| the requirement | the scenario withholds |
|---|---|
Variables.All |
one listed column — every entry must be present |
Variables.Any |
all listed columns; withholding one is not enough |
Variables.None |
nothing — it adds a listed column |
Datasets |
the whole dataset block for that name |
Writing a scenario that proves something¶
Write both directions. A violation scenario and a noViolation scenario. One alone tells you
the rule can fire, or can stay quiet, but not that it distinguishes.
Put the near-miss in the noViolation file. Data the rule nearly catches is what proves it
is not too broad:
CDISCPILOT01 | TS | AGEMIN | 18Y
18Y is malformed — and must not be reported, because its parameter is not AGEMAX. A
scenario built only from data the rule ignores proves nothing; one built from data it nearly
catches proves a great deal.
Cover the shapes this guide warns about, where they apply to your rule: a blank cell against a missing one, a partial date, an absent column, a zero-row dataset. Each of those is a case where a rule can silently do the wrong thing, and a scenario is the only place it becomes visible.
Sharpening the assertion¶
expect=violation says the rule fired. Often you want to say where, and how often:
#expectViolationCount 2
#expectViolationAt row=3 severity=ERROR TSPARMCD=AGEMAX
#expectViolationCount pins the number of findings; #expectViolationAt pins one location, and
optionally the check level it fired at.
Reach for these when the count is the point. A rule that should fire once per subject and fires once per row still passes a bare
expect=violation.
Supplying metadata¶
A rule that reads metadata needs it supplied, or it will skip:
| directive | gives the rule |
|---|---|
#library standard=sdtmig version=3-4 |
inline CDISC-Library metadata |
#library-include <file.yaml> |
the same, from a sidecar |
#library-ref standard=… version=… |
a real CDISC Library |
#define-xml <file.xml> |
a real Define-XML sidecar — serves define_* and define_vlm_* |
#dictionaries dummy |
the checked-in dummy external-dictionary bundle |
#runLevel <LEVEL> sets the run's severity threshold — the weakest check level the scenario
evaluates.
ADaM structure and subclass¶
Scope.Data_Structures and Scope.Subclasses are normally detected from the data. Where a
scenario needs to state them, structure= and subclass= on the dataset header add the declared
tier, using shorthands:
| shorthand | means |
|---|---|
ADSL |
SUBJECT LEVEL ANALYSIS DATASET |
BDS |
BASIC DATA STRUCTURE |
OCCDS |
OCCURRENCE DATA STRUCTURE |
OTHER |
ADAM OTHER |
⚠ These shorthands are the scenario format's, not the rule language's. A rule's
Scope.Data_Structurestakes the full token and rejectsBDSat load. The two vocabularies look alike and are not interchangeable.
One word on ERROR¶
The word means two different things in this format, one line apart:
| spelling | what it is |
|---|---|
#expectViolationAt severity=ERROR |
the level a violation fired at — the rule ran and found something |
#runLevel ERROR |
the run's threshold — which levels are evaluated at all |
expect=executionError |
the rule could not run |
The verdict is spelled executionError rather than error deliberately, and expect=error is
not accepted at all — the parser rejects it and names executionError, rather than guessing
which of the three you meant.
Next: Self-review — what to check before proposing the rule.