inferno_core 1.4.2 → 1.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/inferno/apps/cli/execute_script.rb +8 -4
- data/lib/inferno/apps/cli/main.rb +46 -5
- data/lib/inferno/apps/cli/requirements_coverage_checker.rb +1 -1
- data/lib/inferno/apps/cli/templates/Rakefile.tt +0 -13
- data/lib/inferno/apps/cli/templates/config/nginx.background.conf.tt +0 -30
- data/lib/inferno/apps/cli/templates/config/nginx.conf.tt +0 -30
- data/lib/inferno/apps/cli/templates/docker-compose.background.yml.tt +1 -14
- data/lib/inferno/apps/cli/templates/docker-compose.yml.tt +1 -10
- data/lib/inferno/apps/cli/templates/execution_scripts/{README.md.tt → README.md} +3 -3
- data/lib/inferno/apps/cli/templates/lib/%library_name%/requirements/%library_name%_requirements.csv +3 -0
- data/lib/inferno/apps/cli/templates/lib/%library_name%/requirements/Inferno Requirements Template.xlsx +0 -0
- data/lib/inferno/apps/cli/templates/lib/%library_name%/requirements/README.md +12 -0
- data/lib/inferno/apps/cli/templates/lib/%library_name%/requirements/generated/%test_suite_id%_requirements_coverage.csv.tt +1 -0
- data/lib/inferno/dsl/fhir_resource_navigation.rb +12 -2
- data/lib/inferno/dsl/fhir_resource_validation.rb +186 -4
- data/lib/inferno/dsl/must_support_assessment.rb +19 -8
- data/lib/inferno/dsl/must_support_metadata_extractor.rb +44 -1
- data/lib/inferno/dsl/profile_metadata.rb +113 -0
- data/lib/inferno/dsl/resume_test_route.rb +4 -1
- data/lib/inferno/dsl/suite_endpoint.rb +164 -6
- data/lib/inferno/dsl.rb +1 -0
- data/lib/inferno/entities/ig.rb +51 -8
- data/lib/inferno/public/bundle.js +34 -34
- data/lib/inferno/public/bundle.js.LICENSE.txt +0 -2
- data/lib/inferno/test_runner.rb +55 -40
- data/lib/inferno/utils/execution_script_runner.rb +56 -7
- data/lib/inferno/version.rb +1 -1
- data/spec/spec_helper.rb +18 -0
- metadata +7 -4
- data/lib/inferno/apps/cli/templates/lib/%library_name%/suite.rb.tt +0 -59
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c8c3149fd821b0530f7481d6a01bdef6da32263f1cca74f7b852567edb67af43
|
|
4
|
+
data.tar.gz: df8cd12ccb8054d182655c7537c730cd0a1f6fa538552e98171b93c43300d17b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f6d6c9b29f44b0f3169bee590aac204520aa3fc24d651443b5136958cecfffc889d71172881b4c97323b6b17b65d78afd8fd8027a0b8d933037023345fed975d
|
|
7
|
+
data.tar.gz: 196c88889027e64aa05e3e7a9997749c9dfc5fab9fecdfa1a40437005d2cfd9a5390945f3efc3382ae4c2a3ea3f208ebc5720b5837ca1ad46fd5f08dfdab8678
|
|
@@ -138,7 +138,7 @@ module Inferno
|
|
|
138
138
|
|
|
139
139
|
ExecutionStatus = Struct.new(
|
|
140
140
|
:done, :failed, :timed_out, :cancel_pending, :current_session, :current_timeout, :last_log_time,
|
|
141
|
-
:cross_session_status, :last_step_signatures
|
|
141
|
+
:poll_start_time, :cross_session_status, :last_step_signatures
|
|
142
142
|
)
|
|
143
143
|
|
|
144
144
|
attr_accessor :yaml_file, :options, :execution_status
|
|
@@ -155,6 +155,7 @@ module Inferno
|
|
|
155
155
|
cancel_pending: false,
|
|
156
156
|
current_session: sessions.first,
|
|
157
157
|
current_timeout: options[:default_poll_timeout],
|
|
158
|
+
poll_start_time: nil,
|
|
158
159
|
cross_session_status: {},
|
|
159
160
|
last_step_signatures: {}
|
|
160
161
|
)
|
|
@@ -416,6 +417,7 @@ module Inferno
|
|
|
416
417
|
warn ''
|
|
417
418
|
warn "Polling session: #{session.key} (#{session.session_id}) timeout=#{timeout}s"
|
|
418
419
|
deadline = Time.now + timeout
|
|
420
|
+
execution_status.poll_start_time = Time.now
|
|
419
421
|
execution_status.last_log_time = Time.now - LOG_INTERVAL_SECONDS
|
|
420
422
|
|
|
421
423
|
loop do
|
|
@@ -518,12 +520,14 @@ module Inferno
|
|
|
518
520
|
last_completed = last_completed_from_status(status)
|
|
519
521
|
poll_status_last_test =
|
|
520
522
|
last_completed.present? ? " - last test: #{format_last_completed(last_completed, session_key)}" : ''
|
|
521
|
-
|
|
523
|
+
elapsed = (Time.now - execution_status.poll_start_time).round
|
|
524
|
+
warn " [#{session_key}] #{status['status']} (#{test_progress(status, elapsed)})" \
|
|
525
|
+
"#{poll_status_last_test}"
|
|
522
526
|
execution_status.last_log_time = Time.now
|
|
523
527
|
end
|
|
524
528
|
|
|
525
|
-
def test_progress(status)
|
|
526
|
-
"#{status['completed_test_count']}/#{status['test_count']} tests"
|
|
529
|
+
def test_progress(status, elapsed)
|
|
530
|
+
"completed #{status['completed_test_count']}/#{status['test_count']} tests in #{elapsed}s"
|
|
527
531
|
end
|
|
528
532
|
|
|
529
533
|
def fetch_session_status(session_id)
|
|
@@ -9,6 +9,7 @@ require_relative 'suites'
|
|
|
9
9
|
require_relative 'new'
|
|
10
10
|
require_relative 'execute'
|
|
11
11
|
require_relative 'execute_script'
|
|
12
|
+
require_relative '../../utils/execution_script_runner'
|
|
12
13
|
require_relative '../../version'
|
|
13
14
|
|
|
14
15
|
module Inferno
|
|
@@ -78,8 +79,31 @@ module Inferno
|
|
|
78
79
|
desc 'requirements SUBCOMMAND ...ARGS', 'Perform requirements operations'
|
|
79
80
|
subcommand 'requirements', Requirements
|
|
80
81
|
|
|
81
|
-
desc 'execute_script
|
|
82
|
-
'Run
|
|
82
|
+
desc 'execute_script [PATTERN]',
|
|
83
|
+
'Run one or more session orchestration scripts defined by YAML config file(s).'
|
|
84
|
+
long_desc <<-LONGDESC
|
|
85
|
+
Run a session orchestration script defined by a YAML config file.
|
|
86
|
+
|
|
87
|
+
PATTERN may be a single YAML file, or it may contain wildcards (e.g. glob
|
|
88
|
+
patterns like `*` or `**`) to match and run multiple scripts in sequence.
|
|
89
|
+
When PATTERN matches more than one file, each matching script is run in a
|
|
90
|
+
separate process; a summary of passed and failed scripts is printed at the
|
|
91
|
+
end, and the command exits non-zero if any script failed.
|
|
92
|
+
|
|
93
|
+
If PATTERN is omitted, it defaults to `execution_scripts/**/*.yaml`, running
|
|
94
|
+
every script under the execution_scripts directory.
|
|
95
|
+
|
|
96
|
+
Examples:
|
|
97
|
+
|
|
98
|
+
# Run every script under execution_scripts
|
|
99
|
+
`bundle exec inferno execute_script`
|
|
100
|
+
|
|
101
|
+
# Run a single script
|
|
102
|
+
`bundle exec inferno execute_script execution_scripts/demo/demo_individual_tests.yaml`
|
|
103
|
+
|
|
104
|
+
# Run every script in a directory
|
|
105
|
+
`bundle exec inferno execute_script "execution_scripts/demo/*.yaml"`
|
|
106
|
+
LONGDESC
|
|
83
107
|
option :inferno_base_url,
|
|
84
108
|
aliases: ['-I'],
|
|
85
109
|
type: :string,
|
|
@@ -113,9 +137,26 @@ module Inferno
|
|
|
113
137
|
type: :boolean,
|
|
114
138
|
default: false,
|
|
115
139
|
desc: 'Allow execution script steps that run arbitrary shell commands. ' \
|
|
116
|
-
'Scripts with command: steps will fail unless this flag is set.'
|
|
117
|
-
|
|
118
|
-
|
|
140
|
+
'Scripts with command: steps will fail unless this flag is set. ' \
|
|
141
|
+
'When PATTERN matches multiple scripts, this also applies to all of them ' \
|
|
142
|
+
'(in addition to any individual script whose filename contains "_with_commands").'
|
|
143
|
+
def execute_script(pattern = 'execution_scripts/**/*.yaml')
|
|
144
|
+
matches = Dir.glob(pattern).select { |file| file.end_with?('.yaml', '.yml') }.sort
|
|
145
|
+
|
|
146
|
+
if matches.length > 1 || (matches.empty? && pattern.match?(/[*?\[\]{}]/))
|
|
147
|
+
Utils::ExecutionScriptRunner.run_all(
|
|
148
|
+
pattern:,
|
|
149
|
+
inferno_base_url: options[:inferno_base_url],
|
|
150
|
+
allow_commands: options[:allow_commands],
|
|
151
|
+
compare_messages: options[:compare_messages],
|
|
152
|
+
compare_result_message: options[:compare_result_message],
|
|
153
|
+
poll_interval: options[:poll_interval],
|
|
154
|
+
default_poll_timeout: options[:default_poll_timeout],
|
|
155
|
+
only_different_messages: options[:only_different_messages]
|
|
156
|
+
)
|
|
157
|
+
else
|
|
158
|
+
ExecuteScript.new(matches.first || pattern, options).run
|
|
159
|
+
end
|
|
119
160
|
end
|
|
120
161
|
|
|
121
162
|
desc 'session SUBCOMMAND ...ARGS', 'Perform session operations'
|
|
@@ -119,7 +119,7 @@ module Inferno
|
|
|
119
119
|
suite_runnables.select { |runnable| runnable.verifies_requirements.include? requirement_id }
|
|
120
120
|
|
|
121
121
|
runnables_for_requirement.map do |runnable|
|
|
122
|
-
[requirement_id, runnable.short_id, runnable.id]
|
|
122
|
+
[requirement_id, runnable < Inferno::Entities::TestSuite ? 'suite' : runnable.short_id, runnable.id]
|
|
123
123
|
end
|
|
124
124
|
end
|
|
125
125
|
end
|
|
@@ -5,19 +5,6 @@ begin
|
|
|
5
5
|
rescue LoadError # rubocop:disable Lint/SuppressedException
|
|
6
6
|
end
|
|
7
7
|
|
|
8
|
-
namespace :execute_scripts do
|
|
9
|
-
desc 'Run all execution script YAML files against a local Inferno instance (already running). ' \
|
|
10
|
-
'Optional FILTER env var restricts by File.fnmatch pattern, e.g. FILTER="execution_scripts/demo/*". ' \
|
|
11
|
-
'Optional INFERNO_BASE_URL env var sets the target Inferno URL, e.g. INFERNO_BASE_URL="http://localhost:4567/"'
|
|
12
|
-
task :run_all do
|
|
13
|
-
require 'inferno/utils/execution_script_runner'
|
|
14
|
-
Inferno::Utils::ExecutionScriptRunner.run_all(
|
|
15
|
-
pattern: ENV.fetch('FILTER', 'execution_scripts/**/*.yaml'),
|
|
16
|
-
inferno_base_url: ENV.fetch('INFERNO_BASE_URL', nil)
|
|
17
|
-
)
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
|
|
21
8
|
namespace :db do
|
|
22
9
|
desc 'Apply changes to the database'
|
|
23
10
|
task :migrate do
|
|
@@ -53,36 +53,6 @@ http {
|
|
|
53
53
|
# the server will close connections after this time
|
|
54
54
|
keepalive_timeout 600;
|
|
55
55
|
|
|
56
|
-
location /validator {
|
|
57
|
-
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
58
|
-
proxy_set_header Host $http_host;
|
|
59
|
-
proxy_set_header X-Forwarded-Proto $scheme;
|
|
60
|
-
proxy_set_header X-Forwarded-Port $server_port;
|
|
61
|
-
proxy_redirect off;
|
|
62
|
-
proxy_set_header Connection '';
|
|
63
|
-
proxy_http_version 1.1;
|
|
64
|
-
chunked_transfer_encoding off;
|
|
65
|
-
proxy_buffering off;
|
|
66
|
-
proxy_cache off;
|
|
67
|
-
|
|
68
|
-
proxy_pass http://fhir_validator_app;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
location /validatorapi/ {
|
|
72
|
-
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
73
|
-
proxy_set_header Host $http_host;
|
|
74
|
-
proxy_set_header X-Forwarded-Proto $scheme;
|
|
75
|
-
proxy_set_header X-Forwarded-Port $server_port;
|
|
76
|
-
proxy_redirect off;
|
|
77
|
-
proxy_set_header Connection '';
|
|
78
|
-
proxy_http_version 1.1;
|
|
79
|
-
chunked_transfer_encoding off;
|
|
80
|
-
proxy_buffering off;
|
|
81
|
-
proxy_cache off;
|
|
82
|
-
|
|
83
|
-
proxy_pass http://validator_service:4567/;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
56
|
location /hl7validatorapi/ {
|
|
87
57
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
88
58
|
proxy_set_header Host $http_host;
|
|
@@ -68,36 +68,6 @@ http {
|
|
|
68
68
|
proxy_pass http://inferno:4567;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
location /validator {
|
|
72
|
-
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
73
|
-
proxy_set_header Host $http_host;
|
|
74
|
-
proxy_set_header X-Forwarded-Proto $scheme;
|
|
75
|
-
proxy_set_header X-Forwarded-Port $server_port;
|
|
76
|
-
proxy_redirect off;
|
|
77
|
-
proxy_set_header Connection '';
|
|
78
|
-
proxy_http_version 1.1;
|
|
79
|
-
chunked_transfer_encoding off;
|
|
80
|
-
proxy_buffering off;
|
|
81
|
-
proxy_cache off;
|
|
82
|
-
|
|
83
|
-
proxy_pass http://fhir_validator_app;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
location /validatorapi/ {
|
|
87
|
-
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
88
|
-
proxy_set_header Host $http_host;
|
|
89
|
-
proxy_set_header X-Forwarded-Proto $scheme;
|
|
90
|
-
proxy_set_header X-Forwarded-Port $server_port;
|
|
91
|
-
proxy_redirect off;
|
|
92
|
-
proxy_set_header Connection '';
|
|
93
|
-
proxy_http_version 1.1;
|
|
94
|
-
chunked_transfer_encoding off;
|
|
95
|
-
proxy_buffering off;
|
|
96
|
-
proxy_cache off;
|
|
97
|
-
|
|
98
|
-
proxy_pass http://validator_service:4567/;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
71
|
location /hl7validatorapi/ {
|
|
102
72
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
103
73
|
proxy_set_header Host $http_host;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
version: '3'
|
|
2
1
|
services:
|
|
3
2
|
hl7_validator_service:
|
|
4
3
|
image: infernocommunity/inferno-resource-validator
|
|
@@ -11,18 +10,6 @@ services:
|
|
|
11
10
|
# To let the service share your local FHIR package cache,
|
|
12
11
|
# uncomment the below line
|
|
13
12
|
# - ~/.fhir:/home/ktor/.fhir
|
|
14
|
-
validator_service:
|
|
15
|
-
image: infernocommunity/fhir-validator-service
|
|
16
|
-
# Update this path to match your directory structure
|
|
17
|
-
volumes:
|
|
18
|
-
- ./data/igs:/home/igs
|
|
19
|
-
fhir_validator_app:
|
|
20
|
-
image: infernocommunity/fhir-validator-app
|
|
21
|
-
depends_on:
|
|
22
|
-
- validator_service
|
|
23
|
-
environment:
|
|
24
|
-
EXTERNAL_VALIDATOR_URL: http://localhost/validatorapi
|
|
25
|
-
VALIDATOR_BASE_PATH: /validator
|
|
26
13
|
fhirpath:
|
|
27
14
|
image: infernocommunity/fhirpath-service
|
|
28
15
|
ports:
|
|
@@ -35,7 +22,7 @@ services:
|
|
|
35
22
|
- "80:80"
|
|
36
23
|
command: [nginx, '-g', 'daemon off;']
|
|
37
24
|
depends_on:
|
|
38
|
-
-
|
|
25
|
+
- hl7_validator_service
|
|
39
26
|
- fhirpath
|
|
40
27
|
redis:
|
|
41
28
|
image: redis
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
version: '3'
|
|
2
1
|
services:
|
|
3
2
|
inferno:
|
|
4
3
|
build:
|
|
@@ -6,7 +5,7 @@ services:
|
|
|
6
5
|
volumes:
|
|
7
6
|
- ./data:/opt/inferno/data
|
|
8
7
|
depends_on:
|
|
9
|
-
-
|
|
8
|
+
- hl7_validator_service
|
|
10
9
|
worker:
|
|
11
10
|
build:
|
|
12
11
|
context: ./
|
|
@@ -19,14 +18,6 @@ services:
|
|
|
19
18
|
extends:
|
|
20
19
|
file: docker-compose.background.yml
|
|
21
20
|
service: hl7_validator_service
|
|
22
|
-
validator_service:
|
|
23
|
-
extends:
|
|
24
|
-
file: docker-compose.background.yml
|
|
25
|
-
service: validator_service
|
|
26
|
-
fhir_validator_app:
|
|
27
|
-
extends:
|
|
28
|
-
file: docker-compose.background.yml
|
|
29
|
-
service: fhir_validator_app
|
|
30
21
|
fhirpath:
|
|
31
22
|
extends:
|
|
32
23
|
file: docker-compose.background.yml
|
|
@@ -10,7 +10,7 @@ as `tx.fhir.org`.
|
|
|
10
10
|
|
|
11
11
|
Execution scripts defined here will
|
|
12
12
|
be [executed](https://inferno-framework.github.io/docs/advanced-test-features/scripting-execution.html#execution)
|
|
13
|
-
automatically on pull requests as a part of the
|
|
13
|
+
automatically on pull requests as a part of the GitHub
|
|
14
14
|
workflows and can also be executed locally using the
|
|
15
|
-
[`execute_script` CLI](https://inferno-framework.github.io/docs/getting-started/inferno-cli.html#complex-scripted-execution)
|
|
16
|
-
|
|
15
|
+
[`execute_script` CLI](https://inferno-framework.github.io/docs/getting-started/inferno-cli.html#complex-scripted-execution).
|
|
16
|
+
If no `.yaml` files are found under this directory, the GitHub workflow will be skipped.
|
data/lib/inferno/apps/cli/templates/lib/%library_name%/requirements/%library_name%_requirements.csv
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
Req Set,ID,URL,Requirement,Conformance,Actors,Sub-Requirement(s),Conditionality,Not Tested Reason,Not Tested Details
|
|
2
|
+
sample_v1,12,www.example.com/page.html#section,Requirement text SHALL go here,SHALL,Server,sample_v1@13,false,Not Tested,We've decided not to test this requirement
|
|
3
|
+
sample_v1,13,www.example.com/page.html#section,"This is a subsrequirement, and it SHOULD be implemented.",SHOULD,Server,,false,,
|
|
Binary file
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Using Core Requirements Tools
|
|
2
|
+
|
|
3
|
+
This directory contains [requirement artifacts](https://inferno-framework.github.io/docs/advanced-test-features/requirements.html)
|
|
4
|
+
that contain example requirements that could be linked to tests
|
|
5
|
+
within this test kit and displayed within the UI as justification
|
|
6
|
+
for the tests.
|
|
7
|
+
|
|
8
|
+
Each `.xlsx` file defined in this directory will contribute towards the
|
|
9
|
+
test kit's requirements and a GitHub workflow executed automatically on pull
|
|
10
|
+
requests checks that these files are in sync with the generated requirement
|
|
11
|
+
list and coverage analysis `.csv` files. If no `.xlsx` files are found in
|
|
12
|
+
this directory, the GitHub workflow will be skipped.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Req Set,ID,URL,Requirement,Conformance,Actors,Conditionality,Not Tested Reason,Not Tested Details,<%= title_name %> Short ID(s),<%= title_name %> Full ID(s)
|
|
@@ -42,8 +42,13 @@ module Inferno
|
|
|
42
42
|
# @param given_element [FHIR::Model, Array<FHIR::Model>]
|
|
43
43
|
# @param path [String]
|
|
44
44
|
# @param include_dar [Boolean]
|
|
45
|
+
# @param recursive_segments [Array<String>] names of path segments that are
|
|
46
|
+
# self-referential (eg. `item` in Questionnaire.item.item, via a FHIR
|
|
47
|
+
# `contentReference`). When a segment matches, the search also continues
|
|
48
|
+
# into deeper repeats of that segment (item.item.item...) if the value
|
|
49
|
+
# isn't found at the literal path depth.
|
|
45
50
|
# @return a single matching value (which can include `false`) or `nil` if not found
|
|
46
|
-
def find_a_value_at(given_element, path, include_dar: false, &block)
|
|
51
|
+
def find_a_value_at(given_element, path, include_dar: false, recursive_segments: [], &block)
|
|
47
52
|
return nil if given_element.nil?
|
|
48
53
|
|
|
49
54
|
elements = Array.wrap(given_element)
|
|
@@ -56,8 +61,13 @@ module Inferno
|
|
|
56
61
|
remaining_path = path_segments.join('.')
|
|
57
62
|
elements.each do |element|
|
|
58
63
|
child = get_next_value(element, segment)
|
|
59
|
-
element_found = find_a_value_at(child, remaining_path, include_dar:, &block)
|
|
64
|
+
element_found = find_a_value_at(child, remaining_path, include_dar:, recursive_segments:, &block)
|
|
60
65
|
return element_found if value_not_empty?(element_found)
|
|
66
|
+
|
|
67
|
+
next unless recursive_segments.include?(segment)
|
|
68
|
+
|
|
69
|
+
nested_found = find_a_value_at(child, path, include_dar:, recursive_segments:, &block)
|
|
70
|
+
return nested_found if value_not_empty?(nested_found)
|
|
61
71
|
end
|
|
62
72
|
|
|
63
73
|
nil
|
|
@@ -26,6 +26,7 @@ module Inferno
|
|
|
26
26
|
# allowExampleUrls true
|
|
27
27
|
# txServer nil
|
|
28
28
|
# end
|
|
29
|
+
# expansion_parameters 'path/to/expansion_parameters.json'
|
|
29
30
|
# end
|
|
30
31
|
module FHIRResourceValidation
|
|
31
32
|
def self.included(klass)
|
|
@@ -54,6 +55,19 @@ module Inferno
|
|
|
54
55
|
@validator_session_repo ||= Inferno::Repositories::ValidatorSessions.new
|
|
55
56
|
end
|
|
56
57
|
|
|
58
|
+
# Environment variable that, when set to a truthy-looking value,
|
|
59
|
+
# enables verbose validation logging (the validationContext sent
|
|
60
|
+
# with each request, and the resulting issues including which were
|
|
61
|
+
# filtered out) through Inferno's normal application logger, tagged
|
|
62
|
+
# with the validator definition and, when available, the test
|
|
63
|
+
# session and test that triggered the request.
|
|
64
|
+
VALIDATOR_DEBUG_LOGGING_ENV_VAR = 'FHIR_RESOURCE_VALIDATOR_DEBUG_LOGGING'.freeze
|
|
65
|
+
|
|
66
|
+
# @private
|
|
67
|
+
def debug_logging_enabled?
|
|
68
|
+
ENV.fetch(VALIDATOR_DEBUG_LOGGING_ENV_VAR, nil).present?
|
|
69
|
+
end
|
|
70
|
+
|
|
57
71
|
# Set the url of the validator service
|
|
58
72
|
#
|
|
59
73
|
# @param validator_url [String]
|
|
@@ -118,6 +132,129 @@ module Inferno
|
|
|
118
132
|
|
|
119
133
|
alias cli_context validation_context
|
|
120
134
|
|
|
135
|
+
# Environment variable containing the default expansion parameters.
|
|
136
|
+
# Used by {#expansion_parameters} when no value has been set
|
|
137
|
+
# explicitly. May contain either the raw JSON content of a FHIR
|
|
138
|
+
# Parameters resource (if it starts with `{`) or a path to a file
|
|
139
|
+
# containing one.
|
|
140
|
+
EXPANSION_PARAMETERS_ENV_VAR = 'FHIR_RESOURCE_VALIDATOR_EXPANSION_PARAMETERS'.freeze
|
|
141
|
+
|
|
142
|
+
# Set the expansion parameters to be sent with each validation
|
|
143
|
+
# request. This configures how the validator's terminology engine
|
|
144
|
+
# expands value sets during validation (e.g. designation
|
|
145
|
+
# preferences, forcing the use of the latest terminology versions,
|
|
146
|
+
# etc.). The content is sent inline with every validation request
|
|
147
|
+
# made by this validator, since the validator does not have access
|
|
148
|
+
# to Inferno's filesystem.
|
|
149
|
+
#
|
|
150
|
+
# Accepts either a Hash containing the contents of a FHIR Parameters
|
|
151
|
+
# resource, or a String path to a file (JSON or XML) containing one.
|
|
152
|
+
# The file is read once, the first time it's needed.
|
|
153
|
+
#
|
|
154
|
+
# If never set explicitly, this falls back to the
|
|
155
|
+
# `FHIR_RESOURCE_VALIDATOR_EXPANSION_PARAMETERS` environment
|
|
156
|
+
# variable, if present. This allows a shared set of expansion
|
|
157
|
+
# parameters to be configured once for every test kit that uses a
|
|
158
|
+
# given validator instance, while still letting individual test
|
|
159
|
+
# kits opt out or override it by calling this method themselves.
|
|
160
|
+
# The environment variable's content is treated as raw JSON if it
|
|
161
|
+
# starts with `{`, and otherwise as a file path.
|
|
162
|
+
#
|
|
163
|
+
# @example
|
|
164
|
+
# # Passing a Hash
|
|
165
|
+
# fhir_resource_validator do
|
|
166
|
+
# url 'http://example.com/validator'
|
|
167
|
+
# expansion_parameters({
|
|
168
|
+
# resourceType: 'Parameters',
|
|
169
|
+
# parameter: [{ name: 'excludeNested', valueBoolean: true }]
|
|
170
|
+
# })
|
|
171
|
+
# end
|
|
172
|
+
#
|
|
173
|
+
# @example
|
|
174
|
+
# # Passing a file path
|
|
175
|
+
# fhir_resource_validator do
|
|
176
|
+
# url 'http://example.com/validator'
|
|
177
|
+
# expansion_parameters 'path/to/expansion_parameters.json'
|
|
178
|
+
# end
|
|
179
|
+
#
|
|
180
|
+
# @param value [Hash, String, nil] contents of a Parameters resource
|
|
181
|
+
# as a Hash, or a path to a file (JSON or XML) containing one
|
|
182
|
+
def expansion_parameters(value = nil)
|
|
183
|
+
if value
|
|
184
|
+
@expansion_parameters = build_expansion_parameters(value)
|
|
185
|
+
elsif !@expansion_parameters_resolved
|
|
186
|
+
env_value = ENV.fetch(EXPANSION_PARAMETERS_ENV_VAR, nil)
|
|
187
|
+
@expansion_parameters = build_expansion_parameters_from_env(env_value) if env_value
|
|
188
|
+
end
|
|
189
|
+
@expansion_parameters_resolved = true
|
|
190
|
+
|
|
191
|
+
@expansion_parameters
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# @private
|
|
195
|
+
# Determines whether the environment variable's content is raw JSON
|
|
196
|
+
# or a file path based on its first non-whitespace character.
|
|
197
|
+
def build_expansion_parameters_from_env(env_value)
|
|
198
|
+
if env_value.lstrip.start_with?('{')
|
|
199
|
+
build_expansion_parameters_from_json_content(env_value)
|
|
200
|
+
else
|
|
201
|
+
build_expansion_parameters(env_value)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# @private
|
|
206
|
+
def build_expansion_parameters(value)
|
|
207
|
+
case value
|
|
208
|
+
when Hash
|
|
209
|
+
build_expansion_parameters_from_json_content(value.to_json)
|
|
210
|
+
else
|
|
211
|
+
{
|
|
212
|
+
fileName: File.basename(value),
|
|
213
|
+
fileContent: File.read(value),
|
|
214
|
+
fileType: nil
|
|
215
|
+
}
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# @private
|
|
220
|
+
def build_expansion_parameters_from_json_content(json_content)
|
|
221
|
+
{
|
|
222
|
+
fileName: 'expansion_parameters.json',
|
|
223
|
+
fileContent: json_content,
|
|
224
|
+
fileType: nil
|
|
225
|
+
}
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Environment variable that, when set, enables terminology server
|
|
229
|
+
# request logging by the validator itself. Its value is sent as
|
|
230
|
+
# `txLog` in the validationContext of every validation request,
|
|
231
|
+
# telling the validator where to log the terminology server
|
|
232
|
+
# requests it makes. Note that the log will appear within
|
|
233
|
+
# the container running the validator.
|
|
234
|
+
TX_LOG_ENV_VAR = 'FHIR_RESOURCE_VALIDATOR_TX_LOG'.freeze
|
|
235
|
+
|
|
236
|
+
# @private
|
|
237
|
+
def tx_log
|
|
238
|
+
ENV.fetch(TX_LOG_ENV_VAR, nil).presence
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# @private
|
|
242
|
+
# Builds the validationContext sent with a request for the given
|
|
243
|
+
# profile. Pulled out on its own (rather than inlined in
|
|
244
|
+
# `wrap_target_for_hl7_wrapper`) so the same, resource-content-free
|
|
245
|
+
# context can also be used for logging in `log_validation_result`.
|
|
246
|
+
#
|
|
247
|
+
# @param profile_url [String]
|
|
248
|
+
# @return [Hash]
|
|
249
|
+
def build_validation_context(profile_url)
|
|
250
|
+
context = {
|
|
251
|
+
**validation_context.definition,
|
|
252
|
+
profiles: [profile_url]
|
|
253
|
+
}
|
|
254
|
+
context[:txLog] = tx_log if tx_log
|
|
255
|
+
context
|
|
256
|
+
end
|
|
257
|
+
|
|
121
258
|
# @private
|
|
122
259
|
# Used internally by perform_additional_validation
|
|
123
260
|
def additional_validations
|
|
@@ -240,6 +377,7 @@ module Inferno
|
|
|
240
377
|
|
|
241
378
|
# 4. Mark resources as filtered
|
|
242
379
|
mark_issues_for_filtering(issues)
|
|
380
|
+
log_validation_result(profile_url, issues, runnable) if debug_logging_enabled?
|
|
243
381
|
|
|
244
382
|
# 5. Add error messages to runnable
|
|
245
383
|
filtered_issues = issues.reject(&:filtered)
|
|
@@ -353,12 +491,57 @@ module Inferno
|
|
|
353
491
|
# @private
|
|
354
492
|
def call_validator(target, profile_url)
|
|
355
493
|
request_body = wrap_target_for_hl7_wrapper(target, profile_url)
|
|
494
|
+
|
|
356
495
|
Faraday.new(
|
|
357
496
|
url,
|
|
358
497
|
request: { timeout: 600 }
|
|
359
498
|
).post('validate', request_body, content_type: 'application/json')
|
|
360
499
|
end
|
|
361
500
|
|
|
501
|
+
# @private
|
|
502
|
+
# Logs the validationContext and expansionParameters sent with a
|
|
503
|
+
# request together with the resulting issues (including which were
|
|
504
|
+
# filtered out) in a single entry, tagged with enough context to
|
|
505
|
+
# trace it back to the validator definition and triggering test run.
|
|
506
|
+
#
|
|
507
|
+
# Deliberately omits the resource content itself (sent separately as
|
|
508
|
+
# `filesToValidate`): only the small, non-PHI-bearing
|
|
509
|
+
# validationContext and expansionParameters are logged, not the full
|
|
510
|
+
# request body.
|
|
511
|
+
#
|
|
512
|
+
# @param profile_url [String] the profile URL validated against
|
|
513
|
+
# @param issues [Array<ValidatorIssue>] the resulting issues, already marked for filtering
|
|
514
|
+
# @param runnable [Object] the runnable (typically a Test) that triggered the request
|
|
515
|
+
def log_validation_result(profile_url, issues, runnable)
|
|
516
|
+
payload = {
|
|
517
|
+
validator_name: name,
|
|
518
|
+
test_suite_id: test_suite_id,
|
|
519
|
+
test_session_id: runnable.respond_to?(:test_session_id) ? runnable.test_session_id : nil,
|
|
520
|
+
test_id: runnable.id,
|
|
521
|
+
validation_context: build_validation_context(profile_url),
|
|
522
|
+
expansion_parameters:,
|
|
523
|
+
issues: issues.map { |issue| issue_summary(issue) }
|
|
524
|
+
}.compact
|
|
525
|
+
|
|
526
|
+
Application[:logger].info("FHIR validation result: #{payload.to_json}")
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
# @private
|
|
530
|
+
# Recursively builds a loggable summary of a validation issue,
|
|
531
|
+
# including nested slice_info, without any resource content.
|
|
532
|
+
#
|
|
533
|
+
# @param issue [ValidatorIssue]
|
|
534
|
+
# @return [Hash]
|
|
535
|
+
def issue_summary(issue)
|
|
536
|
+
{
|
|
537
|
+
severity: issue.severity,
|
|
538
|
+
location: issue.location,
|
|
539
|
+
message: issue.message,
|
|
540
|
+
filtered: issue.filtered,
|
|
541
|
+
slice_info: issue.slice_info.any? ? issue.slice_info.map { |nested| issue_summary(nested) } : nil
|
|
542
|
+
}.compact
|
|
543
|
+
end
|
|
544
|
+
|
|
362
545
|
# @private
|
|
363
546
|
# Post an object to the validation service for validating.
|
|
364
547
|
# Returns the raw validator response body.
|
|
@@ -609,10 +792,7 @@ module Inferno
|
|
|
609
792
|
end
|
|
610
793
|
|
|
611
794
|
wrapped_resource = {
|
|
612
|
-
context_key =>
|
|
613
|
-
**validation_context.definition,
|
|
614
|
-
profiles: [profile_url]
|
|
615
|
-
},
|
|
795
|
+
context_key => build_validation_context(profile_url),
|
|
616
796
|
filesToValidate: [
|
|
617
797
|
{
|
|
618
798
|
fileName: "#{profile_url.split('/').last}.json",
|
|
@@ -622,6 +802,8 @@ module Inferno
|
|
|
622
802
|
],
|
|
623
803
|
sessionId: @session_id
|
|
624
804
|
}
|
|
805
|
+
wrapped_resource[:expansionParameters] = expansion_parameters if expansion_parameters
|
|
806
|
+
|
|
625
807
|
wrapped_resource.to_json
|
|
626
808
|
end
|
|
627
809
|
end
|
|
@@ -15,10 +15,14 @@ module Inferno
|
|
|
15
15
|
# @param resources [Array<FHIR::Resource>]
|
|
16
16
|
# @param profile_url [String]
|
|
17
17
|
# @param validator_name [Symbol] Name of the FHIR Validator that references the IG the profile is in
|
|
18
|
-
# @param metadata [
|
|
19
|
-
# if provided the check will use this instead of re-generating metadata from the profile
|
|
18
|
+
# @param metadata [Inferno::DSL::ProfileMetadata, #must_supports] MustSupport Metadata (optional),
|
|
19
|
+
# if provided the check will use this instead of re-generating metadata from the profile.
|
|
20
|
+
# Must respond to `#must_supports`, returning a Hash with `:elements`, `:extensions`, and
|
|
21
|
+
# `:slices` keys (and optionally `:choices` and `:recursive_elements`) -- the shape produced by
|
|
22
|
+
# {MustSupportMetadataExtractor#must_supports}. {Inferno::DSL::ProfileMetadata} is a base class
|
|
23
|
+
# test kits can subclass to build these objects, eg from generated YAML, instead of hand-rolling one.
|
|
20
24
|
# @param requirement_extension [String] Extension URL that implies "required" as an alternative to the MS flag
|
|
21
|
-
# @yield [
|
|
25
|
+
# @yield [MustSupportMetadataExtractor] Customize the metadata before running the test
|
|
22
26
|
# @return [Array<String>] List of missing elements
|
|
23
27
|
def missing_must_support_elements(resources, profile_url, validator_name: :default, metadata: nil,
|
|
24
28
|
requirement_extension: nil, &)
|
|
@@ -199,6 +203,12 @@ module Inferno
|
|
|
199
203
|
metadata.must_supports[:extensions]
|
|
200
204
|
end
|
|
201
205
|
|
|
206
|
+
# Names of path segments that are self-referential (eg 'item' in Questionnaire.item.item)
|
|
207
|
+
# and so should be searched at any depth of nesting, not just the literal depth of a path.
|
|
208
|
+
def recursive_element_segments
|
|
209
|
+
Array.wrap(metadata.must_supports[:recursive_elements])
|
|
210
|
+
end
|
|
211
|
+
|
|
202
212
|
def missing_extensions(resources = [])
|
|
203
213
|
@missing_extensions ||=
|
|
204
214
|
must_support_extensions.select do |extension_definition|
|
|
@@ -212,7 +222,7 @@ module Inferno
|
|
|
212
222
|
normalized_extension_url(extension.url) == expected_url
|
|
213
223
|
end
|
|
214
224
|
else
|
|
215
|
-
extension = find_a_value_at(resource, path) do |el|
|
|
225
|
+
extension = find_a_value_at(resource, path, recursive_segments: recursive_element_segments) do |el|
|
|
216
226
|
normalized_extension_url(el.url) == expected_url
|
|
217
227
|
end
|
|
218
228
|
|
|
@@ -243,9 +253,10 @@ module Inferno
|
|
|
243
253
|
ms_extension_urls = must_support_extensions.select { |ex| ex[:path] == "#{raw_path}.extension" }
|
|
244
254
|
.map { |ex| ex[:url] }
|
|
245
255
|
|
|
246
|
-
value_found =
|
|
247
|
-
|
|
248
|
-
|
|
256
|
+
value_found =
|
|
257
|
+
find_a_value_at(resource, path, recursive_segments: recursive_element_segments) do |potential_value|
|
|
258
|
+
matching_without_extensions?(potential_value, ms_extension_urls, element_definition[:fixed_value])
|
|
259
|
+
end
|
|
249
260
|
|
|
250
261
|
# Note that false.present? => false, which is why we need to add this extra check
|
|
251
262
|
value_found.present? || value_found == false
|
|
@@ -369,7 +380,7 @@ module Inferno
|
|
|
369
380
|
# TODO: there is a lot of similarity
|
|
370
381
|
# between this and FHIRResourceNavigation.matching_slice?
|
|
371
382
|
# Can these be combined?
|
|
372
|
-
find_a_value_at(resource, path) do |element|
|
|
383
|
+
find_a_value_at(resource, path, recursive_segments: recursive_element_segments) do |element|
|
|
373
384
|
case discriminator[:type]
|
|
374
385
|
when 'patternCodeableConcept'
|
|
375
386
|
find_pattern_codeable_concept_slice(element, discriminator)
|