Class: Inferno::CLI::Session::SessionCompare::ComparedTestResult

Inherits:
Object
  • Object
show all
Defined in:
lib/inferno/apps/cli/session/session_compare.rb

Constant Summary collapse

MESSAGE_TYPE_ORDER =
{ 'error' => 0, 'warning' => 1, 'info' => 2 }.freeze
UNKNOWN_MESSAGE_TYPE_ORDER =
99

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(id, expected_result, actual_result, options, short_id_map = {}) ⇒ ComparedTestResult

Returns a new instance of ComparedTestResult.



195
196
197
198
199
200
201
202
# File 'lib/inferno/apps/cli/session/session_compare.rb', line 195

def initialize(id, expected_result, actual_result, options, short_id_map = {})
  @id = id
  @expected_result = expected_result
  @actual_result = actual_result
  @options = options
  @short_id_map = short_id_map
  @same = same_results?
end

Instance Attribute Details

#actual_resultObject (readonly)

Returns the value of attribute actual_result.



193
194
195
# File 'lib/inferno/apps/cli/session/session_compare.rb', line 193

def actual_result
  @actual_result
end

#expected_resultObject (readonly)

Returns the value of attribute expected_result.



193
194
195
# File 'lib/inferno/apps/cli/session/session_compare.rb', line 193

def expected_result
  @expected_result
end

#idObject (readonly)

Returns the value of attribute id.



193
194
195
# File 'lib/inferno/apps/cli/session/session_compare.rb', line 193

def id
  @id
end

#optionsObject (readonly)

Returns the value of attribute options.



193
194
195
# File 'lib/inferno/apps/cli/session/session_compare.rb', line 193

def options
  @options
end

#short_id_mapObject (readonly)

Returns the value of attribute short_id_map.



193
194
195
# File 'lib/inferno/apps/cli/session/session_compare.rb', line 193

def short_id_map
  @short_id_map
end

Instance Method Details

#build_message_comparisonsObject



279
280
281
# File 'lib/inferno/apps/cli/session/session_compare.rb', line 279

def build_message_comparisons
  lcs_align(sorted_messages(expected_result), sorted_messages(actual_result))
end

#collapse_message_lines(lines) ⇒ Object



443
444
445
446
447
# File 'lib/inferno/apps/cli/session/session_compare.rb', line 443

def collapse_message_lines(lines)
  lines.chunk_while { |a, b| a == b }.map do |group|
    group.size > 1 ? "(#{group.size}) #{group.first}" : group.first
  end
end

#comparison_csv_rowObject



401
402
403
404
405
406
407
408
409
410
411
412
413
414
# File 'lib/inferno/apps/cli/session/session_compare.rb', line 401

def comparison_csv_row
  row = [id, short_id, type, different_result?, expected_result&.dig('result'), actual_result&.dig('result')]
  if options[:compare_result_message]
    row << different_result_message?
    row << normalize_string(expected_result&.dig('result_message'))
    row << normalize_string(actual_result&.dig('result_message'))
  end
  if options[:compare_messages]
    row << different_messages?
    row << format_messages_for_csv(expected_result)
    row << format_messages_for_csv(actual_result)
  end
  row
end

#different_messages?Boolean

Returns:

  • (Boolean)


372
373
374
375
376
# File 'lib/inferno/apps/cli/session/session_compare.rb', line 372

def different_messages?
  return false unless type == 'Compared'

  !same_messages?
end

#different_result?Boolean

Returns:

  • (Boolean)


362
363
364
# File 'lib/inferno/apps/cli/session/session_compare.rb', line 362

def different_result?
  !same_result?
end

#different_result_message?Boolean

Returns:

  • (Boolean)


366
367
368
369
370
# File 'lib/inferno/apps/cli/session/session_compare.rb', line 366

def different_result_message?
  return false unless type == 'Compared'

  normalize_string(expected_result['result_message']) != normalize_string(actual_result['result_message'])
end

#format_messages_for_csv(results) ⇒ Object



426
427
428
429
430
431
432
# File 'lib/inferno/apps/cli/session/session_compare.rb', line 426

def format_messages_for_csv(results)
  return '' unless results&.dig('messages').present?

  is_expected = results == expected_result
  lines = message_comparisons.filter_map { |entry| message_line_for_csv(entry, is_expected) }
  collapse_message_lines(lines).join("\n")
end

#lcs_align(expected, actual) ⇒ Object



283
284
285
# File 'lib/inferno/apps/cli/session/session_compare.rb', line 283

def lcs_align(expected, actual)
  lcs_backtrack(lcs_matrix(expected, actual), expected, actual)
end

#lcs_backtrack(matrix, expected, actual) ⇒ Object



303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/inferno/apps/cli/session/session_compare.rb', line 303

def lcs_backtrack(matrix, expected, actual)
  alignment = []
  expected_index = expected.size
  actual_index = actual.size

  while expected_index.positive? || actual_index.positive?
    step = lcs_backtrack_step(matrix, expected, actual, expected_index, actual_index)
    alignment.unshift(step[:entry])
    expected_index = step[:next_expected_index]
    actual_index = step[:next_actual_index]
  end
  alignment
end

#lcs_backtrack_step(matrix, expected, actual, expected_index, actual_index) ⇒ Object

Each step returns { entry:, next_expected_index:, next_actual_index: }. entry is { expected: msg_or_nil, actual: msg_or_nil, match: bool }.



319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# File 'lib/inferno/apps/cli/session/session_compare.rb', line 319

def lcs_backtrack_step(matrix, expected, actual, expected_index, actual_index)
  if expected_index.positive? &&
     actual_index.positive? &&
     same_message?(expected[expected_index - 1], actual[actual_index - 1])

    # Items match: consume both
    entry = { expected: expected[expected_index - 1], actual: actual[actual_index - 1], match: true }
    { entry:, next_expected_index: expected_index - 1, next_actual_index: actual_index - 1 }
  elsif actual_index.positive? &&
        (expected_index.zero? ||
         matrix[expected_index][actual_index - 1] >= matrix[expected_index - 1][actual_index])
    # Item in actual is "Additional": consume only actual
    { entry: { expected: nil, actual: actual[actual_index - 1], match: false },
      next_expected_index: expected_index,
      next_actual_index: actual_index - 1 }
  else
    # Item in expected is "Missing": consume only expected
    { entry: { expected: expected[expected_index - 1], actual: nil, match: false },
      next_expected_index: expected_index - 1,
      next_actual_index: actual_index }
  end
end

#lcs_matrix(expected, actual) ⇒ Object



287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
# File 'lib/inferno/apps/cli/session/session_compare.rb', line 287

def lcs_matrix(expected, actual)
  matrix = Array.new(expected.size + 1) { Array.new(actual.size + 1, 0) }
  (1..expected.size).each do |expected_index|
    (1..actual.size).each do |actual_index|
      matrix[expected_index][actual_index] =
        if same_message?(expected[expected_index - 1], actual[actual_index - 1])
          matrix[expected_index - 1][actual_index - 1] + 1
        else
          [matrix[expected_index - 1][actual_index],
           matrix[expected_index][actual_index - 1]].max
        end
    end
  end
  matrix
end

#message_comparisonsObject



272
273
274
# File 'lib/inferno/apps/cli/session/session_compare.rb', line 272

def message_comparisons
  @message_comparisons ||= build_message_comparisons
end

#message_line_for_csv(entry, is_expected) ⇒ Object



434
435
436
437
438
439
440
441
# File 'lib/inferno/apps/cli/session/session_compare.rb', line 434

def message_line_for_csv(entry, is_expected)
  return if options[:only_different_messages] && entry[:match]

  msg = is_expected ? entry[:expected] : entry[:actual]
  return if msg.nil?

  message_text_for_csv(msg, entry[:match])
end

#message_text_for_csv(message, matches) ⇒ Object



449
450
451
452
453
454
455
456
457
# File 'lib/inferno/apps/cli/session/session_compare.rb', line 449

def message_text_for_csv(message, matches)
  prefix = matches ? '- ' : '! '
  text = normalize_string(message['message'].to_s)
    .gsub("\r\n", '\n')
    .gsub("\n", '\n')
    .gsub("\r", '\r')
    .gsub("\t", '\t')
  "#{prefix}(#{message['type']}) \"#{text}\""
end

#normalize_string(str) ⇒ Object



241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/inferno/apps/cli/session/session_compare.rb', line 241

def normalize_string(str)
  return str unless str.present?

  Array(options[:normalized_strings]).reduce(str) do |s, entry|
    parse_normalize_entry(entry).reduce(s) do |s2, (pattern, replacement)|
      if pattern.is_a?(Regexp)
        s2.gsub(pattern, replacement)
      else
        s2.gsub(pattern, replacement).gsub(CGI.escape(pattern), replacement)
      end
    end
  end
end

#normalizing?Boolean

Returns:

  • (Boolean)


255
256
257
# File 'lib/inferno/apps/cli/session/session_compare.rb', line 255

def normalizing?
  options[:normalized_strings].present?
end

#optional_to_h_fieldsObject



388
389
390
391
392
393
394
395
396
397
398
399
# File 'lib/inferno/apps/cli/session/session_compare.rb', line 388

def optional_to_h_fields
  fields = {}
  if options[:compare_result_message]
    fields[:expected_result_message] = expected_result&.dig('result_message')
    fields[:actual_result_message] = actual_result&.dig('result_message')
  end
  if options[:compare_messages]
    fields[:expected_messages] = expected_result&.dig('messages')
    fields[:actual_messages] = actual_result&.dig('messages')
  end
  fields
end

#parse_normalize_entry(entry) ⇒ Object

Parses a normalize entry into an array of [pattern, replacement] pairs. Entries may be:

- A plain string: literal match, replacement defaults to '<NORMALIZED>'
- A "/pattern/[flags]" string: compiled to Regexp, replacement defaults to '<NORMALIZED>'
- A hash with 'pattern' or 'patterns' and optional 'replacement' keys (from YAML):
  pattern: '/code_challenge=[A-Za-z0-9+\/=_-]{20,}/'
  replacement: '<CODE_CHALLENGE>'
Or multiple patterns sharing one replacement:
  patterns:
    - '/code_challenge=[A-Za-z0-9+\/=_-]{20,}/'
    - '/code_verifier=[A-Za-z0-9+\/=_-]{20,}/'
  replacement: '<PKCE_VALUE>'


220
221
222
223
224
225
226
227
228
229
# File 'lib/inferno/apps/cli/session/session_compare.rb', line 220

def parse_normalize_entry(entry)
  if entry.is_a?(Hash)
    replacement = entry.fetch('replacement', '<NORMALIZED>')
    Array(entry['patterns'] || entry['pattern']).map do |pattern|
      [parse_pattern_string(pattern.to_s), replacement]
    end
  else
    [[parse_pattern_string(entry.to_s), '<NORMALIZED>']]
  end
end

#parse_pattern_string(str) ⇒ Object



231
232
233
234
235
236
237
238
239
# File 'lib/inferno/apps/cli/session/session_compare.rb', line 231

def parse_pattern_string(str)
  return str unless (parsed_regex = str.match(%r{\A/(.+)/([imx]*)\z}m))

  flags = 0
  flags |= Regexp::IGNORECASE if parsed_regex[2].include?('i')
  flags |= Regexp::MULTILINE  if parsed_regex[2].include?('m')
  flags |= Regexp::EXTENDED   if parsed_regex[2].include?('x')
  Regexp.new(parsed_regex[1], flags)
end

#same_message?(expected_message, actual_message) ⇒ Boolean

Returns:

  • (Boolean)


353
354
355
356
# File 'lib/inferno/apps/cli/session/session_compare.rb', line 353

def same_message?(expected_message, actual_message)
  expected_message['type'] == actual_message['type'] &&
    normalize_string(expected_message['message']) == normalize_string(actual_message['message'])
end

#same_messages?Boolean

Returns:

  • (Boolean)


349
350
351
# File 'lib/inferno/apps/cli/session/session_compare.rb', line 349

def same_messages?
  message_comparisons.all? { |entry| entry[:match] }
end

#same_result?Boolean

Returns:

  • (Boolean)


358
359
360
# File 'lib/inferno/apps/cli/session/session_compare.rb', line 358

def same_result?
  @same
end

#same_results?Boolean

Returns:

  • (Boolean)


259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/inferno/apps/cli/session/session_compare.rb', line 259

def same_results?
  return false unless type == 'Compared'
  return false unless expected_result['result'] == actual_result['result']

  if options[:compare_result_message] &&
     normalize_string(expected_result['result_message']) != normalize_string(actual_result['result_message'])
    return false
  end
  return false if options[:compare_messages] && !same_messages?

  true
end

#short_idObject



204
205
206
# File 'lib/inferno/apps/cli/session/session_compare.rb', line 204

def short_id
  short_id_map[id]
end

#sorted_messages(result) ⇒ Object



342
343
344
345
346
347
# File 'lib/inferno/apps/cli/session/session_compare.rb', line 342

def sorted_messages(result)
  Array(result&.dig('messages')).sort_by do |m|
    [MESSAGE_TYPE_ORDER.fetch(m['type'].to_s, UNKNOWN_MESSAGE_TYPE_ORDER),
     normalize_string(m['message'].to_s)]
  end
end

#to_hObject



378
379
380
381
382
383
384
385
386
# File 'lib/inferno/apps/cli/session/session_compare.rb', line 378

def to_h
  {
    id: id,
    type: type,
    matched: same_result?,
    expected_result: expected_result&.dig('result'),
    actual_result: actual_result&.dig('result')
  }.merge(optional_to_h_fields)
end

#typeObject



416
417
418
419
420
421
422
423
424
# File 'lib/inferno/apps/cli/session/session_compare.rb', line 416

def type
  if expected_result.nil?
    'Additional'
  elsif actual_result.nil?
    'Missing'
  else
    'Compared'
  end
end