inferno_core 1.4.3 → 1.4.5
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/main.rb +46 -5
- data/lib/inferno/apps/cli/templates/%library_name%.gemspec.tt +1 -1
- data/lib/inferno/apps/cli/templates/.gitignore +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/%library_name%_script_expected.json.tt +0 -8
- 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/apps/cli/templates/spec/%library_name%/patient_group_spec.rb.tt +3 -3
- data/lib/inferno/config/boot/suites.rb +35 -0
- data/lib/inferno/dsl/fhir_resource_validation.rb +90 -4
- data/lib/inferno/dsl/input_output_handling.rb +101 -5
- data/lib/inferno/dsl/must_support_metadata_extractor.rb +19 -1
- data/lib/inferno/entities/input.rb +57 -0
- data/lib/inferno/public/bundle.js +13 -13
- data/lib/inferno/test_runner.rb +5 -1
- 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 +6 -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: 8cffc2d9b151af63840789863f04e8551fd82077351f85d356b5a88f103f28f2
|
|
4
|
+
data.tar.gz: c6687758255cf5acadb700dfd268ee65f6ab03713dbc17ec412d1240d7f352b7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0f7ca455177f14a4f0ad5dae5852a7b7441e7ef867e554678b8d55410d623cb7b1d0e4e4432dbd8f1c8cdc085943801d2b98601d9ea776cd1e93f6482a02e3b9
|
|
7
|
+
data.tar.gz: d4160ba11c06d24fc185c2b4cde86fc36c519499f383c79053246114b2e69d0e5be4504e87ea476f462e1439383f8c1a07a5efcab2701449cbd6458939faa13b
|
|
@@ -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'
|
|
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
|
|
|
11
11
|
# DESCRIPTION
|
|
12
12
|
# spec.homepage = 'TODO'
|
|
13
13
|
spec.license = 'Apache-2.0'
|
|
14
|
-
spec.add_dependency 'inferno_core', '~> <%= Inferno::VERSION %>'
|
|
14
|
+
spec.add_dependency 'inferno_core', '~> <%= Inferno::VERSION.split('.').first(2).join('.') %>', '>= <%= Inferno::VERSION %>'
|
|
15
15
|
spec.required_ruby_version = Gem::Requirement.new('>= 3.3.6')
|
|
16
16
|
spec.metadata['inferno_test_kit'] = 'true'
|
|
17
17
|
# spec.metadata['homepage_uri'] = spec.homepage
|
|
@@ -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
|
data/lib/inferno/apps/cli/templates/execution_scripts/%library_name%_script_expected.json.tt
CHANGED
|
@@ -126,18 +126,10 @@
|
|
|
126
126
|
"message": "Patient/85: Patient.extension[0]: Unknown extension http://hl7.org/fhir/us/core/StructureDefinition/us-core-race",
|
|
127
127
|
"type": "info"
|
|
128
128
|
},
|
|
129
|
-
{
|
|
130
|
-
"message": "Patient/85: Patient.extension[0].extension[0].value.ofType(Coding): The definition for the Code System with URI 'urn:oid:2.16.840.1.113883.6.238' doesn't provide any codes so the code cannot be validated",
|
|
131
|
-
"type": "info"
|
|
132
|
-
},
|
|
133
129
|
{
|
|
134
130
|
"message": "Patient/85: Patient.extension[1]: Unknown extension http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity",
|
|
135
131
|
"type": "info"
|
|
136
132
|
},
|
|
137
|
-
{
|
|
138
|
-
"message": "Patient/85: Patient.extension[1].extension[0].value.ofType(Coding): The definition for the Code System with URI 'urn:oid:2.16.840.1.113883.6.238' doesn't provide any codes so the code cannot be validated",
|
|
139
|
-
"type": "info"
|
|
140
|
-
},
|
|
141
133
|
{
|
|
142
134
|
"message": "Patient/85: Patient.extension[2]: Unknown extension http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex",
|
|
143
135
|
"type": "info"
|
|
@@ -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)
|
|
@@ -46,7 +46,7 @@ RSpec.describe <%= module_name %>::PatientGroup do
|
|
|
46
46
|
result = run(test, url: url, patient_id: patient_id)
|
|
47
47
|
|
|
48
48
|
expect(result.result).to eq('fail'), result.result_message
|
|
49
|
-
expect(result.result_message).to
|
|
49
|
+
expect(result.result_message).to include('200')
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
it 'fails if a Patient is not received' do
|
|
@@ -57,7 +57,7 @@ RSpec.describe <%= module_name %>::PatientGroup do
|
|
|
57
57
|
result = run(test, url: url, patient_id: patient_id)
|
|
58
58
|
|
|
59
59
|
expect(result.result).to eq('fail'), result.result_message
|
|
60
|
-
expect(result.result_message).to
|
|
60
|
+
expect(result.result_message).to include('Patient')
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
it 'fails if the id received does not match the one requested' do
|
|
@@ -68,7 +68,7 @@ RSpec.describe <%= module_name %>::PatientGroup do
|
|
|
68
68
|
result = run(test, url: url, patient_id: patient_id)
|
|
69
69
|
|
|
70
70
|
expect(result.result).to eq('fail'), result.result_message
|
|
71
|
-
expect(result.result_message).to
|
|
71
|
+
expect(result.result_message).to include('resource with id')
|
|
72
72
|
end
|
|
73
73
|
end
|
|
74
74
|
|
|
@@ -1,3 +1,36 @@
|
|
|
1
|
+
module Inferno
|
|
2
|
+
module Config
|
|
3
|
+
module Boot
|
|
4
|
+
# Extracted from the :suites provider below so the enable_when
|
|
5
|
+
# validation can be unit tested against plain Test/TestGroup/TestSuite
|
|
6
|
+
# classes, instead of requiring a full application boot (which only
|
|
7
|
+
# happens once per process) just to exercise the raise.
|
|
8
|
+
module Suites
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# A circular or undefined enable_when dependency means the input
|
|
12
|
+
# involved can never be enabled, so this is treated as fatal at load
|
|
13
|
+
# time rather than left for developers to discover once the suite is
|
|
14
|
+
# already running. `enable_when` is uninheritable, so a problem local
|
|
15
|
+
# to a group or test can be hidden from the suite's own merged view if
|
|
16
|
+
# a parent re-declares the same input name without repeating
|
|
17
|
+
# `enable_when` -- checking every runnable in the tree, not just the
|
|
18
|
+
# suite, is what catches that case.
|
|
19
|
+
def check_enable_when_errors!(descendant)
|
|
20
|
+
[*descendant.all_descendants.reverse, descendant].each do |runnable|
|
|
21
|
+
enable_when_errors = runnable.enable_when_problem_messages
|
|
22
|
+
next if enable_when_errors.empty?
|
|
23
|
+
|
|
24
|
+
raise StandardError,
|
|
25
|
+
"Error initializing test suite #{descendant.name} (id: #{descendant.id}) " \
|
|
26
|
+
"in '#{runnable.title || runnable.id}' (id: #{runnable.id}):\n- #{enable_when_errors.join("\n- ")}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
1
34
|
Inferno::Application.register_provider(:suites) do
|
|
2
35
|
prepare do
|
|
3
36
|
target_container.start :logging
|
|
@@ -46,6 +79,8 @@ Inferno::Application.register_provider(:suites) do
|
|
|
46
79
|
raise StandardError, "Error initializing test suite #{descendant.name}: test suite ID is not set"
|
|
47
80
|
end
|
|
48
81
|
|
|
82
|
+
Inferno::Config::Boot::Suites.check_enable_when_errors!(descendant)
|
|
83
|
+
|
|
49
84
|
# This will lock the short IDs if a short ID map for this suite is present
|
|
50
85
|
descendant.assign_short_ids
|
|
51
86
|
end
|
|
@@ -55,6 +55,19 @@ module Inferno
|
|
|
55
55
|
@validator_session_repo ||= Inferno::Repositories::ValidatorSessions.new
|
|
56
56
|
end
|
|
57
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
|
+
|
|
58
71
|
# Set the url of the validator service
|
|
59
72
|
#
|
|
60
73
|
# @param validator_url [String]
|
|
@@ -212,6 +225,36 @@ module Inferno
|
|
|
212
225
|
}
|
|
213
226
|
end
|
|
214
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
|
+
|
|
215
258
|
# @private
|
|
216
259
|
# Used internally by perform_additional_validation
|
|
217
260
|
def additional_validations
|
|
@@ -334,6 +377,7 @@ module Inferno
|
|
|
334
377
|
|
|
335
378
|
# 4. Mark resources as filtered
|
|
336
379
|
mark_issues_for_filtering(issues)
|
|
380
|
+
log_validation_result(profile_url, issues, runnable) if debug_logging_enabled?
|
|
337
381
|
|
|
338
382
|
# 5. Add error messages to runnable
|
|
339
383
|
filtered_issues = issues.reject(&:filtered)
|
|
@@ -447,12 +491,57 @@ module Inferno
|
|
|
447
491
|
# @private
|
|
448
492
|
def call_validator(target, profile_url)
|
|
449
493
|
request_body = wrap_target_for_hl7_wrapper(target, profile_url)
|
|
494
|
+
|
|
450
495
|
Faraday.new(
|
|
451
496
|
url,
|
|
452
497
|
request: { timeout: 600 }
|
|
453
498
|
).post('validate', request_body, content_type: 'application/json')
|
|
454
499
|
end
|
|
455
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
|
+
|
|
456
545
|
# @private
|
|
457
546
|
# Post an object to the validation service for validating.
|
|
458
547
|
# Returns the raw validator response body.
|
|
@@ -703,10 +792,7 @@ module Inferno
|
|
|
703
792
|
end
|
|
704
793
|
|
|
705
794
|
wrapped_resource = {
|
|
706
|
-
context_key =>
|
|
707
|
-
**validation_context.definition,
|
|
708
|
-
profiles: [profile_url]
|
|
709
|
-
},
|
|
795
|
+
context_key => build_validation_context(profile_url),
|
|
710
796
|
filesToValidate: [
|
|
711
797
|
{
|
|
712
798
|
fileName: "#{profile_url.split('/').last}.json",
|
|
@@ -16,7 +16,8 @@ module Inferno
|
|
|
16
16
|
# @option input_params [Hash] :options Possible input option formats based on input type
|
|
17
17
|
# @option options [Array] :list_options Array of options for input formats
|
|
18
18
|
# that require a list of possible values (radio and checkbox)
|
|
19
|
-
# @option input_params [Hash] :enable_when Conditions for showing the input
|
|
19
|
+
# @option input_params [Hash] :enable_when Conditions for showing the input and determining
|
|
20
|
+
# whether it is required. Must be a Hash
|
|
20
21
|
# with String :input_name (the name of the controlling input) and String :value (the value
|
|
21
22
|
# that triggers visibility). For checkbox inputs the value must be a JSON-encoded sorted
|
|
22
23
|
# array, e.g. '["a","b"]'.
|
|
@@ -97,9 +98,12 @@ module Inferno
|
|
|
97
98
|
end
|
|
98
99
|
|
|
99
100
|
# @private
|
|
100
|
-
def required_inputs(selected_suite_options)
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
def required_inputs(selected_suite_options, submitted_inputs = nil)
|
|
102
|
+
input_values = (submitted_inputs || []).to_h { |input| [input[:name].to_s, input[:value]] }
|
|
103
|
+
|
|
104
|
+
available = available_inputs(selected_suite_options)
|
|
105
|
+
available
|
|
106
|
+
.reject { |_, input| input.optional || !input.enabled?(input_values, available) }
|
|
103
107
|
.map { |_, input| input.name }
|
|
104
108
|
end
|
|
105
109
|
|
|
@@ -107,7 +111,10 @@ module Inferno
|
|
|
107
111
|
def missing_inputs(submitted_inputs, selected_suite_options)
|
|
108
112
|
submitted_inputs = [] if submitted_inputs.nil?
|
|
109
113
|
|
|
110
|
-
required_inputs(selected_suite_options).map(&:to_s)
|
|
114
|
+
required_input_names = required_inputs(selected_suite_options, submitted_inputs).map(&:to_s)
|
|
115
|
+
submitted_input_names = submitted_inputs.map { |input| input[:name] }
|
|
116
|
+
|
|
117
|
+
required_input_names - submitted_input_names
|
|
111
118
|
end
|
|
112
119
|
|
|
113
120
|
# Define a particular order for inputs to be presented in the API/UI
|
|
@@ -204,6 +211,95 @@ module Inferno
|
|
|
204
211
|
|
|
205
212
|
order_available_inputs(child_inputs.merge(available_inputs))
|
|
206
213
|
end
|
|
214
|
+
|
|
215
|
+
# @private
|
|
216
|
+
# Automatically detect, among this runnable's own inputs: circular
|
|
217
|
+
# `enable_when` dependencies (e.g. input `a` is enabled_when `b`, which
|
|
218
|
+
# is itself enabled_when `a`), and `enable_when` conditions that
|
|
219
|
+
# reference an input not declared on this same runnable (e.g. a typo'd
|
|
220
|
+
# `input_name`). Both silently evaluate as permanently disabled at
|
|
221
|
+
# runtime with no other diagnostic (see `Entities::Input#enabled?`'s
|
|
222
|
+
# `visited` guard and its `input_values.key?` check), so they're
|
|
223
|
+
# treated as fatal at load time instead. Note `enable_when` is an
|
|
224
|
+
# uninheritable attribute -- it only has meaning, and can only
|
|
225
|
+
# reference another input, at the level it's declared -- so a problem
|
|
226
|
+
# local to a group or test can be masked if a parent re-declares the
|
|
227
|
+
# same input name without repeating `enable_when`; checking every
|
|
228
|
+
# runnable in the tree (not just the suite) is what catches those.
|
|
229
|
+
def enable_when_problem_messages
|
|
230
|
+
inputs_by_name = enable_when_local_inputs
|
|
231
|
+
edges = enable_when_edges(inputs_by_name)
|
|
232
|
+
|
|
233
|
+
enable_when_undefined_reference_messages(edges, inputs_by_name) + enable_when_calculated_cycle_messages(edges)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# @private
|
|
237
|
+
# Flags any `enable_when.input_name` that doesn't name an input
|
|
238
|
+
# declared on this same runnable.
|
|
239
|
+
def enable_when_undefined_reference_messages(edges, inputs_by_name)
|
|
240
|
+
local_names = inputs_by_name.values.map(&:name)
|
|
241
|
+
|
|
242
|
+
edges.filter_map do |name, referenced_name|
|
|
243
|
+
next if local_names.include?(referenced_name)
|
|
244
|
+
|
|
245
|
+
"Input '#{name}' has an enable_when condition that references '#{referenced_name}', " \
|
|
246
|
+
'which is not an input defined on the same test/group/suite.'
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# @private
|
|
251
|
+
def enable_when_calculated_cycle_messages(edges)
|
|
252
|
+
globally_visited = {}
|
|
253
|
+
|
|
254
|
+
edges.each_key.filter_map do |start_name|
|
|
255
|
+
next if globally_visited[start_name]
|
|
256
|
+
|
|
257
|
+
cycle = follow_enable_when_chain(start_name, edges, globally_visited)
|
|
258
|
+
next unless cycle
|
|
259
|
+
|
|
260
|
+
chain = (cycle + [cycle.first]).join(' -> ')
|
|
261
|
+
"Circular enable_when dependency detected in input '#{cycle.first}': #{chain}. "
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# @private
|
|
266
|
+
# Maps each input's name to the name of the input it depends on via
|
|
267
|
+
# `enable_when`, for every input in this runnable that has one.
|
|
268
|
+
# `enable_when` is uninheritable (see `Entities::Input::UNINHERITABLE_ATTRIBUTES`),
|
|
269
|
+
# so it's unaffected by the child merge in `available_inputs` -- reading
|
|
270
|
+
# straight from this runnable's own local input config avoids that
|
|
271
|
+
# merge's subtree walk entirely.
|
|
272
|
+
def enable_when_edges(inputs_by_name = enable_when_local_inputs)
|
|
273
|
+
inputs_by_name.each_with_object({}) do |(_, input), depends_on|
|
|
274
|
+
depends_on[input.name] = input.enable_when[:input_name] if input.enable_when.present?
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# @private
|
|
279
|
+
def enable_when_local_inputs
|
|
280
|
+
config.inputs.slice(*inputs)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# @private
|
|
284
|
+
# Follows the `enable_when` chain starting at `start_name`, marking
|
|
285
|
+
# every node visited along the way. Returns the cycle (as an array of
|
|
286
|
+
# input names) if one is found, otherwise nil.
|
|
287
|
+
def follow_enable_when_chain(start_name, depends_on, globally_visited)
|
|
288
|
+
path = []
|
|
289
|
+
node = start_name
|
|
290
|
+
|
|
291
|
+
until node.nil? || globally_visited[node]
|
|
292
|
+
break if path.index(node)
|
|
293
|
+
|
|
294
|
+
path << node
|
|
295
|
+
node = depends_on[node]
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
path.each { |name| globally_visited[name] = true }
|
|
299
|
+
|
|
300
|
+
cycle_start_index = path.index(node)
|
|
301
|
+
cycle_start_index && path[cycle_start_index..]
|
|
302
|
+
end
|
|
207
303
|
end
|
|
208
304
|
end
|
|
209
305
|
end
|