inferno_core 0.0.1 → 0.0.5

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 (67) 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/index.html.erb +44 -0
  10. data/lib/inferno/apps/web/router.rb +15 -0
  11. data/lib/inferno/apps/web/serializers/request.rb +1 -0
  12. data/lib/inferno/apps/web/serializers/result.rb +8 -0
  13. data/lib/inferno/apps/web/serializers/session_data.rb +10 -0
  14. data/lib/inferno/apps/web/serializers/test.rb +1 -3
  15. data/lib/inferno/apps/web/serializers/test_group.rb +2 -3
  16. data/lib/inferno/apps/web/serializers/test_run.rb +1 -0
  17. data/lib/inferno/apps/web/serializers/test_session.rb +1 -1
  18. data/lib/inferno/apps/web/serializers/test_suite.rb +1 -0
  19. data/lib/inferno/config/application.rb +4 -2
  20. data/lib/inferno/config/boot.rb +2 -0
  21. data/lib/inferno/config/boot/db.rb +10 -1
  22. data/lib/inferno/config/boot/sidekiq.rb +11 -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.rb +1 -3
  30. data/lib/inferno/dsl/fhir_client_builder.rb +16 -0
  31. data/lib/inferno/dsl/request_storage.rb +12 -0
  32. data/lib/inferno/dsl/results.rb +49 -0
  33. data/lib/inferno/dsl/resume_test_route.rb +89 -0
  34. data/lib/inferno/dsl/runnable.rb +96 -7
  35. data/lib/inferno/entities.rb +1 -1
  36. data/lib/inferno/entities/header.rb +7 -7
  37. data/lib/inferno/entities/message.rb +8 -6
  38. data/lib/inferno/entities/request.rb +40 -14
  39. data/lib/inferno/entities/result.rb +34 -18
  40. data/lib/inferno/entities/session_data.rb +33 -0
  41. data/lib/inferno/entities/test.rb +20 -7
  42. data/lib/inferno/entities/test_run.rb +13 -6
  43. data/lib/inferno/entities/test_session.rb +8 -8
  44. data/lib/inferno/exceptions.rb +12 -0
  45. data/lib/inferno/jobs.rb +16 -0
  46. data/lib/inferno/jobs/execute_test_run.rb +14 -0
  47. data/lib/inferno/jobs/resume_test_run.rb +14 -0
  48. data/lib/inferno/public/bundle.js +1 -1
  49. data/lib/inferno/repositories/repository.rb +13 -0
  50. data/lib/inferno/repositories/requests.rb +5 -4
  51. data/lib/inferno/repositories/results.rb +151 -3
  52. data/lib/inferno/repositories/session_data.rb +47 -0
  53. data/lib/inferno/repositories/test_runs.rb +66 -0
  54. data/lib/inferno/test_runner.rb +121 -29
  55. data/lib/inferno/utils/middleware/request_logger.rb +16 -3
  56. data/lib/inferno/version.rb +1 -1
  57. data/spec/factories/header.rb +19 -0
  58. data/spec/factories/message.rb +17 -0
  59. data/spec/factories/request.rb +35 -0
  60. data/spec/factories/result.rb +45 -0
  61. data/spec/factories/test_run.rb +24 -0
  62. data/spec/factories/test_session.rb +11 -0
  63. data/spec/fixtures/basic_test_group.rb +9 -0
  64. data/spec/fixtures/basic_test_suite.rb +8 -0
  65. metadata +139 -89
  66. data/lib/inferno/dsl/fhir_manipulation.rb +0 -25
  67. data/lib/inferno/entities/test_input.rb +0 -20
@@ -0,0 +1,9 @@
1
+ Sequel.migration do
2
+ change do
3
+ create_table :requests_results do
4
+ # using results_id instead of result_id to avoid ambiguous column error
5
+ foreign_key :results_id, :results, index: true, type: String, null: false
6
+ foreign_key :requests_id, :requests, index: true, type: String, null: false
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ Sequel.migration do
2
+ change do
3
+ add_index :results, [:test_run_id, :updated_at], concurrently: true
4
+ end
5
+ end
@@ -0,0 +1,154 @@
1
+ Sequel.migration do
2
+ change do
3
+ create_table(:schema_info) do
4
+ Integer :version, :default=>0, :null=>false
5
+ end
6
+
7
+ create_table(:test_sessions) do
8
+ String :id, :size=>255, :null=>false
9
+ String :test_suite_id, :size=>255
10
+ DateTime :created_at, :null=>false
11
+ DateTime :updated_at, :null=>false
12
+
13
+ primary_key [:id]
14
+ end
15
+
16
+ create_table(:session_data, :ignore_index_errors=>true) do
17
+ String :id, :size=>255, :null=>false
18
+ foreign_key :test_session_id, :test_sessions, :type=>String, :size=>255
19
+ String :name, :size=>255
20
+ String :value, :text=>true
21
+
22
+ index [:id], :unique=>true
23
+ index [:test_session_id]
24
+ index [:test_session_id, :name], :unique=>true
25
+ end
26
+
27
+ create_table(:test_runs, :ignore_index_errors=>true) do
28
+ String :id, :size=>255, :null=>false
29
+ String :status, :size=>255
30
+ foreign_key :test_session_id, :test_sessions, :type=>String, :size=>255
31
+ String :test_suite_id, :size=>255
32
+ String :test_group_id, :size=>255
33
+ String :test_id, :size=>255
34
+ DateTime :created_at, :null=>false
35
+ DateTime :updated_at, :null=>false
36
+ String :identifier, :text=>true
37
+ DateTime :wait_timeout
38
+
39
+ primary_key [:id]
40
+
41
+ index [:status, :identifier, :wait_timeout, :updated_at]
42
+ index [:test_group_id]
43
+ index [:test_id]
44
+ index [:test_session_id]
45
+ index [:test_session_id, :status]
46
+ index [:test_suite_id]
47
+ end
48
+
49
+ create_table(:results, :ignore_index_errors=>true) do
50
+ String :id, :size=>255, :null=>false
51
+ foreign_key :test_run_id, :test_runs, :type=>String, :size=>255
52
+ foreign_key :test_session_id, :test_sessions, :type=>String, :size=>255
53
+ String :result, :size=>255
54
+ String :result_message, :size=>255
55
+ String :test_suite_id, :size=>255
56
+ String :test_group_id, :size=>255
57
+ String :test_id, :size=>255
58
+ DateTime :created_at, :null=>false
59
+ DateTime :updated_at, :null=>false
60
+ String :input_json, :text=>true
61
+ String :output_json, :text=>true
62
+
63
+ primary_key [:id]
64
+
65
+ index [:test_run_id]
66
+ index [:test_run_id, :updated_at]
67
+ index [:test_session_id]
68
+ index [:test_session_id, :test_group_id]
69
+ index [:test_session_id, :test_id]
70
+ index [:test_session_id, :test_suite_id]
71
+ end
72
+
73
+ create_table(:messages, :ignore_index_errors=>true) do
74
+ primary_key :index
75
+ String :id, :size=>255, :null=>false
76
+ foreign_key :result_id, :results, :type=>String, :size=>255
77
+ String :type, :size=>255
78
+ String :message, :size=>255
79
+ DateTime :created_at, :null=>false
80
+ DateTime :updated_at, :null=>false
81
+
82
+ index [:id]
83
+ index [:result_id]
84
+ end
85
+
86
+ create_table(:requests, :ignore_index_errors=>true) do
87
+ primary_key :index
88
+ String :id, :size=>255, :null=>false
89
+ String :verb, :size=>255
90
+ String :url, :size=>255
91
+ String :direction, :size=>255
92
+ Integer :status
93
+ String :name, :size=>255
94
+ String :request_body, :text=>true
95
+ String :response_body, :text=>true
96
+ foreign_key :result_id, :results, :type=>String, :size=>255
97
+ foreign_key :test_session_id, :test_sessions, :type=>String, :size=>255
98
+ String :"[:test_session_id, :name]"
99
+ DateTime :created_at, :null=>false
100
+ DateTime :updated_at, :null=>false
101
+
102
+ index [:id]
103
+ index [:result_id]
104
+ index [:test_session_id]
105
+ end
106
+
107
+ create_table(:result_outputs, :ignore_index_errors=>true) do
108
+ String :id, :size=>255, :null=>false
109
+ foreign_key :result_id, :results, :type=>String, :size=>255
110
+ String :test_output_id, :size=>255
111
+ String :value, :size=>255
112
+ DateTime :created_at, :null=>false
113
+ DateTime :updated_at, :null=>false
114
+
115
+ primary_key [:id]
116
+
117
+ index [:result_id]
118
+ end
119
+
120
+ create_table(:result_prompt_values, :ignore_index_errors=>true) do
121
+ String :id, :size=>255, :null=>false
122
+ foreign_key :result_id, :results, :type=>String, :size=>255
123
+ String :test_prompt_id, :size=>255, :null=>false
124
+ String :value, :size=>255, :null=>false
125
+ DateTime :created_at, :null=>false
126
+ DateTime :updated_at, :null=>false
127
+
128
+ primary_key [:id]
129
+
130
+ index [:result_id]
131
+ end
132
+
133
+ create_table(:headers, :ignore_index_errors=>true) do
134
+ String :id, :size=>255, :null=>false
135
+ foreign_key :request_id, :requests, :type=>String, :size=>255
136
+ String :type, :size=>255
137
+ String :name, :size=>255
138
+ String :value, :size=>255
139
+ DateTime :created_at, :null=>false
140
+ DateTime :updated_at, :null=>false
141
+
142
+ index [:id]
143
+ index [:request_id]
144
+ end
145
+
146
+ create_table(:requests_results, :ignore_index_errors=>true) do
147
+ foreign_key :results_id, :results, :type=>String, :size=>255, :null=>false
148
+ foreign_key :requests_id, :requests, :type=>String, :size=>255, :null=>false
149
+
150
+ index [:requests_id]
151
+ index [:results_id]
152
+ end
153
+ end
154
+ end
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