Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions lib/backspin/backspin_result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,18 @@ def error_message
return nil unless verified? == false
return "Output verification failed" unless @command_diff

msg = "Output verification failed:\n\n"
msg += @command_diff.summary
msg += "\n#{@command_diff.diff}" if @command_diff.diff
msg
parts = ["Summary:"]
@command_diff.field_summary.each do |line|
parts << " #{line}"
end

diff = @command_diff.diff
if diff && !diff.empty?
parts << ""
parts << diff
end

parts.join("\n")
end

def success?
Expand Down
102 changes: 93 additions & 9 deletions lib/backspin/command_diff.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
module Backspin
# Represents the difference between expected and actual snapshots.
class CommandDiff
CONTEXT_LINES = 3
MAX_DIFF_LINES = 50

attr_reader :expected, :actual, :matcher

def initialize(expected:, actual:, matcher: nil, filter: nil, filter_on: :both)
Expand All @@ -26,6 +29,15 @@ def verified?
@verified = @matcher.match?
end

# @return [Array<String>] Per-field change status lines
def field_summary
lines = []
lines << stdout_field_summary
lines << stderr_field_summary
lines << status_field_summary
lines
end

# @return [String, nil] Human-readable diff if not verified
def diff
return nil if verified?
Expand All @@ -48,7 +60,8 @@ def diff
parts << "Exit status: expected #{expected_compare.status}, got #{actual_compare.status}"
end

parts.join("\n\n")
result = parts.join("\n\n")
maybe_truncate(result)
end

# @return [String] Single line summary for error messages
Expand All @@ -62,6 +75,30 @@ def summary

private

def stdout_field_summary
if expected_compare.stdout == actual_compare.stdout
"stdout: unchanged"
else
"stdout: changed"
end
end

def stderr_field_summary
if expected_compare.stderr == actual_compare.stderr
"stderr: unchanged"
else
"stderr: changed"
end
end

def status_field_summary
if expected_compare.status == actual_compare.status
"status: unchanged"
else
"status: changed (expected #{expected_compare.status}, actual #{actual_compare.status})"
end
end

def command_types_match?
expected.command_type == actual.command_type
end
Expand All @@ -83,25 +120,72 @@ def stderr_diff(expected, actual)
end

def generate_line_diff(expected, actual)
expected_lines = (expected || "").lines
actual_lines = (actual || "").lines
expected_lines = split_lines(expected)
actual_lines = split_lines(actual)
max_lines = [expected_lines.length, actual_lines.length].max

changed = Array.new(max_lines, false)
max_lines.times do |i|
changed[i] = (expected_lines[i] != actual_lines[i])
end

visible = Array.new(max_lines, false)
max_lines.times do |i|
next unless changed[i]
range_start = [i - CONTEXT_LINES, 0].max
range_end = [i + CONTEXT_LINES, max_lines - 1].min
(range_start..range_end).each { |j| visible[j] = true }
end

diff_lines = []
max_lines = [expected_lines.length, actual_lines.length].max
in_hunk = false

max_lines.times do |i|
expected_line = expected_lines[i]
actual_line = actual_lines[i]
unless visible[i]
if in_hunk
diff_lines << "..."
in_hunk = false
end
next
end

in_hunk = true

if expected_line != actual_line
diff_lines << "-#{expected_line.chomp}" if expected_line
diff_lines << "+#{actual_line.chomp}" if actual_line
if changed[i]
diff_lines << "-#{render_line(expected_lines[i])}" if i < expected_lines.length
diff_lines << "+#{render_line(actual_lines[i])}" if i < actual_lines.length
else
line = expected_lines[i] || actual_lines[i]
diff_lines << " #{render_line(line)}"
end
end

diff_lines.join("\n")
end

def split_lines(value)
(value || "").lines
end

def render_line(line)
line.to_s.chomp
end

def maybe_truncate(diff_text)
return diff_text if full_diff?

lines = diff_text.lines
return diff_text if lines.length <= MAX_DIFF_LINES

truncated = lines.first(MAX_DIFF_LINES).join
truncated.chomp!
truncated + "\n(diff truncated, set BACKSPIN_FULL_DIFF=1 for full output)"
end

def full_diff?
ENV["BACKSPIN_FULL_DIFF"] == "1"
end

attr_reader :expected_compare

attr_reader :actual_compare
Expand Down
4 changes: 2 additions & 2 deletions spec/backspin/backspin_result_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@
command_diff: command_diff
)

expect(result.error_message).to include("Output verification failed:")
expect(result.error_message).to include("Command failed")
expect(result.error_message).to include("Summary:")
expect(result.error_message).to include("stdout: changed")
expect(result.error_message).to include("[stdout]")
end

Expand Down
Loading