diff --git a/conf/development.json b/conf/development.json index 9910444..38972a0 100644 --- a/conf/development.json +++ b/conf/development.json @@ -36,6 +36,132 @@ "groupName": "Uncertainty tests", "testName": "TimeDurationBetweenHourDiffPrecision", "reason": "CQLtoELM - Syntax error at Z" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "Abs", + "testName": "AbsLong", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "Add", + "testName": "Add1L2L", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "Add", + "testName": "Add1L1L", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "MinValue", + "testName": "LongMinValue", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "MaxValue", + "testName": "LongMaxValue", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "Modulo", + "testName": "Modulo4LBy2L", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "Multiply", + "testName": "Multiply2LBy3L", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "Multiply", + "testName": "Multiply1By1L", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "Negate", + "testName": "Negate1L", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "Negate", + "testName": "NegateMaxLong", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "Negate", + "testName": "NegateNeg1L", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "Predecessor", + "testName": "PredecessorOf1L", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "Power", + "testName": "Power2LTo2L", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "Power", + "testName": "Power2LTo3L", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "Subtract", + "testName": "Subtract1LAnd1L", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "Successor", + "testName": "SuccessorOf1L", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "Truncated Divide", + "testName": "TruncatedDivide10LBy3L", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlAggregateFunctionsTest", + "groupName": "Product", + "testName": "ProductLong", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlAggregateFunctionsTest", + "groupName": "Max", + "testName": "MaxTestLong", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlAggregateFunctionsTest", + "groupName": "Min", + "testName": "MinTestLong", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlAggregateFunctionsTest", + "groupName": "Sum", + "testName": "SumTestLong", + "reason": "FHIR R4 does not support Long/Integer64" } ] } diff --git a/conf/localhost.json b/conf/localhost.json index 1b711a0..15fe0ff 100644 --- a/conf/localhost.json +++ b/conf/localhost.json @@ -19,24 +19,150 @@ "Tests": { "ResultsPath": "./results", "SkipList": [ - { - "testsName": "CqlAggregateTest", - "groupName": "AggregateTests", - "testName": "RolledOutIntervals", - "reason": "CQLtoELM - Could not resolve identifier MedicationRequestIntervals in the current library" - }, - { - "testsName": "CqlDateTimeOperatorsTest", - "groupName": "DateTimeComponentFrom", - "testName": "DateTimeComponentFromTimezone", - "reason": "CQLtoElm - Timezone keyword is only valid in 1.3 or lower" - }, - { - "testsName": "CqlDateTimeOperatorsTest", - "groupName": "Uncertainty tests", - "testName": "TimeDurationBetweenHourDiffPrecision", - "reason": "CQLtoELM - Syntax error at Z" - } - ] + { + "testsName": "CqlAggregateTest", + "groupName": "AggregateTests", + "testName": "RolledOutIntervals", + "reason": "CQLtoELM - Could not resolve identifier MedicationRequestIntervals in the current library" + }, + { + "testsName": "CqlDateTimeOperatorsTest", + "groupName": "DateTimeComponentFrom", + "testName": "DateTimeComponentFromTimezone", + "reason": "CQLtoElm - Timezone keyword is only valid in 1.3 or lower" + }, + { + "testsName": "CqlDateTimeOperatorsTest", + "groupName": "Uncertainty tests", + "testName": "TimeDurationBetweenHourDiffPrecision", + "reason": "CQLtoELM - Syntax error at Z" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "Abs", + "testName": "AbsLong", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "Add", + "testName": "Add1L2L", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "Add", + "testName": "Add1L1L", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "MinValue", + "testName": "LongMinValue", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "MaxValue", + "testName": "LongMaxValue", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "Modulo", + "testName": "Modulo4LBy2L", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "Multiply", + "testName": "Multiply2LBy3L", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "Multiply", + "testName": "Multiply1By1L", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "Negate", + "testName": "Negate1L", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "Negate", + "testName": "NegateMaxLong", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "Negate", + "testName": "NegateNeg1L", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "Predecessor", + "testName": "PredecessorOf1L", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "Power", + "testName": "Power2LTo2L", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "Power", + "testName": "Power2LTo3L", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "Subtract", + "testName": "Subtract1LAnd1L", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "Successor", + "testName": "SuccessorOf1L", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlArithmeticFunctionsTest", + "groupName": "Truncated Divide", + "testName": "TruncatedDivide10LBy3L", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlAggregateFunctionsTest", + "groupName": "Product", + "testName": "ProductLong", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlAggregateFunctionsTest", + "groupName": "Max", + "testName": "MaxTestLong", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlAggregateFunctionsTest", + "groupName": "Min", + "testName": "MinTestLong", + "reason": "FHIR R4 does not support Long/Integer64" + }, + { + "testsName": "CqlAggregateFunctionsTest", + "groupName": "Sum", + "testName": "SumTestLong", + "reason": "FHIR R4 does not support Long/Integer64" + } + ] } } diff --git a/src/extractors/value-type-extractors/long-extractor.ts b/src/extractors/value-type-extractors/long-extractor.ts new file mode 100644 index 0000000..960d6f0 --- /dev/null +++ b/src/extractors/value-type-extractors/long-extractor.ts @@ -0,0 +1,37 @@ +import { BaseExtractor } from '../base-extractor.js'; + +const CQL_TYPE_EXTENSION_URL = 'http://hl7.org/fhir/StructureDefinition/cqf-cqlType'; + +function hasCqlLongType(parameter: any): boolean { + const extensions = Array.isArray(parameter?.extension) ? parameter.extension : []; + return extensions.some( + (extension: any) => + extension?.url === CQL_TYPE_EXTENSION_URL && + String(extension?.valueString ?? '').toLowerCase() === 'system.long' + ); +} + +function parseLong(value: unknown): bigint | undefined { + if (value === undefined || value === null) return undefined; + + const text = String(value).trim(); + const normalized = text.endsWith('L') || text.endsWith('l') ? text.slice(0, -1) : text; + + if (!/^[+-]?\d+$/.test(normalized)) return undefined; + + return BigInt(normalized); +} + +export class LongExtractor extends BaseExtractor { + protected _process(parameter: any): any { + if (parameter.hasOwnProperty('valueInteger64')) { + return parseLong(parameter.valueInteger64); + } + + if (parameter.hasOwnProperty('valueString') && hasCqlLongType(parameter)) { + return parseLong(parameter.valueString); + } + + return undefined; + } +} diff --git a/src/server/extractor-builder.ts b/src/server/extractor-builder.ts index 41e125a..a55f658 100644 --- a/src/server/extractor-builder.ts +++ b/src/server/extractor-builder.ts @@ -2,6 +2,7 @@ import { EvaluationErrorExtractor } from '../extractors/evaluation-error-extract import { NullEmptyExtractor } from '../extractors/null-empty-extractor.js'; import { UndefinedExtractor } from '../extractors/undefined-extractor.js'; import { StringExtractor } from '../extractors/value-type-extractors/string-extractor.js'; +import { LongExtractor } from '../extractors/value-type-extractors/long-extractor.js'; import { BooleanExtractor } from '../extractors/value-type-extractors/boolean-extractor.js'; import { IntegerExtractor } from '../extractors/value-type-extractors/integer-extractor.js'; import { DecimalExtractor } from '../extractors/value-type-extractors/decimal-extractor.js'; @@ -24,6 +25,7 @@ export function buildExtractor(): ResultExtractor { extractors .setNextExtractor(new NullEmptyExtractor()) .setNextExtractor(new UndefinedExtractor()) + .setNextExtractor(new LongExtractor()) .setNextExtractor(new StringExtractor()) .setNextExtractor(new BooleanExtractor()) .setNextExtractor(new IntegerExtractor()) diff --git a/src/services/test-runner.ts b/src/services/test-runner.ts index b68627d..883ceb8 100644 --- a/src/services/test-runner.ts +++ b/src/services/test-runner.ts @@ -16,6 +16,7 @@ export interface TestRunnerOptions { useAxios?: boolean; // For backward compatibility with run-tests-command } + export class TestRunner { public async runTests( configData: any, @@ -29,20 +30,20 @@ export class TestRunner { // Verify server connectivity before proceeding await ServerConnectivity.verifyServerConnectivity(serverBaseUrl); - const build = config.Build; - const cqlEngine = new CQLEngine( - serverBaseUrl, - cqlEndpoint, - build.cqlTranslator ?? '', - build.cqlTranslatorVersion ?? '', - build.cqlEngine ?? '', - build.cqlEngineVersion ?? '' - ); - cqlEngine.cqlVersion = '1.5'; //default value - const cqlVersion = config.Build?.CqlVersion; - if (typeof cqlVersion === 'string' && cqlVersion.trim() !== '') { - cqlEngine.cqlVersion = cqlVersion; - } + const build = config.Build; + const cqlEngine = new CQLEngine( + serverBaseUrl, + cqlEndpoint, + build.cqlTranslator ?? '', + build.cqlTranslatorVersion ?? '', + build.cqlEngine ?? '', + build.cqlEngineVersion ?? '' + ); + cqlEngine.cqlVersion = '1.5'; //default value + const cqlVersion = config.Build?.CqlVersion; + if (typeof cqlVersion === 'string' && cqlVersion.trim() !== '') { + cqlEngine.cqlVersion = cqlVersion; + } // Load CVL using dynamic import // @ts-ignore @@ -130,7 +131,7 @@ export class TestRunner { ); return result; } else if (onlySet.size > 0 && !onlySet.has(key)) { - result.SkipMessage = 'Skipped by OnlyList filter'; + result.skipMessage = 'Skipped by OnlyList filter'; result.testStatus = 'skip'; return result; } else if (skipMap.has(key)) { diff --git a/src/shared/results-shared.ts b/src/shared/results-shared.ts index b910d55..dad12cb 100644 --- a/src/shared/results-shared.ts +++ b/src/shared/results-shared.ts @@ -53,9 +53,13 @@ export class Result implements InternalTestResult { this.skipMessage = 'No output specified'; } - this.capability = Array.isArray(test.capability) - ? test.capability.map(({ code, value }) => ({ code, value })) - : []; + const testCapabilities = Array.isArray(test.capability) + ? test.capability + : test.capability + ? [test.capability] + : []; + + this.capability = testCapabilities.map(({ code, value }) => ({ code, value })); } } diff --git a/src/shared/results-utils.ts b/src/shared/results-utils.ts index 1fa30b5..c88096e 100644 --- a/src/shared/results-utils.ts +++ b/src/shared/results-utils.ts @@ -18,6 +18,10 @@ export function resultsEqual(expected: any, actual: any): boolean { return true; } + if (cqlDateTimesEqual(expected, actual)) { + return true; + } + if ( typeof expected !== 'object' || expected === null || @@ -40,3 +44,57 @@ export function resultsEqual(expected: any, actual: any): boolean { return true; } + +interface CqlDateTimeParts { + base: string; + timezone?: string; +} + +function cqlDateTimesEqual(expected: any, actual: any): boolean { + if (typeof expected !== 'string' || typeof actual !== 'string') { + return false; + } + + const expectedDateTime = parseCqlDateTime(expected); + const actualDateTime = parseCqlDateTime(actual); + + if (!expectedDateTime || !actualDateTime) { + return false; + } + + if (expectedDateTime.base !== actualDateTime.base) { + return false; + } + + // If either side omits timezone, compare only the local DateTime components. + // Some engines return the server default timezone for DateTimes that were + // authored without an explicit timezone, e.g. @2014-01-01T08 becomes + // @2014-01-01T08-06:00. For these conformance tests, that offset should not + // make the value fail comparison when the expected value also omits it. + if (!expectedDateTime.timezone || !actualDateTime.timezone) { + return true; + } + + return normalizeTimezone(expectedDateTime.timezone) === normalizeTimezone(actualDateTime.timezone); +} + +function parseCqlDateTime(value: string): CqlDateTimeParts | undefined { + // DateTime literals have a T component. Date-only literals do not. + if (!/^@\d{4}(?:-\d{2})?(?:-\d{2})?T/.test(value)) { + return undefined; + } + + const match = value.match(/^(.*?)(Z|[+-]\d{2}:\d{2})?$/); + if (!match) { + return undefined; + } + + return { + base: match[1], + timezone: match[2], + }; +} + +function normalizeTimezone(timezone: string): string { + return timezone === 'Z' ? '+00:00' : timezone; +} diff --git a/test/extractResults-cql_operations.test.ts b/test/extractResults-cql_operations.test.ts index 3f751d5..2a80ab1 100644 --- a/test/extractResults-cql_operations.test.ts +++ b/test/extractResults-cql_operations.test.ts @@ -108,6 +108,61 @@ test('string response check', () => { ).toBe('abc'); }); + +test('R4 System.Long response check using valueString with L suffix', () => { + expect( + extractor!.extract({ + resourceType: 'Parameters', + parameter: [ + { + name: 'return', + extension: [ + { + url: 'http://hl7.org/fhir/StructureDefinition/cqf-cqlType', + valueString: 'System.Long', + }, + ], + valueString: '1L', + }, + ], + }) + ).toBe(1n); +}); + +test('R4 System.Long response check using valueString without L suffix', () => { + expect( + extractor!.extract({ + resourceType: 'Parameters', + parameter: [ + { + name: 'return', + extension: [ + { + url: 'http://hl7.org/fhir/StructureDefinition/cqf-cqlType', + valueString: 'System.Long', + }, + ], + valueString: '1', + }, + ], + }) + ).toBe(1n); +}); + +test('R5 System.Long response check using valueInteger64', () => { + expect( + extractor!.extract({ + resourceType: 'Parameters', + parameter: [ + { + name: 'return', + valueInteger64: '1', + }, + ], + }) + ).toBe(1n); +}); + test('singleton list-typed return stays array when singletonListKeys includes return (issue #82)', () => { expect( extractor!.extract(