inferno_core 1.1.2 → 1.2.1

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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/lib/inferno/apps/cli/execute_script.rb +918 -0
  3. data/lib/inferno/apps/cli/main.rb +46 -0
  4. data/lib/inferno/apps/cli/session/cancel_run.rb +47 -0
  5. data/lib/inferno/apps/cli/session/connection.rb +47 -0
  6. data/lib/inferno/apps/cli/session/create_session.rb +159 -0
  7. data/lib/inferno/apps/cli/session/errors.rb +45 -0
  8. data/lib/inferno/apps/cli/session/session_compare.rb +391 -0
  9. data/lib/inferno/apps/cli/session/session_data.rb +39 -0
  10. data/lib/inferno/apps/cli/session/session_details.rb +27 -0
  11. data/lib/inferno/apps/cli/session/session_results.rb +39 -0
  12. data/lib/inferno/apps/cli/session/session_status.rb +69 -0
  13. data/lib/inferno/apps/cli/session/start_run.rb +245 -0
  14. data/lib/inferno/apps/cli/session_commands.rb +66 -0
  15. data/lib/inferno/apps/cli/templates/%library_name%.gemspec.tt +1 -1
  16. data/lib/inferno/apps/cli/templates/.gitignore +4 -0
  17. data/lib/inferno/apps/cli/templates/README.md.tt +14 -0
  18. data/lib/inferno/apps/cli/templates/Rakefile.tt +13 -0
  19. data/lib/inferno/apps/cli/templates/execution_scripts/%library_name%_script.yaml.tt +20 -0
  20. data/lib/inferno/apps/cli/templates/execution_scripts/%library_name%_script_expected.json.tt +244 -0
  21. data/lib/inferno/apps/cli/templates/execution_scripts/README.md.tt +16 -0
  22. data/lib/inferno/dsl/fhir_resource_navigation.rb +145 -27
  23. data/lib/inferno/dsl/must_support_assessment.rb +93 -23
  24. data/lib/inferno/dsl/must_support_metadata_extractor.rb +139 -21
  25. data/lib/inferno/dsl/resume_test_route.rb +4 -3
  26. data/lib/inferno/exceptions.rb +6 -0
  27. data/lib/inferno/repositories/test_sessions.rb +3 -0
  28. data/lib/inferno/utils/execution_script_runner.rb +90 -0
  29. data/lib/inferno/utils/preset_processor.rb +2 -0
  30. data/lib/inferno/version.rb +1 -1
  31. 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