inferno_core 0.0.5 → 0.0.8.pre2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/inferno +7 -0
- data/lib/inferno/apps/cli/console.rb +12 -0
- data/lib/inferno/apps/cli/main.rb +18 -0
- data/lib/inferno/apps/cli/migration.rb +14 -0
- data/lib/inferno/apps/cli.rb +8 -0
- data/lib/inferno/apps/web/controllers/test_runs/create.rb +15 -2
- data/lib/inferno/apps/web/index.html.erb +1 -0
- data/lib/inferno/apps/web/serializers/hash_value_extractor.rb +11 -0
- data/lib/inferno/apps/web/serializers/test.rb +3 -4
- data/lib/inferno/apps/web/serializers/test_group.rb +4 -6
- data/lib/inferno/config/application.rb +4 -0
- data/lib/inferno/config/boot/db.rb +1 -9
- data/lib/inferno/config/boot/logging.rb +2 -0
- data/lib/inferno/config/boot/suites.rb +4 -6
- data/lib/inferno/db/migrations/001_create_initial_structure.rb +1 -1
- data/lib/inferno/db/schema.rb +1 -1
- data/lib/inferno/dsl/assertions.rb +85 -0
- data/lib/inferno/dsl/configurable.rb +126 -0
- data/lib/inferno/dsl/fhir_client.rb +22 -16
- data/lib/inferno/dsl/fhir_client_builder.rb +3 -3
- data/lib/inferno/dsl/fhir_validation.rb +105 -1
- data/lib/inferno/dsl/http_client.rb +14 -12
- data/lib/inferno/dsl/http_client_builder.rb +3 -3
- data/lib/inferno/dsl/request_storage.rb +26 -17
- data/lib/inferno/dsl/results.rb +1 -1
- data/lib/inferno/dsl/resume_test_route.rb +10 -10
- data/lib/inferno/dsl/runnable.rb +104 -38
- data/lib/inferno/entities/header.rb +14 -7
- data/lib/inferno/entities/message.rb +16 -8
- data/lib/inferno/entities/request.rb +34 -21
- data/lib/inferno/entities/result.rb +36 -29
- data/lib/inferno/entities/session_data.rb +12 -6
- data/lib/inferno/entities/test.rb +29 -2
- data/lib/inferno/entities/test_group.rb +8 -0
- data/lib/inferno/entities/test_run.rb +29 -6
- data/lib/inferno/entities/test_session.rb +16 -10
- data/lib/inferno/exceptions.rb +12 -0
- data/lib/inferno/public/217.bundle.js +1 -1
- data/lib/inferno/public/bundle.js +154 -1
- data/lib/inferno/public/bundle.js.LICENSE.txt +15 -0
- data/lib/inferno/repositories/in_memory_repository.rb +1 -1
- data/lib/inferno/repositories/results.rb +1 -1
- data/lib/inferno/repositories/test_runs.rb +15 -0
- data/lib/inferno/spec_support.rb +1 -1
- data/lib/inferno/test_runner.rb +21 -19
- data/lib/inferno/utils/markdown_formatter.rb +15 -0
- data/lib/inferno/utils/middleware/request_logger.rb +9 -3
- data/lib/inferno/utils/migration.rb +17 -0
- data/lib/inferno/version.rb +1 -1
- data/lib/inferno.rb +0 -4
- data/spec/factories/request.rb +14 -7
- metadata +46 -10
- data/bin/inferno-console +0 -8
@@ -1,38 +1,113 @@
|
|
1
1
|
module Inferno
|
2
2
|
module DSL
|
3
|
+
# This module contains the methods needed to configure a validator to
|
4
|
+
# perform validation of FHIR resources. The actual validation is performed
|
5
|
+
# by an external FHIR validation service. Tests will typically rely on
|
6
|
+
# `assert_valid_resource` for validation rather than directly calling
|
7
|
+
# methods on a validator.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
#
|
11
|
+
# validator do
|
12
|
+
# url 'http://example.com/validator'
|
13
|
+
# exclude_message { |message| message.type == 'info' }
|
14
|
+
# perform_additional_validation do |resource, profile_url|
|
15
|
+
# if something_is_wrong
|
16
|
+
# { type: 'error', message: 'something is wrong' }
|
17
|
+
# else
|
18
|
+
# { type: 'info', message: 'everything is ok' }
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
# end
|
3
22
|
module FHIRValidation
|
4
23
|
def self.included(klass)
|
5
24
|
klass.extend ClassMethods
|
6
25
|
end
|
7
26
|
|
27
|
+
# Perform validation, and add validation messages to the runnable
|
28
|
+
#
|
29
|
+
# @param resource [FHIR::Model]
|
30
|
+
# @param profile_url [String]
|
31
|
+
# @param validator [Symbol] the name of the validator to use
|
32
|
+
# @return [Boolean] whether the resource is valid
|
8
33
|
def resource_is_valid?(resource: self.resource, profile_url: nil, validator: :default)
|
9
34
|
find_validator(validator).resource_is_valid?(resource, profile_url, self)
|
10
35
|
end
|
11
36
|
|
37
|
+
# Find a particular validator. Looks through a runnable's parents up to
|
38
|
+
# the suite to find a validator with a particular name
|
12
39
|
def find_validator(validator_name)
|
13
40
|
self.class.find_validator(validator_name)
|
14
41
|
end
|
15
42
|
|
16
43
|
class Validator
|
44
|
+
# @private
|
17
45
|
def initialize(&block)
|
18
46
|
instance_eval(&block)
|
19
47
|
end
|
20
48
|
|
49
|
+
# @private
|
21
50
|
def default_validator_url
|
22
51
|
ENV.fetch('VALIDATOR_URL')
|
23
52
|
end
|
24
53
|
|
54
|
+
# Set the url of the validator service
|
55
|
+
#
|
56
|
+
# @param url [String]
|
25
57
|
def url(validator_url = nil)
|
26
58
|
@url = validator_url if validator_url
|
27
59
|
|
28
60
|
@url
|
29
61
|
end
|
30
62
|
|
63
|
+
# @private
|
64
|
+
def additional_validations
|
65
|
+
@additional_validations ||= []
|
66
|
+
end
|
67
|
+
|
68
|
+
# Perform validation steps in addition to FHIR validation.
|
69
|
+
#
|
70
|
+
# @example
|
71
|
+
# perform_additional_validation do |resource, profile_url|
|
72
|
+
# if something_is_wrong
|
73
|
+
# { type: 'error', message: 'something is wrong' }
|
74
|
+
# else
|
75
|
+
# { type: 'info', message: 'everything is ok' }
|
76
|
+
# end
|
77
|
+
# end
|
78
|
+
# @yieldparam resource [FHIR::Model] the resource being validated
|
79
|
+
# @yieldparam profile_url [String] the profile the resource is being
|
80
|
+
# validated against
|
81
|
+
# @yieldreturn [Array<Hash<Symbol, String>>,Hash<Symbol, String>] The
|
82
|
+
# block should return a Hash or an Array of Hashes if any validation
|
83
|
+
# messages should be added. The Hash must contain two keys: `:type`
|
84
|
+
# and `:message`. `:type` can have a value of `'info'`, `'warning'`,
|
85
|
+
# or `'error'`. A type of `'error'` means the resource is invalid.
|
86
|
+
# `:message` contains the message string itself.
|
87
|
+
def perform_additional_validation(&block)
|
88
|
+
additional_validations << block
|
89
|
+
end
|
90
|
+
|
91
|
+
# @private
|
92
|
+
def additional_validation_messages(resource, profile_url)
|
93
|
+
additional_validations
|
94
|
+
.flat_map { |step| step.call(resource, profile_url) }
|
95
|
+
.select { |message| message.is_a? Hash }
|
96
|
+
end
|
97
|
+
|
98
|
+
# Filter out unwanted validation messages
|
99
|
+
#
|
100
|
+
# @example
|
101
|
+
# validator do
|
102
|
+
# exclude_message { |message| message.type == 'info' }
|
103
|
+
# end
|
104
|
+
# @yieldparam message [Inferno::Entities::Message]
|
31
105
|
def exclude_message(&block)
|
32
106
|
@exclude_message = block if block_given?
|
33
107
|
@exclude_message
|
34
108
|
end
|
35
109
|
|
110
|
+
# @see Inferno::DSL::FHIRValidation#resource_is_valid?
|
36
111
|
def resource_is_valid?(resource, profile_url, runnable)
|
37
112
|
profile_url ||= FHIR::Definitions.resource_definition(resource.resourceType).url
|
38
113
|
|
@@ -40,6 +115,8 @@ module Inferno
|
|
40
115
|
|
41
116
|
message_hashes = outcome.issue&.map { |issue| message_hash_from_issue(issue) } || []
|
42
117
|
|
118
|
+
message_hashes.concat(additional_validation_messages(resource, profile_url))
|
119
|
+
|
43
120
|
filter_messages(message_hashes)
|
44
121
|
|
45
122
|
message_hashes
|
@@ -47,10 +124,12 @@ module Inferno
|
|
47
124
|
.none? { |message_hash| message_hash[:type] == 'error' }
|
48
125
|
end
|
49
126
|
|
127
|
+
# @private
|
50
128
|
def filter_messages(message_hashes)
|
51
129
|
message_hashes.reject! { |message| exclude_message.call(Entities::Message.new(message)) } if exclude_message
|
52
130
|
end
|
53
131
|
|
132
|
+
# @private
|
54
133
|
def message_hash_from_issue(issue)
|
55
134
|
{
|
56
135
|
type: issue_severity(issue),
|
@@ -58,6 +137,7 @@ module Inferno
|
|
58
137
|
}
|
59
138
|
end
|
60
139
|
|
140
|
+
# @private
|
61
141
|
def issue_severity(issue)
|
62
142
|
case issue.severity
|
63
143
|
when 'warning'
|
@@ -69,6 +149,7 @@ module Inferno
|
|
69
149
|
end
|
70
150
|
end
|
71
151
|
|
152
|
+
# @private
|
72
153
|
def issue_message(issue)
|
73
154
|
location = if issue.respond_to?(:expression)
|
74
155
|
issue.expression&.join(', ')
|
@@ -79,6 +160,11 @@ module Inferno
|
|
79
160
|
"#{location}: #{issue&.details&.text}"
|
80
161
|
end
|
81
162
|
|
163
|
+
# Post a resource to the validation service for validating.
|
164
|
+
#
|
165
|
+
# @param resource [FHIR::Model]
|
166
|
+
# @param profile_url [String]
|
167
|
+
# @return [String] the body of the validation response
|
82
168
|
def validate(resource, profile_url)
|
83
169
|
RestClient.post(
|
84
170
|
"#{url}/validate",
|
@@ -89,15 +175,33 @@ module Inferno
|
|
89
175
|
end
|
90
176
|
|
91
177
|
module ClassMethods
|
92
|
-
# @
|
178
|
+
# @private
|
93
179
|
def fhir_validators
|
94
180
|
@fhir_validators ||= {}
|
95
181
|
end
|
96
182
|
|
183
|
+
# Define a validator
|
184
|
+
# @example
|
185
|
+
# validator do
|
186
|
+
# url 'http://example.com/validator'
|
187
|
+
# exclude_message { |message| message.type == 'info' }
|
188
|
+
# perform_additional_validation do |resource, profile_url|
|
189
|
+
# if something_is_wrong
|
190
|
+
# { type: 'error', message: 'something is wrong' }
|
191
|
+
# else
|
192
|
+
# { type: 'info', message: 'everything is ok' }
|
193
|
+
# end
|
194
|
+
# end
|
195
|
+
# end
|
196
|
+
#
|
197
|
+
# @param name [Symbol] the name of the validator, only needed if you are
|
198
|
+
# using multiple validators
|
97
199
|
def validator(name = :default, &block)
|
98
200
|
fhir_validators[name] = Inferno::DSL::FHIRValidation::Validator.new(&block)
|
99
201
|
end
|
100
202
|
|
203
|
+
# Find a particular validator. Looks through a runnable's parents up to
|
204
|
+
# the suite to find a validator with a particular name
|
101
205
|
def find_validator(validator_name)
|
102
206
|
validator = fhir_validators[validator_name] || parent&.find_validator(validator_name)
|
103
207
|
|
@@ -16,7 +16,7 @@ module Inferno
|
|
16
16
|
# # performs a GET to https://example.com
|
17
17
|
# get
|
18
18
|
# # performs a GET to https://example.com/abc
|
19
|
-
#
|
19
|
+
# get('abc')
|
20
20
|
#
|
21
21
|
# request # the most recent request
|
22
22
|
# response # the most recent response
|
@@ -27,7 +27,7 @@ module Inferno
|
|
27
27
|
# @see Inferno::FHIRClientBuilder Documentation for the client
|
28
28
|
# configuration DSL
|
29
29
|
module HTTPClient
|
30
|
-
# @
|
30
|
+
# @private
|
31
31
|
def self.included(klass)
|
32
32
|
klass.extend ClassMethods
|
33
33
|
klass.include RequestStorage
|
@@ -47,7 +47,7 @@ module Inferno
|
|
47
47
|
http_clients[client] = HTTPClientBuilder.new.build(self, definition)
|
48
48
|
end
|
49
49
|
|
50
|
-
# @
|
50
|
+
# @private
|
51
51
|
def http_clients
|
52
52
|
@http_clients ||= {}
|
53
53
|
end
|
@@ -60,16 +60,17 @@ module Inferno
|
|
60
60
|
# @param client [Symbol]
|
61
61
|
# @param name [Symbol] Name for this request to allow it to be used by
|
62
62
|
# other tests
|
63
|
-
# @
|
63
|
+
# @option options [Hash] Input headers here - headers are optional and
|
64
|
+
# must be entered as the last piece of input to this method
|
64
65
|
# @return [Inferno::Entities::Request]
|
65
|
-
def get(url = '', client: :default, name: nil, **
|
66
|
+
def get(url = '', client: :default, name: nil, **options)
|
66
67
|
store_request('outgoing', name) do
|
67
68
|
client = http_client(client)
|
68
69
|
|
69
70
|
if client
|
70
|
-
client.get(url)
|
71
|
+
client.get(url, nil, options[:headers])
|
71
72
|
elsif url.match?(%r{\Ahttps?://})
|
72
|
-
Faraday.get(url)
|
73
|
+
Faraday.get(url, nil, options[:headers])
|
73
74
|
else
|
74
75
|
raise StandardError, 'Must use an absolute url or define an HTTP client with a base url'
|
75
76
|
end
|
@@ -85,16 +86,17 @@ module Inferno
|
|
85
86
|
# @param client [Symbol]
|
86
87
|
# @param name [Symbol] Name for this request to allow it to be used by
|
87
88
|
# other tests
|
88
|
-
# @
|
89
|
+
# @option options [Hash] Input headers here - headers are optional and
|
90
|
+
# must be entered as the last piece of input to this method
|
89
91
|
# @return [Inferno::Entities::Request]
|
90
|
-
def post(url = '', body: nil, client: :default, name: nil, **
|
92
|
+
def post(url = '', body: nil, client: :default, name: nil, **options)
|
91
93
|
store_request('outgoing', name) do
|
92
94
|
client = http_client(client)
|
93
95
|
|
94
96
|
if client
|
95
|
-
client.post(url, body)
|
97
|
+
client.post(url, body, options[:headers])
|
96
98
|
elsif url.match?(%r{\Ahttps?://})
|
97
|
-
Faraday.post(url, body)
|
99
|
+
Faraday.post(url, body, options[:headers])
|
98
100
|
else
|
99
101
|
raise StandardError, 'Must use an absolute url or define an HTTP client with a base url'
|
100
102
|
end
|
@@ -102,7 +104,7 @@ module Inferno
|
|
102
104
|
end
|
103
105
|
|
104
106
|
module ClassMethods
|
105
|
-
# @
|
107
|
+
# @private
|
106
108
|
def http_client_definitions
|
107
109
|
@http_client_definitions ||= {}
|
108
110
|
end
|
@@ -4,7 +4,7 @@ module Inferno
|
|
4
4
|
class HTTPClientBuilder
|
5
5
|
attr_accessor :runnable
|
6
6
|
|
7
|
-
# @
|
7
|
+
# @private
|
8
8
|
def build(runnable, block)
|
9
9
|
self.runnable = runnable
|
10
10
|
instance_exec(self, &block)
|
@@ -38,14 +38,14 @@ module Inferno
|
|
38
38
|
@headers ||= headers
|
39
39
|
end
|
40
40
|
|
41
|
-
# @
|
41
|
+
# @private
|
42
42
|
def method_missing(name, *args, &block)
|
43
43
|
return runnable.call(name, *args, &block) if runnable.respond_to? name
|
44
44
|
|
45
45
|
super
|
46
46
|
end
|
47
47
|
|
48
|
-
# @
|
48
|
+
# @private
|
49
49
|
def respond_to_missing?(name)
|
50
50
|
runnable.respond_to?(name) || super
|
51
51
|
end
|
@@ -38,13 +38,14 @@ module Inferno
|
|
38
38
|
|
39
39
|
# TODO: do a check in the test runner
|
40
40
|
def named_request(name)
|
41
|
-
requests.find { |request| request.name == name.to_sym }
|
41
|
+
requests.find { |request| request.name == self.class.config.request_name(name.to_sym) }
|
42
42
|
end
|
43
43
|
|
44
|
-
# @
|
44
|
+
# @private
|
45
45
|
def store_request(direction, name = nil, &block)
|
46
46
|
response = block.call
|
47
47
|
|
48
|
+
name = self.class.config.request_name(name)
|
48
49
|
request =
|
49
50
|
if response.is_a? FHIR::ClientReply
|
50
51
|
Entities::Request.from_fhir_client_reply(
|
@@ -60,52 +61,60 @@ module Inferno
|
|
60
61
|
request
|
61
62
|
end
|
62
63
|
|
63
|
-
# @
|
64
|
+
# @private
|
64
65
|
def load_named_requests
|
65
66
|
requests_repo = Inferno::Repositories::Requests.new
|
66
67
|
self.class.named_requests_used.map do |request_name|
|
67
|
-
|
68
|
-
|
68
|
+
request_alias = self.class.config.request_name(request_name)
|
69
|
+
request = requests_repo.find_named_request(test_session_id, request_alias)
|
70
|
+
raise StandardError, "Unable to find '#{request_alias}' request" if request.nil?
|
69
71
|
|
70
72
|
requests << request
|
71
73
|
end
|
72
74
|
end
|
73
75
|
|
74
76
|
module ClassMethods
|
75
|
-
# @
|
77
|
+
# @private
|
76
78
|
def named_requests_made
|
77
79
|
@named_requests_made ||= []
|
78
80
|
end
|
79
81
|
|
80
|
-
# @
|
82
|
+
# @private
|
81
83
|
def named_requests_used
|
82
84
|
@named_requests_used ||= []
|
83
85
|
end
|
84
86
|
|
85
87
|
# Specify the named requests made by a test
|
86
88
|
#
|
87
|
-
# @param
|
88
|
-
def makes_request(*
|
89
|
-
named_requests_made.concat(
|
89
|
+
# @param identifiers [Symbol] one or more request identifiers
|
90
|
+
def makes_request(*identifiers)
|
91
|
+
named_requests_made.concat(identifiers).uniq!
|
92
|
+
identifiers.each do |identifier|
|
93
|
+
config.add_request(identifier)
|
94
|
+
end
|
90
95
|
end
|
91
96
|
|
92
97
|
# Specify the name for a request received by a test
|
93
98
|
#
|
94
|
-
# @param
|
95
|
-
def receives_request(
|
96
|
-
|
99
|
+
# @param identifier [Symbol]
|
100
|
+
def receives_request(identifier)
|
101
|
+
config.add_request(identifier)
|
102
|
+
@incoming_request_name = identifier
|
97
103
|
end
|
98
104
|
|
99
|
-
# @
|
105
|
+
# @private
|
100
106
|
def incoming_request_name
|
101
107
|
@incoming_request_name
|
102
108
|
end
|
103
109
|
|
104
110
|
# Specify the named requests used by a test
|
105
111
|
#
|
106
|
-
# @param
|
107
|
-
def uses_request(*
|
108
|
-
named_requests_used.concat(
|
112
|
+
# @param identifiers [Symbol] one or more request identifiers
|
113
|
+
def uses_request(*identifiers)
|
114
|
+
named_requests_used.concat(identifiers).uniq!
|
115
|
+
identifiers.each do |identifier|
|
116
|
+
config.add_request(identifier)
|
117
|
+
end
|
109
118
|
end
|
110
119
|
end
|
111
120
|
end
|
data/lib/inferno/dsl/results.rb
CHANGED
@@ -4,7 +4,7 @@ module Inferno
|
|
4
4
|
module DSL
|
5
5
|
# A base class for creating routes to resume test execution upon receiving
|
6
6
|
# an incoming request.
|
7
|
-
# @
|
7
|
+
# @private
|
8
8
|
# @see Inferno::DSL::Runnable#resume_test_route
|
9
9
|
class ResumeTestRoute
|
10
10
|
include Hanami::Action
|
@@ -26,49 +26,49 @@ module Inferno
|
|
26
26
|
@request ||= Inferno::Entities::Request.from_rack_env(@params.env)
|
27
27
|
end
|
28
28
|
|
29
|
-
# @
|
29
|
+
# @private
|
30
30
|
def test_run
|
31
31
|
@test_run ||=
|
32
32
|
test_runs_repo.find_latest_waiting_by_identifier(test_run_identifier)
|
33
33
|
end
|
34
34
|
|
35
|
-
# @
|
35
|
+
# @private
|
36
36
|
def waiting_result
|
37
37
|
@waiting_result ||= results_repo.find_waiting_result(test_run_id: test_run.id)
|
38
38
|
end
|
39
39
|
|
40
|
-
# @
|
40
|
+
# @private
|
41
41
|
def update_result
|
42
42
|
results_repo.pass_waiting_result(waiting_result.id)
|
43
43
|
end
|
44
44
|
|
45
|
-
# @
|
45
|
+
# @private
|
46
46
|
def persist_request
|
47
47
|
requests_repo.create(
|
48
48
|
request.to_hash.merge(
|
49
49
|
test_session_id: test_run.test_session_id,
|
50
50
|
result_id: waiting_result.id,
|
51
|
-
name: test.incoming_request_name
|
51
|
+
name: test.config.request_name(test.incoming_request_name)
|
52
52
|
)
|
53
53
|
)
|
54
54
|
end
|
55
55
|
|
56
|
-
# @
|
56
|
+
# @private
|
57
57
|
def redirect_route
|
58
58
|
"/test_sessions/#{test_run.test_session_id}##{waiting_group_id}"
|
59
59
|
end
|
60
60
|
|
61
|
-
# @
|
61
|
+
# @private
|
62
62
|
def test
|
63
63
|
@test ||= tests_repo.find(waiting_result.test_id)
|
64
64
|
end
|
65
65
|
|
66
|
-
# @
|
66
|
+
# @private
|
67
67
|
def waiting_group_id
|
68
68
|
test.parent.id
|
69
69
|
end
|
70
70
|
|
71
|
-
# @
|
71
|
+
# @private
|
72
72
|
def call(_params)
|
73
73
|
if test_run.nil?
|
74
74
|
status(500, "Unable to find test run with identifier '#{test_run_identifier}'.")
|