inferno_core 0.0.3 → 0.0.4

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/lib/inferno.rb +4 -0
  3. data/lib/inferno/apps/web/controllers/test_runs/create.rb +17 -10
  4. data/lib/inferno/apps/web/controllers/test_runs/show.rb +10 -0
  5. data/lib/inferno/apps/web/controllers/test_sessions/create.rb +1 -0
  6. data/lib/inferno/apps/web/controllers/test_sessions/last_test_run.rb +22 -0
  7. data/lib/inferno/apps/web/controllers/test_sessions/results/index.rb +6 -1
  8. data/lib/inferno/apps/web/controllers/test_sessions/session_data/index.rb +21 -0
  9. data/lib/inferno/apps/web/router.rb +15 -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 +1 -3
  14. data/lib/inferno/apps/web/serializers/test_group.rb +2 -3
  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 +4 -2
  19. data/lib/inferno/config/boot.rb +2 -0
  20. data/lib/inferno/config/boot/db.rb +10 -1
  21. data/lib/inferno/config/boot/sidekiq.rb +11 -0
  22. data/lib/inferno/db/migrations/001_create_initial_structure.rb +0 -21
  23. data/lib/inferno/db/migrations/002_add_wait_support.rb +7 -0
  24. data/lib/inferno/db/migrations/003_update_session_data.rb +18 -0
  25. data/lib/inferno/db/migrations/004_add_request_results_table.rb +9 -0
  26. data/lib/inferno/db/migrations/005_add_updated_at_index_to_results.rb +5 -0
  27. data/lib/inferno/db/schema.rb +154 -0
  28. data/lib/inferno/dsl.rb +1 -3
  29. data/lib/inferno/dsl/fhir_client_builder.rb +16 -0
  30. data/lib/inferno/dsl/request_storage.rb +12 -0
  31. data/lib/inferno/dsl/results.rb +49 -0
  32. data/lib/inferno/dsl/resume_test_route.rb +89 -0
  33. data/lib/inferno/dsl/runnable.rb +96 -7
  34. data/lib/inferno/entities.rb +1 -1
  35. data/lib/inferno/entities/header.rb +7 -7
  36. data/lib/inferno/entities/message.rb +8 -6
  37. data/lib/inferno/entities/request.rb +40 -14
  38. data/lib/inferno/entities/result.rb +34 -18
  39. data/lib/inferno/entities/session_data.rb +33 -0
  40. data/lib/inferno/entities/test.rb +20 -7
  41. data/lib/inferno/entities/test_run.rb +13 -6
  42. data/lib/inferno/entities/test_session.rb +8 -8
  43. data/lib/inferno/exceptions.rb +12 -0
  44. data/lib/inferno/jobs.rb +16 -0
  45. data/lib/inferno/jobs/execute_test_run.rb +14 -0
  46. data/lib/inferno/jobs/resume_test_run.rb +14 -0
  47. data/lib/inferno/public/bundle.js +1 -1
  48. data/lib/inferno/repositories/repository.rb +13 -0
  49. data/lib/inferno/repositories/requests.rb +5 -4
  50. data/lib/inferno/repositories/results.rb +151 -3
  51. data/lib/inferno/repositories/session_data.rb +47 -0
  52. data/lib/inferno/repositories/test_runs.rb +66 -0
  53. data/lib/inferno/test_runner.rb +121 -29
  54. data/lib/inferno/utils/middleware/request_logger.rb +16 -3
  55. data/lib/inferno/version.rb +1 -1
  56. data/spec/factories/result.rb +8 -0
  57. data/spec/factories/test_run.rb +2 -0
  58. metadata +32 -5
  59. data/lib/inferno/dsl/fhir_manipulation.rb +0 -25
  60. data/lib/inferno/entities/test_input.rb +0 -20
data/lib/inferno/dsl.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  require_relative 'dsl/assertions'
2
2
  require_relative 'dsl/fhir_client'
3
- require_relative 'dsl/fhir_manipulation'
4
3
  require_relative 'dsl/fhir_validation'
5
4
  require_relative 'dsl/http_client'
6
5
  require_relative 'dsl/results'
@@ -14,8 +13,7 @@ module Inferno
14
13
  FHIRClient,
15
14
  HTTPClient,
16
15
  Results,
17
- FHIRValidation,
18
- FHIRManipulation
16
+ FHIRValidation
19
17
  ].freeze
20
18
 
21
19
  EXTENDABLE_DSL_MODULES = [
@@ -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]
@@ -89,6 +89,18 @@ module Inferno
89
89
  named_requests_made.concat(names)
90
90
  end
91
91
 
92
+ # Specify the name for a request received by a test
93
+ #
94
+ # @param *names [Symbol] one or more Symbols
95
+ def receives_request(name)
96
+ @incoming_request_name = name
97
+ end
98
+
99
+ # @api private
100
+ def incoming_request_name
101
+ @incoming_request_name
102
+ end
103
+
92
104
  # Specify the named requests used by a test
93
105
  #
94
106
  # @param *names [Symbol] one or more Symbols
@@ -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.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,5 @@
1
+ require_relative 'resume_test_route'
2
+
1
3
  module Inferno
2
4
  module DSL
3
5
  # This module contains the DSL for defining child entities in the test
@@ -37,7 +39,7 @@ module Inferno
37
39
  # @api private
38
40
  def copy_instance_variables(subclass)
39
41
  instance_variables.each do |variable|
40
- next if [:@id, :@groups, :@tests, :@parent, :@children].include?(variable)
42
+ next if [:@id, :@groups, :@tests, :@parent, :@children, :@test_count].include?(variable)
41
43
 
42
44
  subclass.instance_variable_set(variable, instance_variable_get(variable).dup)
43
45
  end
@@ -125,9 +127,9 @@ module Inferno
125
127
  # @api private
126
128
  def configure_child_class(klass, hash_args) # rubocop:disable Metrics/CyclomaticComplexity
127
129
  inputs.each do |input_definition|
128
- next if klass.inputs.include? input_definition
130
+ next if klass.inputs.any? { |input| input[:name] == input_definition[:name] }
129
131
 
130
- klass.input input_definition
132
+ klass.input input_definition[:name], input_definition
131
133
  end
132
134
 
133
135
  outputs.each do |output_definition|
@@ -196,12 +198,28 @@ module Inferno
196
198
 
197
199
  # Define inputs
198
200
  #
199
- # @param inputs [Symbol]
201
+ # @param name [Symbol] name of the input
202
+ # @param other_names [Symbol] array of symbols if specifying multiple inputs
203
+ # @param input_definition [Hash] options for input such as type, description, or title
204
+ # @option input_definition [String] :title Human readable title for input
205
+ # @option input_definition [String] :description Description for the input
206
+ # @option input_definition [String] :type text | textarea
207
+ # @option input_definition [String] :default The default value for the input
200
208
  # @return [void]
201
209
  # @example
202
- # input :patient_id, :bearer_token
203
- def input(*input_definitions)
204
- inputs.concat(input_definitions)
210
+ # input :patientid, title: 'Patient ID', description: 'The ID of the patient being searched for',
211
+ # default: 'default_patient_id'
212
+ # @example
213
+ # input :textarea, title: 'Textarea Input Example', type: 'textarea'
214
+ def input(name, *other_names, **input_definition)
215
+ if other_names.present?
216
+ [name, *other_names].each do |input_name|
217
+ inputs.push({ name: input_name, title: nil, description: nil, type: 'text' })
218
+ end
219
+ else
220
+ input_definition[:type] = 'text' unless input_definition.key? :type
221
+ inputs.push({ name: name }.merge(input_definition))
222
+ end
205
223
  end
206
224
 
207
225
  # Define outputs
@@ -229,6 +247,7 @@ module Inferno
229
247
  @outputs ||= []
230
248
  end
231
249
 
250
+ # @api private
232
251
  def child_types
233
252
  return [] if ancestors.include? Inferno::Entities::Test
234
253
  return [:groups] if ancestors.include? Inferno::Entities::TestSuite
@@ -236,6 +255,7 @@ module Inferno
236
255
  [:groups, :tests]
237
256
  end
238
257
 
258
+ # @api private
239
259
  def children
240
260
  @children ||= []
241
261
  end
@@ -245,6 +265,75 @@ module Inferno
245
265
 
246
266
  @validator_url = url
247
267
  end
268
+
269
+ # @api private
270
+ def suite
271
+ return self if ancestors.include? Inferno::Entities::TestSuite
272
+
273
+ parent.suite
274
+ end
275
+
276
+ # Create a route which will resume a test run when a request is received
277
+ #
278
+ # @see Inferno::DSL::Results#wait
279
+ # @example
280
+ # resume_test_route :get, '/launch' do
281
+ # request.query_parameters['iss']
282
+ # end
283
+ #
284
+ # test do
285
+ # input :issuer
286
+ # receives_request :launch
287
+ #
288
+ # run do
289
+ # wait(
290
+ # identifier: issuer,
291
+ # message: "Wating to receive a request with an issuer of #{issuer}"
292
+ # )
293
+ # end
294
+ # end
295
+ #
296
+ # @param method [Symbol] the HTTP request type (:get, :post, etc.) for the
297
+ # incoming request
298
+ # @param path [String] the path for this request. The route will be served
299
+ # with a prefix of `/custom/TEST_SUITE_ID` to prevent path conflicts.
300
+ # [Any of the path options available in Hanami
301
+ # Router](https://github.com/hanami/router/tree/f41001d4c3ee9e2d2c7bb142f74b43f8e1d3a265#a-beautiful-dsl)
302
+ # can be used here.
303
+ # @yield This method takes a block which must return the identifier
304
+ # defined when a test was set to wait for the test run that hit this
305
+ # route. The block has access to the `request` method which returns a
306
+ # {Inferno::DSL::Request} object with the information for the incoming
307
+ # request.
308
+ def resume_test_route(method, path, &block)
309
+ route_class = Class.new(ResumeTestRoute) do
310
+ define_method(:test_run_identifier, &block)
311
+ define_method(:request_name, -> { options[:name] })
312
+ end
313
+
314
+ route(method, path, route_class)
315
+ end
316
+
317
+ # Create a route to handle a request
318
+ #
319
+ # @param method [Symbol] the HTTP request type (:get, :post, etc.) for the
320
+ # incoming request. `:all` will accept all HTTP request types.
321
+ # @param path [String] the path for this request. The route will be served
322
+ # with a prefix of `/custom/TEST_SUITE_ID` to prevent path conflicts.
323
+ # [Any of the path options available in Hanami
324
+ # Router](https://github.com/hanami/router/tree/f41001d4c3ee9e2d2c7bb142f74b43f8e1d3a265#a-beautiful-dsl)
325
+ # can be used here.
326
+ # @param handler [#call] the route handler. This can be any Rack
327
+ # compatible object (e.g. a `Proc` object, a [Sinatra
328
+ # app](http://sinatrarb.com/)) as described in the [Hanami Router
329
+ # documentation.](https://github.com/hanami/router/tree/f41001d4c3ee9e2d2c7bb142f74b43f8e1d3a265#mount-rack-applications)
330
+ def route(method, path, handler)
331
+ Inferno.routes << { method: method, path: path, handler: handler, suite: suite }
332
+ end
333
+
334
+ def test_count
335
+ @test_count ||= children&.reduce(0) { |sum, child| sum + child.test_count } || 0
336
+ end
248
337
  end
249
338
  end
250
339
  end
@@ -4,9 +4,9 @@ require_relative 'entities/header'
4
4
  require_relative 'entities/message'
5
5
  require_relative 'entities/request'
6
6
  require_relative 'entities/result'
7
+ require_relative 'entities/session_data'
7
8
  require_relative 'entities/test'
8
9
  require_relative 'entities/test_group'
9
- require_relative 'entities/test_input'
10
10
  require_relative 'entities/test_run'
11
11
  require_relative 'entities/test_session'
12
12
  require_relative 'entities/test_suite'
@@ -2,13 +2,13 @@ module Inferno
2
2
  module Entities
3
3
  # A `Header` represents an HTTP request/response header
4
4
  #
5
- # @attr_reader [String] id of the header
6
- # @attr_reader [String] request_id index of the HTTP request
7
- # @attr_reader [String] name header name
8
- # @attr_reader [String] value header value
9
- # @attr_reader [String] type request/response
10
- # @attr_reader [Time] created_at
11
- # @attr_reader [Time] updated_at
5
+ # @attr_accessor [String] id of the header
6
+ # @attr_accessor [String] request_id index of the HTTP request
7
+ # @attr_accessor [String] name header name
8
+ # @attr_accessor [String] value header value
9
+ # @attr_accessor [String] type request/response
10
+ # @attr_accessor [Time] created_at
11
+ # @attr_accessor [Time] updated_at
12
12
  class Header < Entity
13
13
  ATTRIBUTES = [:id, :request_id, :name, :type, :value, :created_at, :updated_at].freeze
14
14
 
@@ -2,12 +2,14 @@ module Inferno
2
2
  module Entities
3
3
  # A `Message` represents a message generated during a test.
4
4
  #
5
- # @attr_reader [String] id of the message
6
- # @attr_reader [String] index of the message. Used for ordering.
7
- # @attr_reader [String] result_id
8
- # @attr_reader [Inferno::Entities::Result] result
9
- # @attr_reader [String] type
10
- # @attr_reader [String] message
5
+ # @attr_accessor [String] id of the message
6
+ # @attr_accessor [String] index of the message. Used for ordering.
7
+ # @attr_accessor [String] result_id
8
+ # @attr_accessor [Inferno::Entities::Result] result
9
+ # @attr_accessor [String] type
10
+ # @attr_accessor [String] message
11
+ # @attr_accessor [Time] created_at
12
+ # @attr_accessor [Time] updated_at
11
13
  class Message < Entity
12
14
  ATTRIBUTES = [:id, :index, :message, :result_id, :result, :type, :created_at, :updated_at].freeze
13
15
  TYPES = ['error', 'warning', 'info'].freeze
@@ -2,21 +2,21 @@ module Inferno
2
2
  module Entities
3
3
  # A `Request` represents a request and response issued during a test.
4
4
  #
5
- # @attr_reader [String] id of the request
6
- # @attr_reader [String] index of the request. Used for ordering.
7
- # @attr_reader [String] verb http verb
8
- # @attr_reader [String] url request url
9
- # @attr_reader [String] direction incoming/outgoing
10
- # @attr_reader [String] name name for the request
11
- # @attr_reader [String] status http response status code
12
- # @attr_reader [String] request_body body of the http request
13
- # @attr_reader [String] response_body body of the http response
14
- # @attr_reader [Array<Inferno::Entities::Header>] headers http
5
+ # @attr_accessor [String] id of the request
6
+ # @attr_accessor [String] index of the request. Used for ordering.
7
+ # @attr_accessor [String] verb http verb
8
+ # @attr_accessor [String] url request url
9
+ # @attr_accessor [String] direction incoming/outgoing
10
+ # @attr_accessor [String] name name for the request
11
+ # @attr_accessor [String] status http response status code
12
+ # @attr_accessor [String] request_body body of the http request
13
+ # @attr_accessor [String] response_body body of the http response
14
+ # @attr_accessor [Array<Inferno::Entities::Header>] headers http
15
15
  # request/response headers
16
- # @attr_reader [String] result_id id of the result for this request
17
- # @attr_reader [String] test_session_id id of the test session for this request
18
- # @attr_reader [Time] created_at creation timestamp
19
- # @attr_reader [Time] updated_at update timestamp
16
+ # @attr_accessor [String] result_id id of the result for this request
17
+ # @attr_accessor [String] test_session_id id of the test session for this request
18
+ # @attr_accessor [Time] created_at creation timestamp
19
+ # @attr_accessor [Time] updated_at update timestamp
20
20
  class Request < Entity
21
21
  ATTRIBUTES = [
22
22
  :id, :index, :verb, :url, :direction, :name, :status,
@@ -36,6 +36,11 @@ module Inferno
36
36
  @headers = params[:headers]&.map { |header| header.is_a?(Hash) ? Header.new(header) : header } || []
37
37
  end
38
38
 
39
+ # @return [Hash<String, String>]
40
+ def query_parameters
41
+ Addressable::URI.parse(url).query_values || {}
42
+ end
43
+
39
44
  # Find a response header
40
45
  #
41
46
  # @param name [String] the header name
@@ -117,6 +122,27 @@ module Inferno
117
122
  end
118
123
 
119
124
  class << self
125
+ # @api private
126
+ def from_rack_env(env, name: nil)
127
+ rack_request = env['router.request'].rack_request
128
+ url = "#{rack_request.base_url}#{rack_request.path}"
129
+ url += "?#{rack_request.query_string}" if rack_request.query_string.present?
130
+ request_headers =
131
+ env
132
+ .select { |key, _| key.start_with? 'HTTP_' }
133
+ .transform_keys { |key| key.delete_prefix('HTTP_').tr('_', '-').downcase }
134
+ .map { |header_name, value| Header.new(name: header_name, value: value, type: 'request') }
135
+
136
+ new(
137
+ verb: rack_request.request_method.downcase,
138
+ url: url,
139
+ direction: 'incoming',
140
+ name: name,
141
+ request_body: rack_request.body.string,
142
+ headers: request_headers
143
+ )
144
+ end
145
+
120
146
  # @api private
121
147
  def from_http_response(response, test_session_id:, direction: 'outgoing', name: nil)
122
148
  request_headers =