minitest-holdify 1.1.3 → 1.2.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: 7560a67982ab6acf747a90307f0fc5fca6ff30c4675aefaf92c973afaa01fe65
4
- data.tar.gz: 928f88bd3d8711d13817c924a5c587117e9e61d1cb3a7301b2a097f7cfafb95a
3
+ metadata.gz: 056a8cc8b2a69276cf8adc189965b484cb9742b6c4e71efaca3b194b56bc1965
4
+ data.tar.gz: 1570f826edd18de9ee3dbbe6fa39d759f597ab5f5f4ebf452b015ae9cc26f240
5
5
  SHA512:
6
- metadata.gz: b2cb240ed6c9d90797003b00508b4640d694617b78661fd8ff59a7533eb771a58e12d1f24024cf09d71224c490446c92f325b3bc4b0217ee2c2774d3f78ab4f9
7
- data.tar.gz: aa038a10daa87f5d4700d7778dd86e75049d06036f2435b769c455b2f4f37e08fac5a21a91d7a375f06a53dda9a5c2ac50858f4e26cdde27e4d2a6732976fa7c
6
+ metadata.gz: c6caaf67f67c9992df20d00ab2fc498365ae09f13225e7469aa370b68f0a42cbb77c97c7e68825c21b42600a606ef4c468bcec011c54e2380945bb8f9394741f
7
+ data.tar.gz: 30bf7d38f0df80f1a2b581c12ee7fbd1ca6811369fe4a2977e286d1876d37daabb1eb5cd8b8d9b81ceb40103dcf430003ef1ed8240db79ce5c5579680395ca7b
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'tempfile'
5
+ require 'open3'
6
+
7
+ module Holdify
8
+ # Diff print failure report
9
+ class Failure
10
+ GUTTER_WIDTH = 3
11
+
12
+ def initialize(expected, actual, message, location:, metadata:)
13
+ @expected = expected
14
+ @actual = actual
15
+ @message = message
16
+ @location = location
17
+ @metadata = metadata
18
+ @config = Holdify.config
19
+ end
20
+
21
+ def message
22
+ output = add_headers
23
+ @config[:git] ? output.push(*add_diff) : output.push(@message)
24
+ output.join("\n")
25
+ end
26
+
27
+ def add_headers
28
+ ["#{ansi[:red]}--- @yaml#{ansi[:reset]} >>> #{@metadata[:path]}:#{@metadata[:line]}",
29
+ "#{ansi[:green]}+++ @test#{ansi[:reset]} >>> #{@location.path}:#{@location.lineno}",
30
+ "#{ansi[:inverse]}<-- #{@metadata[:key]} #{ansi[:reset]}"]
31
+ end
32
+
33
+ def ansi
34
+ @config[:color] ? { inverse: "\e[7m", reset: "\e[0m", red: "\e[31m", green: "\e[32m" } : {}
35
+ end
36
+
37
+ def git_command
38
+ if @config[:color]
39
+ regex = '[^[:space:]]+|[[:space:]]+'
40
+ %W[git diff --no-index --color-words=#{regex} --unified=1000 #{@exp_file.path} #{@act_file.path}]
41
+ else
42
+ %W[git diff --no-index --no-color --unified=1000 #{@exp_file.path} #{@act_file.path}]
43
+ end
44
+ end
45
+
46
+ def add_diff
47
+ @exp_file = create_tempfile(@expected)
48
+ @act_file = create_tempfile(@actual)
49
+ @config[:color] ? color_process_lines : process_lines
50
+ rescue Errno::ENOENT
51
+ []
52
+ ensure
53
+ @exp_file&.close!
54
+ @act_file&.close!
55
+ end
56
+
57
+ def diff_lines
58
+ stdout, = Open3.capture3(*git_command)
59
+ return [] if stdout.to_s.empty?
60
+
61
+ # Match everything from the start up to the LAST @@...--- sequence
62
+ # .* is greedy, so it skips all earlier hunks
63
+ regex = /\A.*@@.*?\r?\n[[:blank:]]*---(\e\[[0-9;]*m)?\r?\n/m
64
+ body = stdout.sub(regex, '')
65
+
66
+ body.split(/\r?\n/)
67
+ end
68
+
69
+ def color_process_lines
70
+ line_num = @metadata[:line]
71
+ diff_lines.map do |line|
72
+ clean = line.gsub(/\e\[(1|22|0)m/, '').lstrip
73
+ added = clean.start_with?(ansi[:green]) && !line.include?(ansi[:red])
74
+
75
+ if added
76
+ gutter = ' ' * GUTTER_WIDTH
77
+ else
78
+ gutter = line_num.to_s.rjust(GUTTER_WIDTH)
79
+ line_num += 1
80
+ end
81
+
82
+ "#{ansi[:inverse]}#{gutter}#{ansi[:reset]} #{line}"
83
+ end
84
+ end
85
+
86
+ def process_lines
87
+ line_num = @metadata[:line]
88
+ diff_lines.map do |line|
89
+ type = line[0]
90
+ line = line[1..]
91
+
92
+ if type == '+'
93
+ gutter = ' ' * GUTTER_WIDTH
94
+ else
95
+ gutter = line_num.to_s.rjust(GUTTER_WIDTH)
96
+ line_num += 1
97
+ end
98
+
99
+ "#{gutter} #{type} #{line}"
100
+ end
101
+ end
102
+
103
+ def create_tempfile(obj)
104
+ Tempfile.new(%w[holdify .yaml]).tap do |file|
105
+ file.write(YAML.dump(obj))
106
+ file.flush
107
+ end
108
+ end
109
+ end
110
+ end
data/lib/holdify/hold.rb CHANGED
@@ -3,15 +3,15 @@
3
3
  module Holdify
4
4
  # The *_hold statement (Assertion/Expectation)
5
5
  class Hold
6
- attr_reader :forced
6
+ attr_reader :forced, :store
7
7
 
8
8
  def initialize(test)
9
9
  @test = test
10
10
  @path, = test.method(test.name).source_location
11
11
  @store = Holdify.stores(@path)
12
12
  @session = Hash.new { |h, k| h[k] = [] } # { line => [values] }
13
- @forced = [] # [ line ]
14
- @added = [] # [ line ]
13
+ @forced = [] # [ lines ]
14
+ @added = [] # [ lines ]
15
15
  end
16
16
 
17
17
  def call(actual, force: false)
@@ -41,9 +41,12 @@ module Holdify
41
41
  @session.each { |line, values| @store.set(line, values) }
42
42
  end
43
43
 
44
+ # The index of the current test/value
45
+ def current_index(line) = @session[line].size - 1
46
+
44
47
  # Find the location in the test that triggered the hold
45
48
  def find_location
46
- caller_locations.find do |location|
49
+ caller_locations(2).find do |location|
47
50
  next unless location.path == @path
48
51
 
49
52
  label = location.base_label
@@ -28,10 +28,34 @@ module Holdify
28
28
  output["L#{line} #{xxh}"] = @data[line]
29
29
  end
30
30
 
31
- content = YAML.dump(output, line_width: 78) # Ensure 80 columns (including pretty gutter)
31
+ content = YAML.dump(output)
32
32
  File.write(@path, content)
33
33
  end
34
34
 
35
+ # Used in Failure to get the actual storage metadata
36
+ def lookup(lineno, index = 0)
37
+ return unless File.exist?(@path)
38
+
39
+ xxh = @source.xxh(lineno)
40
+ key = "L#{lineno} #{xxh}"
41
+
42
+ found = false
43
+ count = -1
44
+
45
+ File.foreach(@path).with_index(1) do |content, ln|
46
+ if found
47
+ next unless content.start_with?('-')
48
+
49
+ count += 1
50
+ next unless count == index
51
+
52
+ return { path: @path, line: ln, key: key }
53
+ else
54
+ found = content.match(/\b#{xxh}\b/)
55
+ end
56
+ end
57
+ end
58
+
35
59
  private
36
60
 
37
61
  def load_and_align
@@ -42,13 +66,13 @@ module Holdify
42
66
  next if lines.empty?
43
67
 
44
68
  # Position of the held lines compared to the source lines
45
- stayed, moved = entries.map { |key, values| { line: key[/\d+/].to_i, values: values } }
46
- .partition { |c| lines.include?(c[:line]) }
47
- moved.sort_by! { |c| c[:line] }
69
+ stayed, moved = entries.map { |key, values| { line: key[/\d+/].to_i, values: } }
70
+ .partition { lines.include?(_1[:line]) }
71
+ moved.sort_by! { _1[:line] }
48
72
 
49
73
  # Align lines
50
74
  lines.each do |line|
51
- match = stayed.find { |c| c[:line] == line } || moved.shift
75
+ match = stayed.find { _1[:line] == line } || moved.shift
52
76
  aligned[line] = match[:values] if match
53
77
  end
54
78
  end
data/lib/holdify/store.rb CHANGED
@@ -15,6 +15,6 @@ module Holdify
15
15
  end
16
16
 
17
17
  def_delegator :@source, :xxh
18
- def_delegators :@ledger, :get, :set, :persist
18
+ def_delegators :@ledger, :get, :set, :persist, :lookup
19
19
  end
20
20
  end
data/lib/holdify.rb CHANGED
@@ -5,12 +5,16 @@ require_relative 'holdify/hold'
5
5
 
6
6
  # The container module
7
7
  module Holdify
8
- VERSION = '1.1.3'
8
+ VERSION = '1.2.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
+ def config
15
+ @config ||= { git: system('git --version', out: File::NULL, err: File::NULL),
16
+ color: !ENV.key?('NO_COLOR') }
17
+ end
14
18
 
15
19
  def stores(path = nil)
16
20
  return @stores unless path
@@ -23,13 +27,6 @@ module Holdify
23
27
  def persist_all!
24
28
  @stores&.each_value(&:persist)
25
29
  end
26
-
27
- def pretty
28
- return @pretty unless @pretty.nil?
29
-
30
- @pretty = $stdout.tty? && !ENV.key?('NO_COLOR') && ENV['TERM'] != 'dumb' &&
31
- system('git --version', out: File::NULL, err: File::NULL)
32
- end
33
30
  end
34
31
  @mutex = Mutex.new
35
32
  @stores = {}
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'holdify'
4
- require 'holdify/pretty'
4
+ require 'holdify/failure'
5
5
 
6
6
  # Implement the minitest plugin
7
7
  module Minitest
@@ -17,10 +17,6 @@ module Minitest
17
17
  Holdify.quiet = true
18
18
  end
19
19
 
20
- opts.on '--holdify-pretty', 'Format the stored values with pretty print' do
21
- Holdify.pretty = true
22
- end
23
-
24
20
  opts.on '--holdify-quiet', 'Skip the warning on storing a new value' do
25
21
  Holdify.quiet = true
26
22
  end
@@ -37,10 +33,9 @@ module Minitest
37
33
  return unless @hold.forced.any?
38
34
 
39
35
  path, = method(name).source_location
40
- msg = <<~MSG.chomp
41
- [holdify] the value has been stored: remove the "!" suffix to pass the test
42
- #{@hold.forced.uniq.map { |l| " #{path}:#{l}" }.join("\n")}
43
- MSG
36
+ msg = +%([holdify] the value has been stored: remove the "!" suffix to pass the test\n)
37
+ msg << @hold.forced.uniq.map { |line| " #{path}:#{line}" }.join("\n")
38
+
44
39
  raise Minitest::Assertion, msg
45
40
  end
46
41
  end
@@ -60,13 +55,12 @@ module Minitest
60
55
  else
61
56
  send(assertion || :assert_equal, expected, actual, message)
62
57
  end
63
- rescue Minitest::Assertion
64
- raise unless Holdify.pretty
65
-
66
- diff = Holdify::Pretty.call(expected, actual)
67
- raise unless diff
58
+ rescue Minitest::Assertion => e
59
+ location = @hold.find_location
60
+ metadata = @hold.store.lookup(location.lineno, @hold.current_index(location.lineno))
61
+ hold_msg = Holdify::Failure.new(expected, actual, e.message, metadata:, location:).message
68
62
 
69
- msg = message ? "#{message}\n#{diff}" : diff
63
+ msg = message ? "#{message}\n#{hold_msg}" : hold_msg
70
64
  raise Minitest::Assertion, msg
71
65
  end
72
66
 
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.1.3
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Domizio Demichelis
@@ -47,9 +47,9 @@ extra_rdoc_files: []
47
47
  files:
48
48
  - LICENSE.txt
49
49
  - lib/holdify.rb
50
+ - lib/holdify/failure.rb
50
51
  - lib/holdify/hold.rb
51
52
  - lib/holdify/ledger.rb
52
- - lib/holdify/pretty.rb
53
53
  - lib/holdify/source.rb
54
54
  - lib/holdify/store.rb
55
55
  - lib/minitest-holdify.rb
@@ -1,90 +0,0 @@
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 pretty gutter)
85
- file.flush
86
- end
87
- end
88
- end
89
- end
90
- end