minitest-holdify 1.0.2 → 1.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 +4 -4
- data/lib/holdify/hold.rb +65 -0
- data/lib/holdify/pretty.rb +90 -0
- data/lib/holdify/store.rb +32 -17
- data/lib/holdify.rb +8 -53
- data/lib/minitest/holdify_plugin.rb +34 -29
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c91100bb90de5695419fdb279520529462f67f37d2e580ce6d61c496cf43cbcb
|
|
4
|
+
data.tar.gz: 48b9d63b74bdea68de55c7a2790e8fb1fef59dc1eb956bce96b02d7ef0b141b3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c17c6cfea957cbad41b2f272da130226a1c878546b46559ec80ccb1f939cb8c395c3b91a91e98ada8a1760bf3b661568364f6ca22f2e34ac957961c6011b4a40
|
|
7
|
+
data.tar.gz: 89f7c53a3ae05ef7e302efff24398e1579067d246a913c2ca91029185e08af9d8b4dae35da3a6686def351ce6fa89b0b9da1a34d388526dfa1455f5a5950e170
|
data/lib/holdify/hold.rb
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Holdify
|
|
4
|
+
# The *_hold statement (Assertion/Expectation)
|
|
5
|
+
class Hold
|
|
6
|
+
attr_reader :forced
|
|
7
|
+
|
|
8
|
+
def initialize(test)
|
|
9
|
+
@test = test
|
|
10
|
+
@path, = test.method(test.name).source_location
|
|
11
|
+
@store = Holdify.stores[@path] ||= Store.new(@path)
|
|
12
|
+
@session = Hash.new { |h, k| h[k] = [] } # { lineno => [values] }
|
|
13
|
+
@forced = [] # [ "file:lineno" ]
|
|
14
|
+
@added = [] # [ "file:lineno" ]
|
|
15
|
+
@index = {} # { lineno => index }
|
|
16
|
+
@counts = Hash.new(0) # { id => count }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def call(actual, force: false)
|
|
20
|
+
location = find_location
|
|
21
|
+
lineno = location.lineno
|
|
22
|
+
id = @store.id_at(lineno)
|
|
23
|
+
raise "Could not find holdify statement at line #{lineno}" unless id
|
|
24
|
+
|
|
25
|
+
unless @index.key?(lineno)
|
|
26
|
+
@index[lineno] = @counts[id]
|
|
27
|
+
@counts[id] += 1
|
|
28
|
+
end
|
|
29
|
+
index = @index[lineno]
|
|
30
|
+
|
|
31
|
+
@session[lineno] << actual
|
|
32
|
+
@forced << "#{location.path}:#{lineno}" if force
|
|
33
|
+
|
|
34
|
+
return actual if force || Holdify.reconcile
|
|
35
|
+
|
|
36
|
+
stored = @store.stored(id, index)
|
|
37
|
+
index = @session[lineno].size - 1
|
|
38
|
+
return stored[index] if stored && index < stored.size
|
|
39
|
+
|
|
40
|
+
@added << "#{location.path}:#{lineno}"
|
|
41
|
+
actual
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def save
|
|
45
|
+
return unless @test.failures.empty?
|
|
46
|
+
|
|
47
|
+
@added.each { |loc| warn "[holdify] Held new value for #{loc}" } unless Holdify.quiet
|
|
48
|
+
@session.each do |lineno, values|
|
|
49
|
+
id = @store.id_at(lineno)
|
|
50
|
+
index = @index[lineno]
|
|
51
|
+
@store.update(lineno, id, values, index)
|
|
52
|
+
end
|
|
53
|
+
@store.save
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def find_location
|
|
57
|
+
caller_locations.find do |location|
|
|
58
|
+
next unless location.path == @path
|
|
59
|
+
|
|
60
|
+
label = location.base_label
|
|
61
|
+
label == @test.name || label == '<top (required)>' || label == '<main>' || label.start_with?('<class:', '<module:')
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
require 'tempfile'
|
|
5
|
+
require 'open3'
|
|
6
|
+
|
|
7
|
+
module Holdify
|
|
8
|
+
# Generates a pretty diff using git
|
|
9
|
+
module Pretty
|
|
10
|
+
RESET = "\e[0m"
|
|
11
|
+
RED_FG = "\e[31m"
|
|
12
|
+
GREEN_FG = "\e[32m"
|
|
13
|
+
|
|
14
|
+
G_BASE = "\e[100m #{RESET} ".freeze
|
|
15
|
+
G_EXP = "\e[41m\e[97m\e[1m-#{RESET} ".freeze
|
|
16
|
+
G_ACT = "\e[42m\e[97m\e[1m+#{RESET} ".freeze
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
def call(expected, actual)
|
|
20
|
+
exp_file = create_tempfile(expected)
|
|
21
|
+
act_file = create_tempfile(actual)
|
|
22
|
+
|
|
23
|
+
cmd = %W[git diff --no-index --word-diff=porcelain --unified=1000 #{exp_file.path} #{act_file.path}]
|
|
24
|
+
stdout, _stderr, _status = Open3.capture3(*cmd)
|
|
25
|
+
|
|
26
|
+
return nil if stdout.empty?
|
|
27
|
+
|
|
28
|
+
format(stdout)
|
|
29
|
+
rescue Errno::ENOENT
|
|
30
|
+
nil
|
|
31
|
+
ensure
|
|
32
|
+
exp_file&.close!
|
|
33
|
+
act_file&.close!
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def format(diff)
|
|
39
|
+
lines = diff.lines.map(&:chomp)
|
|
40
|
+
start = lines.index { |l| l.start_with?('@@') }
|
|
41
|
+
return nil unless start
|
|
42
|
+
|
|
43
|
+
output = ["#{G_EXP}#{RED_FG}Held (Expected)#{RESET}", "#{G_ACT}#{GREEN_FG}Current (Actual)#{RESET}"]
|
|
44
|
+
exp_buf = +''
|
|
45
|
+
act_buf = +''
|
|
46
|
+
|
|
47
|
+
lines[(start + 1)..].each do |line|
|
|
48
|
+
char = line[0]
|
|
49
|
+
text = line[1..]
|
|
50
|
+
|
|
51
|
+
# :nocov:
|
|
52
|
+
case char
|
|
53
|
+
# :nocov:
|
|
54
|
+
when ' '
|
|
55
|
+
exp_buf << text
|
|
56
|
+
act_buf << text
|
|
57
|
+
when '-'
|
|
58
|
+
exp_buf << RED_FG << text << RESET
|
|
59
|
+
when '+'
|
|
60
|
+
act_buf << GREEN_FG << text << RESET
|
|
61
|
+
when '~'
|
|
62
|
+
flush_buffers(output, exp_buf, act_buf)
|
|
63
|
+
exp_buf.clear
|
|
64
|
+
act_buf.clear
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
flush_buffers(output, exp_buf, act_buf) unless exp_buf.empty? && act_buf.empty?
|
|
69
|
+
|
|
70
|
+
output.join("\n")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def flush_buffers(output, exp, act)
|
|
74
|
+
if exp == act
|
|
75
|
+
output << "#{G_BASE}#{exp}"
|
|
76
|
+
else
|
|
77
|
+
output << "#{G_EXP}#{exp}"
|
|
78
|
+
output << "#{G_ACT}#{act}"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def create_tempfile(obj)
|
|
83
|
+
Tempfile.new(%w[holdify .yaml]).tap do |file|
|
|
84
|
+
file.write(YAML.dump(obj, line_width: 78)) # Ensure 80 columns (including gutter)
|
|
85
|
+
file.flush
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
data/lib/holdify/store.rb
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require 'yaml'
|
|
4
4
|
require 'fileutils'
|
|
5
|
+
require 'digest/sha1'
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
module Holdify
|
|
7
8
|
# A simple Hash-based store that syncs with a static source map
|
|
8
9
|
class Store
|
|
9
10
|
def initialize(source_path)
|
|
@@ -16,38 +17,52 @@ class Holdify
|
|
|
16
17
|
@source[lineno] = Digest::SHA1.hexdigest(content) unless content.empty?
|
|
17
18
|
end
|
|
18
19
|
|
|
20
|
+
@index = Hash.new { |h, k| h[k] = [] } # { id => ["L123 id", "L124 id"] }
|
|
19
21
|
@data = (File.exist?(@path) && YAML.unsafe_load_file(@path)) || {} # { key => [values] }
|
|
20
|
-
@index = {} # { id => "L123 id" }
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
@data.keep_if do |key, _|
|
|
24
|
-
id = key.split.last
|
|
25
|
-
next false unless valid_ids.include?(id)
|
|
26
|
-
|
|
27
|
-
@index[id] = key
|
|
28
|
-
true
|
|
29
|
-
end
|
|
23
|
+
organize_data
|
|
30
24
|
end
|
|
31
25
|
|
|
32
|
-
def id_at(lineno)
|
|
33
|
-
def stored(id)
|
|
26
|
+
def id_at(lineno) = @source[lineno]
|
|
27
|
+
def stored(id, index) = @data[@index[id][index]]
|
|
34
28
|
|
|
35
29
|
# Overwrite the entry for a given ID with a new list of values
|
|
36
|
-
def update(id, values)
|
|
37
|
-
new_key = "L#{
|
|
38
|
-
old_key = @index[id]
|
|
30
|
+
def update(lineno, id, values, index)
|
|
31
|
+
new_key = "L#{lineno} #{id}"
|
|
32
|
+
old_key = @index[id][index]
|
|
39
33
|
@data.delete(old_key) if old_key && old_key != new_key
|
|
40
|
-
@data[
|
|
34
|
+
@data[new_key] = values
|
|
35
|
+
@index[id][index] = new_key
|
|
41
36
|
end
|
|
42
37
|
|
|
43
38
|
def save
|
|
44
39
|
return FileUtils.rm_f(@path) if @data.empty?
|
|
45
40
|
|
|
46
41
|
sorted = @data.sort_by { |k, _| k[/\d+/].to_i }.to_h
|
|
47
|
-
content = YAML.dump(sorted, line_width:
|
|
42
|
+
content = YAML.dump(sorted, line_width: 78)
|
|
48
43
|
return if File.exist?(@path) && File.read(@path) == content
|
|
49
44
|
|
|
50
45
|
File.write(@path, content)
|
|
51
46
|
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def organize_data
|
|
51
|
+
source_counts = @source.values.tally
|
|
52
|
+
sorted_keys = @data.keys.sort_by { |k| k[/\d+/].to_i }
|
|
53
|
+
|
|
54
|
+
sorted_keys.each do |key|
|
|
55
|
+
id = key.split.last
|
|
56
|
+
next unless source_counts[id]
|
|
57
|
+
|
|
58
|
+
if @index[id].size < source_counts[id]
|
|
59
|
+
@index[id] << key
|
|
60
|
+
else
|
|
61
|
+
@data.delete(key)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
@data.keep_if { |key, _| source_counts[key.split.last] }
|
|
66
|
+
end
|
|
52
67
|
end
|
|
53
68
|
end
|
data/lib/holdify.rb
CHANGED
|
@@ -1,69 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'digest/sha1'
|
|
4
3
|
require_relative 'holdify/store'
|
|
4
|
+
require_relative 'holdify/hold'
|
|
5
5
|
|
|
6
6
|
# Add description
|
|
7
|
-
|
|
8
|
-
VERSION = '1.0
|
|
7
|
+
module Holdify
|
|
8
|
+
VERSION = '1.1.0'
|
|
9
9
|
CONFIG = { ext: '.yaml' }.freeze
|
|
10
10
|
|
|
11
11
|
class << self
|
|
12
12
|
attr_accessor :reconcile, :quiet
|
|
13
|
+
attr_writer :pretty
|
|
13
14
|
|
|
14
15
|
def stores = @stores ||= {}
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
attr_reader :forced
|
|
18
|
-
|
|
19
|
-
def initialize(test)
|
|
20
|
-
@test = test
|
|
21
|
-
@path, = test.method(test.name).source_location
|
|
22
|
-
@store = self.class.stores[@path] ||= Store.new(@path)
|
|
23
|
-
@session = Hash.new { |h, k| h[k] = [] }
|
|
24
|
-
@forced = []
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def hold(actual, force: false)
|
|
28
|
-
location = find_location
|
|
29
|
-
id = @store.id_at(location.lineno)
|
|
30
|
-
raise "Could not find holdify statement at line #{location.lineno}" unless id
|
|
31
|
-
|
|
32
|
-
@session[id] << actual
|
|
33
|
-
@forced << "#{location.path}:#{location.lineno}" if force
|
|
34
|
-
|
|
35
|
-
return actual if force || self.class.reconcile
|
|
36
|
-
|
|
37
|
-
stored = @store.stored(id)
|
|
38
|
-
index = @session[id].size - 1
|
|
39
|
-
return stored[index] if stored && index < stored.size
|
|
40
|
-
|
|
41
|
-
# :nocov:
|
|
42
|
-
warn "[holdify] Held new value for #{location.path}:#{location.lineno}" unless self.class.quiet
|
|
43
|
-
# :nocov:
|
|
44
|
-
actual
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def save
|
|
48
|
-
return unless @test.failures.empty?
|
|
49
|
-
|
|
50
|
-
@session.each { |id, values| @store.update(id, values) }
|
|
51
|
-
@store.save
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def find_location
|
|
55
|
-
caller_locations.find do |location|
|
|
56
|
-
next unless location.path == @path
|
|
57
16
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
label = ::Regexp.last_match(1) if label.start_with?('block ') && label =~ / in (.+)$/
|
|
61
|
-
# :nocov:
|
|
17
|
+
def pretty
|
|
18
|
+
return @pretty unless @pretty.nil?
|
|
62
19
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
label == '<main>' ||
|
|
66
|
-
label.start_with?('<class:', '<module:')
|
|
20
|
+
@pretty = $stdout.tty? && !ENV.key?('NO_COLOR') && ENV['TERM'] != 'dumb' &&
|
|
21
|
+
system('git --version', out: File::NULL, err: File::NULL)
|
|
67
22
|
end
|
|
68
23
|
end
|
|
69
24
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'holdify'
|
|
4
|
+
require 'holdify/pretty'
|
|
4
5
|
|
|
5
6
|
# Implement the minitest plugin
|
|
6
7
|
module Minitest
|
|
@@ -10,30 +11,25 @@ module Minitest
|
|
|
10
11
|
Holdify.reconcile = true
|
|
11
12
|
Holdify.quiet = true
|
|
12
13
|
end
|
|
13
|
-
# :nocov:
|
|
14
14
|
opts.on '--holdify-quiet', 'Skip the warning on storing a new value' do
|
|
15
15
|
Holdify.quiet = true
|
|
16
16
|
end
|
|
17
|
-
|
|
17
|
+
opts.on '--holdify-pretty', 'Format the stored values with pretty print' do
|
|
18
|
+
Holdify.pretty = true
|
|
19
|
+
end
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
# Reopen the minitest class
|
|
21
23
|
class Test
|
|
22
|
-
# Create the @holdify instance for each test
|
|
23
|
-
def after_setup
|
|
24
|
-
super
|
|
25
|
-
@holdify ||= Holdify.new(self) # rubocop:disable Naming/MemoizedInstanceVariableName
|
|
26
|
-
end
|
|
27
|
-
|
|
28
24
|
# Ensure store is tidied and saved after the test runs
|
|
29
25
|
def before_teardown
|
|
30
26
|
super
|
|
31
|
-
@
|
|
32
|
-
return unless @
|
|
27
|
+
@hold&.save
|
|
28
|
+
return unless @hold&.forced&.any? && failures.empty?
|
|
33
29
|
|
|
34
30
|
msg = <<~MSG.chomp
|
|
35
31
|
[holdify] the value has been stored: remove the "!" suffix to pass the test
|
|
36
|
-
#{@
|
|
32
|
+
#{@hold.forced.uniq.map { |l| " #{l}" }.join("\n")}
|
|
37
33
|
MSG
|
|
38
34
|
raise Minitest::Assertion, msg
|
|
39
35
|
end
|
|
@@ -42,38 +38,47 @@ module Minitest
|
|
|
42
38
|
# Reopen the minitest module
|
|
43
39
|
module Assertions
|
|
44
40
|
# Main assertion
|
|
45
|
-
def assert_hold(actual, *args)
|
|
46
|
-
@
|
|
41
|
+
def assert_hold(actual, *args, inspect: false, **)
|
|
42
|
+
@hold ||= Holdify::Hold.new(self)
|
|
47
43
|
assertion, message = args
|
|
48
44
|
assertion, message = message, assertion unless assertion.nil? || assertion.is_a?(Symbol)
|
|
45
|
+
expected = @hold.(actual, **)
|
|
46
|
+
|
|
47
|
+
begin
|
|
48
|
+
if actual.nil?
|
|
49
|
+
assert_nil expected, message
|
|
50
|
+
else
|
|
51
|
+
send(assertion || :assert_equal, expected, actual, message)
|
|
52
|
+
end
|
|
53
|
+
rescue Minitest::Assertion
|
|
54
|
+
raise unless Holdify.pretty
|
|
55
|
+
|
|
56
|
+
diff = Holdify::Pretty.call(expected, actual)
|
|
57
|
+
raise unless diff
|
|
49
58
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
assert_nil expected, message
|
|
53
|
-
else
|
|
54
|
-
send(assertion || :assert_equal, expected, actual, message)
|
|
59
|
+
msg = message ? "#{message}\n#{diff}" : diff
|
|
60
|
+
raise Minitest::Assertion, msg
|
|
55
61
|
end
|
|
62
|
+
|
|
63
|
+
if inspect
|
|
64
|
+
location = @hold.find_location
|
|
65
|
+
warn "[holdify] The value from #{location.path}:#{location.lineno} is:\n[holdify] => #{actual.inspect}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
expected
|
|
56
69
|
end
|
|
57
70
|
|
|
58
71
|
# Temporarily used to store the actual value, useful for reconciliation of expected changed values
|
|
59
|
-
def assert_hold!(
|
|
60
|
-
@holdify ||= Holdify.new(self)
|
|
61
|
-
@holdify.hold(actual, force: true)
|
|
62
|
-
end
|
|
72
|
+
def assert_hold!(*, **) = assert_hold(*, **, force: true)
|
|
63
73
|
|
|
64
74
|
# Temporarily used for development feedback to print to STDERR the actual value
|
|
65
|
-
def
|
|
66
|
-
@holdify ||= Holdify.new(self)
|
|
67
|
-
location = @holdify.find_location
|
|
68
|
-
warn "[holdify] Actual value from: #{location.path}:#{location.lineno}\n=> #{actual.inspect}"
|
|
69
|
-
@holdify.hold(actual)
|
|
70
|
-
end
|
|
75
|
+
def assert_hold?(*, **) = assert_hold(*, **, inspect: true)
|
|
71
76
|
end
|
|
72
77
|
|
|
73
78
|
# Register expectations only if minitest/spec is loaded; ensure the right class in 6.0 and < 6.0
|
|
74
79
|
# :nocov:
|
|
75
80
|
if (expectation_class = defined?(Spec) && (defined?(Expectation) ? Expectation : Expectations))
|
|
76
|
-
%w[hold hold!
|
|
81
|
+
%w[hold hold! hold?].each do |suffix|
|
|
77
82
|
expectation_class.infect_an_assertion :"assert_#{suffix}", :"must_#{suffix}", :reverse
|
|
78
83
|
expectation_class.alias_method :"to_#{suffix}", :"must_#{suffix}"
|
|
79
84
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: minitest-holdify
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Domizio Demichelis
|
|
@@ -33,6 +33,8 @@ extra_rdoc_files: []
|
|
|
33
33
|
files:
|
|
34
34
|
- LICENSE.txt
|
|
35
35
|
- lib/holdify.rb
|
|
36
|
+
- lib/holdify/hold.rb
|
|
37
|
+
- lib/holdify/pretty.rb
|
|
36
38
|
- lib/holdify/store.rb
|
|
37
39
|
- lib/minitest-holdify.rb
|
|
38
40
|
- lib/minitest/holdify_plugin.rb
|
|
@@ -58,7 +60,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
58
60
|
- !ruby/object:Gem::Version
|
|
59
61
|
version: '0'
|
|
60
62
|
requirements: []
|
|
61
|
-
rubygems_version:
|
|
63
|
+
rubygems_version: 4.0.3
|
|
62
64
|
specification_version: 4
|
|
63
65
|
summary: Hardcoded values suck! Holdify them.
|
|
64
66
|
test_files: []
|