inferno_core 1.2.1 → 1.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 173f64337d9dc56eff789e35b65fa26656ef256316e42fd68ab0c84765175bbc
4
- data.tar.gz: b6bd2746f865f1f40c3771ef90b01d97ecf3fcd5a40f44dc8ddee6bf46ee616a
3
+ metadata.gz: e3ab1df7a5983dabeaaac0fdfa75d5235815b48841dc94db70c55438f91c438d
4
+ data.tar.gz: 30da5ad7c4c0b379b91c49197b9d458fdfa912dbd5058b75793d46ebf87be46a
5
5
  SHA512:
6
- metadata.gz: b20f45985bd53be776773bc5bfe75c693891abee583469768348472f33b1f6afa4ee88da47130624a8cae5a23617ffffb061f2f7ea08a68f3699aa7a0c1c4d96
7
- data.tar.gz: b709e8ffdf296b5aea82febeb6e1164843f402bb7e8b090ca09ac04e80956732d6df2a796f63e16cce00f1688cf70b8ab83bd0bfbb43ae8cb9d257231a3ba858
6
+ metadata.gz: 4397d0025d25280991e60ffe4c2ff1b660b64ebcb067f5988b1edbafd4b51d06c07639508bf1c9590718ae597ae6f9e4e1586348a83a84e6baff86de18e0fba9
7
+ data.tar.gz: c2c18fef0b14842503aba9367c7faec52f5c68bbda66694ddf22a99fd927eabcd53503d1b724430534f3f2ab06d662be3dcd1496822c14b49463a440a23d3516
@@ -909,6 +909,7 @@ module Inferno
909
909
  expected_results_file: expected_file,
910
910
  compare_messages: options[:compare_messages],
911
911
  compare_result_message: options[:compare_result_message],
912
+ only_different_messages: options[:only_different_messages],
912
913
  inferno_base_url: options[:inferno_base_url],
913
914
  normalized_strings: Array(comparison_config['normalized_strings'])
914
915
  }
@@ -104,6 +104,11 @@ module Inferno
104
104
  type: :numeric,
105
105
  default: 120,
106
106
  desc: 'Default seconds to wait for a matching step before timing out.'
107
+ option :only_different_messages,
108
+ aliases: ['-d'],
109
+ type: :boolean,
110
+ default: true,
111
+ desc: 'Only show messages that differ when comparing results.'
107
112
  option :allow_commands,
108
113
  type: :boolean,
109
114
  default: false,
@@ -40,6 +40,13 @@ module Inferno
40
40
  type: :array,
41
41
  desc: 'Literal strings or regexes to normalize away before comparing ' \
42
42
  '(URL-encoded form of literal strings will also be normalized).'
43
+ },
44
+ only_different_messages: {
45
+ aliases: ['-d'],
46
+ type: :boolean,
47
+ default: false,
48
+ desc: 'When displaying messages in CSV output, only show mismatched messages ' \
49
+ '(hide matching ones).'
43
50
  }
44
51
  }.freeze
45
52
  def run
@@ -270,10 +277,66 @@ module Inferno
270
277
  UNKNOWN_MESSAGE_TYPE_ORDER = 99
271
278
 
272
279
  def build_message_comparisons
273
- expected_msgs = sorted_messages(expected_result)
274
- actual_msgs = sorted_messages(actual_result)
275
- max_length = [expected_msgs.size, actual_msgs.size].max
276
- (0...max_length).map { |i| messages_match?(expected_msgs[i], actual_msgs[i]) }
280
+ lcs_align(sorted_messages(expected_result), sorted_messages(actual_result))
281
+ end
282
+
283
+ def lcs_align(expected, actual)
284
+ lcs_backtrack(lcs_matrix(expected, actual), expected, actual)
285
+ end
286
+
287
+ def lcs_matrix(expected, actual)
288
+ matrix = Array.new(expected.size + 1) { Array.new(actual.size + 1, 0) }
289
+ (1..expected.size).each do |expected_index|
290
+ (1..actual.size).each do |actual_index|
291
+ matrix[expected_index][actual_index] =
292
+ if same_message?(expected[expected_index - 1], actual[actual_index - 1])
293
+ matrix[expected_index - 1][actual_index - 1] + 1
294
+ else
295
+ [matrix[expected_index - 1][actual_index],
296
+ matrix[expected_index][actual_index - 1]].max
297
+ end
298
+ end
299
+ end
300
+ matrix
301
+ end
302
+
303
+ def lcs_backtrack(matrix, expected, actual)
304
+ alignment = []
305
+ expected_index = expected.size
306
+ actual_index = actual.size
307
+
308
+ while expected_index.positive? || actual_index.positive?
309
+ step = lcs_backtrack_step(matrix, expected, actual, expected_index, actual_index)
310
+ alignment.unshift(step[:entry])
311
+ expected_index = step[:next_expected_index]
312
+ actual_index = step[:next_actual_index]
313
+ end
314
+ alignment
315
+ end
316
+
317
+ # Each step returns { entry:, next_expected_index:, next_actual_index: }.
318
+ # entry is { expected: msg_or_nil, actual: msg_or_nil, match: bool }.
319
+ def lcs_backtrack_step(matrix, expected, actual, expected_index, actual_index)
320
+ if expected_index.positive? &&
321
+ actual_index.positive? &&
322
+ same_message?(expected[expected_index - 1], actual[actual_index - 1])
323
+
324
+ # Items match: consume both
325
+ entry = { expected: expected[expected_index - 1], actual: actual[actual_index - 1], match: true }
326
+ { entry:, next_expected_index: expected_index - 1, next_actual_index: actual_index - 1 }
327
+ elsif actual_index.positive? &&
328
+ (expected_index.zero? ||
329
+ matrix[expected_index][actual_index - 1] >= matrix[expected_index - 1][actual_index])
330
+ # Item in actual is "Additional": consume only actual
331
+ { entry: { expected: nil, actual: actual[actual_index - 1], match: false },
332
+ next_expected_index: expected_index,
333
+ next_actual_index: actual_index - 1 }
334
+ else
335
+ # Item in expected is "Missing": consume only expected
336
+ { entry: { expected: expected[expected_index - 1], actual: nil, match: false },
337
+ next_expected_index: expected_index - 1,
338
+ next_actual_index: actual_index }
339
+ end
277
340
  end
278
341
 
279
342
  def sorted_messages(result)
@@ -283,15 +346,8 @@ module Inferno
283
346
  end
284
347
  end
285
348
 
286
- def messages_match?(expected_message, actual_message)
287
- expected_message.present? && actual_message.present? && same_message?(expected_message, actual_message)
288
- end
289
-
290
349
  def same_messages?
291
- return false unless expected_result['messages']&.size == actual_result['messages']&.size
292
- return true unless expected_result['messages'].present?
293
-
294
- message_comparisons.all?
350
+ message_comparisons.all? { |entry| entry[:match] }
295
351
  end
296
352
 
297
353
  def same_message?(expected_message, actual_message)
@@ -370,13 +426,28 @@ module Inferno
370
426
  def format_messages_for_csv(results)
371
427
  return '' unless results&.dig('messages').present?
372
428
 
373
- sorted_messages(results).each_with_index.map do |message, index|
374
- message_text_for_csv(message, index)
375
- end.join("\n")
429
+ is_expected = results == expected_result
430
+ lines = message_comparisons.filter_map { |entry| message_line_for_csv(entry, is_expected) }
431
+ collapse_message_lines(lines).join("\n")
432
+ end
433
+
434
+ def message_line_for_csv(entry, is_expected)
435
+ return if options[:only_different_messages] && entry[:match]
436
+
437
+ msg = is_expected ? entry[:expected] : entry[:actual]
438
+ return if msg.nil?
439
+
440
+ message_text_for_csv(msg, entry[:match])
441
+ end
442
+
443
+ def collapse_message_lines(lines)
444
+ lines.chunk_while { |a, b| a == b }.map do |group|
445
+ group.size > 1 ? "(#{group.size}) #{group.first}" : group.first
446
+ end
376
447
  end
377
448
 
378
- def message_text_for_csv(message, index)
379
- prefix = message_comparisons[index] ? '- ' : '! '
449
+ def message_text_for_csv(message, matches)
450
+ prefix = matches ? '- ' : '! '
380
451
  text = normalize_string(message['message'].to_s)
381
452
  .gsub("\r\n", '\n')
382
453
  .gsub("\n", '\n')
@@ -16,6 +16,7 @@ module Inferno
16
16
  field :locked, if: :field_present?
17
17
  field :hidden, if: :field_present?
18
18
  field :value, if: :field_present?
19
+ field :enable_when, if: :field_present?
19
20
  end
20
21
  end
21
22
  end
@@ -1,3 +1,4 @@
1
+ require 'logger'
1
2
  require 'active_support/all'
2
3
  require 'dotenv'
3
4
  require 'dry/system'
@@ -16,6 +16,10 @@ module Inferno
16
16
  # @option input_params [Hash] :options Possible input option formats based on input type
17
17
  # @option options [Array] :list_options Array of options for input formats
18
18
  # that require a list of possible values (radio and checkbox)
19
+ # @option input_params [Hash] :enable_when Conditions for showing the input. Must be a Hash
20
+ # with String :input_name (the name of the controlling input) and String :value (the value
21
+ # that triggers visibility). For checkbox inputs the value must be a JSON-encoded sorted
22
+ # array, e.g. '["a","b"]'.
19
23
  # @return [void]
20
24
  # @example
21
25
  # input :patient_id, title: 'Patient ID', description: 'The ID of the patient being searched for',
@@ -15,22 +15,25 @@ module Inferno
15
15
  :options,
16
16
  :locked,
17
17
  :hidden,
18
- :value
18
+ :value,
19
+ :enable_when
19
20
  ].freeze
20
21
  include Entities::Attributes
21
22
 
22
23
  # These attributes require special handling when merging input
23
24
  # definitions.
24
25
  UNINHERITABLE_ATTRIBUTES = [
25
- # Locking or hiding an input only has meaning at the level it is applied.
26
- # Consider:
26
+ # Locking, hiding, or conditional display only have meaning at the level
27
+ # they are applied. Consider:
27
28
  # - ParentGroup
28
29
  # - Group 1, input :a
29
- # - Group 2, input :a, locked: true, hidden: true, optional: true
30
- # The input 'a' should only be locked or hidden when running Group 2 in isolation.
31
- # It should not be locked or hidden when running Group 1 or the ParentGroup.
30
+ # - Group 2, input :a, locked: true, hidden: true, enable_when: {...}, optional: true
31
+ # The input 'a' should only be locked, hidden, or conditionally shown when
32
+ # running Group 2 in isolation. It should not inherit those when running
33
+ # Group 1 or the ParentGroup.
32
34
  :locked,
33
35
  :hidden,
36
+ :enable_when,
34
37
  # Input type is sometimes only a UI concern (e.g. text vs. textarea), so
35
38
  # it is common to not redeclare the type everywhere it's used and needs
36
39
  # special handling to avoid clobbering the type with the default (text)
@@ -59,6 +62,8 @@ module Inferno
59
62
  )
60
63
  end
61
64
 
65
+ assert_enable_when_shape!(params)
66
+
62
67
  params
63
68
  .compact
64
69
  .each { |key, value| send("#{key}=", value) }
@@ -66,6 +71,26 @@ module Inferno
66
71
  self.name = name.to_s if params[:name].present?
67
72
  end
68
73
 
74
+ def assert_enable_when_shape!(params)
75
+ enable_when = params[:enable_when]
76
+ return if enable_when.blank?
77
+ return if enable_when_valid?(enable_when)
78
+
79
+ raise Exceptions::InvalidAttributeException.new(
80
+ :enable_when,
81
+ self.class,
82
+ 'must be a Hash with a non-empty String :input_name and a String :value'
83
+ )
84
+ end
85
+
86
+ def enable_when_valid?(enable_when)
87
+ type_is_hash = enable_when.is_a?(Hash)
88
+ input_name_string_exists = enable_when[:input_name].is_a?(String) && enable_when[:input_name].present?
89
+ value_string_exists = enable_when.key?(:value) && enable_when[:value].is_a?(String)
90
+
91
+ type_is_hash && input_name_string_exists && value_string_exists
92
+ end
93
+
69
94
  # @private
70
95
  # Merge this input with an input belonging to a child. Fields defined on
71
96
  # this input take precedence over those defined on the child input.