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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6a833738964de4fd5b67fbb6c68a1238e62077ed8716c4c37016e430234280a9
4
- data.tar.gz: bf3e156990631e915295f4109b3bacd6f3e4553e3f155a5d0559ea9172421e42
3
+ metadata.gz: c91100bb90de5695419fdb279520529462f67f37d2e580ce6d61c496cf43cbcb
4
+ data.tar.gz: 48b9d63b74bdea68de55c7a2790e8fb1fef59dc1eb956bce96b02d7ef0b141b3
5
5
  SHA512:
6
- metadata.gz: 7cdf43d2f02bbf34a5011d8808b27d0ca8ec1f9619d7c1caefc01a9e8b68f4176bd7ff1cfe5c301ec93d9176925c9ea2033374f7753b57b629846c4c69abc2aa
7
- data.tar.gz: a1db8f0d6d12bf7b68fca7144a073dfc6ad55676452bb187003c9c4dced2a6a910db9156cb86eadd2b393859a5f7a12ddc0845b783a0094e750994a24c29823c
6
+ metadata.gz: c17c6cfea957cbad41b2f272da130226a1c878546b46559ec80ccb1f939cb8c395c3b91a91e98ada8a1760bf3b661568364f6ca22f2e34ac957961c6011b4a40
7
+ data.tar.gz: 89f7c53a3ae05ef7e302efff24398e1579067d246a913c2ca91029185e08af9d8b4dae35da3a6686def351ce6fa89b0b9da1a34d388526dfa1455f5a5950e170
@@ -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
- class Holdify
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
- valid_ids = @source.values
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) = @source[lineno]
33
- def stored(id) = @data[@index[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#{@source.key(id)} #{id}"
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[@index[id] = new_key] = values
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: -1)
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
- class Holdify
8
- VERSION = '1.0.2'
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
- label = location.base_label
59
- # :nocov:
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
- label == @test.name ||
64
- label == '<top (required)>' ||
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
- # :nocov:
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
- @holdify&.save
32
- return unless @holdify&.forced&.any? && failures.empty?
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
- #{@holdify.forced.uniq.map { |l| " #{l}" }.join("\n")}
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
- @holdify ||= Holdify.new(self)
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
- expected = @holdify.hold(actual)
51
- if actual.nil?
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!(actual, *)
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 assert_hold_?(actual, *)
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! hold_?].each do |suffix|
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.2
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: 3.6.9
63
+ rubygems_version: 4.0.3
62
64
  specification_version: 4
63
65
  summary: Hardcoded values suck! Holdify them.
64
66
  test_files: []