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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/README.md +156 -0
- data/TODO.md +17 -0
- data/exe/megatest +7 -0
- data/lib/megatest/assertions.rb +474 -0
- data/lib/megatest/backtrace.rb +70 -0
- data/lib/megatest/cli.rb +249 -0
- data/lib/megatest/compat.rb +74 -0
- data/lib/megatest/config.rb +281 -0
- data/lib/megatest/differ.rb +136 -0
- data/lib/megatest/dsl.rb +164 -0
- data/lib/megatest/executor.rb +104 -0
- data/lib/megatest/multi_process.rb +263 -0
- data/lib/megatest/output.rb +158 -0
- data/lib/megatest/patience_diff.rb +340 -0
- data/lib/megatest/pretty_print.rb +309 -0
- data/lib/megatest/queue.rb +239 -0
- data/lib/megatest/queue_monitor.rb +35 -0
- data/lib/megatest/queue_reporter.rb +42 -0
- data/lib/megatest/redis_queue.rb +459 -0
- data/lib/megatest/reporters.rb +266 -0
- data/lib/megatest/runner.rb +119 -0
- data/lib/megatest/runtime.rb +168 -0
- data/lib/megatest/selector.rb +293 -0
- data/lib/megatest/state.rb +708 -0
- data/lib/megatest/subprocess/main.rb +8 -0
- data/lib/megatest/subprocess.rb +48 -0
- data/lib/megatest/test.rb +115 -0
- data/lib/megatest/test_task.rb +132 -0
- data/lib/megatest/version.rb +5 -0
- data/lib/megatest.rb +123 -0
- metadata +80 -0
|
@@ -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
|