Skip to content
Open
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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,17 @@ you'd get this instead:

[user-docs]: ./docs/users/getting-started.md

### Optional Extensions

If you need diffs for binary strings (`Encoding::ASCII_8BIT`),
require the binary string integration:

```ruby
require "super_diff/binary_string"
```

This enables hex-dump diffs and keeps binary data out of the expectation text.

## Support

My goal for this library is to improve your development experience.
Expand Down
12 changes: 12 additions & 0 deletions docs/users/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,18 @@ such as matchers.

You can now continue on to [customizing SuperDiff](./customization.md).

## Binary Strings

SuperDiff can diff binary strings (`Encoding::ASCII_8BIT`) using a hex-dump
format and a binary-safe inspection label.
To enable this, add:

```ruby
require "super_diff/binary_string"
```

You can create binary strings with `String#b` or by forcing the encoding.

## Using parts of SuperDiff directly

Although SuperDiff is primarily designed to integrate with RSpec,
Expand Down
28 changes: 28 additions & 0 deletions lib/super_diff/binary_string.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

require 'super_diff/binary_string/differs'
require 'super_diff/binary_string/inspection_tree_builders'
require 'super_diff/binary_string/operation_trees'
require 'super_diff/binary_string/operation_tree_builders'
require 'super_diff/binary_string/operation_tree_flatteners'

module SuperDiff
module BinaryString
def self.applies_to?(*values)
values.all? { |value| value.is_a?(::String) && value.encoding == Encoding::ASCII_8BIT }
end

SuperDiff.configure do |config|
config.prepend_extra_differ_classes(Differs::BinaryString)
config.prepend_extra_operation_tree_builder_classes(
OperationTreeBuilders::BinaryString
)
config.prepend_extra_operation_tree_classes(
OperationTrees::BinaryString
)
config.prepend_extra_inspection_tree_builder_classes(
InspectionTreeBuilders::BinaryString
)
end
end
end
12 changes: 12 additions & 0 deletions lib/super_diff/binary_string/differs.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

module SuperDiff
module BinaryString
module Differs
autoload(
:BinaryString,
'super_diff/binary_string/differs/binary_string'
)
end
end
end
19 changes: 19 additions & 0 deletions lib/super_diff/binary_string/differs/binary_string.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

module SuperDiff
module BinaryString
module Differs
class BinaryString < Core::AbstractDiffer
def self.applies_to?(expected, actual)
SuperDiff::BinaryString.applies_to?(expected, actual)
end

protected

def operation_tree_builder_class
OperationTreeBuilders::BinaryString
end
end
end
end
end
12 changes: 12 additions & 0 deletions lib/super_diff/binary_string/inspection_tree_builders.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

module SuperDiff
module BinaryString
module InspectionTreeBuilders
autoload(
:BinaryString,
'super_diff/binary_string/inspection_tree_builders/binary_string'
)
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

module SuperDiff
module BinaryString
module InspectionTreeBuilders
class BinaryString < Core::AbstractInspectionTreeBuilder
def self.applies_to?(value)
SuperDiff::BinaryString.applies_to?(value)
end

def call
Core::InspectionTree.new do |t|
t.add_text "<binary string (#{object.bytesize} bytes)>"
end
end
end
end
end
end
12 changes: 12 additions & 0 deletions lib/super_diff/binary_string/operation_tree_builders.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

module SuperDiff
module BinaryString
module OperationTreeBuilders
autoload(
:BinaryString,
'super_diff/binary_string/operation_tree_builders/binary_string'
)
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# frozen_string_literal: true

module SuperDiff
module BinaryString
module OperationTreeBuilders
class BinaryString < Basic::OperationTreeBuilders::MultilineString
BYTES_PER_LINE = 16
private_constant :BYTES_PER_LINE

def self.applies_to?(expected, actual)
SuperDiff::BinaryString.applies_to?(expected, actual)
end

def initialize(*args)
args.first[:expected] = binary_to_hex(args.first[:expected])
args.first[:actual] = binary_to_hex(args.first[:actual])

super
end

protected

def build_operation_tree
OperationTrees::BinaryString.new([])
end

# Prevent creation of BinaryOperation objects which the flattener
# cannot handle
def should_compare?(_operation, _next_operation)
false
end

private

def split_into_lines(string)
super.map { |line| line.delete_suffix("\n") }.reject(&:empty?)
end

def binary_to_hex(data)
data
.each_byte
.each_slice(BYTES_PER_LINE)
.with_index
.map { |bytes, index| format_hex_line(index * BYTES_PER_LINE, bytes) }
.join("\n")
end

def format_hex_line(offset, bytes)
hex_pairs = bytes
.map { |b| format('%02x', b) }
.each_slice(2)
.map(&:join)
.join(' ')

ascii = bytes.map { |b| printable_char(b) }.join

format('%08x: %-39s %s', offset, hex_pairs, ascii)
end

def printable_char(byte)
byte >= 32 && byte < 127 ? byte.chr : '.'
end
end
end
end
end
12 changes: 12 additions & 0 deletions lib/super_diff/binary_string/operation_tree_flatteners.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

module SuperDiff
module BinaryString
module OperationTreeFlatteners
autoload(
:BinaryString,
'super_diff/binary_string/operation_tree_flatteners/binary_string'
)
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

module SuperDiff
module BinaryString
module OperationTreeFlatteners
class BinaryString < Core::AbstractOperationTreeFlattener
def build_tiered_lines
operation_tree.map do |operation|
Core::Line.new(
type: operation.name,
indentation_level: indentation_level,
value: operation.value
)
end
end
end
end
end
end
12 changes: 12 additions & 0 deletions lib/super_diff/binary_string/operation_trees.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

module SuperDiff
module BinaryString
module OperationTrees
autoload(
:BinaryString,
'super_diff/binary_string/operation_trees/binary_string'
)
end
end
end
19 changes: 19 additions & 0 deletions lib/super_diff/binary_string/operation_trees/binary_string.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

module SuperDiff
module BinaryString
module OperationTrees
class BinaryString < Core::AbstractOperationTree
def self.applies_to?(value)
SuperDiff::BinaryString.applies_to?(value)
end

protected

def operation_tree_flattener_class
OperationTreeFlatteners::BinaryString
end
end
end
end
end
23 changes: 13 additions & 10 deletions lib/super_diff/core/tiered_lines_elider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,8 @@ def one_dimensional_line_tree?
end

def all_indentation_levels
lines
.map(&:indentation_level)
.select(&:positive?)
.uniq
levels = lines.map(&:indentation_level).uniq
normalized_indentation_levels(levels)
end

def find_boxes_to_elide_within(pane)
Expand Down Expand Up @@ -146,13 +144,10 @@ def normalized_box_groups_at_decreasing_indentation_levels_within(pane)
def box_groups_at_decreasing_indentation_levels_within(pane)
boxes_within_pane = boxes.select { |box| box.fits_fully_within?(pane) }

levels = boxes_within_pane.map(&:indentation_level).uniq

possible_indentation_levels =
boxes_within_pane
.map(&:indentation_level)
.select(&:positive?)
.uniq
.sort
.reverse
normalized_indentation_levels(levels).sort.reverse

possible_indentation_levels.map do |indentation_level|
boxes_within_pane.select do |box|
Expand All @@ -174,6 +169,14 @@ def filter_out_boxes_fully_contained_in_others(boxes)
end
end

def normalized_indentation_levels(levels)
# For flat structures (strings), include level 0
return levels if levels.all?(&:zero?)

# For nested structures (arrays, hashes), exclude level 0 (brackets)
levels.select(&:positive?)
end

def combine_congruent_boxes(boxes)
combine(boxes, on: :indentation_level)
end
Expand Down
6 changes: 6 additions & 0 deletions lib/super_diff/rspec/differ.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,16 @@ def comparing_proc_values?
end

def comparing_singleline_strings?
return false if comparing_binary_strings?

expected.is_a?(String) && actual.is_a?(String) &&
!expected.include?("\n") && !actual.include?("\n")
end

def comparing_binary_strings?
defined?(BinaryString) && BinaryString.applies_to?(expected, actual)
end

def helpers
@helpers ||= RSpecHelpers.new
end
Expand Down
Loading