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
@@ -7,13 +7,13 @@ module Inferno
7
7
  request = self.class::Model.create(db_params(params))
8
8
 
9
9
  request_headers = (params[:request_headers] || []).map do |header|
10
- headers_repo.create(header.merge(request_id: request.index, type: 'request'))
10
+ request.add_header(header.merge(request_id: request.index, type: 'request'))
11
11
  end
12
12
  response_headers = (params[:response_headers] || []).map do |header|
13
- headers_repo.create(header.merge(request_id: request.index, type: 'response'))
13
+ request.add_header(header.merge(request_id: request.index, type: 'response'))
14
14
  end
15
15
 
16
- headers = request_headers + response_headers
16
+ headers = (request_headers + response_headers).map { |header| headers_repo.build_entity(header.to_hash) }
17
17
 
18
18
  build_entity(
19
19
  request.to_hash
@@ -73,7 +73,8 @@ module Inferno
73
73
  end
74
74
 
75
75
  class Model < Sequel::Model(db)
76
- many_to_one :result, class: 'Inferno::Repositories::Results::Model', key: :result_id
76
+ many_to_many :result, class: 'Inferno::Repositories::Results::Model', join_table: :requests_results,
77
+ left_key: :request_id, right_key: :result_id
77
78
  one_to_many :headers, class: 'Inferno::Repositories::Headers::Model', key: :request_id
78
79
 
79
80
  def before_create
@@ -15,11 +15,43 @@ module Inferno
15
15
  messages = params.delete(:messages) || []
16
16
  requests = params.delete(:requests) || []
17
17
  super(params).tap do |result|
18
+ result_model = self.class::Model.find(id: result.id)
18
19
  messages.each { |message| messages_repo.create(message.merge(result_id: result.id)) }
19
- requests.each { |request| requests_repo.create(request.to_hash.merge(result_id: result.id)) }
20
+ requests.each do |request|
21
+ request_id =
22
+ if request.id.present?
23
+ request.id
24
+ else
25
+ requests_repo.create(request.to_hash.merge(result_id: result.id)).id
26
+ end
27
+ request_model = requests_repo.class::Model.find(id: request_id)
28
+ result_model.add_request(request_model)
29
+ end
20
30
  end
21
31
  end
22
32
 
33
+ # Get the current result for a particular test/group
34
+ # @api private
35
+ # @example
36
+ # repo.current_result_for_test_session(
37
+ # test_session_id,
38
+ # test_id: 'test_id'
39
+ # )
40
+ def current_result_for_test_session(test_session_id, **params)
41
+ self.class::Model
42
+ .where({ test_session_id: test_session_id }.merge(params))
43
+ .order(Sequel.desc(:updated_at))
44
+ .limit(1)
45
+ .all
46
+ .map! do |result_hash|
47
+ build_entity(
48
+ result_hash
49
+ .to_json_data(json_serializer_options)
50
+ .deep_symbolize_keys!
51
+ )
52
+ end.first
53
+ end
54
+
23
55
  def build_entity(params)
24
56
  runnable =
25
57
  if params[:test_id]
@@ -34,12 +66,85 @@ module Inferno
34
66
  entity_class.new(params.merge(runnable))
35
67
  end
36
68
 
69
+ def result_for_test_run(test_run_id:, **params)
70
+ result_hash =
71
+ self.class::Model
72
+ .find({ test_run_id: test_run_id }.merge(params))
73
+ &.to_hash
74
+
75
+ return nil if result_hash.nil?
76
+
77
+ build_entity(result_hash)
78
+ end
79
+
80
+ def test_run_results_after(test_run_id:, after:)
81
+ Model
82
+ .where(test_run_id: test_run_id)
83
+ .where { updated_at >= after }
84
+ .to_a
85
+ .map! do |result_hash|
86
+ build_entity(
87
+ result_hash
88
+ .to_json_data(json_serializer_options)
89
+ .deep_symbolize_keys!
90
+ )
91
+ end
92
+ end
93
+
94
+ def find_waiting_result(test_run_id:)
95
+ result_hash =
96
+ Model
97
+ .where(test_run_id: test_run_id, result: 'wait')
98
+ .where { test_id !~ nil }
99
+ .limit(1)
100
+ .to_a
101
+ .first
102
+ &.to_hash
103
+
104
+ return nil if result_hash.nil?
105
+
106
+ build_entity(result_hash)
107
+ end
108
+
109
+ # Get all of the current results for a test session
110
+ def current_results_for_test_session(test_session_id)
111
+ self.class::Model
112
+ .current_results_for_test_session(test_session_id)
113
+ .eager(:messages)
114
+ .eager(requests: proc { |requests| requests.select(*Entities::Request::SUMMARY_FIELDS) })
115
+ .all
116
+ .map! do |result_hash|
117
+ build_entity(
118
+ result_hash
119
+ .to_json_data(json_serializer_options)
120
+ .deep_symbolize_keys!
121
+ )
122
+ end
123
+ end
124
+
125
+ # Get the current results for a list of runnables
126
+ def current_results_for_test_session_and_runnables(test_session_id, runnables)
127
+ self.class::Model
128
+ .current_results_for_test_session_and_runnables(test_session_id, runnables)
129
+ .all
130
+ .map! do |result_hash|
131
+ build_entity(
132
+ result_hash
133
+ .to_json_data(json_serializer_options)
134
+ .deep_symbolize_keys!
135
+ )
136
+ end
137
+ end
138
+
139
+ def pass_waiting_result(result_id, message = nil)
140
+ update(result_id, result: 'pass', result_message: message)
141
+ end
142
+
37
143
  def json_serializer_options
38
144
  {
39
145
  include: {
40
146
  messages: {},
41
147
  requests: {
42
- include: { headers: {} },
43
148
  only: Entities::Request::SUMMARY_FIELDS
44
149
  }
45
150
  }
@@ -49,8 +154,33 @@ module Inferno
49
154
  class Model < Sequel::Model(db)
50
155
  include ValidateRunnableReference
51
156
 
157
+ def self.current_results_sql(with_runnables_filter: false)
158
+ query = <<~SQL.gsub(/\s+/, ' ').freeze
159
+ SELECT * FROM results a
160
+ WHERE test_session_id = :test_session_id
161
+ SQL
162
+ runnables_filter = <<~SQL.gsub(/\s+/, ' ').freeze
163
+ AND (test_id IN :test_ids OR test_group_id IN :test_group_ids OR test_suite_id IN :test_suite_ids)
164
+ SQL
165
+ subquery = <<~SQL.gsub(/\s+/, ' ').freeze
166
+ AND a.id IN (
167
+ SELECT id
168
+ FROM results b
169
+ WHERE (b.test_session_id = a.test_session_id AND b.test_id = a.test_id) OR
170
+ (b.test_session_id = a.test_session_id AND b.test_group_id = a.test_group_id) OR
171
+ (b.test_session_id = a.test_session_id AND b.test_suite_id = a.test_suite_id)
172
+ ORDER BY updated_at DESC
173
+ LIMIT 1
174
+ )
175
+ SQL
176
+ return "#{query} #{runnables_filter} #{subquery}" if with_runnables_filter
177
+
178
+ "#{query} #{subquery}"
179
+ end
180
+
52
181
  one_to_many :messages, class: 'Inferno::Repositories::Messages::Model', key: :result_id
53
- one_to_many :requests, class: 'Inferno::Repositories::Requests::Model', key: :result_id
182
+ many_to_many :requests, class: 'Inferno::Repositories::Requests::Model', join_table: :requests_results,
183
+ left_key: :results_id, right_key: :requests_id
54
184
  many_to_one :test_run, class: 'Inferno::Repositories::TestRuns::Model', key: :test_run_id
55
185
  many_to_one :test_session, class: 'Inferno::Repositories::TestSessions::Model', key: :test_session_id
56
186
 
@@ -66,6 +196,24 @@ module Inferno
66
196
  super
67
197
  errors.add(:result, "'#{result}' is not valid") unless Entities::Result::RESULT_OPTIONS.include?(result)
68
198
  end
199
+
200
+ def self.current_results_for_test_session(test_session_id)
201
+ fetch(current_results_sql, test_session_id: test_session_id)
202
+ end
203
+
204
+ def self.current_results_for_test_session_and_runnables(test_session_id, runnables)
205
+ test_ids = runnables.select { |runnable| runnable < Entities::Test }.map!(&:id)
206
+ test_group_ids = runnables.select { |runnable| runnable < Entities::TestGroup }.map!(&:id)
207
+ test_suite_ids = runnables.select { |runnable| runnable < Entities::TestSuite }.map!(&:id)
208
+
209
+ fetch(
210
+ current_results_sql(with_runnables_filter: true),
211
+ test_session_id: test_session_id,
212
+ test_ids: test_ids,
213
+ test_group_ids: test_group_ids,
214
+ test_suite_ids: test_suite_ids
215
+ )
216
+ end
69
217
  end
70
218
  end
71
219
  end
@@ -0,0 +1,47 @@
1
+ module Inferno
2
+ module Repositories
3
+ class SessionData < Repository
4
+ def save(params)
5
+ name = params[:name].to_s.downcase
6
+ test_session_id = params[:test_session_id]
7
+ db
8
+ .insert_conflict(
9
+ target: :id,
10
+ update: { value: params[:value] }
11
+ ).insert(
12
+ id: "#{test_session_id}_#{name}",
13
+ name: name,
14
+ value: params[:value],
15
+ test_session_id: test_session_id
16
+ )
17
+ end
18
+
19
+ def load(test_session_id:, name:)
20
+ self.class::Model
21
+ .find(test_session_id: test_session_id, name: name.to_s.downcase)
22
+ &.value
23
+ end
24
+
25
+ def get_all_from_session(test_session_id)
26
+ self.class::Model
27
+ .where(test_session_id: test_session_id)
28
+ .all
29
+ .map! do |session_data_hash|
30
+ build_entity(
31
+ session_data_hash
32
+ .to_json_data
33
+ .deep_symbolize_keys!
34
+ )
35
+ end
36
+ end
37
+
38
+ def entity_class_name
39
+ 'SessionData'
40
+ end
41
+
42
+ class Model < Sequel::Model(db)
43
+ many_to_one :test_session, class: 'Inferno::Repositories::TestSessions::Model', key: :test_session_id
44
+ end
45
+ end
46
+ end
47
+ end
@@ -24,6 +24,65 @@ module Inferno
24
24
  .map! { |result| results_repo.build_entity(result) }
25
25
  end
26
26
 
27
+ def find_latest_waiting_by_identifier(identifier)
28
+ test_run_hash =
29
+ self.class::Model
30
+ .where(status: 'waiting')
31
+ .where(identifier: identifier)
32
+ .where { wait_timeout >= Time.now }
33
+ .order(Sequel.desc(:updated_at))
34
+ .limit(1)
35
+ .to_a
36
+ &.first
37
+ &.to_hash
38
+
39
+ return nil if test_run_hash.nil?
40
+
41
+ build_entity(test_run_hash)
42
+ end
43
+
44
+ def last_test_run(test_session_id)
45
+ test_run_hash =
46
+ self.class::Model
47
+ .where(test_session_id: test_session_id)
48
+ .order(Sequel.desc(:updated_at))
49
+ .limit(1)
50
+ .to_a
51
+ .map { |record| record.to_json_data(json_serializer_options).deep_symbolize_keys! }
52
+ &.first
53
+ &.to_hash
54
+
55
+ return nil if test_run_hash.nil?
56
+
57
+ build_entity(test_run_hash)
58
+ end
59
+
60
+ def mark_as_running(test_run_id)
61
+ update(test_run_id, status: 'running')
62
+ end
63
+
64
+ def mark_as_done(test_run_id)
65
+ update(test_run_id, status: 'done')
66
+ end
67
+
68
+ def mark_as_waiting(test_run_id, identifier, timeout)
69
+ update(
70
+ test_run_id,
71
+ status: 'waiting',
72
+ identifier: identifier,
73
+ wait_timeout: Time.now + timeout.seconds
74
+ )
75
+ end
76
+
77
+ def mark_as_no_longer_waiting(test_run_id)
78
+ update(
79
+ test_run_id,
80
+ status: 'queued',
81
+ identifier: nil,
82
+ wait_timeout: nil
83
+ )
84
+ end
85
+
27
86
  class Model < Sequel::Model(db)
28
87
  include ValidateRunnableReference
29
88
 
@@ -33,6 +92,13 @@ module Inferno
33
92
  key: :test_run_id
34
93
  many_to_one :test_session, class: 'Inferno::Repositories::TestSessions::Model', key: :test_session_id
35
94
 
95
+ def validate
96
+ super
97
+ if status.present? && !Entities::TestRun::STATUS_OPTIONS.include?(status) # rubocop:disable Style/GuardClause
98
+ errors.add(:status, "'#{status}' is not valid")
99
+ end
100
+ end
101
+
36
102
  def before_create
37
103
  self.id = SecureRandom.uuid
38
104
  time = Time.now
@@ -1,73 +1,165 @@
1
1
  module Inferno
2
2
  # @api private
3
3
  class TestRunner
4
- attr_reader :test_session, :test_run
4
+ attr_reader :test_session, :test_run, :resuming
5
5
 
6
- def initialize(test_session:, test_run:)
6
+ def initialize(test_session:, test_run:, resume: false)
7
7
  @test_session = test_session
8
8
  @test_run = test_run
9
+ @resuming = resume
10
+ end
11
+
12
+ def run_results
13
+ @run_results ||= {}
9
14
  end
10
15
 
11
16
  def results_repo
12
17
  @results_repo ||= Repositories::Results.new
13
18
  end
14
19
 
15
- def run(runnable, inputs = {}, outputs = {})
20
+ def test_runs_repo
21
+ @test_runs_repo ||= Repositories::TestRuns.new
22
+ end
23
+
24
+ def session_data_repo
25
+ @session_data_repo ||= Repositories::SessionData.new
26
+ end
27
+
28
+ def start
29
+ test_runs_repo.mark_as_running(test_run.id)
30
+
31
+ run(test_run.runnable)
32
+
33
+ test_runs_repo.mark_as_done(test_run.id) unless run_results.values.any?(&:waiting?)
34
+
35
+ run_results.values
36
+ end
37
+
38
+ def run(runnable)
16
39
  if runnable < Entities::Test
17
- run_test(runnable, inputs, outputs)
40
+ return existing_test_result(runnable) || run_test(runnable) if resuming
41
+
42
+ run_test(runnable)
18
43
  else
19
- run_group(runnable, inputs, outputs)
44
+ run_group(runnable)
20
45
  end
21
46
  end
22
47
 
23
- def run_test(runnable, inputs = {}, outputs = {})
24
- test_instance = runnable.new(inputs: inputs.merge(outputs), test_session_id: test_session.id)
48
+ def existing_test_result(runnable)
49
+ results_repo.result_for_test_run(runnable.reference_hash.merge(test_run_id: test_run.id))
50
+ end
51
+
52
+ def run_test(test)
53
+ inputs = load_inputs(test)
54
+
55
+ input_json_string = JSON.generate(inputs)
56
+ test_instance = test.new(inputs: inputs, test_session_id: test_session.id)
25
57
 
26
58
  result = begin
27
- inputs.merge(outputs).each do |key, value|
28
- test_instance.instance_variable_set("@#{key}", value)
29
- end
30
59
  test_instance.load_named_requests
31
- test_instance.instance_eval(&runnable.block)
60
+ test_instance.instance_eval(&test.block)
32
61
  'pass'
33
62
  rescue Exceptions::TestResultException => e
34
63
  test_instance.result_message = e.message
35
64
  e.result
36
65
  rescue StandardError => e
66
+ Application['logger'].error(e.full_message)
37
67
  test_instance.result_message = "Error: #{e.message}"
38
68
  'error'
39
69
  end
40
70
 
41
- runnable.outputs.each do |output|
42
- outputs[output] = test_instance.send(output)
71
+ outputs = save_outputs(test_instance)
72
+ output_json_string = JSON.generate(outputs)
73
+
74
+ if result == 'wait'
75
+ test_runs_repo.mark_as_waiting(test_run.id, test_instance.identifier, test_instance.wait_timeout)
43
76
  end
44
77
 
45
- [persist_result(
78
+ test_result = persist_result(
46
79
  {
47
- test_session_id: test_session.id,
48
- test_run_id: test_run.id,
49
80
  messages: test_instance.messages,
50
81
  requests: test_instance.requests,
51
82
  result: result,
52
- result_message: test_instance.result_message
53
- }.merge(runnable.reference_hash)
54
- )]
83
+ result_message: test_instance.result_message,
84
+ input_json: input_json_string,
85
+ output_json: output_json_string
86
+ }.merge(test.reference_hash)
87
+ )
88
+
89
+ # If running a single test, update its parents' results. If running a
90
+ # group or suite, #run_group handles updating the parents.
91
+ return test_result if test_run.test_id.blank?
92
+
93
+ update_parent_result(test.parent)
94
+
95
+ test_result
55
96
  end
56
97
 
57
- def run_group(runnable, inputs = {}, outputs = {})
58
- results = runnable.children.flat_map { |child| run(child, inputs, outputs) }
98
+ def run_group(group)
99
+ results = []
100
+ group.children.each do |child|
101
+ result = run(child)
102
+ results << result
103
+ break if results.last.waiting?
104
+ end
59
105
 
60
- results << persist_result(
61
- {
62
- test_session_id: test_session.id,
63
- test_run_id: test_run.id,
64
- result: roll_up_result(results)
65
- }.merge(runnable.reference_hash)
66
- )
106
+ results.flatten!
107
+
108
+ group_result = persist_result(group.reference_hash.merge(result: roll_up_result(results)))
109
+
110
+ update_parent_result(group.parent)
111
+
112
+ group_result
113
+ end
114
+
115
+ def update_parent_result(parent)
116
+ return if parent.nil?
117
+
118
+ children = parent.children
119
+ child_results = results_repo.current_results_for_test_session_and_runnables(test_session.id, children)
120
+ return if children.length != child_results.length
121
+
122
+ old_result = results_repo.current_result_for_test_session(test_session.id, parent.reference_hash)&.result
123
+ new_result = roll_up_result(child_results)
124
+
125
+ if new_result != old_result
126
+ persist_result(parent.reference_hash.merge(result: new_result))
127
+
128
+ update_parent_result(parent.parent)
129
+ end
130
+
131
+ new_result
132
+ end
133
+
134
+ def load_inputs(runnable)
135
+ runnable.inputs.each_with_object({}) do |input, input_hash|
136
+ name = input[:name]
137
+ input_hash[name] = session_data_repo.load(test_session_id: test_session.id, name: name)
138
+ end
139
+ end
140
+
141
+ def save_outputs(runnable_instance)
142
+ outputs =
143
+ runnable_instance.outputs.map do |output_name|
144
+ {
145
+ name: output_name,
146
+ value: runnable_instance.send(output_name)
147
+ }
148
+ end
149
+ outputs.compact!
150
+ outputs.each do |output|
151
+ session_data_repo.save(output.merge(test_session_id: test_session.id))
152
+ end
153
+
154
+ outputs
67
155
  end
68
156
 
69
157
  def persist_result(params)
70
- results_repo.create(params)
158
+ result = results_repo.create(
159
+ params.merge(test_run_id: test_run.id, test_session_id: test_session.id)
160
+ )
161
+
162
+ run_results[result.runnable.id] = result
71
163
  end
72
164
 
73
165
  def roll_up_result(results)