inferno_core 0.0.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 (86) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/bin/inferno-console +8 -0
  4. data/lib/inferno.rb +15 -0
  5. data/lib/inferno/apps/web/application.rb +12 -0
  6. data/lib/inferno/apps/web/controllers/controller.rb +34 -0
  7. data/lib/inferno/apps/web/controllers/requests/show.rb +16 -0
  8. data/lib/inferno/apps/web/controllers/test_runs/create.rb +39 -0
  9. data/lib/inferno/apps/web/controllers/test_runs/results/index.rb +18 -0
  10. data/lib/inferno/apps/web/controllers/test_runs/show.rb +16 -0
  11. data/lib/inferno/apps/web/controllers/test_sessions/create.rb +26 -0
  12. data/lib/inferno/apps/web/controllers/test_sessions/results/index.rb +17 -0
  13. data/lib/inferno/apps/web/controllers/test_sessions/show.rb +16 -0
  14. data/lib/inferno/apps/web/controllers/test_suites/index.rb +13 -0
  15. data/lib/inferno/apps/web/controllers/test_suites/show.rb +16 -0
  16. data/lib/inferno/apps/web/router.rb +27 -0
  17. data/lib/inferno/apps/web/serializers/header.rb +12 -0
  18. data/lib/inferno/apps/web/serializers/input.rb +16 -0
  19. data/lib/inferno/apps/web/serializers/message.rb +10 -0
  20. data/lib/inferno/apps/web/serializers/request.rb +26 -0
  21. data/lib/inferno/apps/web/serializers/result.rb +21 -0
  22. data/lib/inferno/apps/web/serializers/serializer.rb +16 -0
  23. data/lib/inferno/apps/web/serializers/test.rb +17 -0
  24. data/lib/inferno/apps/web/serializers/test_group.rb +23 -0
  25. data/lib/inferno/apps/web/serializers/test_run.rb +19 -0
  26. data/lib/inferno/apps/web/serializers/test_session.rb +17 -0
  27. data/lib/inferno/apps/web/serializers/test_suite.rb +18 -0
  28. data/lib/inferno/config/application.rb +22 -0
  29. data/lib/inferno/config/boot.rb +5 -0
  30. data/lib/inferno/config/boot/db.rb +23 -0
  31. data/lib/inferno/config/boot/logging.rb +16 -0
  32. data/lib/inferno/config/boot/suites.rb +27 -0
  33. data/lib/inferno/config/boot/web.rb +17 -0
  34. data/lib/inferno/db/migrations/001_create_initial_structure.rb +165 -0
  35. data/lib/inferno/dsl.rb +35 -0
  36. data/lib/inferno/dsl/assertions.rb +93 -0
  37. data/lib/inferno/dsl/fhir_client.rb +152 -0
  38. data/lib/inferno/dsl/fhir_client_builder.rb +56 -0
  39. data/lib/inferno/dsl/fhir_manipulation.rb +25 -0
  40. data/lib/inferno/dsl/fhir_validation.rb +111 -0
  41. data/lib/inferno/dsl/http_client.rb +122 -0
  42. data/lib/inferno/dsl/http_client_builder.rb +54 -0
  43. data/lib/inferno/dsl/request_storage.rb +101 -0
  44. data/lib/inferno/dsl/results.rb +54 -0
  45. data/lib/inferno/dsl/runnable.rb +250 -0
  46. data/lib/inferno/entities.rb +19 -0
  47. data/lib/inferno/entities/attributes.rb +9 -0
  48. data/lib/inferno/entities/entity.rb +15 -0
  49. data/lib/inferno/entities/header.rb +42 -0
  50. data/lib/inferno/entities/message.rb +22 -0
  51. data/lib/inferno/entities/request.rb +166 -0
  52. data/lib/inferno/entities/result.rb +49 -0
  53. data/lib/inferno/entities/test.rb +173 -0
  54. data/lib/inferno/entities/test_group.rb +87 -0
  55. data/lib/inferno/entities/test_input.rb +20 -0
  56. data/lib/inferno/entities/test_run.rb +63 -0
  57. data/lib/inferno/entities/test_session.rb +26 -0
  58. data/lib/inferno/entities/test_suite.rb +73 -0
  59. data/lib/inferno/exceptions.rb +42 -0
  60. data/lib/inferno/public/217.bundle.js +1 -0
  61. data/lib/inferno/public/assets.json +6 -0
  62. data/lib/inferno/public/bundle.js +2 -0
  63. data/lib/inferno/public/bundle.js.LICENSE.txt +65 -0
  64. data/lib/inferno/public/e09b16f5cb645eb05f90c8f38f3409fb.png +0 -0
  65. data/lib/inferno/public/favicon.ico +0 -0
  66. data/lib/inferno/public/logo192.png +0 -0
  67. data/lib/inferno/public/logo512.png +0 -0
  68. data/lib/inferno/repositories.rb +27 -0
  69. data/lib/inferno/repositories/headers.rb +22 -0
  70. data/lib/inferno/repositories/in_memory_repository.rb +41 -0
  71. data/lib/inferno/repositories/messages.rb +35 -0
  72. data/lib/inferno/repositories/repository.rb +106 -0
  73. data/lib/inferno/repositories/requests.rb +89 -0
  74. data/lib/inferno/repositories/results.rb +72 -0
  75. data/lib/inferno/repositories/test_groups.rb +9 -0
  76. data/lib/inferno/repositories/test_runs.rb +46 -0
  77. data/lib/inferno/repositories/test_sessions.rb +56 -0
  78. data/lib/inferno/repositories/test_suites.rb +9 -0
  79. data/lib/inferno/repositories/tests.rb +9 -0
  80. data/lib/inferno/repositories/validate_runnable_reference.rb +42 -0
  81. data/lib/inferno/spec_support.rb +9 -0
  82. data/lib/inferno/test_runner.rb +81 -0
  83. data/lib/inferno/utils/middleware/request_logger.rb +55 -0
  84. data/lib/inferno/version.rb +3 -0
  85. data/spec/support/factory_bot.rb +21 -0
  86. metadata +514 -0
@@ -0,0 +1,122 @@
1
+ require_relative 'request_storage'
2
+
3
+ module Inferno
4
+ module DSL
5
+ # This module contains the HTTP DSL available to test writers.
6
+ #
7
+ # @example
8
+ # class MyTestGroup < Inferno::TestGroup
9
+ # # create a "default" client for a group
10
+ # http_client do
11
+ # url 'https://example.com/'
12
+ # end
13
+ #
14
+ # test :some_test do
15
+ # run do
16
+ # # performs a GET to https://example.com
17
+ # get
18
+ # # performs a GET to https://example.com/abc
19
+ # get('abc')
20
+ #
21
+ # request # the most recent request
22
+ # response # the most recent response
23
+ # requests # all of the requests which have been made in this test
24
+ # end
25
+ # end
26
+ # end
27
+ # @see Inferno::FHIRClientBuilder Documentation for the client
28
+ # configuration DSL
29
+ module HTTPClient
30
+ # @api private
31
+ def self.included(klass)
32
+ klass.extend ClassMethods
33
+ klass.include RequestStorage
34
+ end
35
+
36
+ # Return a previously defined HTTP client
37
+ #
38
+ # @param client [Symbol] the name of the client
39
+ # @return [Faraday::Connection]
40
+ # @see Inferno::HTTPClientBuilder
41
+ def http_client(client = :default)
42
+ return http_clients[client] if http_clients[client]
43
+
44
+ definition = self.class.http_client_definitions[client]
45
+ return nil if definition.nil?
46
+
47
+ http_clients[client] = HTTPClientBuilder.new.build(self, definition)
48
+ end
49
+
50
+ # @api private
51
+ def http_clients
52
+ @http_clients ||= {}
53
+ end
54
+
55
+ # Perform an HTTP GET request
56
+ #
57
+ # @param url [String] if this request is using a defined client, this will
58
+ # be appended to the client's url. Must be an absolute url for requests
59
+ # made without a defined client
60
+ # @param client [Symbol]
61
+ # @param name [Symbol] Name for this request to allow it to be used by
62
+ # other tests
63
+ # @param _options [Hash] TODO
64
+ # @return [Inferno::Entities::Request]
65
+ def get(url = '', client: :default, name: nil, **_options)
66
+ store_request('outgoing', name) do
67
+ client = http_client(client)
68
+
69
+ if client
70
+ client.get(url)
71
+ elsif url.match?(%r{\Ahttps?://})
72
+ Faraday.get(url)
73
+ else
74
+ raise StandardError, 'Must use an absolute url or define an HTTP client with a base url'
75
+ end
76
+ end
77
+ end
78
+
79
+ # Perform an HTTP POST request
80
+ #
81
+ # @param url [String] if this request is using a defined client, this will
82
+ # be appended to the client's url. Must be an absolute url for requests
83
+ # made without a defined client
84
+ # @param body [String]
85
+ # @param client [Symbol]
86
+ # @param name [Symbol] Name for this request to allow it to be used by
87
+ # other tests
88
+ # @param _options [Hash] TODO
89
+ # @return [Inferno::Entities::Request]
90
+ def post(url = '', body: nil, client: :default, name: nil, **_options)
91
+ store_request('outgoing', name) do
92
+ client = http_client(client)
93
+
94
+ if client
95
+ client.post(url, body)
96
+ elsif url.match?(%r{\Ahttps?://})
97
+ Faraday.post(url, body)
98
+ else
99
+ raise StandardError, 'Must use an absolute url or define an HTTP client with a base url'
100
+ end
101
+ end
102
+ end
103
+
104
+ module ClassMethods
105
+ # @api private
106
+ def http_client_definitions
107
+ @http_client_definitions ||= {}
108
+ end
109
+
110
+ # Define a HTTP client to be used by a Runnable.
111
+ #
112
+ # @param name [Symbol] a name used to reference this particular client
113
+ # @param block a block to configure the client
114
+ # @see Inferno::HTTPClientBuilder Documentation for the client
115
+ # configuration DSL
116
+ def http_client(name = :default, &block)
117
+ http_client_definitions[name] = block
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,54 @@
1
+ module Inferno
2
+ module DSL
3
+ # This module contains the HTTP DSL available to test writers.
4
+ class HTTPClientBuilder
5
+ attr_accessor :runnable
6
+
7
+ # @api private
8
+ def build(runnable, block)
9
+ self.runnable = runnable
10
+ instance_exec(self, &block)
11
+
12
+ params = { url: url }
13
+ params.merge!(headers: headers) if headers
14
+
15
+ Faraday.new(params)
16
+ end
17
+
18
+ # Define the base url for an HTTP client. A string or symbol can be
19
+ # provided. A string is interpreted as a url. A symbol is interpreted as
20
+ # the name of an input to the Runnable.
21
+ #
22
+ # @param url [String, Symbol]
23
+ # @return [void]
24
+ def url(url = nil)
25
+ @url ||=
26
+ if url.is_a? Symbol
27
+ runnable.send(url)
28
+ else
29
+ url
30
+ end
31
+ end
32
+
33
+ # Define custom headers for a client
34
+ #
35
+ # @param headers [Hash]
36
+ # @return [void]
37
+ def headers(headers = nil)
38
+ @headers ||= headers
39
+ end
40
+
41
+ # @api private
42
+ def method_missing(name, *args, &block)
43
+ return runnable.call(name, *args, &block) if runnable.respond_to? name
44
+
45
+ super
46
+ end
47
+
48
+ # @api private
49
+ def respond_to_missing?(name)
50
+ runnable.respond_to?(name) || super
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,101 @@
1
+ module Inferno
2
+ module DSL
3
+ # This module handles storing and retrieving requests/responses made by
4
+ # FHIR/HTTP clients
5
+ module RequestStorage
6
+ def self.included(klass)
7
+ klass.extend ClassMethods
8
+ end
9
+
10
+ # Returns the FHIR/HTTP requests that have been made in this `Test`
11
+ #
12
+ # @return [Array<Inferno::Entities::Request>]
13
+ def requests
14
+ @requests ||= []
15
+ end
16
+
17
+ # Returns the most recent FHIR/HTTP request
18
+ #
19
+ # @return [Inferno::Entities::Request]
20
+ def request
21
+ requests.last
22
+ end
23
+
24
+ # Returns the response from the most recent FHIR/HTTP request
25
+ #
26
+ # @return [Hash, nil]
27
+ def response
28
+ request&.response
29
+ end
30
+
31
+ # Returns the FHIR resource from the response to the most recent FHIR
32
+ # request
33
+ #
34
+ # @return [FHIR::Model, nil]
35
+ def resource
36
+ request&.resource
37
+ end
38
+
39
+ # TODO: do a check in the test runner
40
+ def named_request(name)
41
+ requests.find { |request| request.name == name.to_sym }
42
+ end
43
+
44
+ # @api private
45
+ def store_request(direction, name = nil, &block)
46
+ response = block.call
47
+
48
+ request =
49
+ if response.is_a? FHIR::ClientReply
50
+ Entities::Request.from_fhir_client_reply(
51
+ response, direction: direction, name: name, test_session_id: test_session_id
52
+ )
53
+ else
54
+ Entities::Request.from_http_response(
55
+ response, direction: direction, name: name, test_session_id: test_session_id
56
+ )
57
+ end
58
+
59
+ requests << request
60
+ request
61
+ end
62
+
63
+ # @api private
64
+ def load_named_requests
65
+ requests_repo = Inferno::Repositories::Requests.new
66
+ 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?
69
+
70
+ requests << request
71
+ end
72
+ end
73
+
74
+ module ClassMethods
75
+ # @api private
76
+ def named_requests_made
77
+ @named_requests_made ||= []
78
+ end
79
+
80
+ # @api private
81
+ def named_requests_used
82
+ @named_requests_used ||= []
83
+ end
84
+
85
+ # Specify the named requests made by a test
86
+ #
87
+ # @param *names [Symbol] one or more Symbols
88
+ def makes_request(*names)
89
+ named_requests_made.concat(names)
90
+ end
91
+
92
+ # Specify the named requests used by a test
93
+ #
94
+ # @param *names [Symbol] one or more Symbols
95
+ def uses_request(*names)
96
+ named_requests_used.concat(names)
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,54 @@
1
+ module Inferno
2
+ module DSL
3
+ # This module contains methods to set test results.
4
+ module Results
5
+ # Halt execution of the current test and mark it as passed.
6
+ #
7
+ # @param message [String]
8
+ def pass(message = '')
9
+ raise Exceptions::PassException, message
10
+ end
11
+
12
+ # Halt execution of the current test and mark it as passed if a condition
13
+ # is true.
14
+ #
15
+ # @param test [Boolean]
16
+ # @param message [String]
17
+ def pass_if(test, message = '')
18
+ raise Exceptions::PassException, message if test
19
+ end
20
+
21
+ # Halt execution of the current test and mark it as skipped.
22
+ #
23
+ # @param message [String]
24
+ def skip(message = '')
25
+ raise Exceptions::SkipException, message
26
+ end
27
+
28
+ # Halt execution of the current test and mark it as skipped if a condition
29
+ # is true.
30
+ #
31
+ # @param test [Boolean]
32
+ # @param message [String]
33
+ def skip_if(test, message = '')
34
+ raise Exceptions::SkipException, message if test
35
+ end
36
+
37
+ # Halt execution of the current test and mark it as omitted.
38
+ #
39
+ # @param message [String]
40
+ def omit(message = '')
41
+ raise Exceptions::OmitException, message
42
+ end
43
+
44
+ # Halt execution of the current test and mark it as omitted if a condition
45
+ # is true.
46
+ #
47
+ # @param test [Boolean]
48
+ # @param message [String]
49
+ def omit_if(test, message = '')
50
+ raise Exceptions::OmitException, message if test
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,250 @@
1
+ module Inferno
2
+ module DSL
3
+ # This module contains the DSL for defining child entities in the test
4
+ # definition framework.
5
+ module Runnable
6
+ attr_accessor :parent
7
+
8
+ # When a class (e.g. TestSuite/TestGroup) uses this module, set it up
9
+ # so that subclassing it works correctly.
10
+ # - add the subclass to the relevant repository when it is created
11
+ # - copy the class instance variables from the superclass
12
+ # - add a hook to the subclass so that its subclasses do the same
13
+ # @api private
14
+ def self.extended(extending_class)
15
+ super
16
+
17
+ extending_class.define_singleton_method(:inherited) do |subclass|
18
+ copy_instance_variables(subclass)
19
+
20
+ # Whenever the definition of a Runnable class ends, keep track of the
21
+ # file it came from. Once the Suite loader successfully loads a file,
22
+ # it will add all of the Runnable classes from that file to the
23
+ # appropriate repositories.
24
+ TracePoint.trace(:end) do |trace|
25
+ if trace.self == subclass
26
+ subclass.add_self_to_repository
27
+ trace.disable
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ # Class instance variables are used to hold the metadata for Runnable
34
+ # classes. When inheriting from a Runnable class, these class instance
35
+ # variables need to be copied. Any child Runnable classes will themselves
36
+ # need to be subclassed so that their parent can be updated.
37
+ # @api private
38
+ def copy_instance_variables(subclass)
39
+ instance_variables.each do |variable|
40
+ next if [:@id, :@groups, :@tests, :@parent, :@children].include?(variable)
41
+
42
+ subclass.instance_variable_set(variable, instance_variable_get(variable).dup)
43
+ end
44
+
45
+ child_types.each do |child_type|
46
+ new_children = send(child_type).map do |child|
47
+ Class.new(child).tap do |subclass_child|
48
+ subclass_child.parent = subclass
49
+ end
50
+ end
51
+
52
+ subclass.instance_variable_set(:"@#{child_type}", new_children)
53
+ subclass.children.concat(new_children)
54
+ end
55
+ end
56
+
57
+ # @api private
58
+ def add_self_to_repository
59
+ repository.insert(self)
60
+ end
61
+
62
+ # An instance of the repository for the class using this module
63
+ def repository
64
+ nil
65
+ end
66
+
67
+ # This method defines a child entity. Classes using this module should
68
+ # alias the method name they wish to use to define child entities to this
69
+ # method.
70
+ # @api private
71
+ def define_child(*args, &block)
72
+ hash_args = process_args(args)
73
+
74
+ klass = create_child_class(hash_args)
75
+
76
+ klass.parent = self
77
+
78
+ child_metadata[:collection] << klass
79
+ children << klass
80
+
81
+ configure_child_class(klass, hash_args)
82
+
83
+ handle_child_definition_block(klass, &block)
84
+
85
+ klass.add_self_to_repository
86
+
87
+ klass
88
+ end
89
+
90
+ # @api private
91
+ def process_args(args)
92
+ hash_args =
93
+ if args[0].is_a? Hash
94
+ args[0]
95
+ elsif args[1].is_a? Hash
96
+ args[1]
97
+ else
98
+ {}
99
+ end
100
+
101
+ hash_args[:title] = args[0] if args[0].is_a? String
102
+
103
+ hash_args
104
+ end
105
+
106
+ # @api private
107
+ def child_metadata(metadata = nil)
108
+ @child_metadata = metadata if metadata
109
+ @child_metadata
110
+ end
111
+
112
+ # @api private
113
+ def create_child_class(hash_args)
114
+ superclass_id = hash_args.delete :from
115
+
116
+ return Class.new(child_metadata[:class]) if superclass_id.blank?
117
+
118
+ superclass = child_metadata[:repo].find(superclass_id)
119
+
120
+ raise Exceptions::ParentNotLoadedException.new(child_metadata[:class], superclass_id) unless superclass
121
+
122
+ Class.new(superclass)
123
+ end
124
+
125
+ # @api private
126
+ 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
129
+
130
+ klass.input input_definition
131
+ end
132
+
133
+ outputs.each do |output_definition|
134
+ next if klass.outputs.include? output_definition
135
+
136
+ klass.output output_definition
137
+ end
138
+
139
+ new_fhir_client_definitions = klass.instance_variable_get(:@fhir_client_definitions) || {}
140
+ fhir_client_definitions.each do |name, definition|
141
+ next if new_fhir_client_definitions.include? name
142
+
143
+ new_fhir_client_definitions[name] = definition.dup
144
+ end
145
+ klass.instance_variable_set(:@fhir_client_definitions, new_fhir_client_definitions)
146
+
147
+ new_http_client_definitions = klass.instance_variable_get(:@http_client_definitions) || {}
148
+ http_client_definitions.each do |name, definition|
149
+ next if new_http_client_definitions.include? name
150
+
151
+ new_http_client_definitions[name] = definition.dup
152
+ end
153
+ klass.instance_variable_set(:@http_client_definitions, new_http_client_definitions)
154
+
155
+ hash_args.each do |key, value|
156
+ klass.send(key, *value)
157
+ end
158
+
159
+ klass.children.each do |child_class|
160
+ klass.configure_child_class(child_class, {})
161
+ child_class.add_self_to_repository
162
+ end
163
+ end
164
+
165
+ # @api private
166
+ def handle_child_definition_block(klass, &block)
167
+ klass.class_eval(&block) if block_given?
168
+ end
169
+
170
+ def id(new_id = nil)
171
+ return @id if new_id.nil? && @id.present?
172
+
173
+ prefix =
174
+ if parent
175
+ "#{parent.id}-"
176
+ else
177
+ ''
178
+ end
179
+
180
+ @base_id = new_id || @base_id || default_id
181
+
182
+ @id = "#{prefix}#{@base_id}"
183
+ end
184
+
185
+ def title(new_title = nil)
186
+ return @title if new_title.nil?
187
+
188
+ @title = new_title
189
+ end
190
+
191
+ def description(new_description = nil)
192
+ return @description if new_description.nil?
193
+
194
+ @description = new_description
195
+ end
196
+
197
+ # Define inputs
198
+ #
199
+ # @param inputs [Symbol]
200
+ # @return [void]
201
+ # @example
202
+ # input :patient_id, :bearer_token
203
+ def input(*input_definitions)
204
+ inputs.concat(input_definitions)
205
+ end
206
+
207
+ # Define outputs
208
+ #
209
+ # @param output_definitions [Symbol]
210
+ # @return [void]
211
+ # @example
212
+ # output :patient_id, :bearer_token
213
+ def output(*output_definitions)
214
+ outputs.concat(output_definitions)
215
+ end
216
+
217
+ # @api private
218
+ def default_id
219
+ to_s
220
+ end
221
+
222
+ # @api private
223
+ def inputs
224
+ @inputs ||= []
225
+ end
226
+
227
+ # @api private
228
+ def outputs
229
+ @outputs ||= []
230
+ end
231
+
232
+ def child_types
233
+ return [] if ancestors.include? Inferno::Entities::Test
234
+ return [:groups] if ancestors.include? Inferno::Entities::TestSuite
235
+
236
+ [:groups, :tests]
237
+ end
238
+
239
+ def children
240
+ @children ||= []
241
+ end
242
+
243
+ def validator_url(url = nil)
244
+ return @validator_url ||= parent&.validator_url if url.nil?
245
+
246
+ @validator_url = url
247
+ end
248
+ end
249
+ end
250
+ end