megatest 0.1.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.
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :stopdoc:
4
+
5
+ module Megatest
6
+ class Output
7
+ module ANSIColors
8
+ extend self
9
+
10
+ def strip(text)
11
+ text.gsub(/\e\[(\d+(;\d+)?)?m/, "")
12
+ end
13
+
14
+ def red(text)
15
+ colorize(text, 31)
16
+ end
17
+
18
+ def green(text)
19
+ colorize(text, 32)
20
+ end
21
+
22
+ def yellow(text)
23
+ colorize(text, 33)
24
+ end
25
+
26
+ def blue(text)
27
+ colorize(text, 34)
28
+ end
29
+
30
+ def magenta(text)
31
+ colorize(text, 35)
32
+ end
33
+
34
+ def cyan(text)
35
+ colorize(text, 36)
36
+ end
37
+
38
+ def grey(text)
39
+ # TODO: somehow grey is invisible on my terminal (Terminal.app, Pro theme)
40
+ # Grey for unchanged lines in diff seems like a great idea, but need to figure out
41
+ # when it's safe to use.
42
+ # colorize(text, 8)
43
+ text
44
+ end
45
+
46
+ private
47
+
48
+ def colorize(text, color_code)
49
+ if text.end_with?("\n")
50
+ "\e[#{color_code}m#{text.delete_suffix("\n")}\e[0m\n"
51
+ else
52
+ "\e[#{color_code}m#{text}\e[0m"
53
+ end
54
+ end
55
+ end
56
+
57
+ module NoColors
58
+ extend self
59
+
60
+ def red(text)
61
+ text
62
+ end
63
+ alias_method :green, :red
64
+ alias_method :yellow, :red
65
+ alias_method :blue, :red
66
+ alias_method :magenta, :red
67
+ alias_method :cyan, :red
68
+ alias_method :grey, :red
69
+ end
70
+
71
+ attr_reader :color
72
+
73
+ def initialize(io, colors: nil)
74
+ raise ArgumentError, "don't nest outputs" if io.is_a?(Output)
75
+
76
+ @io = io
77
+ colors = io.tty? if colors.nil?
78
+ case colors
79
+ when true
80
+ @colors = true
81
+ @color = ANSIColors
82
+ when false
83
+ @colors = false
84
+ @color = NoColors
85
+ else
86
+ @color = colors
87
+ @colors = @color != NoColors
88
+ end
89
+ end
90
+
91
+ def colors?
92
+ @colors
93
+ end
94
+
95
+ def indent(text, depth: 2)
96
+ prefix = " " * depth
97
+ lines = text.lines
98
+ lines.map! { |l| "#{prefix}#{l}" }
99
+ lines.join
100
+ end
101
+
102
+ def colored(text)
103
+ if @colors
104
+ text
105
+ else
106
+ ANSIColors.strip(text)
107
+ end
108
+ end
109
+
110
+ def warning(message)
111
+ puts(yellow(message))
112
+ end
113
+
114
+ def error(message)
115
+ puts(red(message))
116
+ end
117
+
118
+ def print(*args)
119
+ @io.print(*args)
120
+ end
121
+
122
+ def <<(str)
123
+ @io << str
124
+ end
125
+
126
+ def puts(*args)
127
+ @io.puts(*args)
128
+ end
129
+
130
+ def red(text)
131
+ @color.red(text)
132
+ end
133
+
134
+ def green(text)
135
+ @color.green(text)
136
+ end
137
+
138
+ def yellow(text)
139
+ @color.yellow(text)
140
+ end
141
+
142
+ def blue(text)
143
+ @color.blue(text)
144
+ end
145
+
146
+ def magenta(text)
147
+ @color.magenta(text)
148
+ end
149
+
150
+ def cyan(text)
151
+ @color.cyan(text)
152
+ end
153
+
154
+ def grey(text)
155
+ @color.grey(text)
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,340 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :stopdoc:
4
+
5
+ module Megatest
6
+ # This code is a simplified version of the patience_diff gem
7
+ module PatienceDiff
8
+ # rubocop:disable Naming/MethodParameterName
9
+
10
+ # Matches indexed data (generally text) using the Patience diff algorithm.
11
+ class SequenceMatcher
12
+ attr_accessor :context
13
+
14
+ Card = Struct.new(:index, :value, :previous)
15
+
16
+ def initialize(context: 3)
17
+ @context = context
18
+ end
19
+
20
+ # Generate a diff of a and b using #diff_opcodes, and split the opcode into groups
21
+ # whenever an :equal range is encountered that is longer than @context * 2.
22
+ # Returns an array of arrays of 5-tuples as described for #diff_opcodes.
23
+ def grouped_opcodes(a, b)
24
+ groups = []
25
+ last_group = []
26
+ diff_opcodes(a, b).each do |opcode|
27
+ if opcode[0] == :equal
28
+ if @context.zero?
29
+ groups << last_group
30
+ last_group = []
31
+ next
32
+ end
33
+
34
+ code, a_start, a_end, b_start, b_end = *opcode
35
+
36
+ if (a_start.zero? && b_start.zero?) || (a_end == a.length - 1 && b_end == b.length - 1)
37
+ threshold = @context
38
+ else
39
+ threshold = @context * 2
40
+ end
41
+
42
+ if (b_end - b_start + 1) > threshold
43
+ unless last_group.empty?
44
+ last_group << [
45
+ code,
46
+ a_start,
47
+ a_start + @context - 1,
48
+ b_start,
49
+ b_start + @context - 1,
50
+ ]
51
+ groups << last_group
52
+ last_group = []
53
+ end
54
+ opcode = [
55
+ code,
56
+ a_end - @context + 1,
57
+ a_end,
58
+ b_end - @context + 1,
59
+ b_end,
60
+ ]
61
+ end
62
+ end
63
+ last_group << opcode
64
+ end
65
+ groups << last_group unless last_group.one? && (last_group.first[0] == :equal)
66
+ groups
67
+ end
68
+
69
+ # Generate a diff of a and b, and return an array of opcodes describing that diff.
70
+ # Each opcode represents a range in a and b that is either equal, only in a,
71
+ # or only in b. Opcodes are 5-tuples, in the format:
72
+ # 0: code
73
+ # A symbol indicating the diff operation. Can be :equal, :delete, or :insert.
74
+ # 1: a_start
75
+ # Index in a where the range begins
76
+ # 2: a_end
77
+ # Index in a where the range ends.
78
+ # 3: b_start
79
+ # Index in b where the range begins
80
+ # 4: b_end
81
+ # Index in b where the range ends.
82
+ #
83
+ # For :equal, (a_end - a_start) == (b_end - b_start).
84
+ # For :delete, a_start == a_end.
85
+ # For :insert, b_start == b_end.
86
+ def diff_opcodes(a, b)
87
+ sequences = collapse_matches(match(a, b))
88
+ sequences << [a.length, b.length, 0]
89
+
90
+ a_pos = b_pos = 0
91
+ opcodes = []
92
+ sequences.each do |(i, j, len)|
93
+ if a_pos < i
94
+ opcodes << [:delete, a_pos, i - 1, b_pos, b_pos]
95
+ end
96
+ if b_pos < j
97
+ opcodes << [:insert, a_pos, a_pos, b_pos, j - 1]
98
+ end
99
+ if len.positive?
100
+ opcodes << [:equal, i, i + len - 1, j, j + len - 1]
101
+ end
102
+ a_pos = i + len
103
+ b_pos = j + len
104
+ end
105
+ opcodes
106
+ end
107
+
108
+ private
109
+
110
+ def match(a, b)
111
+ matches = []
112
+ recursively_match(a, b, 0, 0, a.length, b.length) do |match|
113
+ matches << match
114
+ end
115
+ matches
116
+ end
117
+
118
+ def recursively_match(a, b, a_lo, b_lo, a_hi, b_hi, &block)
119
+ return if (a_lo == a_hi) || (b_lo == b_hi)
120
+
121
+ last_a_pos = a_lo - 1
122
+ last_b_pos = b_lo - 1
123
+
124
+ longest_unique_subsequence(a[a_lo...a_hi], b[b_lo...b_hi]).each do |(a_pos, b_pos)|
125
+ # recurse betwen unique lines
126
+ a_pos += a_lo
127
+ b_pos += b_lo
128
+ if (last_a_pos + 1 != a_pos) || (last_b_pos + 1 != b_pos)
129
+ recursively_match(a, b, last_a_pos + 1, last_b_pos + 1, a_pos, b_pos, &block)
130
+ end
131
+ last_a_pos = a_pos
132
+ last_b_pos = b_pos
133
+ yield [a_pos, b_pos]
134
+ end
135
+
136
+ if (last_a_pos >= a_lo) || (last_b_pos >= b_lo)
137
+ # there was at least one match
138
+ # recurse between last match and end
139
+ recursively_match(a, b, last_a_pos + 1, last_b_pos + 1, a_hi, b_hi, &block)
140
+ elsif a[a_lo] == b[b_lo]
141
+ # no unique lines
142
+ # diff forward from beginning
143
+ while (a_lo < a_hi) && (b_lo < b_hi) && (a[a_lo] == b[b_lo])
144
+ yield [a_lo, b_lo]
145
+ a_lo += 1
146
+ b_lo += 1
147
+ end
148
+ recursively_match(a, b, a_lo, b_lo, a_hi, b_hi, &block)
149
+ elsif a[a_hi - 1] == b[b_hi - 1]
150
+ # no unique lines
151
+ # diff back from end
152
+ a_mid = a_hi - 1
153
+ b_mid = b_hi - 1
154
+ while (a_mid > a_lo) && (b_mid > b_lo) && (a[a_mid - 1] == b[b_mid - 1])
155
+ a_mid -= 1
156
+ b_mid -= 1
157
+ end
158
+ recursively_match(a, b, a_lo, b_lo, a_mid, b_mid, &block)
159
+ (0...(a_hi - a_mid)).each do |i|
160
+ yield [a_mid + i, b_mid + i]
161
+ end
162
+ end
163
+ end
164
+
165
+ def collapse_matches(matches)
166
+ return [] if matches.empty?
167
+
168
+ sequences = []
169
+ start_a, start_b = *matches.first
170
+ len = 1
171
+ matches[1..].each do |(i_a, i_b)|
172
+ if (i_a == start_a + len) && (i_b == start_b + len)
173
+ len += 1
174
+ else
175
+ sequences << [start_a, start_b, len]
176
+ start_a = i_a
177
+ start_b = i_b
178
+ len = 1
179
+ end
180
+ end
181
+ sequences << [start_a, start_b, len]
182
+ sequences
183
+ end
184
+
185
+ def longest_unique_subsequence(a, b)
186
+ deck = Array.new(b.length)
187
+ unique_a = {}
188
+ unique_b = {}
189
+
190
+ a.each_with_index do |val, index|
191
+ if unique_a.key? val
192
+ unique_a[val] = nil
193
+ else
194
+ unique_a[val] = index
195
+ end
196
+ end
197
+
198
+ b.each_with_index do |val, index|
199
+ a_index = unique_a[val]
200
+ next unless a_index
201
+
202
+ dupe_index = unique_b[val]
203
+ if dupe_index
204
+ deck[dupe_index] = nil
205
+ unique_a.delete(val)
206
+ else
207
+ unique_b[val] = index
208
+ deck[index] = a_index
209
+ end
210
+ end
211
+
212
+ card = patience_sort(deck).last
213
+ result = []
214
+ while card
215
+ result.unshift [card.value, card.index]
216
+ card = card.previous
217
+ end
218
+ result
219
+ end
220
+
221
+ def patience_sort(deck)
222
+ piles = []
223
+ pile = 0
224
+ deck.each_with_index do |card_value, index|
225
+ next if card_value.nil?
226
+
227
+ card = Card.new(index, card_value)
228
+
229
+ if piles.any? && (piles.last.value < card_value)
230
+ pile = piles.size
231
+ elsif piles.any? && (piles[pile].value < card_value) &&
232
+ ((pile == piles.size - 1) || (piles[pile + 1].value > card_value))
233
+ pile += 1
234
+ else
235
+ pile = bisect(piles, card_value)
236
+ end
237
+
238
+ card.previous = piles[pile - 1] if pile.positive?
239
+
240
+ if pile < piles.size
241
+ # puts "putting card #{card.value} on pile #{pile}"
242
+ piles[pile] = card
243
+ else
244
+ # puts "putting card #{card.value} on new pile"
245
+ piles << card
246
+ end
247
+ end
248
+
249
+ piles
250
+ end
251
+
252
+ def bisect(piles, target)
253
+ low = 0
254
+ high = piles.size - 1
255
+ while low <= high
256
+ mid = (low + high) / 2
257
+ if piles[mid].value < target
258
+ low = mid + 1
259
+ else
260
+ high = mid - 1
261
+ end
262
+ end
263
+ low
264
+ end
265
+ end
266
+
267
+ # Formats a plaintext unified diff.
268
+ class Formatter
269
+ def initialize(differ, color)
270
+ @differ = differ
271
+ @color = color
272
+ end
273
+
274
+ def render_hunk_marker(opcodes)
275
+ a_start = opcodes.first[1] + 1
276
+ a_end = opcodes.last[2] + 2
277
+ b_start = opcodes.first[3] + 1
278
+ b_end = opcodes.last[4] + 2
279
+
280
+ @color.magenta(format("@@ -%d,%d +%d,%d @@", a_start, a_end - a_start, b_start, b_end - b_start))
281
+ end
282
+
283
+ def render_hunk(a, b, opcodes)
284
+ opcodes.flat_map do |(code, a_start, a_end, b_start, b_end)|
285
+ case code
286
+ when :equal
287
+ b[b_start..b_end].map { |line| @color.grey(" #{line}") }
288
+ when :delete
289
+ a[a_start..a_end].map { |line| @color.red("-#{line}") }
290
+ when :insert
291
+ b[b_start..b_end].map { |line| @color.green("+#{line}") }
292
+ end
293
+ end
294
+ end
295
+ end
296
+
297
+ class Differ
298
+ attr_reader :matcher
299
+
300
+ def initialize(color)
301
+ @formatter = Formatter.new(self, color)
302
+ @matcher = SequenceMatcher.new
303
+ end
304
+
305
+ # Generate a unified diff of the data specified. The left and right values should be strings, or any other indexable, sortable data.
306
+ # File names and timestamps do not affect the diff algorithm, but are used in the header text.
307
+ def diff_sequences(left, right)
308
+ hunks = @matcher.grouped_opcodes(left, right)
309
+
310
+ return nil if hunks.empty?
311
+
312
+ lines = []
313
+ first_hunk = true
314
+ hunks.each do |opcodes|
315
+ if first_hunk
316
+ first_hunk = false
317
+ else
318
+ lines << @formatter.render_hunk_marker(opcodes)
319
+ end
320
+ lines << @formatter.render_hunk(left, right, opcodes)
321
+ end
322
+ lines.flatten!
323
+ lines.compact!
324
+ lines
325
+ end
326
+
327
+ def diff_text(left, right)
328
+ left_lines = left.lines
329
+ right_lines = right.lines
330
+
331
+ left_lines[-1] += "\n" unless left_lines.empty? || left_lines.last.end_with?("\n")
332
+ right_lines[-1] += "\n" unless right_lines.empty? || right_lines.last.end_with?("\n")
333
+
334
+ diff_sequences(left_lines, right_lines)
335
+ end
336
+ end
337
+
338
+ # rubocop:enable Naming/MethodParameterName
339
+ end
340
+ end