inferno_core 0.0.2 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/lib/inferno/apps/web/controllers/test_runs/create.rb +30 -10
  3. data/lib/inferno/apps/web/controllers/test_runs/show.rb +10 -0
  4. data/lib/inferno/apps/web/controllers/test_sessions/create.rb +1 -0
  5. data/lib/inferno/apps/web/controllers/test_sessions/last_test_run.rb +22 -0
  6. data/lib/inferno/apps/web/controllers/test_sessions/results/index.rb +6 -1
  7. data/lib/inferno/apps/web/controllers/test_sessions/session_data/index.rb +21 -0
  8. data/lib/inferno/apps/web/router.rb +15 -0
  9. data/lib/inferno/apps/web/serializers/hash_value_extractor.rb +11 -0
  10. data/lib/inferno/apps/web/serializers/request.rb +1 -0
  11. data/lib/inferno/apps/web/serializers/result.rb +8 -0
  12. data/lib/inferno/apps/web/serializers/session_data.rb +10 -0
  13. data/lib/inferno/apps/web/serializers/test.rb +3 -6
  14. data/lib/inferno/apps/web/serializers/test_group.rb +5 -8
  15. data/lib/inferno/apps/web/serializers/test_run.rb +1 -0
  16. data/lib/inferno/apps/web/serializers/test_session.rb +1 -1
  17. data/lib/inferno/apps/web/serializers/test_suite.rb +1 -0
  18. data/lib/inferno/config/application.rb +5 -2
  19. data/lib/inferno/config/boot/db.rb +10 -1
  20. data/lib/inferno/config/boot/sidekiq.rb +11 -0
  21. data/lib/inferno/config/boot/suites.rb +4 -6
  22. data/lib/inferno/config/boot.rb +2 -0
  23. data/lib/inferno/db/migrations/001_create_initial_structure.rb +0 -21
  24. data/lib/inferno/db/migrations/002_add_wait_support.rb +7 -0
  25. data/lib/inferno/db/migrations/003_update_session_data.rb +18 -0
  26. data/lib/inferno/db/migrations/004_add_request_results_table.rb +9 -0
  27. data/lib/inferno/db/migrations/005_add_updated_at_index_to_results.rb +5 -0
  28. data/lib/inferno/db/schema.rb +154 -0
  29. data/lib/inferno/dsl/assertions.rb +20 -0
  30. data/lib/inferno/dsl/configurable.rb +126 -0
  31. data/lib/inferno/dsl/fhir_client.rb +4 -2
  32. data/lib/inferno/dsl/fhir_client_builder.rb +16 -0
  33. data/lib/inferno/dsl/http_client.rb +10 -8
  34. data/lib/inferno/dsl/request_storage.rb +30 -9
  35. data/lib/inferno/dsl/results.rb +49 -0
  36. data/lib/inferno/dsl/resume_test_route.rb +89 -0
  37. data/lib/inferno/dsl/runnable.rb +153 -16
  38. data/lib/inferno/dsl.rb +1 -3
  39. data/lib/inferno/entities/header.rb +7 -7
  40. data/lib/inferno/entities/message.rb +8 -6
  41. data/lib/inferno/entities/request.rb +42 -16
  42. data/lib/inferno/entities/result.rb +34 -18
  43. data/lib/inferno/entities/session_data.rb +33 -0
  44. data/lib/inferno/entities/test.rb +35 -8
  45. data/lib/inferno/entities/test_group.rb +8 -0
  46. data/lib/inferno/entities/test_run.rb +13 -6
  47. data/lib/inferno/entities/test_session.rb +8 -8
  48. data/lib/inferno/entities.rb +1 -1
  49. data/lib/inferno/exceptions.rb +24 -0
  50. data/lib/inferno/jobs/execute_test_run.rb +14 -0
  51. data/lib/inferno/jobs/resume_test_run.rb +14 -0
  52. data/lib/inferno/jobs.rb +16 -0
  53. data/lib/inferno/public/bundle.js +1 -1
  54. data/lib/inferno/repositories/repository.rb +13 -0
  55. data/lib/inferno/repositories/requests.rb +5 -4
  56. data/lib/inferno/repositories/results.rb +151 -3
  57. data/lib/inferno/repositories/session_data.rb +47 -0
  58. data/lib/inferno/repositories/test_runs.rb +81 -0
  59. data/lib/inferno/test_runner.rb +125 -31
  60. data/lib/inferno/utils/markdown_formatter.rb +15 -0
  61. data/lib/inferno/utils/middleware/request_logger.rb +16 -3
  62. data/lib/inferno/version.rb +1 -1
  63. data/lib/inferno.rb +4 -0
  64. data/spec/factories/header.rb +19 -0
  65. data/spec/factories/message.rb +17 -0
  66. data/spec/factories/request.rb +42 -0
  67. data/spec/factories/result.rb +45 -0
  68. data/spec/factories/test_run.rb +24 -0
  69. data/spec/factories/test_session.rb +11 -0
  70. data/spec/fixtures/basic_test_group.rb +9 -0
  71. data/spec/fixtures/basic_test_suite.rb +8 -0
  72. metadata +57 -5
  73. data/lib/inferno/dsl/fhir_manipulation.rb +0 -25
  74. data/lib/inferno/entities/test_input.rb +0 -20
@@ -0,0 +1,126 @@
1
+ module Inferno
2
+ module DSL
3
+ # This module contains the DSL for managing runnable configuration.
4
+ module Configurable
5
+ def self.extended(klass)
6
+ klass.extend Forwardable
7
+ klass.def_delegator 'self.class', :config
8
+ end
9
+
10
+ def config(new_configuration = {})
11
+ @config ||= Configuration.new
12
+
13
+ return @config if new_configuration.blank?
14
+
15
+ @config.apply(new_configuration)
16
+ end
17
+
18
+ # @api private
19
+ class Configuration
20
+ attr_accessor :configuration
21
+
22
+ def initialize(configuration = {})
23
+ self.configuration = configuration
24
+ end
25
+
26
+ def apply(new_configuration)
27
+ config_to_apply =
28
+ if new_configuration.is_a? Configuration
29
+ new_configuration.configuration
30
+ else
31
+ new_configuration
32
+ end
33
+
34
+ self.configuration = configuration.deep_merge(config_to_apply)
35
+ end
36
+
37
+ def options
38
+ configuration[:options] ||= {}
39
+ end
40
+
41
+ ### Input Configuration ###
42
+
43
+ def inputs
44
+ configuration[:inputs] ||= {}
45
+ end
46
+
47
+ def add_input(identifier, new_config = {})
48
+ existing_config = input_config(identifier) || {}
49
+ inputs[identifier] = default_input_config(identifier).merge(existing_config, new_config)
50
+ end
51
+
52
+ def default_input_config(identifier)
53
+ { name: identifier, type: 'text' }
54
+ end
55
+
56
+ def input_config_exists?(identifier)
57
+ inputs.key? identifier
58
+ end
59
+
60
+ def input_config(identifier)
61
+ inputs[identifier]
62
+ end
63
+
64
+ def input_name(identifier)
65
+ inputs.dig(identifier, :name) || identifier
66
+ end
67
+
68
+ ### Output Configuration ###
69
+
70
+ def outputs
71
+ configuration[:outputs] ||= {}
72
+ end
73
+
74
+ def add_output(identifier)
75
+ return if output_config_exists?(identifier)
76
+
77
+ outputs[identifier] = default_output_config(identifier)
78
+ end
79
+
80
+ def default_output_config(identifier)
81
+ { name: identifier }
82
+ end
83
+
84
+ def output_config_exists?(identifier)
85
+ outputs.key? identifier
86
+ end
87
+
88
+ def output_config(identifier)
89
+ outputs[identifier]
90
+ end
91
+
92
+ def output_name(identifier)
93
+ outputs.dig(identifier, :name) || identifier
94
+ end
95
+
96
+ ### Request Configuration ###
97
+
98
+ def requests
99
+ configuration[:requests] ||= {}
100
+ end
101
+
102
+ def add_request(identifier)
103
+ return if request_config_exists?(identifier)
104
+
105
+ requests[identifier] = default_request_config(identifier)
106
+ end
107
+
108
+ def default_request_config(identifier)
109
+ { name: identifier }
110
+ end
111
+
112
+ def request_config_exists?(identifier)
113
+ requests.key? identifier
114
+ end
115
+
116
+ def request_config(identifier)
117
+ requests[identifier]
118
+ end
119
+
120
+ def request_name(identifier)
121
+ requests.dig(identifier, :name) || identifier
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -69,12 +69,14 @@ module Inferno
69
69
  # @param client [Symbol]
70
70
  # @param name [Symbol] Name for this request to allow it to be used by
71
71
  # other tests
72
- # @param _options [Hash] TODO
72
+ # @option options [Hash] Input headers here - headers are optional and
73
+ # must be entered as the last piece of input to this method
73
74
  # @return [Inferno::Entities::Request]
74
- def fhir_operation(path, body: nil, client: :default, name: nil, **_options)
75
+ def fhir_operation(path, body: nil, client: :default, name: nil, **options)
75
76
  store_request('outgoing', name) do
76
77
  headers = fhir_client(client).fhir_headers
77
78
  headers.merge!('Content-Type' => 'application/fhir+json') if body.present?
79
+ headers.merge!(options[:headers]) if options[:headers].present?
78
80
  fhir_client(client).send(:post, path, body, headers)
79
81
  end
80
82
  end
@@ -14,6 +14,7 @@ module Inferno
14
14
  FHIR::Client.new(url).tap do |client|
15
15
  client.additional_headers = headers if headers
16
16
  client.default_json
17
+ client.set_bearer_token bearer_token if bearer_token
17
18
  end
18
19
  end
19
20
 
@@ -32,6 +33,21 @@ module Inferno
32
33
  end
33
34
  end
34
35
 
36
+ # Define the bearer token for a client. A string or symbol can be provided.
37
+ # A string is interpreted as a token. A symbol is interpreted as the name of
38
+ # an input to the Runnable.
39
+ #
40
+ # @param bearer_token [String, Symbol]
41
+ # @return [void]
42
+ def bearer_token(bearer_token = nil)
43
+ @bearer_token ||=
44
+ if bearer_token.is_a? Symbol
45
+ runnable.send(bearer_token)
46
+ else
47
+ bearer_token
48
+ end
49
+ end
50
+
35
51
  # Define custom headers for a client
36
52
  #
37
53
  # @param headers [Hash]
@@ -60,16 +60,17 @@ module Inferno
60
60
  # @param client [Symbol]
61
61
  # @param name [Symbol] Name for this request to allow it to be used by
62
62
  # other tests
63
- # @param _options [Hash] TODO
63
+ # @option options [Hash] Input headers here - headers are optional and
64
+ # must be entered as the last piece of input to this method
64
65
  # @return [Inferno::Entities::Request]
65
- def get(url = '', client: :default, name: nil, **_options)
66
+ def get(url = '', client: :default, name: nil, **options)
66
67
  store_request('outgoing', name) do
67
68
  client = http_client(client)
68
69
 
69
70
  if client
70
- client.get(url)
71
+ client.get(url, nil, options[:headers])
71
72
  elsif url.match?(%r{\Ahttps?://})
72
- Faraday.get(url)
73
+ Faraday.get(url, nil, options[:headers])
73
74
  else
74
75
  raise StandardError, 'Must use an absolute url or define an HTTP client with a base url'
75
76
  end
@@ -85,16 +86,17 @@ module Inferno
85
86
  # @param client [Symbol]
86
87
  # @param name [Symbol] Name for this request to allow it to be used by
87
88
  # other tests
88
- # @param _options [Hash] TODO
89
+ # @option options [Hash] Input headers here - headers are optional and
90
+ # must be entered as the last piece of input to this method
89
91
  # @return [Inferno::Entities::Request]
90
- def post(url = '', body: nil, client: :default, name: nil, **_options)
92
+ def post(url = '', body: nil, client: :default, name: nil, **options)
91
93
  store_request('outgoing', name) do
92
94
  client = http_client(client)
93
95
 
94
96
  if client
95
- client.post(url, body)
97
+ client.post(url, body, options[:headers])
96
98
  elsif url.match?(%r{\Ahttps?://})
97
- Faraday.post(url, body)
99
+ Faraday.post(url, body, options[:headers])
98
100
  else
99
101
  raise StandardError, 'Must use an absolute url or define an HTTP client with a base url'
100
102
  end
@@ -38,13 +38,14 @@ module Inferno
38
38
 
39
39
  # TODO: do a check in the test runner
40
40
  def named_request(name)
41
- requests.find { |request| request.name == name.to_sym }
41
+ requests.find { |request| request.name == self.class.config.request_name(name.to_sym) }
42
42
  end
43
43
 
44
44
  # @api private
45
45
  def store_request(direction, name = nil, &block)
46
46
  response = block.call
47
47
 
48
+ name = self.class.config.request_name(name)
48
49
  request =
49
50
  if response.is_a? FHIR::ClientReply
50
51
  Entities::Request.from_fhir_client_reply(
@@ -64,8 +65,9 @@ module Inferno
64
65
  def load_named_requests
65
66
  requests_repo = Inferno::Repositories::Requests.new
66
67
  self.class.named_requests_used.map do |request_name|
67
- request = requests_repo.find_named_request(test_session_id, request_name)
68
- raise StandardError, "Unable to find '#{request_name}' request" if request.nil?
68
+ request_alias = self.class.config.request_name(request_name)
69
+ request = requests_repo.find_named_request(test_session_id, request_alias)
70
+ raise StandardError, "Unable to find '#{request_alias}' request" if request.nil?
69
71
 
70
72
  requests << request
71
73
  end
@@ -84,16 +86,35 @@ module Inferno
84
86
 
85
87
  # Specify the named requests made by a test
86
88
  #
87
- # @param *names [Symbol] one or more Symbols
88
- def makes_request(*names)
89
- named_requests_made.concat(names)
89
+ # @param *identifiers [Symbol] one or more Symbols
90
+ def makes_request(*identifiers)
91
+ named_requests_made.concat(identifiers).uniq!
92
+ identifiers.each do |identifier|
93
+ config.add_request(identifier)
94
+ end
95
+ end
96
+
97
+ # Specify the name for a request received by a test
98
+ #
99
+ # @param *identifiers [Symbol] one or more Symbols
100
+ def receives_request(identifier)
101
+ config.add_request(identifier)
102
+ @incoming_request_name = identifier
103
+ end
104
+
105
+ # @api private
106
+ def incoming_request_name
107
+ @incoming_request_name
90
108
  end
91
109
 
92
110
  # Specify the named requests used by a test
93
111
  #
94
- # @param *names [Symbol] one or more Symbols
95
- def uses_request(*names)
96
- named_requests_used.concat(names)
112
+ # @param *identifiers [Symbol] one or more Symbols
113
+ def uses_request(*identifiers)
114
+ named_requests_used.concat(identifiers).uniq!
115
+ identifiers.each do |identifier|
116
+ config.add_request(identifier)
117
+ end
97
118
  end
98
119
  end
99
120
  end
@@ -49,6 +49,55 @@ module Inferno
49
49
  def omit_if(test, message = '')
50
50
  raise Exceptions::OmitException, message if test
51
51
  end
52
+
53
+ # Halt execution of the current test and wait for execution to resume.
54
+ #
55
+ # @see Inferno::DSL::Runnable#resume_test_route
56
+ # @example
57
+ # resume_test_route :get, '/launch' do
58
+ # request.query_parameters['iss']
59
+ # end
60
+ #
61
+ # test do
62
+ # input :issuer
63
+ # receives_request :launch
64
+ #
65
+ # run do
66
+ # wait(
67
+ # identifier: issuer,
68
+ # message: "Wating to receive a request with an issuer of #{issuer}"
69
+ # )
70
+ # end
71
+ # end
72
+ # @param identifier [String] An identifier which can uniquely identify
73
+ # this test run based on an incoming request. This is necessary so that
74
+ # the correct test run can be resumed.
75
+ # @param message [String]
76
+ # @param timeout [Integer] Number of seconds to wait for an incoming
77
+ # request
78
+ def wait(identifier:, message: '', timeout: 300)
79
+ identifier(identifier)
80
+ wait_timeout(timeout)
81
+
82
+ raise Exceptions::WaitException, message
83
+ end
84
+
85
+ def identifier(identifier = nil)
86
+ @identifier ||= identifier
87
+ end
88
+
89
+ def wait_timeout(timeout = nil)
90
+ @wait_timeout ||= timeout
91
+ end
92
+
93
+ # Halt execution of the current test. This provided for testing purposes
94
+ # and should not be used in real tests.
95
+ #
96
+ # @param message [String]
97
+ # @api private
98
+ def cancel(message = '')
99
+ raise Exceptions::CancelException, message
100
+ end
52
101
  end
53
102
  end
54
103
  end
@@ -0,0 +1,89 @@
1
+ require 'hanami-controller'
2
+
3
+ module Inferno
4
+ module DSL
5
+ # A base class for creating routes to resume test execution upon receiving
6
+ # an incoming request.
7
+ # @api private
8
+ # @see Inferno::DSL::Runnable#resume_test_route
9
+ class ResumeTestRoute
10
+ include Hanami::Action
11
+ include Import[
12
+ requests_repo: 'repositories.requests',
13
+ results_repo: 'repositories.results',
14
+ test_runs_repo: 'repositories.test_runs',
15
+ tests_repo: 'repositories.tests'
16
+ ]
17
+
18
+ def self.call(params)
19
+ new.call(params)
20
+ end
21
+
22
+ # The incoming request
23
+ #
24
+ # @return [Inferno::Entities::Request]
25
+ def request
26
+ @request ||= Inferno::Entities::Request.from_rack_env(@params.env)
27
+ end
28
+
29
+ # @api private
30
+ def test_run
31
+ @test_run ||=
32
+ test_runs_repo.find_latest_waiting_by_identifier(test_run_identifier)
33
+ end
34
+
35
+ # @api private
36
+ def waiting_result
37
+ @waiting_result ||= results_repo.find_waiting_result(test_run_id: test_run.id)
38
+ end
39
+
40
+ # @api private
41
+ def update_result
42
+ results_repo.pass_waiting_result(waiting_result.id)
43
+ end
44
+
45
+ # @api private
46
+ def persist_request
47
+ requests_repo.create(
48
+ request.to_hash.merge(
49
+ test_session_id: test_run.test_session_id,
50
+ result_id: waiting_result.id,
51
+ name: test.config.request_name(test.incoming_request_name)
52
+ )
53
+ )
54
+ end
55
+
56
+ # @api private
57
+ def redirect_route
58
+ "/test_sessions/#{test_run.test_session_id}##{waiting_group_id}"
59
+ end
60
+
61
+ # @api private
62
+ def test
63
+ @test ||= tests_repo.find(waiting_result.test_id)
64
+ end
65
+
66
+ # @api private
67
+ def waiting_group_id
68
+ test.parent.id
69
+ end
70
+
71
+ # @api private
72
+ def call(_params)
73
+ if test_run.nil?
74
+ status(500, "Unable to find test run with identifier '#{test_run_identifier}'.")
75
+ return
76
+ end
77
+
78
+ test_runs_repo.mark_as_no_longer_waiting(test_run.id)
79
+
80
+ update_result
81
+ persist_request
82
+
83
+ Jobs.perform(Jobs::ResumeTestRun, test_run.id)
84
+
85
+ redirect_to redirect_route
86
+ end
87
+ end
88
+ end
89
+ end
@@ -1,3 +1,7 @@
1
+ require_relative 'configurable'
2
+ require_relative 'resume_test_route'
3
+ require_relative '../utils/markdown_formatter'
4
+
1
5
  module Inferno
2
6
  module DSL
3
7
  # This module contains the DSL for defining child entities in the test
@@ -5,6 +9,8 @@ module Inferno
5
9
  module Runnable
6
10
  attr_accessor :parent
7
11
 
12
+ include Inferno::Utils::MarkdownFormatter
13
+
8
14
  # When a class (e.g. TestSuite/TestGroup) uses this module, set it up
9
15
  # so that subclassing it works correctly.
10
16
  # - add the subclass to the relevant repository when it is created
@@ -13,6 +19,7 @@ module Inferno
13
19
  # @api private
14
20
  def self.extended(extending_class)
15
21
  super
22
+ extending_class.extend Configurable
16
23
 
17
24
  extending_class.define_singleton_method(:inherited) do |subclass|
18
25
  copy_instance_variables(subclass)
@@ -37,11 +44,13 @@ module Inferno
37
44
  # @api private
38
45
  def copy_instance_variables(subclass)
39
46
  instance_variables.each do |variable|
40
- next if [:@id, :@groups, :@tests, :@parent, :@children].include?(variable)
47
+ next if [:@id, :@groups, :@tests, :@parent, :@children, :@test_count, :@config].include?(variable)
41
48
 
42
49
  subclass.instance_variable_set(variable, instance_variable_get(variable).dup)
43
50
  end
44
51
 
52
+ subclass.config(config)
53
+
45
54
  child_types.each do |child_type|
46
55
  new_children = send(child_type).map do |child|
47
56
  Class.new(child).tap do |subclass_child|
@@ -124,16 +133,16 @@ module Inferno
124
133
 
125
134
  # @api private
126
135
  def configure_child_class(klass, hash_args) # rubocop:disable Metrics/CyclomaticComplexity
127
- inputs.each do |input_definition|
128
- next if klass.inputs.include? input_definition
136
+ inputs.each do |name|
137
+ next if klass.inputs.any? { |klass_input_name| klass_input_name == name }
129
138
 
130
- klass.input input_definition
139
+ klass.input name
131
140
  end
132
141
 
133
- outputs.each do |output_definition|
134
- next if klass.outputs.include? output_definition
142
+ outputs.each do |output_name|
143
+ next if klass.outputs.include? output_name
135
144
 
136
- klass.output output_definition
145
+ klass.output output_name
137
146
  end
138
147
 
139
148
  new_fhir_client_definitions = klass.instance_variable_get(:@fhir_client_definitions) || {}
@@ -152,8 +161,14 @@ module Inferno
152
161
  end
153
162
  klass.instance_variable_set(:@http_client_definitions, new_http_client_definitions)
154
163
 
164
+ klass.config(config)
165
+
155
166
  hash_args.each do |key, value|
156
- klass.send(key, *value)
167
+ if value.is_a? Array
168
+ klass.send(key, *value)
169
+ else
170
+ klass.send(key, value)
171
+ end
157
172
  end
158
173
 
159
174
  klass.children.each do |child_class|
@@ -191,27 +206,48 @@ module Inferno
191
206
  def description(new_description = nil)
192
207
  return @description if new_description.nil?
193
208
 
194
- @description = new_description
209
+ @description = format_markdown(new_description)
195
210
  end
196
211
 
197
212
  # Define inputs
198
213
  #
199
- # @param inputs [Symbol]
214
+ # @param identifier [Symbol] identifier for the input
215
+ # @param other_identifiers [Symbol] array of symbols if specifying multiple inputs
216
+ # @param input_definition [Hash] options for input such as type, description, or title
217
+ # @option input_definition [String] :title Human readable title for input
218
+ # @option input_definition [String] :description Description for the input
219
+ # @option input_definition [String] :type text | textarea
220
+ # @option input_definition [String] :default The default value for the input
221
+ # @option input_definition [Boolean] :optional Set to true to not require input for test execution
200
222
  # @return [void]
201
223
  # @example
202
- # input :patient_id, :bearer_token
203
- def input(*input_definitions)
204
- inputs.concat(input_definitions)
224
+ # input :patientid, title: 'Patient ID', description: 'The ID of the patient being searched for',
225
+ # default: 'default_patient_id'
226
+ # @example
227
+ # input :textarea, title: 'Textarea Input Example', type: 'textarea', optional: true
228
+ def input(identifier, *other_identifiers, **input_definition)
229
+ if other_identifiers.present?
230
+ [identifier, *other_identifiers].compact.each do |input_identifier|
231
+ inputs << input_identifier
232
+ config.add_input(input_identifier)
233
+ end
234
+ else
235
+ inputs << identifier
236
+ config.add_input(identifier, input_definition)
237
+ end
205
238
  end
206
239
 
207
240
  # Define outputs
208
241
  #
209
- # @param output_definitions [Symbol]
242
+ # @param output_lists [Symbol]
210
243
  # @return [void]
211
244
  # @example
212
245
  # output :patient_id, :bearer_token
213
- def output(*output_definitions)
214
- outputs.concat(output_definitions)
246
+ def output(*output_list)
247
+ output_list.each do |output_identifier|
248
+ outputs << output_identifier
249
+ config.add_output(output_identifier)
250
+ end
215
251
  end
216
252
 
217
253
  # @api private
@@ -224,11 +260,20 @@ module Inferno
224
260
  @inputs ||= []
225
261
  end
226
262
 
263
+ def input_definitions
264
+ config.inputs.slice(*inputs)
265
+ end
266
+
267
+ def output_definitions
268
+ config.outputs.slice(*outputs)
269
+ end
270
+
227
271
  # @api private
228
272
  def outputs
229
273
  @outputs ||= []
230
274
  end
231
275
 
276
+ # @api private
232
277
  def child_types
233
278
  return [] if ancestors.include? Inferno::Entities::Test
234
279
  return [:groups] if ancestors.include? Inferno::Entities::TestSuite
@@ -236,6 +281,7 @@ module Inferno
236
281
  [:groups, :tests]
237
282
  end
238
283
 
284
+ # @api private
239
285
  def children
240
286
  @children ||= []
241
287
  end
@@ -245,6 +291,97 @@ module Inferno
245
291
 
246
292
  @validator_url = url
247
293
  end
294
+
295
+ # @api private
296
+ def suite
297
+ return self if ancestors.include? Inferno::Entities::TestSuite
298
+
299
+ parent.suite
300
+ end
301
+
302
+ # Create a route which will resume a test run when a request is received
303
+ #
304
+ # @see Inferno::DSL::Results#wait
305
+ # @example
306
+ # resume_test_route :get, '/launch' do
307
+ # request.query_parameters['iss']
308
+ # end
309
+ #
310
+ # test do
311
+ # input :issuer
312
+ # receives_request :launch
313
+ #
314
+ # run do
315
+ # wait(
316
+ # identifier: issuer,
317
+ # message: "Wating to receive a request with an issuer of #{issuer}"
318
+ # )
319
+ # end
320
+ # end
321
+ #
322
+ # @param method [Symbol] the HTTP request type (:get, :post, etc.) for the
323
+ # incoming request
324
+ # @param path [String] the path for this request. The route will be served
325
+ # with a prefix of `/custom/TEST_SUITE_ID` to prevent path conflicts.
326
+ # [Any of the path options available in Hanami
327
+ # Router](https://github.com/hanami/router/tree/f41001d4c3ee9e2d2c7bb142f74b43f8e1d3a265#a-beautiful-dsl)
328
+ # can be used here.
329
+ # @yield This method takes a block which must return the identifier
330
+ # defined when a test was set to wait for the test run that hit this
331
+ # route. The block has access to the `request` method which returns a
332
+ # {Inferno::DSL::Request} object with the information for the incoming
333
+ # request.
334
+ def resume_test_route(method, path, &block)
335
+ route_class = Class.new(ResumeTestRoute) do
336
+ define_method(:test_run_identifier, &block)
337
+ define_method(:request_name, -> { options[:name] })
338
+ end
339
+
340
+ route(method, path, route_class)
341
+ end
342
+
343
+ # Create a route to handle a request
344
+ #
345
+ # @param method [Symbol] the HTTP request type (:get, :post, etc.) for the
346
+ # incoming request. `:all` will accept all HTTP request types.
347
+ # @param path [String] the path for this request. The route will be served
348
+ # with a prefix of `/custom/TEST_SUITE_ID` to prevent path conflicts.
349
+ # [Any of the path options available in Hanami
350
+ # Router](https://github.com/hanami/router/tree/f41001d4c3ee9e2d2c7bb142f74b43f8e1d3a265#a-beautiful-dsl)
351
+ # can be used here.
352
+ # @param handler [#call] the route handler. This can be any Rack
353
+ # compatible object (e.g. a `Proc` object, a [Sinatra
354
+ # app](http://sinatrarb.com/)) as described in the [Hanami Router
355
+ # documentation.](https://github.com/hanami/router/tree/f41001d4c3ee9e2d2c7bb142f74b43f8e1d3a265#mount-rack-applications)
356
+ def route(method, path, handler)
357
+ Inferno.routes << { method: method, path: path, handler: handler, suite: suite }
358
+ end
359
+
360
+ def test_count
361
+ @test_count ||= children&.reduce(0) { |sum, child| sum + child.test_count } || 0
362
+ end
363
+
364
+ def required_inputs(prior_outputs = [])
365
+ required_inputs = inputs.select do |input|
366
+ !input_definitions[input][:optional] && !prior_outputs.include?(input)
367
+ end
368
+ required_inputs.map! { |input_identifier| input_definitions[input_identifier][:name] }
369
+ children_required_inputs = children.flat_map { |child| child.required_inputs(prior_outputs) }
370
+ prior_outputs.concat(outputs)
371
+ (required_inputs + children_required_inputs).flatten.uniq
372
+ end
373
+
374
+ def missing_inputs(submitted_inputs)
375
+ submitted_inputs = [] if submitted_inputs.nil?
376
+
377
+ required_inputs.map(&:to_s) - submitted_inputs.map { |input| input[:name] }
378
+ end
379
+
380
+ def user_runnable?
381
+ @user_runnable ||= parent.nil? ||
382
+ !parent.respond_to?(:run_as_group?) ||
383
+ (parent.user_runnable? && !parent.run_as_group?)
384
+ end
248
385
  end
249
386
  end
250
387
  end