inferno_core 1.1.2 → 1.2.0
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 +918 -0
- data/lib/inferno/apps/cli/main.rb +46 -0
- data/lib/inferno/apps/cli/session/cancel_run.rb +47 -0
- data/lib/inferno/apps/cli/session/connection.rb +47 -0
- data/lib/inferno/apps/cli/session/create_session.rb +159 -0
- data/lib/inferno/apps/cli/session/errors.rb +45 -0
- data/lib/inferno/apps/cli/session/session_compare.rb +390 -0
- data/lib/inferno/apps/cli/session/session_data.rb +39 -0
- data/lib/inferno/apps/cli/session/session_details.rb +27 -0
- data/lib/inferno/apps/cli/session/session_results.rb +39 -0
- data/lib/inferno/apps/cli/session/session_status.rb +69 -0
- data/lib/inferno/apps/cli/session/start_run.rb +245 -0
- data/lib/inferno/apps/cli/session_commands.rb +66 -0
- data/lib/inferno/apps/cli/templates/%library_name%.gemspec.tt +1 -1
- data/lib/inferno/apps/cli/templates/.gitignore +4 -0
- data/lib/inferno/apps/cli/templates/README.md.tt +14 -0
- data/lib/inferno/apps/cli/templates/Rakefile.tt +13 -0
- data/lib/inferno/apps/cli/templates/execution_scripts/%library_name%_script.yaml.tt +20 -0
- data/lib/inferno/apps/cli/templates/execution_scripts/%library_name%_script_expected.json.tt +244 -0
- data/lib/inferno/apps/cli/templates/execution_scripts/README.md.tt +16 -0
- data/lib/inferno/dsl/fhir_resource_navigation.rb +145 -27
- data/lib/inferno/dsl/must_support_assessment.rb +93 -23
- data/lib/inferno/dsl/must_support_metadata_extractor.rb +139 -21
- data/lib/inferno/dsl/resume_test_route.rb +4 -3
- data/lib/inferno/exceptions.rb +6 -0
- data/lib/inferno/repositories/test_sessions.rb +3 -0
- data/lib/inferno/utils/execution_script_runner.rb +90 -0
- data/lib/inferno/utils/preset_processor.rb +2 -0
- data/lib/inferno/version.rb +1 -1
- metadata +18 -2
|
@@ -3,15 +3,23 @@ require_relative 'evaluate'
|
|
|
3
3
|
require_relative 'migration'
|
|
4
4
|
require_relative 'requirements'
|
|
5
5
|
require_relative 'services'
|
|
6
|
+
require_relative 'session_commands'
|
|
6
7
|
require_relative 'suite'
|
|
7
8
|
require_relative 'suites'
|
|
8
9
|
require_relative 'new'
|
|
9
10
|
require_relative 'execute'
|
|
11
|
+
require_relative 'execute_script'
|
|
10
12
|
require_relative '../../version'
|
|
11
13
|
|
|
12
14
|
module Inferno
|
|
13
15
|
module CLI
|
|
14
16
|
class Main < Thor
|
|
17
|
+
def initialize(args = [], local_options = {}, config = {})
|
|
18
|
+
super
|
|
19
|
+
return unless @options[:inferno_base_url]
|
|
20
|
+
|
|
21
|
+
@options = @options.merge(inferno_base_url: "#{@options[:inferno_base_url].delete_suffix('/')}/")
|
|
22
|
+
end
|
|
15
23
|
desc 'evaluate', 'Run a FHIR Data Evaluator.'
|
|
16
24
|
long_desc <<-LONGDESC
|
|
17
25
|
Evaluate FHIR data in the context of a given Implementation Guide,
|
|
@@ -70,6 +78,44 @@ module Inferno
|
|
|
70
78
|
desc 'requirements SUBCOMMAND ...ARGS', 'Perform requirements operations'
|
|
71
79
|
subcommand 'requirements', Requirements
|
|
72
80
|
|
|
81
|
+
desc 'execute_script YAML_FILE',
|
|
82
|
+
'Run a session orchestration script defined by a YAML config file.'
|
|
83
|
+
option :inferno_base_url,
|
|
84
|
+
aliases: ['-I'],
|
|
85
|
+
type: :string,
|
|
86
|
+
desc: 'URL of the target Inferno service.'
|
|
87
|
+
option :compare_messages,
|
|
88
|
+
aliases: ['-m'],
|
|
89
|
+
type: :boolean,
|
|
90
|
+
default: true,
|
|
91
|
+
desc: 'Compare messages array when comparing results.'
|
|
92
|
+
option :compare_result_message,
|
|
93
|
+
aliases: ['-r'],
|
|
94
|
+
type: :boolean,
|
|
95
|
+
default: true,
|
|
96
|
+
desc: 'Compare result_message field when comparing results.'
|
|
97
|
+
option :poll_interval,
|
|
98
|
+
aliases: ['-p'],
|
|
99
|
+
type: :numeric,
|
|
100
|
+
default: 3,
|
|
101
|
+
desc: 'Seconds between status polls.'
|
|
102
|
+
option :default_poll_timeout,
|
|
103
|
+
aliases: ['-t'],
|
|
104
|
+
type: :numeric,
|
|
105
|
+
default: 120,
|
|
106
|
+
desc: 'Default seconds to wait for a matching step before timing out.'
|
|
107
|
+
option :allow_commands,
|
|
108
|
+
type: :boolean,
|
|
109
|
+
default: false,
|
|
110
|
+
desc: 'Allow execution script steps that run arbitrary shell commands. ' \
|
|
111
|
+
'Scripts with command: steps will fail unless this flag is set.'
|
|
112
|
+
def execute_script(yaml_file)
|
|
113
|
+
ExecuteScript.new(yaml_file, options).run
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
desc 'session SUBCOMMAND ...ARGS', 'Perform session operations'
|
|
117
|
+
subcommand 'session', Session::SessionCommands
|
|
118
|
+
|
|
73
119
|
desc 'start', 'Start Inferno'
|
|
74
120
|
option :watch,
|
|
75
121
|
default: false,
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require 'faraday'
|
|
2
|
+
require_relative 'connection'
|
|
3
|
+
require_relative 'errors'
|
|
4
|
+
require_relative 'session_status'
|
|
5
|
+
|
|
6
|
+
module Inferno
|
|
7
|
+
module CLI
|
|
8
|
+
module Session
|
|
9
|
+
class CancelRun
|
|
10
|
+
include Connection
|
|
11
|
+
include Errors
|
|
12
|
+
|
|
13
|
+
attr_accessor :session_id, :options
|
|
14
|
+
|
|
15
|
+
CANCELLABLE_STATUSES = %w[queued running waiting].freeze
|
|
16
|
+
|
|
17
|
+
def initialize(session_id, options)
|
|
18
|
+
self.session_id = session_id
|
|
19
|
+
self.options = options
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def run
|
|
23
|
+
puts JSON.pretty_generate(cancel_run(last_test_run))
|
|
24
|
+
exit(0)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def cancel_run(current_run)
|
|
28
|
+
run_id = current_run['id']
|
|
29
|
+
|
|
30
|
+
unless CANCELLABLE_STATUSES.include?(current_run['status'])
|
|
31
|
+
error = { errors: "Run '#{run_id}' cannot be cancelled: status is '#{current_run['status']}'" }
|
|
32
|
+
puts JSON.pretty_generate(error)
|
|
33
|
+
exit(3)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
response = delete("api/test_runs/#{run_id}")
|
|
37
|
+
handle_web_api_error(response, :cancel_run) if response.status != 204
|
|
38
|
+
{ run_id: run_id, cancelled: true }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def last_test_run
|
|
42
|
+
SessionStatus.new(session_id, options).last_test_run
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module Inferno
|
|
2
|
+
module CLI
|
|
3
|
+
module Session
|
|
4
|
+
module Connection
|
|
5
|
+
def connection
|
|
6
|
+
@connection ||= Faraday.new(
|
|
7
|
+
base_url,
|
|
8
|
+
request: { timeout: 600 }
|
|
9
|
+
)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def base_url
|
|
13
|
+
@base_url ||=
|
|
14
|
+
"#{(options[:inferno_base_url].presence || Inferno::Application['base_url']).to_s.delete_suffix('/')}/"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def get(path, params = nil, headers = {})
|
|
18
|
+
connection.get(path, params, headers)
|
|
19
|
+
rescue Faraday::Error => e
|
|
20
|
+
handle_connection_error(e)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def post(path, body = nil, headers = {})
|
|
24
|
+
connection.post(path, body, headers)
|
|
25
|
+
rescue Faraday::Error => e
|
|
26
|
+
handle_connection_error(e)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def delete(path, params = nil, headers = {})
|
|
30
|
+
connection.delete(path, params, headers)
|
|
31
|
+
rescue Faraday::Error => e
|
|
32
|
+
handle_connection_error(e)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def handle_connection_error(error)
|
|
36
|
+
puts JSON.pretty_generate({ errors: "Could not connect to Inferno at '#{base_url}': #{error.message}" })
|
|
37
|
+
exit(3)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def check_session_exists
|
|
41
|
+
response = get("api/test_sessions/#{session_id}", nil, content_type: 'application/json')
|
|
42
|
+
handle_web_api_error(response, :session_details) if response.status != 200
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
require 'faraday'
|
|
2
|
+
require_relative 'connection'
|
|
3
|
+
require_relative 'errors'
|
|
4
|
+
|
|
5
|
+
module Inferno
|
|
6
|
+
module CLI
|
|
7
|
+
module Session
|
|
8
|
+
class CreateSession
|
|
9
|
+
include Connection
|
|
10
|
+
include Errors
|
|
11
|
+
|
|
12
|
+
COMMAND_OPTIONS = {
|
|
13
|
+
suite_options: {
|
|
14
|
+
aliases: ['-o'],
|
|
15
|
+
type: :hash,
|
|
16
|
+
desc: 'Suite options used to initialize the session.'
|
|
17
|
+
},
|
|
18
|
+
preset: {
|
|
19
|
+
aliases: ['-p'],
|
|
20
|
+
type: :string,
|
|
21
|
+
desc: 'Preset to apply when creating the session (internal ID or title).'
|
|
22
|
+
}
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
attr_accessor :options
|
|
26
|
+
attr_reader :suite
|
|
27
|
+
|
|
28
|
+
def initialize(suite, options)
|
|
29
|
+
@suite = suite
|
|
30
|
+
self.options = options
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def run
|
|
34
|
+
puts JSON.pretty_generate(create_session)
|
|
35
|
+
exit(0)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def create_session
|
|
39
|
+
request_body = { test_suite_id: suite_id }
|
|
40
|
+
request_body[:preset_id] = preset_id if options[:preset].present?
|
|
41
|
+
request_body[:suite_options] = suite_options_list if options[:suite_options].present?
|
|
42
|
+
|
|
43
|
+
response = post('api/test_sessions', request_body.to_json, content_type: 'application/json')
|
|
44
|
+
|
|
45
|
+
handle_web_api_error(response, :session_create) if response.status != 200
|
|
46
|
+
|
|
47
|
+
JSON.parse(response.body)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def suite_id
|
|
51
|
+
@suite_id ||= resolve_suite_identifier
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def all_suite_definitions
|
|
55
|
+
@all_suite_definitions ||= begin
|
|
56
|
+
response = get('api/test_suites')
|
|
57
|
+
if response.status != 200
|
|
58
|
+
puts JSON.pretty_generate({ errors: "Could not fetch test suites list from '#{base_url}'" })
|
|
59
|
+
exit(3)
|
|
60
|
+
end
|
|
61
|
+
JSON.parse(response.body)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def resolve_suite_identifier
|
|
66
|
+
matched = all_suite_definitions.find { |s| s['id'] == suite }
|
|
67
|
+
return matched['id'] if matched.present?
|
|
68
|
+
|
|
69
|
+
matched = all_suite_definitions.find { |s| s['title'] == suite || s['short_title'] == suite }
|
|
70
|
+
return matched['id'] if matched.present?
|
|
71
|
+
|
|
72
|
+
valid_suites = all_suite_definitions.map { |s| "#{s['id']} (#{s['title']})" }.join(', ')
|
|
73
|
+
puts JSON.pretty_generate(
|
|
74
|
+
{ errors: "Suite '#{suite}' not found. Valid suites: #{valid_suites}" }
|
|
75
|
+
)
|
|
76
|
+
exit(3)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def preset_id
|
|
80
|
+
@preset_id ||= resolve_preset_identifier
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def suite_presets
|
|
84
|
+
suite_def = all_suite_definitions.find { |s| s['id'] == suite_id }
|
|
85
|
+
suite_def&.fetch('presets', []) || []
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def resolve_preset_identifier
|
|
89
|
+
return unless options[:preset].present?
|
|
90
|
+
|
|
91
|
+
preset = options[:preset]
|
|
92
|
+
return preset if suite_presets.any? { |p| p['id'] == preset }
|
|
93
|
+
|
|
94
|
+
matched = suite_presets.find { |p| p['title'] == preset }
|
|
95
|
+
return matched['id'] if matched.present?
|
|
96
|
+
|
|
97
|
+
valid_presets = suite_presets.map { |p| "#{p['id']} (#{p['title']})" }.join(', ')
|
|
98
|
+
puts JSON.pretty_generate(
|
|
99
|
+
{ errors: "Preset '#{preset}' not found for suite '#{suite_id}'. Valid presets: #{valid_presets}" }
|
|
100
|
+
)
|
|
101
|
+
exit(3)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def suite_option_definitions
|
|
105
|
+
suite_def = all_suite_definitions.find { |s| s['id'] == suite_id }
|
|
106
|
+
suite_def&.fetch('suite_options', []) || []
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def suite_options_list
|
|
110
|
+
options[:suite_options].keys.map do |option_key|
|
|
111
|
+
option_id = resolve_suite_option_key(option_key)
|
|
112
|
+
{ id: option_id, value: resolve_suite_option_value(option_id, options[:suite_options][option_key]) }
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def resolve_suite_option_key(option_key)
|
|
117
|
+
return option_key if suite_option_definitions.any? { |d| d['id'] == option_key }
|
|
118
|
+
|
|
119
|
+
matched = suite_option_definitions.find { |d| d['title'] == option_key }
|
|
120
|
+
return matched['id'] if matched.present?
|
|
121
|
+
|
|
122
|
+
valid_options = suite_option_definitions.map { |d| "#{d['id']} (#{d['title']})" }.join(', ')
|
|
123
|
+
puts JSON.pretty_generate({
|
|
124
|
+
errors: "Unknown suite option '#{option_key}' for suite '#{suite_id}'. " \
|
|
125
|
+
"Valid options: #{valid_options}"
|
|
126
|
+
})
|
|
127
|
+
exit(3)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def resolve_suite_option_value(option_id, provided_value)
|
|
131
|
+
provided_value = provided_value.to_s
|
|
132
|
+
list_options = suite_option_list_options(option_id)
|
|
133
|
+
|
|
134
|
+
return provided_value if list_options.blank?
|
|
135
|
+
return provided_value if list_options.any? { |o| o['value'] == provided_value }
|
|
136
|
+
|
|
137
|
+
resolve_suite_option_value_by_label(option_id, provided_value, list_options)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def suite_option_list_options(option_id)
|
|
141
|
+
definition = suite_option_definitions.find { |d| d['id'] == option_id }
|
|
142
|
+
definition&.fetch('list_options', nil)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def resolve_suite_option_value_by_label(option_id, provided_value, list_options)
|
|
146
|
+
matched = list_options.find { |o| o['label'] == provided_value }
|
|
147
|
+
return matched['value'] if matched
|
|
148
|
+
|
|
149
|
+
valid_options = list_options.map { |o| "#{o['value']} (#{o['label']})" }.join(', ')
|
|
150
|
+
puts JSON.pretty_generate({
|
|
151
|
+
errors: "Invalid value '#{provided_value}' for suite option '#{option_id}'. " \
|
|
152
|
+
"Valid values: #{valid_options}"
|
|
153
|
+
})
|
|
154
|
+
exit(3)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Inferno
|
|
2
|
+
module CLI
|
|
3
|
+
module Session
|
|
4
|
+
module Errors
|
|
5
|
+
def handle_web_api_error(response, api = nil)
|
|
6
|
+
error = parse_error_response(response, api)
|
|
7
|
+
puts JSON.pretty_generate(error)
|
|
8
|
+
exit(3)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def parse_error_response(response, api)
|
|
12
|
+
JSON.parse(response.body)
|
|
13
|
+
rescue JSON::ParserError
|
|
14
|
+
{ errors: text_error_message(response, api) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def text_error_message(response, api)
|
|
18
|
+
if response.body == 'Not Found' || response.status == 404
|
|
19
|
+
not_found_error_message(api)
|
|
20
|
+
elsif api == :test_run_results && response.status == 500
|
|
21
|
+
test_run_not_found_message(response)
|
|
22
|
+
else
|
|
23
|
+
response.body
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def not_found_error_message(api)
|
|
28
|
+
case api
|
|
29
|
+
when :session_create, :run_create
|
|
30
|
+
"Running Inferno host not found at '#{base_url}'"
|
|
31
|
+
when :session_details, :session_data, :last_session_run
|
|
32
|
+
"Session '#{session_id}' not found on Inferno host at '#{base_url}'"
|
|
33
|
+
else
|
|
34
|
+
'Not Found'
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test_run_not_found_message(response)
|
|
39
|
+
test_run_id = response.env.url.to_s.split('/')[-2]
|
|
40
|
+
"Test Run '#{test_run_id}' for session '#{session_id}' not found on Inferno host at '#{base_url}'"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|