inferno_core 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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