Skip to content

Commit 20b92de

Browse files
lukedegruchyclaude
andauthored
Add capability to log CQL evaluation results during Measure integration testing to help debug (#1006)
Add logEvaluationResults() to measure testing framework for CQL debugging Thread CQL EvaluationResult objects through the Measure and MultiMeasure test DSLs so they are accessible from SelectedMeasureDef and SelectedMeasureDefCollection. Add logEvaluationResults() on SelectedMeasureDef and logAllMeasureEvaluationResults() on SelectedMeasureDefCollection to pretty-print evaluation results per subject, with separator lines for readability. This mirrors the existing logReportJson() pattern on SelectedMeasureReport. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4ba174a commit 20b92de

8 files changed

Lines changed: 133 additions & 9 deletions

File tree

.claude/skills/measure-testing-framework.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ public interface Selector<T, S> { T select(S from); }
4242

4343
### Return Records (test infrastructure)
4444

45-
- **`MeasureDefAndR4MeasureReport`**: Pairs a single `MeasureDef` with a single `MeasureReport` (used by `Measure`)
46-
- **`MeasureDefAndR4ParametersWithMeasureReports`**: Pairs `List<MeasureDef>` with `Parameters` containing bundled `MeasureReport`s (used by `MultiMeasure`)
45+
- **`MeasureDefAndR4MeasureReport`**: Pairs a single `MeasureDef` with a single `MeasureReport` and `Map<String, EvaluationResult>` (used by `Measure`)
46+
- **`MeasureDefAndR4ParametersWithMeasureReports`**: Pairs `List<MeasureDef>` with `Parameters` containing bundled `MeasureReport`s and `Map<MeasureDef, Map<String, EvaluationResult>>` evaluation results per measure (used by `MultiMeasure`)
4747

4848
## Given Phase - Test Setup
4949

@@ -405,6 +405,9 @@ All classes in `selected/def/` extend `Measure.Selected<T, P>`.
405405
.group("groupId") -> SelectedMeasureDefGroup (by ID)
406406
.group(0) -> SelectedMeasureDefGroup (by index)
407407

408+
// Logging (debug aid, similar to SelectedMeasureReport.logReportJson())
409+
.logEvaluationResults() // Logs formatted CQL evaluation results (per subject) at INFO level
410+
408411
// Assertions
409412
.hasNoErrors()
410413
.hasErrors(2)
@@ -421,6 +424,10 @@ All classes in `selected/def/` extend `Measure.Selected<T, P>`.
421424
**Parent**: generic `<P>` (used by `MultiMeasure.Then`)
422425

423426
```java
427+
// Logging (debug aid)
428+
.logAllMeasureEvaluationResults() // Logs formatted CQL evaluation results for ALL measures at once
429+
430+
// Navigation
424431
.hasCount(3)
425432
.first() -> SelectedMeasureDef
426433
.get(0) -> SelectedMeasureDef (by index)

cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/EvaluationResultFormatter.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public class EvaluationResultFormatter {
2424
private static final String DATE_FORMAT = "yyyy-MM-dd";
2525
private static final String DATE_TIME_FORMAT = "yyyy-MM-dd:HH:mm:ss";
2626
private static final String INDENT = " ";
27+
private static final String SEPARATOR = "----------------------------------------";
2728

2829
private EvaluationResultFormatter() {
2930
// Static utility class
@@ -217,6 +218,38 @@ private static String indent(int level) {
217218
return INDENT.repeat(Math.max(0, level));
218219
}
219220

221+
/**
222+
* Formats evaluation results for a single measure, with separator lines between subjects.
223+
*
224+
* @param measureId the measure ID for the header
225+
* @param evaluationResults map of subject ID to EvaluationResult
226+
* @return formatted string
227+
*/
228+
public static String formatMeasureEvaluationResults(
229+
String measureId, Map<String, EvaluationResult> evaluationResults) {
230+
StringBuilder sb = new StringBuilder();
231+
sb.append(SEPARATOR).append("\n");
232+
sb.append("Evaluation Results for Measure: ").append(measureId).append("\n");
233+
sb.append(SEPARATOR).append("\n");
234+
235+
if (evaluationResults.isEmpty()) {
236+
sb.append(" (no evaluation results available)\n");
237+
} else {
238+
boolean first = true;
239+
for (Map.Entry<String, EvaluationResult> entry : evaluationResults.entrySet()) {
240+
if (!first) {
241+
sb.append(" ").append(SEPARATOR).append("\n");
242+
}
243+
first = false;
244+
sb.append(" Subject: ").append(entry.getKey()).append("\n");
245+
sb.append(format(entry.getValue(), 2));
246+
}
247+
}
248+
249+
sb.append(SEPARATOR).append("\n");
250+
return sb.toString();
251+
}
252+
220253
public static Object printSubjectResources(PopulationDef populationDef, String subjectId) {
221254
if (populationDef == null) {
222255
return "{empty}";

cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureDefAndR4ParametersWithMeasureReports.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import com.google.common.annotations.VisibleForTesting;
44
import java.util.List;
5+
import java.util.Map;
56
import org.hl7.fhir.r4.model.Parameters;
7+
import org.opencds.cqf.cql.engine.execution.EvaluationResult;
68
import org.opencds.cqf.fhir.cr.measure.common.MeasureDef;
79

810
/**
@@ -35,4 +37,15 @@
3537
* @param parameters Parameters resource containing bundled R4 MeasureReports
3638
*/
3739
@VisibleForTesting
38-
public record MeasureDefAndR4ParametersWithMeasureReports(List<MeasureDef> measureDefs, Parameters parameters) {}
40+
public record MeasureDefAndR4ParametersWithMeasureReports(
41+
List<MeasureDef> measureDefs,
42+
Parameters parameters,
43+
Map<MeasureDef, Map<String, EvaluationResult>> evaluationResultsPerMeasure) {
44+
45+
/**
46+
* Backwards-compatible constructor for callers that do not need evaluation results.
47+
*/
48+
public MeasureDefAndR4ParametersWithMeasureReports(List<MeasureDef> measureDefs, Parameters parameters) {
49+
this(measureDefs, parameters, Map.of());
50+
}
51+
}

cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MultiMeasureService.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.hl7.fhir.r4.model.Parameters;
2626
import org.hl7.fhir.r4.model.Resource;
2727
import org.opencds.cqf.cql.engine.execution.CqlEngine;
28+
import org.opencds.cqf.cql.engine.execution.EvaluationResult;
2829
import org.opencds.cqf.fhir.cql.Engines;
2930
import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions;
3031
import org.opencds.cqf.fhir.cr.measure.common.CompositeEvaluationResultsPerMeasure;
@@ -484,6 +485,7 @@ private MeasureDefAndR4ParametersWithMeasureReports toMeasureDefAndParametersRes
484485
List<List<MeasureDefAndR4MeasureReport>> results) {
485486

486487
final List<MeasureDef> measureDefs = new ArrayList<>();
488+
final Map<MeasureDef, Map<String, EvaluationResult>> evaluationResultsPerMeasure = new HashMap<>();
487489

488490
// Create Parameters to hold the bundle(s)
489491
final Parameters parameters = new Parameters();
@@ -496,6 +498,12 @@ private MeasureDefAndR4ParametersWithMeasureReports toMeasureDefAndParametersRes
496498
for (MeasureDefAndR4MeasureReport measureDefAndR4MeasureReport : result) {
497499
measureDefs.add(measureDefAndR4MeasureReport.measureDef());
498500

501+
if (!measureDefAndR4MeasureReport.evaluationResults().isEmpty()) {
502+
evaluationResultsPerMeasure.put(
503+
measureDefAndR4MeasureReport.measureDef(),
504+
measureDefAndR4MeasureReport.evaluationResults());
505+
}
506+
499507
// add report to bundle
500508
final MeasureReport measureReport = measureDefAndR4MeasureReport.measureReport();
501509

@@ -515,7 +523,7 @@ private MeasureDefAndR4ParametersWithMeasureReports toMeasureDefAndParametersRes
515523
}
516524
}
517525

518-
return new MeasureDefAndR4ParametersWithMeasureReports(measureDefs, parameters);
526+
return new MeasureDefAndR4ParametersWithMeasureReports(measureDefs, parameters, evaluationResultsPerMeasure);
519527
}
520528

521529
protected List<String> getSubjects(R4RepositorySubjectProvider subjectProvider, String subjectId) {

cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/Measure.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ public MeasureReport measureReport() {
286286
* @return SelectedMeasureDef for fluent MeasureDef assertions
287287
*/
288288
public SelectedMeasureDef<Then> def() {
289-
return new SelectedMeasureDef<>(evaluation.measureDef(), this);
289+
return new SelectedMeasureDef<>(evaluation.measureDef(), this, evaluation.evaluationResults());
290290
}
291291

292292
// Backward compatibility - delegate to report()

cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MultiMeasure.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,8 @@ public MultiMeasure.SelectedReport report() {
299299
* @return SelectedMeasureDefCollection for fluent MeasureDef collection assertions
300300
*/
301301
public SelectedMeasureDefCollection<Then> defs() {
302-
return new SelectedMeasureDefCollection<>(evaluation.measureDefs(), this);
302+
return new SelectedMeasureDefCollection<>(
303+
evaluation.measureDefs(), this, evaluation.evaluationResultsPerMeasure());
303304
}
304305

305306
// Backward compatibility - delegate to report()

cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/selected/def/SelectedMeasureDef.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@
55
import static org.junit.jupiter.api.Assertions.assertNotNull;
66
import static org.junit.jupiter.api.Assertions.assertTrue;
77

8+
import java.util.Map;
9+
import org.opencds.cqf.cql.engine.execution.EvaluationResult;
10+
import org.opencds.cqf.fhir.cr.measure.common.EvaluationResultFormatter;
811
import org.opencds.cqf.fhir.cr.measure.common.GroupDef;
912
import org.opencds.cqf.fhir.cr.measure.common.MeasureDef;
13+
import org.slf4j.Logger;
14+
import org.slf4j.LoggerFactory;
1015

1116
/**
1217
* Fluent assertion API for MeasureDef objects.
@@ -39,8 +44,17 @@
3944
*/
4045
public class SelectedMeasureDef<P> extends org.opencds.cqf.fhir.cr.measure.r4.Measure.Selected<MeasureDef, P> {
4146

47+
private static final Logger logger = LoggerFactory.getLogger(SelectedMeasureDef.class);
48+
49+
private final Map<String, EvaluationResult> evaluationResults;
50+
4251
public SelectedMeasureDef(MeasureDef value, P parent) {
52+
this(value, parent, Map.of());
53+
}
54+
55+
public SelectedMeasureDef(MeasureDef value, P parent, Map<String, EvaluationResult> evaluationResults) {
4356
super(value, parent);
57+
this.evaluationResults = evaluationResults;
4458
}
4559

4660
// ==================== Navigation Methods ====================
@@ -90,6 +104,19 @@ public SelectedMeasureDefGroup<SelectedMeasureDef<P>> group(int index) {
90104
return new SelectedMeasureDefGroup<>(value().groups().get(index), this);
91105
}
92106

107+
// ==================== Logging Methods ====================
108+
109+
/**
110+
* Log the CQL evaluation results for this measure, formatted for readability.
111+
* Similar to {@link org.opencds.cqf.fhir.cr.measure.r4.selected.report.SelectedMeasureReport#logReportJson()}.
112+
*
113+
* @return this SelectedMeasureDef for chaining
114+
*/
115+
public SelectedMeasureDef<P> logEvaluationResults() {
116+
logger.info(EvaluationResultFormatter.formatMeasureEvaluationResults(value().id(), evaluationResults));
117+
return this;
118+
}
119+
93120
// ==================== Assertion Methods ====================
94121

95122
/**

cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/selected/def/SelectedMeasureDefCollection.java

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,33 @@
33
import static org.junit.jupiter.api.Assertions.*;
44

55
import java.util.List;
6+
import java.util.Map;
7+
import org.opencds.cqf.cql.engine.execution.EvaluationResult;
8+
import org.opencds.cqf.fhir.cr.measure.common.EvaluationResultFormatter;
69
import org.opencds.cqf.fhir.cr.measure.common.MeasureDef;
710
import org.opencds.cqf.fhir.cr.measure.r4.Measure;
11+
import org.slf4j.Logger;
12+
import org.slf4j.LoggerFactory;
813

914
/**
1015
* Fluent API for asserting on collections of MeasureDefs from multi-measure evaluation.
1116
*/
1217
public class SelectedMeasureDefCollection<P> extends Measure.Selected<List<MeasureDef>, P> {
1318

19+
private static final Logger logger = LoggerFactory.getLogger(SelectedMeasureDefCollection.class);
20+
21+
private final Map<MeasureDef, Map<String, EvaluationResult>> evaluationResultsPerMeasure;
22+
1423
public SelectedMeasureDefCollection(List<MeasureDef> measureDefs, P parent) {
24+
this(measureDefs, parent, Map.of());
25+
}
26+
27+
public SelectedMeasureDefCollection(
28+
List<MeasureDef> measureDefs,
29+
P parent,
30+
Map<MeasureDef, Map<String, EvaluationResult>> evaluationResultsPerMeasure) {
1531
super(measureDefs, parent);
32+
this.evaluationResultsPerMeasure = evaluationResultsPerMeasure;
1633
}
1734

1835
// Assert count
@@ -26,28 +43,46 @@ public SelectedMeasureDef<SelectedMeasureDefCollection<P>> get(int index) {
2643
assertTrue(
2744
index >= 0 && index < value.size(),
2845
"Index " + index + " out of bounds for " + value.size() + " MeasureDefs");
29-
return new SelectedMeasureDef<>(value.get(index), this);
46+
MeasureDef def = value.get(index);
47+
return new SelectedMeasureDef<>(def, this, evaluationResultsPerMeasure.getOrDefault(def, Map.of()));
3048
}
3149

3250
// Access first
3351
public SelectedMeasureDef<SelectedMeasureDefCollection<P>> first() {
3452
return get(0);
3553
}
3654

55+
/**
56+
* Log evaluation results for all measures in this collection at once.
57+
* Each measure's results are formatted with separator lines between subjects and between measures.
58+
*
59+
* @return this SelectedMeasureDefCollection for chaining
60+
*/
61+
public SelectedMeasureDefCollection<P> logAllMeasureEvaluationResults() {
62+
StringBuilder sb = new StringBuilder();
63+
sb.append("\n");
64+
for (MeasureDef def : value) {
65+
Map<String, EvaluationResult> results = evaluationResultsPerMeasure.getOrDefault(def, Map.of());
66+
sb.append(EvaluationResultFormatter.formatMeasureEvaluationResults(def.id(), results));
67+
}
68+
logger.info(sb.toString());
69+
return this;
70+
}
71+
3772
// Access by measure URL - returns collection (can be multiple in subject evaluation)
3873
public SelectedMeasureDefCollection<SelectedMeasureDefCollection<P>> byMeasureUrl(String measureUrl) {
3974
List<MeasureDef> found =
4075
value.stream().filter(def -> measureUrl.equals(def.url())).toList();
4176
assertFalse(found.isEmpty(), "No MeasureDefs found for measure URL: " + measureUrl);
42-
return new SelectedMeasureDefCollection<>(found, this);
77+
return new SelectedMeasureDefCollection<>(found, this, evaluationResultsPerMeasure);
4378
}
4479

4580
// Access by measure ID - returns collection (can be multiple in subject evaluation)
4681
public SelectedMeasureDefCollection<SelectedMeasureDefCollection<P>> byMeasureId(String measureId) {
4782
List<MeasureDef> found =
4883
value.stream().filter(def -> measureId.equals(def.id())).toList();
4984
assertFalse(found.isEmpty(), "No MeasureDefs found for measure ID: " + measureId);
50-
return new SelectedMeasureDefCollection<>(found, this);
85+
return new SelectedMeasureDefCollection<>(found, this, evaluationResultsPerMeasure);
5186
}
5287

5388
// TODO: Implement subject-level filtering once subject tracking API is clarified

0 commit comments

Comments
 (0)