Skip to content

Commit d7e4cc6

Browse files
committed
Add opt-in binary string diff support
Add a `SuperDiff::BinaryString` extension that renders `ASCII-8BIT` strings as hex dumps. Users opt-in by requiring the module: ``` require 'super_diff/binary_string' ``` Includes a dedicated differ, inspection tree builder, operation tree builder/tree/flattener, and updates SuperDiff::RSpec::Differ to allow diffs for binary strings (skipping the single-line string short- circuit). Hex dumps use 16 bytes per line with offset, hex pairs, and ASCII representation (xxd-style output).
1 parent aa57610 commit d7e4cc6

18 files changed

Lines changed: 536 additions & 0 deletions

File tree

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,17 @@ you'd get this instead:
9292

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

95+
### Optional Extensions
96+
97+
If you need diffs for binary strings (`Encoding::ASCII_8BIT`),
98+
require the binary string integration:
99+
100+
```ruby
101+
require "super_diff/binary_string"
102+
```
103+
104+
This enables hex-dump diffs and keeps binary data out of the expectation text.
105+
95106
## Support
96107

97108
My goal for this library is to improve your development experience.

docs/users/getting-started.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,18 @@ such as matchers.
8686

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

89+
## Binary Strings
90+
91+
SuperDiff can diff binary strings (`Encoding::ASCII_8BIT`) using a hex-dump
92+
format and a binary-safe inspection label.
93+
To enable this, add:
94+
95+
```ruby
96+
require "super_diff/binary_string"
97+
```
98+
99+
You can create binary strings with `String#b` or by forcing the encoding.
100+
89101
## Using parts of SuperDiff directly
90102

91103
Although SuperDiff is primarily designed to integrate with RSpec,

lib/super_diff/binary_string.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# frozen_string_literal: true
2+
3+
require 'super_diff/binary_string/differs'
4+
require 'super_diff/binary_string/inspection_tree_builders'
5+
require 'super_diff/binary_string/operation_trees'
6+
require 'super_diff/binary_string/operation_tree_builders'
7+
require 'super_diff/binary_string/operation_tree_flatteners'
8+
9+
module SuperDiff
10+
module BinaryString
11+
def self.applies_to?(*values)
12+
values.all? { |value| value.is_a?(::String) && value.encoding == Encoding::ASCII_8BIT }
13+
end
14+
15+
SuperDiff.configure do |config|
16+
config.prepend_extra_differ_classes(Differs::BinaryString)
17+
config.prepend_extra_operation_tree_builder_classes(
18+
OperationTreeBuilders::BinaryString
19+
)
20+
config.prepend_extra_operation_tree_classes(
21+
OperationTrees::BinaryString
22+
)
23+
config.prepend_extra_inspection_tree_builder_classes(
24+
InspectionTreeBuilders::BinaryString
25+
)
26+
end
27+
end
28+
end
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# frozen_string_literal: true
2+
3+
module SuperDiff
4+
module BinaryString
5+
module Differs
6+
autoload(
7+
:BinaryString,
8+
'super_diff/binary_string/differs/binary_string'
9+
)
10+
end
11+
end
12+
end
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# frozen_string_literal: true
2+
3+
module SuperDiff
4+
module BinaryString
5+
module Differs
6+
class BinaryString < Core::AbstractDiffer
7+
def self.applies_to?(expected, actual)
8+
SuperDiff::BinaryString.applies_to?(expected, actual)
9+
end
10+
11+
protected
12+
13+
def operation_tree_builder_class
14+
OperationTreeBuilders::BinaryString
15+
end
16+
end
17+
end
18+
end
19+
end
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# frozen_string_literal: true
2+
3+
module SuperDiff
4+
module BinaryString
5+
module InspectionTreeBuilders
6+
autoload(
7+
:BinaryString,
8+
'super_diff/binary_string/inspection_tree_builders/binary_string'
9+
)
10+
end
11+
end
12+
end
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# frozen_string_literal: true
2+
3+
module SuperDiff
4+
module BinaryString
5+
module InspectionTreeBuilders
6+
class BinaryString < Core::AbstractInspectionTreeBuilder
7+
def self.applies_to?(value)
8+
SuperDiff::BinaryString.applies_to?(value)
9+
end
10+
11+
def call
12+
Core::InspectionTree.new do |t|
13+
t.add_text "<binary string (#{object.bytesize} bytes)>"
14+
end
15+
end
16+
end
17+
end
18+
end
19+
end
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# frozen_string_literal: true
2+
3+
module SuperDiff
4+
module BinaryString
5+
module OperationTreeBuilders
6+
autoload(
7+
:BinaryString,
8+
'super_diff/binary_string/operation_tree_builders/binary_string'
9+
)
10+
end
11+
end
12+
end
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# frozen_string_literal: true
2+
3+
module SuperDiff
4+
module BinaryString
5+
module OperationTreeBuilders
6+
class BinaryString < Basic::OperationTreeBuilders::MultilineString
7+
BYTES_PER_LINE = 16
8+
private_constant :BYTES_PER_LINE
9+
10+
def self.applies_to?(expected, actual)
11+
SuperDiff::BinaryString.applies_to?(expected, actual)
12+
end
13+
14+
def initialize(*args)
15+
args.first[:expected] = binary_to_hex(args.first[:expected])
16+
args.first[:actual] = binary_to_hex(args.first[:actual])
17+
18+
super
19+
end
20+
21+
protected
22+
23+
def build_operation_tree
24+
OperationTrees::BinaryString.new([])
25+
end
26+
27+
# Prevent creation of BinaryOperation objects which the flattener
28+
# cannot handle
29+
def should_compare?(_operation, _next_operation)
30+
false
31+
end
32+
33+
private
34+
35+
def split_into_lines(string)
36+
super.map { |line| line.delete_suffix("\n") }.reject(&:empty?)
37+
end
38+
39+
def binary_to_hex(data)
40+
data
41+
.each_byte
42+
.each_slice(BYTES_PER_LINE)
43+
.with_index
44+
.map { |bytes, index| format_hex_line(index * BYTES_PER_LINE, bytes) }
45+
.join("\n")
46+
end
47+
48+
def format_hex_line(offset, bytes)
49+
hex_pairs = bytes
50+
.map { |b| format('%02x', b) }
51+
.each_slice(2)
52+
.map(&:join)
53+
.join(' ')
54+
55+
ascii = bytes.map { |b| printable_char(b) }.join
56+
57+
format('%<offset>08x: %<hex>-39s %<ascii>s', offset:, hex: hex_pairs, ascii:)
58+
end
59+
60+
def printable_char(byte)
61+
byte >= 32 && byte < 127 ? byte.chr : '.'
62+
end
63+
end
64+
end
65+
end
66+
end
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# frozen_string_literal: true
2+
3+
module SuperDiff
4+
module BinaryString
5+
module OperationTreeFlatteners
6+
autoload(
7+
:BinaryString,
8+
'super_diff/binary_string/operation_tree_flatteners/binary_string'
9+
)
10+
end
11+
end
12+
end

0 commit comments

Comments
 (0)