testscript_engine 0.0.0
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.
- checksums.yaml +7 -0
- data/bin/testscript_engine +108 -0
- data/lib/testscript_engine/assertion.rb +284 -0
- data/lib/testscript_engine/message_handler.rb +332 -0
- data/lib/testscript_engine/operation.rb +160 -0
- data/lib/testscript_engine/testreport_handler.rb +247 -0
- data/lib/testscript_engine/testscript_runnable.rb +252 -0
- data/lib/testscript_engine.rb +134 -0
- metadata +140 -0
@@ -0,0 +1,332 @@
|
|
1
|
+
module MessageHandler
|
2
|
+
attr_accessor :debug_mode, :modify_report
|
3
|
+
|
4
|
+
def space
|
5
|
+
@space ||= ''
|
6
|
+
end
|
7
|
+
|
8
|
+
def unit_of_space
|
9
|
+
" "
|
10
|
+
end
|
11
|
+
|
12
|
+
def increase_space
|
13
|
+
space << unit_of_space
|
14
|
+
end
|
15
|
+
|
16
|
+
def decrease_space
|
17
|
+
@space.chomp!(unit_of_space)
|
18
|
+
end
|
19
|
+
|
20
|
+
def newline
|
21
|
+
puts
|
22
|
+
end
|
23
|
+
|
24
|
+
def print_out(message)
|
25
|
+
if message.start_with?("START")
|
26
|
+
newline
|
27
|
+
increase_space
|
28
|
+
elsif message.start_with?("FINISH")
|
29
|
+
else
|
30
|
+
print unit_of_space
|
31
|
+
end
|
32
|
+
print space
|
33
|
+
puts message
|
34
|
+
|
35
|
+
if message.start_with?("FINISH")
|
36
|
+
decrease_space
|
37
|
+
end
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
|
41
|
+
def info(message_type, *options)
|
42
|
+
print_out messages(message_type, *options)
|
43
|
+
end
|
44
|
+
|
45
|
+
def cascade_skips(message_type, actions, *options)
|
46
|
+
print_out "#{outcome_symbol("SKIP")} #{messages(message_type, *options)}"
|
47
|
+
increase_space
|
48
|
+
counter = 0
|
49
|
+
while counter < actions.length
|
50
|
+
action = actions[counter]
|
51
|
+
if action.operation
|
52
|
+
id = "Operation: [#{(action.operation.id || action.operation.label || 'unlabeled')}]"
|
53
|
+
else
|
54
|
+
id = "Assert: [#{(action.assert.id || action.assert.label || 'unlabeled')}]" if action.assert
|
55
|
+
end
|
56
|
+
skip(:eval_assert_result, "#{id} skipped.")
|
57
|
+
counter += 1
|
58
|
+
end
|
59
|
+
decrease_space
|
60
|
+
end
|
61
|
+
|
62
|
+
def load_scripts
|
63
|
+
print_out messages(:begin_loading_scripts, testscript_path)
|
64
|
+
super
|
65
|
+
print_out messages(:finish_loading_scripts)
|
66
|
+
end
|
67
|
+
|
68
|
+
def make_runnables
|
69
|
+
print_out messages(:begin_creating_runnables)
|
70
|
+
increase_space
|
71
|
+
super
|
72
|
+
decrease_space
|
73
|
+
print_out messages(:finish_creating_runnables)
|
74
|
+
end
|
75
|
+
|
76
|
+
def run(*args)
|
77
|
+
print_out messages(:begin_runnable_execution, script.id)
|
78
|
+
result = super
|
79
|
+
puts
|
80
|
+
print_out messages(:finish_runnable_execution)
|
81
|
+
result
|
82
|
+
end
|
83
|
+
|
84
|
+
def preprocess
|
85
|
+
print_out messages(:begin_preprocess)
|
86
|
+
super
|
87
|
+
print_out messages(:finish_preprocess)
|
88
|
+
end
|
89
|
+
|
90
|
+
def setup
|
91
|
+
print_out messages(:begin_setup)
|
92
|
+
super
|
93
|
+
print_out messages(:finish_setup)
|
94
|
+
end
|
95
|
+
|
96
|
+
def test
|
97
|
+
print_out messages(:begin_test)
|
98
|
+
super
|
99
|
+
print_out messages(:finish_test)
|
100
|
+
end
|
101
|
+
|
102
|
+
def teardown
|
103
|
+
print_out messages(:begin_teardown)
|
104
|
+
super
|
105
|
+
print_out messages(:finish_teardown)
|
106
|
+
end
|
107
|
+
|
108
|
+
def postprocessing
|
109
|
+
end
|
110
|
+
|
111
|
+
def load_fixtures
|
112
|
+
increase_space
|
113
|
+
super
|
114
|
+
decrease_space
|
115
|
+
end
|
116
|
+
|
117
|
+
def pass(message_type, *options)
|
118
|
+
message = messages(message_type, *options)
|
119
|
+
super()
|
120
|
+
print_out "#{outcome_symbol("INFO")} #{message}"
|
121
|
+
end
|
122
|
+
|
123
|
+
def fail(message_type, *options)
|
124
|
+
message = messages(message_type, *options)
|
125
|
+
super(message) if modify_report
|
126
|
+
print_out "#{outcome_symbol("FAIL")} #{message}"
|
127
|
+
end
|
128
|
+
|
129
|
+
def skip(message_type, *options)
|
130
|
+
message = messages(message_type, *options)
|
131
|
+
super(message) if modify_report
|
132
|
+
print_out "#{outcome_symbol("SKIP")} #{message}"
|
133
|
+
end
|
134
|
+
|
135
|
+
def warning(message_type, *options)
|
136
|
+
message = messages(message_type, *options)
|
137
|
+
super(message) if modify_report
|
138
|
+
print_out "#{outcome_symbol("WARN")} #{message}"
|
139
|
+
end
|
140
|
+
|
141
|
+
def error(message_type, *options)
|
142
|
+
message = messages(message_type, *options)
|
143
|
+
super(message) if modify_report
|
144
|
+
print_out "#{outcome_symbol("ERROR")} #{message}"
|
145
|
+
end
|
146
|
+
|
147
|
+
def print_action_header(action_type)
|
148
|
+
return false if @previous_action_type == action_type
|
149
|
+
|
150
|
+
@previous_action_type = action_type
|
151
|
+
true
|
152
|
+
end
|
153
|
+
|
154
|
+
# < ---- TO REVIEW ---- >
|
155
|
+
def client(*args)
|
156
|
+
client = super
|
157
|
+
FHIR.logger.formatter = logger_formatter_with_spacing
|
158
|
+
client
|
159
|
+
end
|
160
|
+
|
161
|
+
def logger_formatters_with_spacing
|
162
|
+
@logger_formatters_with_spacing ||= {}
|
163
|
+
end
|
164
|
+
|
165
|
+
def logger_formatter_with_spacing
|
166
|
+
logger_formatters_with_spacing[space.length] || begin
|
167
|
+
new_logger_formatter = proc do |severity, datetime, progname, msg|
|
168
|
+
"#{space}#{unit_of_space}#{msg}\n"
|
169
|
+
end
|
170
|
+
logger_formatters_with_spacing[space.length] = new_logger_formatter
|
171
|
+
new_logger_formatter
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def outcome_symbol(outcome)
|
176
|
+
symbol = begin
|
177
|
+
case outcome
|
178
|
+
when "UNKNOWN"
|
179
|
+
"?¿?"
|
180
|
+
when "FATAL"
|
181
|
+
[023042].pack("U*")
|
182
|
+
when "ERROR"
|
183
|
+
[10071].pack("U*")
|
184
|
+
when "WARN"
|
185
|
+
[023220].pack("U*")
|
186
|
+
when "INFO"
|
187
|
+
[10003].pack("U*")
|
188
|
+
when "DEBUG"
|
189
|
+
[0372415].pack("U*")
|
190
|
+
when "FAIL"
|
191
|
+
[10007].pack("U*")
|
192
|
+
when "SKIP"
|
193
|
+
"\u21BB".encode('utf-8')
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
"(#{symbol})"
|
198
|
+
end
|
199
|
+
|
200
|
+
def begin_symbol
|
201
|
+
[10551].pack("U*")
|
202
|
+
end
|
203
|
+
|
204
|
+
def finish_symbol
|
205
|
+
[024465].pack("U*")
|
206
|
+
end
|
207
|
+
|
208
|
+
def messages(message, *options)
|
209
|
+
message_text = case message
|
210
|
+
when :abort_test
|
211
|
+
"Due to an unsuccessful action in the [#{options[0]}] phase, remaining actions in this test will be skipped. Skipping the next #{options[1]} action(s)."
|
212
|
+
when :bad_script
|
213
|
+
"Given non-TestScript resource. Can not create runnable."
|
214
|
+
when :bad_serialized_script
|
215
|
+
"Can not deserialize resource into TestScript: [#{options[0]}]."
|
216
|
+
when :begin_initialize_client
|
217
|
+
start_message_format("INITIALIZE CLIENT")
|
218
|
+
when :begin_creating_runnables
|
219
|
+
start_message_format("MAKE RUNNABLE(S)")
|
220
|
+
when :created_runnable
|
221
|
+
"Created runnable from TestScript: [#{options[0]}]."
|
222
|
+
when :finish_creating_runnables
|
223
|
+
finish_message_format("MAKING RUNNABLE(S)")
|
224
|
+
when :finish_initialize_client
|
225
|
+
finish_message_format("INITIALIZING CLIENT")
|
226
|
+
when :invalid_script
|
227
|
+
"Can not load TestScript. Invalid resource: [#{options[0]}]."
|
228
|
+
when :invalid_script
|
229
|
+
"Given invalid TestScript resource. Can not create runnable."
|
230
|
+
when :loaded_script
|
231
|
+
"Loaded TestScript: [#{options[0]}]."
|
232
|
+
when :loaded_static_fixture
|
233
|
+
"Loaded static fixture [#{options[0]}]."
|
234
|
+
when :no_postprocess
|
235
|
+
"Nothing to postprocess."
|
236
|
+
when :no_preprocess
|
237
|
+
"Nothing to preprocess."
|
238
|
+
when :no_setup
|
239
|
+
"Nothing to setup."
|
240
|
+
when :no_teardown
|
241
|
+
"Nothing to teardown."
|
242
|
+
when :overwrite_existing_script
|
243
|
+
"Overwriting previously loaded TestScript: [#{options[0]}]."
|
244
|
+
when :skip_on_fail
|
245
|
+
"Due to the preceeding unsuccessful action, skipping the next #{options[0]} action(s)."
|
246
|
+
when :unable_to_create_runnable
|
247
|
+
"Can not create runnable from TestScript: [#{options[0]}]."
|
248
|
+
when :unable_to_locate_runnable
|
249
|
+
"Can not locate runnable with id: [#{options[0]}]. Can not execute."
|
250
|
+
when :assertion_error
|
251
|
+
"ERROR: Unable to process assertion: #{options[0]}"
|
252
|
+
when :assertion_exception
|
253
|
+
"#{options[0]}"
|
254
|
+
when :bad_reference
|
255
|
+
"Unable to read contents of reference: [#{options[0]}]. No reference extracted."
|
256
|
+
when :bad_request
|
257
|
+
"Unable to create a request from operation."
|
258
|
+
when :bad_static_fixture_reference
|
259
|
+
"Static fixture included unresolvable reference. Can not load fixture. Moving on."
|
260
|
+
when :begin_loading_scripts
|
261
|
+
start_message_format("LOAD TESTSCRIPTS", options[0])
|
262
|
+
when :begin_preprocess
|
263
|
+
start_message_format("PREPROCESS", options[0])
|
264
|
+
when :begin_runnable_execution
|
265
|
+
start_message_format("EXECUTE RUNNABLE", options[0])
|
266
|
+
when :begin_setup
|
267
|
+
start_message_format("SETUP")
|
268
|
+
when :begin_teardown
|
269
|
+
start_message_format("TEARDOWN")
|
270
|
+
when :begin_test
|
271
|
+
start_message_format("TEST")
|
272
|
+
when :eval_assert_result
|
273
|
+
"#{options[0]}"
|
274
|
+
when :evaluate_assert
|
275
|
+
"EVALUATING ASSERTION"
|
276
|
+
when :execute_operation
|
277
|
+
"OPERATION EXECUTION"
|
278
|
+
when :execute_operation_error
|
279
|
+
"Unable to execute operation. ERROR: [#{options[0]}]. [#{options[1]}]"
|
280
|
+
when :finish_loading_scripts
|
281
|
+
finish_message_format("LOADING SCRIPTS")
|
282
|
+
when :finish_preprocess
|
283
|
+
finish_message_format("PREPROCESS")
|
284
|
+
when :finish_runnable_execution
|
285
|
+
finish_message_format("EXECUTING RUNNABLE. FINAL EXECUTION SCORE: [#{testreport.score}]")
|
286
|
+
when :finish_setup
|
287
|
+
finish_message_format("SETUP")
|
288
|
+
when :finish_teardown
|
289
|
+
finish_message_format("TEARDOWN")
|
290
|
+
when :finish_test
|
291
|
+
finish_message_format("TEST")
|
292
|
+
when :invalid_assert
|
293
|
+
"Invalid assert. Can not evaluate."
|
294
|
+
when :invalid_dump
|
295
|
+
"Validation error: [#{options[0]}]"
|
296
|
+
when :invalid_operation
|
297
|
+
"Invalid operation. Can not execute."
|
298
|
+
when :invalid_request
|
299
|
+
"Unable to create a request using operation: [#{options[0]}]. Can not execute."
|
300
|
+
when :no_contained_resource
|
301
|
+
"Reference [#{options[0]}] refers to a contained resource that does not exist. Moving on."
|
302
|
+
when :no_path
|
303
|
+
"Unable to extract path from operation."
|
304
|
+
when :no_reference
|
305
|
+
"Reference element of reference object is nil. Can not get resource from reference."
|
306
|
+
when :no_static_fixture_id
|
307
|
+
"No ID for static fixture. Can not load."
|
308
|
+
when :no_static_fixture_reference
|
309
|
+
"No reference for static fixture. Can not load."
|
310
|
+
when :no_static_fixture_resource
|
311
|
+
"No resource for static fixture. Can not load."
|
312
|
+
when :pass_execute_operation
|
313
|
+
"Executed Operation: [#{options[0]}]"
|
314
|
+
when :resource_extraction
|
315
|
+
"Unable to extract resource referenced by [#{options[0]}]. Encountered: [#{options[1]}]."
|
316
|
+
when :uncaught_error
|
317
|
+
"Uncaught error: [#{options[0]}]."
|
318
|
+
when :unsupported_ref
|
319
|
+
"Remote reference: [#{options[0]}] not supported. No reference extracted."
|
320
|
+
else
|
321
|
+
"! unknown message type !"
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
def start_message_format(phase, *options)
|
327
|
+
"STARTING TO #{phase}" + (options[0] ? ": [#{options[0]}]" : '')
|
328
|
+
end
|
329
|
+
|
330
|
+
def finish_message_format(phase)
|
331
|
+
"FINISHED #{phase}."
|
332
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Operation
|
3
|
+
class OperationException < StandardError
|
4
|
+
attr_reader :details
|
5
|
+
|
6
|
+
def initialize(details)
|
7
|
+
@details = details
|
8
|
+
super()
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
OPERATION_NEEDS_RESOURCE_TYPE = %w[
|
13
|
+
put
|
14
|
+
patch
|
15
|
+
delete
|
16
|
+
read
|
17
|
+
vread
|
18
|
+
update
|
19
|
+
create
|
20
|
+
].freeze
|
21
|
+
|
22
|
+
INTERACTION_NEEDS_PAYLOAD = %w[
|
23
|
+
patch
|
24
|
+
post
|
25
|
+
put
|
26
|
+
].freeze
|
27
|
+
|
28
|
+
def execute(operation)
|
29
|
+
request = build_request(operation)
|
30
|
+
|
31
|
+
begin
|
32
|
+
client.send(*request)
|
33
|
+
rescue
|
34
|
+
raise OperationException, :bad_request
|
35
|
+
end
|
36
|
+
|
37
|
+
storage(operation)
|
38
|
+
pass(:pass_execute_operation, operation.label || 'unlabeled')
|
39
|
+
end
|
40
|
+
|
41
|
+
def build_request(operation)
|
42
|
+
path = get_path(operation)
|
43
|
+
headers = get_headers(operation)
|
44
|
+
interaction = get_interaction(operation)
|
45
|
+
payload = get_payload(operation, interaction)
|
46
|
+
|
47
|
+
[interaction, path, payload, headers].compact
|
48
|
+
end
|
49
|
+
|
50
|
+
def get_path(operation)
|
51
|
+
return replace_variables(operation.url) if operation.url
|
52
|
+
|
53
|
+
if operation.params
|
54
|
+
# [type]/[params]
|
55
|
+
"#{get_resource_type(operation)}#{replace_variables(operation.params)}"
|
56
|
+
elsif operation.targetId
|
57
|
+
resource = get_resource(operation.targetId)
|
58
|
+
# TODO: Move this exception into the get_resource method in runnable/utilities
|
59
|
+
# just need to make sure that raising this exception is always going to be acceptbale i.e. in any action ever,
|
60
|
+
# if the resource isn't available, we should dip
|
61
|
+
(raise OperationException, :noResource) unless resource
|
62
|
+
path = resource.resourceType
|
63
|
+
id = get_id(operation, resource)
|
64
|
+
vid = get_vid(operation, resource)
|
65
|
+
"#{path}#{id}" + (operation.type&.code == 'history' ? '/_history' : vid)
|
66
|
+
elsif operation.sourceId
|
67
|
+
resource = get_resource(operation.sourceId)
|
68
|
+
raise OperationException, :noResource unless resource
|
69
|
+
|
70
|
+
%w[batch transaction].include?(operation.type&.code) ? '' : resource.resourceType
|
71
|
+
else
|
72
|
+
raise OperationException, :no_path
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def get_headers(operation)
|
77
|
+
headers = {
|
78
|
+
'Content-Type' => get_format(operation.contentType),
|
79
|
+
'Accept' => get_format(operation.accept)
|
80
|
+
}
|
81
|
+
|
82
|
+
operation.requestHeader.each_with_object(headers) do |header|
|
83
|
+
headers[header.field] = header.value
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def get_interaction(operation)
|
88
|
+
interaction = operation.local_method || operation_to_interaction(operation.type&.code)
|
89
|
+
interaction || (raise OperationException, :noInteraction)
|
90
|
+
end
|
91
|
+
|
92
|
+
def get_payload(operation, interaction)
|
93
|
+
return unless INTERACTION_NEEDS_PAYLOAD.include?(interaction)
|
94
|
+
|
95
|
+
payload = get_resource(operation.sourceId)
|
96
|
+
raise OperationException, :noPayload if INTERACTION_NEEDS_PAYLOAD.include?(interaction) && !payload
|
97
|
+
payload
|
98
|
+
end
|
99
|
+
|
100
|
+
def get_resource_type(operation)
|
101
|
+
operation_type = operation.local_method || operation.type.code
|
102
|
+
|
103
|
+
if OPERATION_NEEDS_RESOURCE_TYPE.include?(operation_type) && operation.resource.nil?
|
104
|
+
raise OperationException, :noResourceType
|
105
|
+
end
|
106
|
+
|
107
|
+
operation.resource
|
108
|
+
end
|
109
|
+
|
110
|
+
def get_id(operation, resource)
|
111
|
+
id = resource.id
|
112
|
+
(raise OperationException, :noId) ['read', 'vread'].include?(operation.type&.code) if id.nil?
|
113
|
+
"/#{id}"
|
114
|
+
end
|
115
|
+
|
116
|
+
def get_vid(operation, resource)
|
117
|
+
vid = resource.meta&.versionId
|
118
|
+
if operation.type&.code == 'vread'
|
119
|
+
(raise OperationException, :noVid) unless vid
|
120
|
+
"/_history/#{vid}"
|
121
|
+
else
|
122
|
+
''
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def get_format(format)
|
127
|
+
return 'application/fhir+xml' if format == 'xml'
|
128
|
+
|
129
|
+
'application/fhir+json'
|
130
|
+
end
|
131
|
+
|
132
|
+
def operation_to_interaction(interaction)
|
133
|
+
case interaction
|
134
|
+
when 'read', 'vread', 'search', 'search-type', 'search-system', 'capabilities', 'history', 'history-instance', 'history-type', 'history-system', 'operation'
|
135
|
+
'get'
|
136
|
+
when 'create', 'batch', 'transaction'
|
137
|
+
'post'
|
138
|
+
when 'update'
|
139
|
+
'put'
|
140
|
+
when 'patch'
|
141
|
+
'patch'
|
142
|
+
when 'delete'
|
143
|
+
'delete'
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def create_operation(source_id)
|
148
|
+
FHIR::TestScript::Setup::Action::Operation.new({
|
149
|
+
sourceId: source_id,
|
150
|
+
method: 'post'
|
151
|
+
})
|
152
|
+
end
|
153
|
+
|
154
|
+
def delete_operation(source_id)
|
155
|
+
FHIR::TestScript::Setup::Action::Operation.new({
|
156
|
+
targetId: id_map[source_id],
|
157
|
+
method: 'delete'
|
158
|
+
})
|
159
|
+
end
|
160
|
+
end
|