inferno_core 0.6.9 → 0.6.11
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/evaluate/database.yml +15 -0
- data/lib/inferno/apps/cli/evaluate/docker-compose.evaluate.yml +16 -0
- data/lib/inferno/apps/cli/evaluate.rb +52 -4
- data/lib/inferno/apps/cli/execute.rb +17 -21
- data/lib/inferno/apps/cli/main.rb +5 -1
- data/lib/inferno/apps/cli/requirements.rb +28 -0
- data/lib/inferno/apps/cli/requirements_exporter.rb +194 -0
- data/lib/inferno/apps/cli/suite.rb +21 -0
- data/lib/inferno/apps/cli/templates/lib/%library_name%/example_suite/patient_group.rb.tt +141 -0
- data/lib/inferno/apps/cli/templates/lib/%library_name%/example_suite.rb.tt +128 -0
- data/lib/inferno/apps/cli/templates/lib/%library_name%/metadata.rb.tt +65 -3
- data/lib/inferno/apps/cli/templates/lib/%library_name%/version.rb.tt +1 -0
- data/lib/inferno/apps/cli/templates/lib/%library_name%.rb.tt +1 -1
- data/lib/inferno/apps/web/router.rb +10 -5
- data/lib/inferno/apps/web/serializers/input.rb +1 -0
- data/lib/inferno/apps/web/serializers/serializer.rb +7 -0
- data/lib/inferno/apps/web/serializers/test.rb +1 -0
- data/lib/inferno/apps/web/serializers/test_group.rb +1 -0
- data/lib/inferno/apps/web/serializers/test_suite.rb +2 -1
- data/lib/inferno/config/boot/suites.rb +3 -0
- data/lib/inferno/dsl/fhir_evaluation/default.yml +68 -0
- data/lib/inferno/dsl/fhir_evaluation/evaluator.rb +3 -5
- data/lib/inferno/dsl/fhir_evaluation/rules/all_defined_extensions_have_examples.rb +2 -2
- data/lib/inferno/dsl/fhir_evaluation/rules/all_extensions_used.rb +2 -2
- data/lib/inferno/dsl/fhir_evaluation/rules/all_must_supports_present.rb +1 -1
- data/lib/inferno/dsl/fhir_evaluation/rules/all_profiles_have_examples.rb +1 -1
- data/lib/inferno/dsl/fhir_evaluation/rules/all_references_resolve.rb +2 -2
- data/lib/inferno/dsl/fhir_evaluation/rules/all_resources_reachable.rb +2 -2
- data/lib/inferno/dsl/fhir_evaluation/rules/all_search_parameters_have_examples.rb +22 -11
- data/lib/inferno/dsl/fhir_evaluation/rules/differential_content_has_examples.rb +2 -2
- data/lib/inferno/dsl/fhir_evaluation/rules/value_sets_demonstrate.rb +4 -4
- data/lib/inferno/dsl/fhir_resource_validation.rb +25 -3
- data/lib/inferno/dsl/fhirpath_evaluation.rb +25 -1
- data/lib/inferno/dsl/input_output_handling.rb +1 -0
- data/lib/inferno/dsl/runnable.rb +5 -0
- data/lib/inferno/dsl/short_id_manager.rb +55 -0
- data/lib/inferno/entities/input.rb +14 -5
- data/lib/inferno/entities/requirement.rb +15 -3
- data/lib/inferno/entities/test.rb +3 -1
- data/lib/inferno/entities/test_group.rb +3 -1
- data/lib/inferno/entities/test_suite.rb +2 -0
- data/lib/inferno/exceptions.rb +6 -0
- data/lib/inferno/feature.rb +9 -0
- data/lib/inferno/public/237.bundle.js +1 -1
- data/lib/inferno/public/bundle.js +54 -54
- data/lib/inferno/public/bundle.js.LICENSE.txt +3 -36
- data/lib/inferno/repositories/requirements.rb +6 -2
- data/lib/inferno/version.rb +1 -1
- data/spec/shared/test_kit_examples.rb +32 -0
- metadata +25 -4
- data/lib/inferno/apps/cli/templates/lib/%library_name%/patient_group.rb.tt +0 -44
- data/spec/features_helper.rb +0 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b899985c0f229c1f91aa9218d09f9f502ed315c537398edc911e989cbe61c6a8
|
4
|
+
data.tar.gz: '08cca457d4bb79bdb70487a14fa07f8274dac19faae5623bedef8797541e993b'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 36b765180f9d7168c91238e1c7a63e8cc281f3a795f068e3d6ddb317d419d90dc2401585b8fde37e3d27c0c82fda9ef66dba3c6bd96201f1dafa9d3be301b53f
|
7
|
+
data.tar.gz: d83ffd4c0fec0cf1d3621a7f563c9d369d671bfce5f11c9bdcac9f287c5918a3c37f3ff340cd3429b4dbb1e96e9daa936ce50fe4bd889420cb66c0a41020a777
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# Inferno is using `Psych::safe_load` so YAML anchors are disabled
|
2
|
+
development:
|
3
|
+
adapter: sqlite
|
4
|
+
database: ':memory:'
|
5
|
+
max_connections: 10
|
6
|
+
|
7
|
+
production:
|
8
|
+
adapter: sqlite
|
9
|
+
database: ':memory:'
|
10
|
+
max_connections: 10
|
11
|
+
|
12
|
+
test:
|
13
|
+
adapter: sqlite
|
14
|
+
database: ':memory:'
|
15
|
+
max_connections: 10
|
@@ -0,0 +1,16 @@
|
|
1
|
+
name: inferno_evaluator_services
|
2
|
+
|
3
|
+
services:
|
4
|
+
hl7_validator_service:
|
5
|
+
image: infernocommunity/inferno-resource-validator
|
6
|
+
volumes:
|
7
|
+
- ${TMPDIR}/data/igs:/app/igs
|
8
|
+
# To let the service share your local FHIR package cache,
|
9
|
+
# uncomment the below line
|
10
|
+
# - ~/.fhir:/home/ktor/.fhir
|
11
|
+
ports:
|
12
|
+
- "3501:3500"
|
13
|
+
fhirpath:
|
14
|
+
image: infernocommunity/fhirpath-service
|
15
|
+
ports:
|
16
|
+
- "6790:6789"
|
@@ -2,13 +2,62 @@ require_relative '../../dsl/fhir_evaluation/evaluator'
|
|
2
2
|
require_relative '../../dsl/fhir_evaluation/config'
|
3
3
|
require_relative '../../entities'
|
4
4
|
require_relative '../../utils/ig_downloader'
|
5
|
+
require_relative 'migration'
|
5
6
|
|
7
|
+
require 'fileutils'
|
6
8
|
require 'tempfile'
|
7
9
|
|
8
10
|
module Inferno
|
9
11
|
module CLI
|
10
|
-
class Evaluate
|
11
|
-
|
12
|
+
class Evaluate
|
13
|
+
# @see Inferno::CLI::Main#evaluate
|
14
|
+
def run(ig_path, data_path, options)
|
15
|
+
tmpdir = Dir.mktmpdir
|
16
|
+
Dir.mkdir("#{tmpdir}/data")
|
17
|
+
Dir.mkdir("#{tmpdir}/data/igs")
|
18
|
+
Dir.mkdir("#{tmpdir}/config")
|
19
|
+
FileUtils.cp(File.expand_path('evaluate/database.yml', __dir__), "#{tmpdir}/config/database.yml")
|
20
|
+
|
21
|
+
ENV['TMPDIR'] = tmpdir
|
22
|
+
ENV['FHIRPATH_URL'] = 'http://localhost:6790'
|
23
|
+
ENV['FHIR_RESOURCE_VALIDATOR_URL'] = 'http://localhost:3501'
|
24
|
+
|
25
|
+
puts 'Starting Inferno Evaluator Services...'
|
26
|
+
system("#{services_base_command} up -d #{services_names}")
|
27
|
+
|
28
|
+
ig_path = absolute_path_with_home_expansion(ig_path)
|
29
|
+
data_path = absolute_path_with_home_expansion(data_path) if data_path
|
30
|
+
|
31
|
+
Dir.chdir(tmpdir) do
|
32
|
+
Migration.new.run(Logger::FATAL) # Hide migration output for evaluator
|
33
|
+
evaluate(ig_path, data_path, options)
|
34
|
+
end
|
35
|
+
ensure
|
36
|
+
system("#{services_base_command} down #{services_names}")
|
37
|
+
puts 'Stopped Inferno Evaluator Services'
|
38
|
+
|
39
|
+
FileUtils.remove_entry_secure tmpdir
|
40
|
+
end
|
41
|
+
|
42
|
+
def services_base_command
|
43
|
+
"docker compose -f #{File.join(__dir__, 'evaluate', 'docker-compose.evaluate.yml')}"
|
44
|
+
end
|
45
|
+
|
46
|
+
def services_names
|
47
|
+
'hl7_validator_service fhirpath'
|
48
|
+
end
|
49
|
+
|
50
|
+
# @private
|
51
|
+
def absolute_path_with_home_expansion(path)
|
52
|
+
if path.starts_with? '~'
|
53
|
+
path.sub('~', Dir.home)
|
54
|
+
else
|
55
|
+
File.absolute_path(path)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# @see Inferno::CLI::Main#evaluate
|
60
|
+
def evaluate(ig_path, data_path, options)
|
12
61
|
# NOTE: repositories is required here rather than at the top of the file because
|
13
62
|
# the tree of requires means that this file and its requires get required by every CLI app.
|
14
63
|
# Sequel::Model, used in some repositories, fetches the table schema at instantiation.
|
@@ -22,7 +71,7 @@ module Inferno
|
|
22
71
|
|
23
72
|
data =
|
24
73
|
if data_path
|
25
|
-
DatasetLoader.from_path(File.join(__dir__, data_path))
|
74
|
+
Inferno::DSL::FHIREvaluation::DatasetLoader.from_path(File.join(__dir__, data_path))
|
26
75
|
else
|
27
76
|
ig.examples
|
28
77
|
end
|
@@ -55,7 +104,6 @@ module Inferno
|
|
55
104
|
def setup_validator(ig_path)
|
56
105
|
igs_directory = File.join(Dir.pwd, 'data', 'igs')
|
57
106
|
if File.exist?(ig_path) && !File.realpath(ig_path).start_with?(igs_directory)
|
58
|
-
puts "Copying #{File.basename(ig_path)} to data/igs so it is accessible to validator"
|
59
107
|
destination_file_path = File.join(igs_directory, File.basename(ig_path))
|
60
108
|
FileUtils.copy_file(ig_path, destination_file_path, true)
|
61
109
|
ig_path = "igs/#{File.basename(ig_path)}"
|
@@ -6,6 +6,7 @@ Dir[File.join(__dir__, 'execute', '*_outputter.rb')].each { |outputter| require
|
|
6
6
|
|
7
7
|
module Inferno
|
8
8
|
module CLI
|
9
|
+
# @private
|
9
10
|
class Execute
|
10
11
|
include ::Inferno::Utils::VerifyRunnable
|
11
12
|
include ::Inferno::Utils::PersistInputs
|
@@ -45,7 +46,6 @@ module Inferno
|
|
45
46
|
self.options = options
|
46
47
|
|
47
48
|
outputter.print_start_message(self.options)
|
48
|
-
|
49
49
|
load_preset_file_and_set_preset_id
|
50
50
|
|
51
51
|
results = []
|
@@ -104,18 +104,6 @@ module Inferno
|
|
104
104
|
@outputter ||= OUTPUTTERS[options[:outputter]].new
|
105
105
|
end
|
106
106
|
|
107
|
-
def load_preset_file_and_set_preset_id
|
108
|
-
return unless options[:preset_file]
|
109
|
-
raise StandardError, 'Cannot use `--preset-id` and `--preset-file` options together' if options[:preset_id]
|
110
|
-
|
111
|
-
raise StandardError, "File #{options[:preset_file]} not found" unless File.exist? options[:preset_file]
|
112
|
-
|
113
|
-
options[:preset_id] = JSON.parse(File.read(options[:preset_file]))['id']
|
114
|
-
raise StandardError, "Preset #{options[:preset_file]} is missing id" if options[:preset_id].nil?
|
115
|
-
|
116
|
-
presets_repo.insert_from_file(options[:preset_file])
|
117
|
-
end
|
118
|
-
|
119
107
|
def all_selected_groups_and_tests
|
120
108
|
@all_selected_groups_and_tests ||= runnables_by_short_id + groups + tests
|
121
109
|
end
|
@@ -123,7 +111,7 @@ module Inferno
|
|
123
111
|
def run_one(runnable, test_run)
|
124
112
|
verify_runnable(
|
125
113
|
runnable,
|
126
|
-
|
114
|
+
all_inputs,
|
127
115
|
test_session.suite_options
|
128
116
|
)
|
129
117
|
|
@@ -132,15 +120,14 @@ module Inferno
|
|
132
120
|
dispatch_job(test_run)
|
133
121
|
end
|
134
122
|
|
135
|
-
def
|
123
|
+
def all_inputs
|
136
124
|
if preset
|
137
|
-
|
138
|
-
|
139
|
-
end
|
125
|
+
test_sessions_repo.apply_preset(test_session, preset.id)
|
126
|
+
preset_inputs = session_data_repo.get_all_from_session(test_session.id)
|
140
127
|
|
141
|
-
options.fetch(:inputs, {}).
|
128
|
+
thor_hash_to_inputs_array(options.fetch(:inputs, {})) + preset_inputs.map(&:to_hash)
|
142
129
|
else
|
143
|
-
options.fetch(:inputs, {})
|
130
|
+
thor_hash_to_inputs_array(options.fetch(:inputs, {}))
|
144
131
|
end
|
145
132
|
end
|
146
133
|
|
@@ -159,6 +146,15 @@ module Inferno
|
|
159
146
|
@preset
|
160
147
|
end
|
161
148
|
|
149
|
+
def load_preset_file_and_set_preset_id
|
150
|
+
return unless options[:preset_file]
|
151
|
+
raise StandardError, 'Cannot use `--preset-id` and `--preset-file` options together' if options[:preset_id]
|
152
|
+
|
153
|
+
raise StandardError, "File #{options[:preset_file]} not found" unless File.exist? options[:preset_file]
|
154
|
+
|
155
|
+
options[:preset_id] = presets_repo.insert_from_file(options[:preset_file]).id
|
156
|
+
end
|
157
|
+
|
162
158
|
def suite
|
163
159
|
@suite ||= Inferno::Repositories::TestSuites.new.find(options[:suite])
|
164
160
|
|
@@ -214,7 +210,7 @@ module Inferno
|
|
214
210
|
{
|
215
211
|
test_session_id: test_session.id,
|
216
212
|
runnable_id_key(runnable) => runnable.id,
|
217
|
-
inputs:
|
213
|
+
inputs: all_inputs
|
218
214
|
}
|
219
215
|
end
|
220
216
|
|
@@ -1,6 +1,7 @@
|
|
1
1
|
require_relative 'console'
|
2
2
|
require_relative 'evaluate'
|
3
3
|
require_relative 'migration'
|
4
|
+
require_relative 'requirements'
|
4
5
|
require_relative 'services'
|
5
6
|
require_relative 'suite'
|
6
7
|
require_relative 'suites'
|
@@ -45,7 +46,7 @@ module Inferno
|
|
45
46
|
type: :string,
|
46
47
|
desc: 'Export evaluation result to outcome.json as an OperationOutcome'
|
47
48
|
def evaluate(ig_path)
|
48
|
-
Evaluate.new.
|
49
|
+
Evaluate.new.run(ig_path, options[:data_path], options)
|
49
50
|
end
|
50
51
|
|
51
52
|
desc 'console', 'Start an interactive console session with Inferno'
|
@@ -59,6 +60,9 @@ module Inferno
|
|
59
60
|
Migration.new.run
|
60
61
|
end
|
61
62
|
|
63
|
+
desc 'requirements SUBCOMMAND ...ARGS', 'Perform requirements operations'
|
64
|
+
subcommand 'requirements', Requirements
|
65
|
+
|
62
66
|
desc 'start', 'Start Inferno'
|
63
67
|
option :watch,
|
64
68
|
default: false,
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require_relative 'requirements_exporter'
|
2
|
+
|
3
|
+
module Inferno
|
4
|
+
module CLI
|
5
|
+
class Requirements < Thor
|
6
|
+
desc 'export_csv', 'Export a CSV represantation of requirements from an excel file'
|
7
|
+
long_desc <<~LONGDESC
|
8
|
+
Creates CSV files for tested requirements and requirements which are not
|
9
|
+
planned to be tested based on the excel files located in
|
10
|
+
"lib/test_kit_name/requirements"
|
11
|
+
LONGDESC
|
12
|
+
def export_csv
|
13
|
+
ENV['NO_DB'] = 'true'
|
14
|
+
RequirementsExporter.new.run
|
15
|
+
end
|
16
|
+
|
17
|
+
desc 'check', 'Check whether the current requirements CSV files are up to date'
|
18
|
+
long_desc <<~LONGDESC
|
19
|
+
Check whether the requirements CSV files are up to date with the excel
|
20
|
+
files in "lib/test_kit_name/requirements"
|
21
|
+
LONGDESC
|
22
|
+
def check
|
23
|
+
ENV['NO_DB'] = 'true'
|
24
|
+
RequirementsExporter.new.run_check
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,194 @@
|
|
1
|
+
require 'csv'
|
2
|
+
require 'roo'
|
3
|
+
|
4
|
+
module Inferno
|
5
|
+
module CLI
|
6
|
+
class RequirementsExporter
|
7
|
+
INPUT_HEADERS =
|
8
|
+
[
|
9
|
+
'ID*',
|
10
|
+
'URL*',
|
11
|
+
'Requirement*',
|
12
|
+
'Conformance*',
|
13
|
+
'Actor*',
|
14
|
+
'Sub-Requirement(s)',
|
15
|
+
'Conditionality',
|
16
|
+
'Verifiable?',
|
17
|
+
'Verifiability Details',
|
18
|
+
'Planning To Test?',
|
19
|
+
'Planning To Test Details'
|
20
|
+
].freeze
|
21
|
+
REQUIREMENTS_OUTPUT_HEADERS =
|
22
|
+
[
|
23
|
+
'Req Set',
|
24
|
+
'ID',
|
25
|
+
'URL',
|
26
|
+
'Requirement',
|
27
|
+
'Conformance',
|
28
|
+
'Actor',
|
29
|
+
'Sub-Requirement(s)',
|
30
|
+
'Conditionality',
|
31
|
+
'Not Tested Reason',
|
32
|
+
'Not Tested Details'
|
33
|
+
].freeze
|
34
|
+
|
35
|
+
def local_test_kit_gem
|
36
|
+
@local_test_kit_gem ||= Bundler.definition.specs.find { |spec| spec.full_gem_path == Dir.pwd }
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_kit_name
|
40
|
+
local_test_kit_gem.name
|
41
|
+
end
|
42
|
+
|
43
|
+
def base_requirements_folder
|
44
|
+
@base_requirements_folder ||= Dir.glob(File.join(Dir.pwd, 'lib', '*', 'requirements')).first
|
45
|
+
end
|
46
|
+
|
47
|
+
def requirements_output_file_name
|
48
|
+
"#{test_kit_name}_requirements.csv"
|
49
|
+
end
|
50
|
+
|
51
|
+
def requirements_output_file_path
|
52
|
+
File.join(base_requirements_folder, requirements_output_file_name).freeze
|
53
|
+
end
|
54
|
+
|
55
|
+
def available_input_worksheets
|
56
|
+
@available_input_worksheets ||=
|
57
|
+
Dir.glob(File.join(base_requirements_folder, '*.xlsx'))
|
58
|
+
.reject { |f| f.include?('~$') }
|
59
|
+
end
|
60
|
+
|
61
|
+
def requirement_set_id(worksheet)
|
62
|
+
sheet = worksheet.sheet('Metadata')
|
63
|
+
id_row = sheet.column(1).find_index('Id') + 1
|
64
|
+
sheet.row(id_row)[1]
|
65
|
+
end
|
66
|
+
|
67
|
+
# Of the form:
|
68
|
+
# {
|
69
|
+
# requirement_set_id_1: [row1, row2, row 3, ...],
|
70
|
+
# requirement_set_id_2: [row1, row2, row 3, ...]
|
71
|
+
# }
|
72
|
+
def input_requirement_sets
|
73
|
+
requirement_set_hash = Hash.new { |hash, key| hash[key] = [] }
|
74
|
+
available_input_worksheets.each_with_object(requirement_set_hash) do |worksheet_file, requirement_sets|
|
75
|
+
worksheet = Roo::Spreadsheet.open(worksheet_file)
|
76
|
+
set_identifier = requirement_set_id(worksheet)
|
77
|
+
|
78
|
+
CSV.parse(
|
79
|
+
worksheet.sheet('Requirements').to_csv,
|
80
|
+
headers: true
|
81
|
+
).each do |row|
|
82
|
+
row_hash = row.to_h.slice(*INPUT_HEADERS)
|
83
|
+
row_hash['Sub-Requirement(s)']&.delete_prefix!('mailto:')
|
84
|
+
|
85
|
+
requirement_sets[set_identifier] << row_hash
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def new_requirements_csv # rubocop:disable Metrics/CyclomaticComplexity
|
91
|
+
@new_requirements_csv ||=
|
92
|
+
CSV.generate(+"\xEF\xBB\xBF") do |csv| # start with an unnecessary BOM to make viewing in excel easier
|
93
|
+
csv << REQUIREMENTS_OUTPUT_HEADERS
|
94
|
+
|
95
|
+
input_requirement_sets.each do |requirement_set_id, input_rows|
|
96
|
+
input_rows.each do |row| # NOTE: use row order from source file
|
97
|
+
csv << REQUIREMENTS_OUTPUT_HEADERS.map do |header|
|
98
|
+
(
|
99
|
+
case header
|
100
|
+
when 'Req Set'
|
101
|
+
requirement_set_id
|
102
|
+
when 'Not Tested Reason'
|
103
|
+
if spreadsheet_value_falsy?(row['Verifiable?'])
|
104
|
+
'Not Verifiable'
|
105
|
+
elsif spreadsheet_value_falsy?(row['Planning To Test?'])
|
106
|
+
'Not Tested'
|
107
|
+
end
|
108
|
+
when 'Not Tested Details'
|
109
|
+
if spreadsheet_value_falsy?(row['Verifiable?'])
|
110
|
+
row['Verifiability Details']
|
111
|
+
elsif spreadsheet_value_falsy?(row['Planning To Test?'])
|
112
|
+
row['Planning To Test Details']
|
113
|
+
end
|
114
|
+
else
|
115
|
+
row[header] || row["#{header}*"]
|
116
|
+
end
|
117
|
+
)&.strip
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def old_requirements_csv
|
125
|
+
@old_requirements_csv ||= File.read(requirements_output_file_path)
|
126
|
+
end
|
127
|
+
|
128
|
+
def run
|
129
|
+
check_presence_of_input_files
|
130
|
+
|
131
|
+
update_requirements =
|
132
|
+
if File.exist?(requirements_output_file_path)
|
133
|
+
if old_requirements_csv == new_requirements_csv
|
134
|
+
puts "'#{requirements_output_file_name}' file is up to date."
|
135
|
+
false
|
136
|
+
else
|
137
|
+
puts 'Requirements set has changed.'
|
138
|
+
true
|
139
|
+
end
|
140
|
+
else
|
141
|
+
puts "No existing #{requirements_output_file_name}."
|
142
|
+
true
|
143
|
+
end
|
144
|
+
|
145
|
+
if update_requirements
|
146
|
+
puts "Writing to file #{requirements_output_file_name}..."
|
147
|
+
File.write(requirements_output_file_path, new_requirements_csv, encoding: Encoding::UTF_8)
|
148
|
+
end
|
149
|
+
|
150
|
+
puts 'Done.'
|
151
|
+
end
|
152
|
+
|
153
|
+
def run_check
|
154
|
+
check_presence_of_input_files
|
155
|
+
|
156
|
+
requirements_ok =
|
157
|
+
if File.exist?(requirements_output_file_path)
|
158
|
+
if old_requirements_csv == new_requirements_csv
|
159
|
+
puts "'#{requirements_output_file_name}' file is up to date."
|
160
|
+
true
|
161
|
+
else
|
162
|
+
puts "#{requirements_output_file_name} file is out of date."
|
163
|
+
false
|
164
|
+
end
|
165
|
+
else
|
166
|
+
puts "No existing #{requirements_output_file_name} file."
|
167
|
+
false
|
168
|
+
end
|
169
|
+
|
170
|
+
return if requirements_ok
|
171
|
+
|
172
|
+
puts <<~MESSAGE
|
173
|
+
Check Failed. To resolve, run:
|
174
|
+
|
175
|
+
bundle exec inferno requirements export_csv
|
176
|
+
|
177
|
+
MESSAGE
|
178
|
+
exit(1)
|
179
|
+
end
|
180
|
+
|
181
|
+
def check_presence_of_input_files
|
182
|
+
return if available_input_worksheets.present?
|
183
|
+
|
184
|
+
puts 'Could not find any input files in directory ' \
|
185
|
+
"#{base_requirements_folder}. Aborting requirements collection."
|
186
|
+
exit(1)
|
187
|
+
end
|
188
|
+
|
189
|
+
def spreadsheet_value_falsy?(string)
|
190
|
+
['no', 'false'].include? string&.downcase
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
@@ -52,6 +52,27 @@ module Inferno
|
|
52
52
|
|
53
53
|
puts TTY::Markdown.parse(description)
|
54
54
|
end
|
55
|
+
|
56
|
+
desc 'lock_short_ids SUITE_ID', 'Persist the current short_id map for a suite'
|
57
|
+
long_desc <<~LONGDESC
|
58
|
+
Loads the given suite and writes its current short_id map to its corresponding YAML file.
|
59
|
+
LONGDESC
|
60
|
+
def lock_short_ids(suite_id)
|
61
|
+
ENV['NO_DB'] = 'true'
|
62
|
+
Inferno::Application.start(:suites)
|
63
|
+
|
64
|
+
suite = Inferno::Repositories::TestSuites.new.find(suite_id)
|
65
|
+
|
66
|
+
if suite.blank?
|
67
|
+
message = "No suite found with id `#{suite_id}`. Run `inferno suites` to see a list of available suites"
|
68
|
+
|
69
|
+
puts TTY::Markdown.parse(message)
|
70
|
+
return
|
71
|
+
end
|
72
|
+
|
73
|
+
File.write(suite.short_id_file_path, suite.current_short_id_map.to_yaml)
|
74
|
+
puts "Short ID map saved to #{suite.short_id_file_path}"
|
75
|
+
end
|
55
76
|
end
|
56
77
|
end
|
57
78
|
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
module <%= module_name %>
|
2
|
+
class PatientGroup < Inferno::TestGroup
|
3
|
+
title 'Patient'
|
4
|
+
|
5
|
+
description <<~DESCRIPTION
|
6
|
+
This scenario verifies the ability of a system to provide a Patient as described in the Example criterion.
|
7
|
+
|
8
|
+
*or*
|
9
|
+
|
10
|
+
The Example Patient sequence verifies that the system under test is able to provide correct responses for Patient queries.
|
11
|
+
|
12
|
+
## Requirements
|
13
|
+
|
14
|
+
Patient queries must contain resources conforming to the Example Patient as specified in the Example Implementation Guide.
|
15
|
+
|
16
|
+
*or*
|
17
|
+
|
18
|
+
All Must Support elements must be seen before the test can pass, as well as Data Absent Reason to demonstrate that the server
|
19
|
+
can properly handle missing data. Note that Organization, Practitioner, and RelatedPerson resources must be accessible as
|
20
|
+
references in some Example profiles to satisfy must support requirements, and those references will be validated to their Example
|
21
|
+
profile. These resources will not be tested for FHIR search support.
|
22
|
+
|
23
|
+
## <*If applicable*> Dependencies
|
24
|
+
Prior to running this scenario, systems must recieve a verified access token from one of the previous SMART App Launch scenarios.
|
25
|
+
|
26
|
+
*or*
|
27
|
+
|
28
|
+
Prior to running this scenario, first run the Single Patient API tests using resource-level scopes, as this scenario uses content
|
29
|
+
saved from that scenario as a baseline for comparison when finer-grained scopes are granted.
|
30
|
+
|
31
|
+
## <*If applicable*> Methodology
|
32
|
+
|
33
|
+
*Only include if different from instructions included in a parent group or suite*
|
34
|
+
|
35
|
+
The test begins by searching by one or more patients, with the expectation that the Bearer token provided to the test grants
|
36
|
+
access to all Resources. It uses results returned from that query to generate other queries and checks that the results are
|
37
|
+
consistent with the provided search parameters. It then performs a read on each Resource returned and validates the response
|
38
|
+
against the relevant profile as currently defined in the Example Implementation Guide.
|
39
|
+
|
40
|
+
*or*
|
41
|
+
|
42
|
+
### Searching
|
43
|
+
|
44
|
+
This test sequence will first perform each required search associated with this resource.
|
45
|
+
This sequence will perform searches with the following parameters:
|
46
|
+
- _id
|
47
|
+
- identifier
|
48
|
+
- name
|
49
|
+
- birthdate + name
|
50
|
+
- gender + name
|
51
|
+
|
52
|
+
#### Search Parameters
|
53
|
+
|
54
|
+
The first search uses the selected patient(s) from the prior launch sequence. Any subsequent searches will look for its parameter
|
55
|
+
values from the results of the first search. For example, the `identifier` search in the patient sequence is performed by looking
|
56
|
+
for an existing `Patient.identifier` from any of the resources returned in the `_id` search. If a value cannot be found this way,
|
57
|
+
the search is skipped.
|
58
|
+
|
59
|
+
#### Search Validation
|
60
|
+
|
61
|
+
Inferno will retrieve up to the first 20 bundle pages of the reply for Patient resources and save them for subsequent tests.
|
62
|
+
Each of these resources is then checked to see if it matches the searched parameters in accordance with [FHIR search guidelines](https://www.hl7.org/fhir/search.html).
|
63
|
+
The test will fail, for example, if a Patient search for gender=male returns a female patient.
|
64
|
+
|
65
|
+
### Must Support
|
66
|
+
|
67
|
+
Each profile contains elements marked as "must support". This test sequence expects to see each of these elements at least once.
|
68
|
+
If at least one cannot be found, the test will fail. The test will look through the Patient resources found in the first test
|
69
|
+
for these elements.
|
70
|
+
|
71
|
+
### Profile Validation
|
72
|
+
|
73
|
+
Each resource returned from the first search is expected to conform to the [Example Patient Profile](https://www.example.com/patient/profile).
|
74
|
+
Each element is checked against teminology binding and cardinality requirements.
|
75
|
+
|
76
|
+
Elements with a required binding are validated against their bound ValueSet. If the code/system in the element is not part of the
|
77
|
+
ValueSet, then the test will fail.
|
78
|
+
|
79
|
+
### Reference Validation
|
80
|
+
|
81
|
+
At least one instance of each external reference in elements marked as "must support" within the resources provided by the
|
82
|
+
system must resolve. The test will attempt to read each reference found and will fail if no read succeeds.
|
83
|
+
|
84
|
+
## <*If applicable*> Running the Tests
|
85
|
+
|
86
|
+
*Only include if different from instructions included in a parent group or suite*
|
87
|
+
|
88
|
+
Register Inferno as an EHR-launched application using patient-level scopes and the following URIs:
|
89
|
+
- Launch URI: https://inferno.healthit.gov/suites/custom/smart/launch
|
90
|
+
- Redirect URI: https://inferno.healthit.gov/suites/custom/smart/redirect
|
91
|
+
|
92
|
+
## <*If top-level group for criteria*> Relevant Specifications
|
93
|
+
|
94
|
+
The following implementation specifications are relevant to this scenario:
|
95
|
+
- [Specification 1 v1](https://www.example.com/spec1/v1)
|
96
|
+
- [Specification 1 v2](https://www.example.com/spec1/v2)
|
97
|
+
- [Specification 2 v5](https://www.example.com/spec1/v1)
|
98
|
+
|
99
|
+
DESCRIPTION
|
100
|
+
|
101
|
+
id :patient_group
|
102
|
+
|
103
|
+
test do
|
104
|
+
title 'Server returns requested Patient resource from the Patient read interaction'
|
105
|
+
description %(
|
106
|
+
Verify that Patient resources can be read from the server. Expects a 200 response that includes a Patient
|
107
|
+
resource whose ID matches the requested patient ID.
|
108
|
+
)
|
109
|
+
|
110
|
+
input :patient_id,
|
111
|
+
title: 'Patient ID'
|
112
|
+
|
113
|
+
# Named requests can be used by other tests
|
114
|
+
makes_request :patient
|
115
|
+
|
116
|
+
run do
|
117
|
+
fhir_read(:patient, patient_id, name: :patient)
|
118
|
+
|
119
|
+
assert_response_status(200)
|
120
|
+
assert_resource_type(:patient)
|
121
|
+
assert resource.id == patient_id,
|
122
|
+
"Requested resource with id #{patient_id}, received resource with id #{resource.id}"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
test do
|
127
|
+
title 'Patient resource is valid'
|
128
|
+
description %(
|
129
|
+
Verify that the Patient resource returned from the server is a valid FHIR resource.
|
130
|
+
)
|
131
|
+
# This test will use the response from the :patient request in the
|
132
|
+
# previous test
|
133
|
+
uses_request :patient
|
134
|
+
|
135
|
+
run do
|
136
|
+
assert_resource_type(:patient)
|
137
|
+
assert_valid_resource
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|