minitest-holdify 1.1.3 → 1.3.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: b2fecbc870f5205d62da0200a806adc5dc5d47b99a81e53c1af4d50f3f8aaa21
4
+ data.tar.gz: 06aed82a631a3632c531ef311a53f89e339a00d122fbc75eb31c52f1723e8d24
5
5
  SHA512:
6
- metadata.gz: b2cb240ed6c9d90797003b00508b4640d694617b78661fd8ff59a7533eb771a58e12d1f24024cf09d71224c490446c92f325b3bc4b0217ee2c2774d3f78ab4f9
7
- data.tar.gz: aa038a10daa87f5d4700d7778dd86e75049d06036f2435b769c455b2f4f37e08fac5a21a91d7a375f06a53dda9a5c2ac50858f4e26cdde27e4d2a6732976fa7c
6
+ metadata.gz: ebd7c7cbf15fc450a1a657affe961399ac916c3d57aafc74a89067638ba2d569c59ea134eecd811361d6970989a227dc6814bc1bec07673ee5ea2ca86ea5f877
7
+ data.tar.gz: c14c8219c1fbaaf1f10a6f28eef93f4696f71d59020090f85d96afe0ae431fa6a52db0a91d6f37f713ea266a6ef61369589bd85544a70c681f73c1902f0055eb
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'tempfile'
5
+ require 'open3'
6
+
7
+ module Holdify
8
+ # Feedback report on failure
9
+ class Feedback
10
+ attr_reader :xxhi_ref
11
+
12
+ def initialize(hold, hold_ref, *args)
13
+ test_lno = hold.test_loc.lineno
14
+ index = hold.session[test_lno].size - 1 # current index
15
+ xxh = hold.store.xxh(test_lno)
16
+ yaml_path = hold.store.path
17
+
18
+ @xxhi_ref = "<<< @xxh[i] --> #{xxh}[#{index}]"
19
+
20
+ @yaml_lno = find_yaml_lno(yaml_path, test_lno, xxh, index)
21
+ @yaml_ref = Holdify.relative("#{yaml_path}:#{@yaml_lno}")
22
+
23
+ @hold_ref = Holdify.relative(hold_ref)
24
+ test_ref = Holdify.relative(hold.test_loc.to_s.sub(/:in .*$/, ''))
25
+ @test_ref = test_ref unless @hold_ref == test_ref
26
+
27
+ @expected, @actual, @message = *args
28
+
29
+ # Extend with features
30
+ extend(Color) if Holdify.color
31
+ return unless Holdify.git
32
+
33
+ extend GitDiff
34
+ extend(Color::GitDiff) if Holdify.color
35
+ end
36
+
37
+ def message = [@message, xxhi_ref, *file_refs, *diff, ''].join("\n")
38
+
39
+ def file_refs
40
+ ["--- @stored --> #{@yaml_ref}", "+++ @tested --> #{@hold_ref}"].tap do |refs|
41
+ refs << " --> #{@test_ref}" if @test_ref
42
+ end
43
+ end
44
+
45
+ def diff = ["- #{@expected.inspect}", "+ #{@actual.inspect}"]
46
+
47
+ private
48
+
49
+ def find_yaml_lno(yaml_path, test_lno, xxh, index)
50
+ found = false
51
+ count = -1
52
+ File.foreach(yaml_path).with_index(1) do |line, ln|
53
+ if found
54
+ next unless line.start_with?('-')
55
+
56
+ count += 1
57
+ next unless count == index
58
+
59
+ return ln
60
+ else
61
+ found = line.match(/^L#{test_lno} #{xxh}:$/)
62
+ end
63
+ end
64
+ end
65
+
66
+ # Methods enabling the git-diff feedback (no-color)
67
+ module GitDiff
68
+ def git_command = "git diff --no-index --no-color --unified=1000 #{@exp_path} #{@act_path}"
69
+
70
+ def diff
71
+ @exp_path = create_tempfile(@expected).path
72
+ @act_path = create_tempfile(@actual).path
73
+
74
+ stdout, = Open3.capture3(git_command)
75
+ regex = /\A[^@]*\r?\n/m # cleanup git headers
76
+ lines = stdout.sub(regex, '').split(/\r?\n/)
77
+
78
+ process_lines(lines)
79
+ ensure
80
+ # :nocov:
81
+ File.unlink(@exp_path) if @exp_path
82
+ File.unlink(@act_path) if @act_path
83
+ # :nocov:
84
+ end
85
+
86
+ def create_tempfile(obj)
87
+ Tempfile.create.tap do |file|
88
+ file.write(YAML.dump(obj))
89
+ file.close
90
+ end
91
+ end
92
+
93
+ def process_lines(lines)
94
+ width = 0
95
+ lineno = [@yaml_lno - 1]
96
+ lines.map.with_index do |line, i|
97
+ if i.zero? # @@ ... @@
98
+ w = line.scan(/,(\d+)/).flatten.map(&:to_i).max
99
+ width = (@yaml_lno + w).to_s.length
100
+ next line
101
+ end
102
+ next if i == 1 # ---
103
+
104
+ render_line(line, lineno, width)
105
+ end.compact
106
+ end
107
+
108
+ def render_line(line, lineno, width)
109
+ type = line[0]
110
+ line = line[1..]
111
+ gutter = if type == '+'
112
+ ' ' * width
113
+ else
114
+ lineno[0] += 1
115
+ lineno[0].to_s.rjust(width)
116
+ end
117
+
118
+ "#{type}#{gutter} #{line}"
119
+ end
120
+ end
121
+
122
+ # Methods enabling ANSI feedback
123
+ module Color
124
+ SGR = { clear: "\e[0m",
125
+ red: "\e[31m",
126
+ green: "\e[32m",
127
+ yellow: "\e[33m",
128
+ magenta: "\e[35m" }.freeze
129
+
130
+ def wrap(color, string) = "#{SGR[color]}#{string}#{SGR[:clear]}"
131
+
132
+ def file_refs
133
+ refs = super
134
+ [wrap(:red, refs.shift), *refs.map { wrap(:green, _1) }]
135
+ end
136
+
137
+ def diff = [wrap(:red, "- #{@expected.inspect}"), wrap(:green, "+ #{@actual.inspect}")]
138
+
139
+ def xxhi_ref = wrap(:magenta, @xxhi_ref)
140
+
141
+ # Methods enabling the git-diff ANSI feedback
142
+ module GitDiff
143
+ def git_command = "git diff --no-index --color-words='[^ ]+|[ ]+' --unified=1000 #{@exp_path} #{@act_path}"
144
+
145
+ def render_line(line, lineno, width)
146
+ clean = line.gsub(/\e\[(1|22|0)m/, '').lstrip
147
+ added = clean.start_with?(SGR[:green]) && !line.include?(SGR[:red])
148
+ removed = clean.start_with?(SGR[:red]) && !line.include?(SGR[:green])
149
+ changed = line.include?(SGR[:red]) || line.include?(SGR[:green])
150
+
151
+ type, color = (added && ['+', :green]) || (removed && ['-', :red]) || (changed && ['~', :yellow])
152
+
153
+ sgr = SGR[color].to_s if color
154
+ gutter = if added
155
+ "#{sgr}#{type} #{' ' * width}#{SGR[:clear]}"
156
+ else
157
+ lineno[0] += 1
158
+ "#{sgr}#{type || ' '}#{lineno[0].to_s.rjust(width)}#{SGR[:clear]}"
159
+ end
160
+
161
+ "#{gutter} #{line}"
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
data/lib/holdify/hold.rb CHANGED
@@ -1,54 +1,60 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Holdify
4
- # The *_hold statement (Assertion/Expectation)
4
+ # The *_hold Assertion/Expectation
5
5
  class Hold
6
- attr_reader :forced
6
+ attr_reader :forced, :store, :test_loc, :session
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
- @session = Hash.new { |h, k| h[k] = [] } # { line => [values] }
13
- @forced = [] # [ line ]
14
- @added = [] # [ line ]
12
+ @session = Hash.new { |h, k| h[k] = [] } # { lineno => [values] }
13
+ @forced = [] # [ lines ]
14
+ @added = [] # [ lines ]
15
15
  end
16
16
 
17
17
  def call(actual, force: false)
18
- location = find_location
19
- line = location.lineno
20
- raise "Could not find holdify statement at line #{line}" unless @store.xxh(line)
18
+ @test_loc = find_test_loc
19
+ lineno = @test_loc.lineno
20
+ raise "Could not find holdify statement at lineno #{lineno}" unless @store.xxh(lineno)
21
21
 
22
- @forced << line if force
22
+ @forced << lineno if force
23
23
 
24
- values = @store.get(line)
25
- index = @session[line].size
24
+ values = @store.get_values(lineno)
25
+ index = @session[lineno].size
26
26
  value = if force || Holdify.reconcile
27
27
  actual
28
28
  elsif values && index < values.size
29
29
  values[index]
30
30
  else
31
- @added << line
31
+ @added << lineno
32
32
  actual
33
33
  end
34
34
 
35
- @session[line] << value
35
+ @session[lineno] << value
36
36
  value
37
37
  end
38
38
 
39
39
  def save
40
- @added.each { |line| warn "[holdify] Held new value for #{@path}:#{line}" } unless Holdify.quiet
41
- @session.each { |line, values| @store.set(line, values) }
40
+ @added.each { |lineno| warn "[holdify] Held new value for #{Holdify.relative(@path)}:#{lineno}" } unless Holdify.quiet
41
+ @session.each { |lineno, values| @store.set_values(lineno, values) }
42
42
  end
43
43
 
44
- # Find the location in the test that triggered the hold
45
- def find_location
46
- caller_locations.find do |location|
44
+ # Find the triggering LOC inside the test block/method
45
+ def find_test_loc
46
+ caller_locations(2).find do |location|
47
47
  next unless location.path == @path
48
48
 
49
- label = location.base_label
49
+ label = location.base_label
50
50
  label == @test.name || label == '<top (required)>' || label == '<main>' || label.start_with?('<class:', '<module:')
51
51
  end
52
52
  end
53
+
54
+ def warn_for(actual)
55
+ warn("[holdify] The value from #{Holdify.relative(@test_loc.path)}:#{@test_loc.lineno} is:\n[holdify] => #{actual.inspect}")
56
+ end
57
+
58
+ def feedback(*) = Feedback.new(self, *).message
53
59
  end
54
60
  end
@@ -7,7 +7,7 @@ module Holdify
7
7
  class Source
8
8
  def initialize(path) = (@line_xxh, @xxh_lines = parse(path))
9
9
 
10
- def xxh(line) = @line_xxh[line]
10
+ def xxh(lineno) = @line_xxh[lineno]
11
11
 
12
12
  def lines(xxh) = @xxh_lines[xxh]
13
13
 
@@ -17,13 +17,13 @@ module Holdify
17
17
  line_xxh = {}
18
18
  xxh_lines = Hash.new { |h, k| h[k] = [] }
19
19
 
20
- File.foreach(path).with_index(1) do |text, line|
20
+ File.foreach(path).with_index(1) do |text, lineno|
21
21
  content = text.strip
22
22
  next if content.empty?
23
23
 
24
- xxh = Digest::XXH3_64bits.hexdigest(content)
25
- line_xxh[line] = xxh
26
- xxh_lines[xxh] << line
24
+ xxh = Digest::XXH3_64bits.hexdigest(content)
25
+ line_xxh[lineno] = xxh
26
+ xxh_lines[xxh] << lineno
27
27
  end
28
28
 
29
29
  [line_xxh, xxh_lines]
data/lib/holdify/store.rb CHANGED
@@ -1,20 +1,73 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'yaml'
4
+ require 'fileutils'
3
5
  require 'forwardable'
4
- require_relative 'source'
5
- require_relative 'ledger'
6
6
 
7
7
  module Holdify
8
8
  # Interface to the Source (code) and the Ledger (data)
9
9
  class Store
10
+ class NoAliasVisitor < Psych::Visitors::YAMLTree # :nodoc:
11
+ def register(target, obj); end
12
+ end
13
+
10
14
  extend Forwardable
11
15
 
16
+ def_delegator :@source, :xxh
17
+ attr_reader :path
18
+
12
19
  def initialize(path)
20
+ @path = "#{path}#{Holdify.store_ext}"
13
21
  @source = Source.new(path)
14
- @ledger = Ledger.new(path, @source)
22
+ FileUtils.rm_f(@path) if Holdify.reconcile
23
+ @data = File.exist?(@path) ? load_and_align : {}
24
+ end
25
+
26
+ def get_values(lineno) = @data[lineno]
27
+
28
+ def set_values(lineno, values) = (@data[lineno] = values)
29
+
30
+ def persist
31
+ return FileUtils.rm_f(@path) if @data.empty?
32
+
33
+ output = {}
34
+ @data.keys.sort.each do |lineno|
35
+ xxh = @source.xxh(lineno)
36
+ next unless xxh
37
+
38
+ output["L#{lineno} #{xxh}"] = @data[lineno]
39
+ end
40
+
41
+ File.write(@path, hold_dump(output))
42
+ end
43
+
44
+ private
45
+
46
+ def hold_dump(obj)
47
+ visitor = NoAliasVisitor.create
48
+ visitor << obj
49
+ visitor.tree.to_yaml
15
50
  end
16
51
 
17
- def_delegator :@source, :xxh
18
- def_delegators :@ledger, :get, :set, :persist
52
+ def load_and_align
53
+ {}.tap do |aligned|
54
+ data = YAML.unsafe_load_file(@path) || {}
55
+ data.group_by { |k, _| k.split.last }.each do |xxh, entries|
56
+ lines = @source.lines(xxh)
57
+ next if lines.empty?
58
+
59
+ # Position of the held lines compared to the source lines
60
+ stayed, moved = entries.map { |key, values| { line: key[/\d+/].to_i, values: } }
61
+ .partition { lines.include?(_1[:line]) }
62
+ moved.sort_by! { _1[:line] }
63
+
64
+ # Align lines
65
+ lines.each do |lineno|
66
+ match = stayed.find { _1[:line] == lineno } || moved.shift
67
+ aligned[lineno] = match[:values] if match
68
+ end
69
+ end
70
+ end
71
+ end
19
72
  end
20
73
  end
data/lib/holdify.rb CHANGED
@@ -1,16 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'holdify/store'
4
3
  require_relative 'holdify/hold'
4
+ require_relative 'holdify/feedback'
5
+ require_relative 'holdify/source'
6
+ require_relative 'holdify/store'
5
7
 
6
8
  # The container module
7
9
  module Holdify
8
- VERSION = '1.1.3'
9
- CONFIG = { ext: '.yaml' }.freeze
10
+ VERSION = '1.3.0'
10
11
 
11
12
  class << self
12
- attr_accessor :reconcile, :quiet
13
- attr_writer :pretty
13
+ attr_accessor :reconcile, :quiet, :git, :pwd, :color, :rel_paths, :store_ext
14
+
15
+ def persist_all! = @stores&.each_value(&:persist)
16
+
17
+ def relative(path)
18
+ return path unless rel_paths
19
+
20
+ path.sub(%r{^#{pwd}/}, '')
21
+ end
14
22
 
15
23
  def stores(path = nil)
16
24
  return @stores unless path
@@ -19,17 +27,6 @@ module Holdify
19
27
  @stores[path] ||= Store.new(path)
20
28
  end
21
29
  end
22
-
23
- def persist_all!
24
- @stores&.each_value(&:persist)
25
- 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,29 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'holdify'
4
- require 'holdify/pretty'
5
4
 
6
5
  # Implement the minitest plugin
7
6
  module Minitest
8
- # Register the after_run hook to persist all data
9
- def self.plugin_holdify_init(_options)
10
- Minitest.after_run { Holdify.persist_all! }
11
- end
12
-
13
7
  # Set the Holdify options
14
- def self.plugin_holdify_options(opts, _options)
15
- opts.on '--holdify-reconcile', 'Reconcile the held values with the new ones' do
16
- Holdify.reconcile = true
17
- Holdify.quiet = true
18
- end
19
-
20
- opts.on '--holdify-pretty', 'Format the stored values with pretty print' do
21
- Holdify.pretty = true
8
+ def self.plugin_holdify_options(opts, options)
9
+ opts.on '--holdify-reconcile', 'Reconcile the held values with the new ones (enables quiet)' do
10
+ options[:holdify_reconcile] = true
11
+ options[:holdify_quiet] = true
22
12
  end
23
-
24
13
  opts.on '--holdify-quiet', 'Skip the warning on storing a new value' do
25
- Holdify.quiet = true
14
+ options[:holdify_quiet] = true
15
+ end
16
+ opts.on '--holdify-no-git-diff', 'Disable git-diff' do
17
+ options[:holdify_no_git_diff] = true
18
+ end
19
+ opts.on '--holdify-no-color', 'Disable colored output' do
20
+ options[:holdify_no_color] = true
21
+ end
22
+ opts.on '--holdify-no-rel-paths', 'Disable relative paths in file references' do
23
+ options[:holdify_no_rel_paths] = true
26
24
  end
25
+ opts.on '--holdify-store-ext EXT', 'The yaml store extension (default .yaml)' do |ext|
26
+ options[:holdify_store_ext] = ext
27
+ end
28
+ end
29
+
30
+ # Register the after_run hook to persist all data
31
+ def self.plugin_holdify_init(options)
32
+ Holdify.reconcile = options[:holdify_reconcile]
33
+ Holdify.quiet = options[:holdify_quiet]
34
+ Holdify.git = system('git --version', out: File::NULL, err: File::NULL) unless options[:holdify_no_git_diff]
35
+ Holdify.pwd = Holdify.git ? `git rev-parse --show-toplevel`.strip : Dir.pwd
36
+ Holdify.color = !ENV.key?('NO_COLOR') unless options[:holdify_no_color]
37
+ Holdify.rel_paths = true unless options[:holdify_no_rel_paths]
38
+ Holdify.store_ext = options[:holdify_store_ext] || '.yaml'
39
+
40
+ Minitest.after_run { Holdify.persist_all! }
27
41
  end
28
42
 
29
43
  # Reopen the minitest class
@@ -37,10 +51,9 @@ module Minitest
37
51
  return unless @hold.forced.any?
38
52
 
39
53
  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
54
+ msg = +%([holdify] the value has been stored: remove the "!" suffix to pass the test\n)
55
+ msg << @hold.forced.uniq.map { |lineno| " #{path}:#{lineno}" }.join("\n")
56
+
44
57
  raise Minitest::Assertion, msg
45
58
  end
46
59
  end
@@ -60,20 +73,11 @@ module Minitest
60
73
  else
61
74
  send(assertion || :assert_equal, expected, actual, message)
62
75
  end
63
- rescue Minitest::Assertion
64
- raise unless Holdify.pretty
65
-
66
- diff = Holdify::Pretty.call(expected, actual)
67
- raise unless diff
68
-
69
- msg = message ? "#{message}\n#{diff}" : diff
70
- raise Minitest::Assertion, msg
76
+ rescue Minitest::Assertion => e
77
+ raise Minitest::Assertion, @hold.feedback(e.location, expected, actual, message)
71
78
  end
72
79
 
73
- if inspect
74
- location = @hold.find_location
75
- warn "[holdify] The value from #{location.path}:#{location.lineno} is:\n[holdify] => #{actual.inspect}"
76
- end
80
+ @hold.warn_for(actual) if inspect
77
81
 
78
82
  expected
79
83
  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.1.3
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Domizio Demichelis
@@ -47,9 +47,8 @@ extra_rdoc_files: []
47
47
  files:
48
48
  - LICENSE.txt
49
49
  - lib/holdify.rb
50
+ - lib/holdify/feedback.rb
50
51
  - lib/holdify/hold.rb
51
- - lib/holdify/ledger.rb
52
- - lib/holdify/pretty.rb
53
52
  - lib/holdify/source.rb
54
53
  - lib/holdify/store.rb
55
54
  - lib/minitest-holdify.rb
@@ -1,58 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'yaml'
4
- require 'fileutils'
5
-
6
- module Holdify
7
- # Manages the persistence and alignment of stored values
8
- class Ledger
9
- def initialize(path, source)
10
- @path = "#{path}#{CONFIG[:ext]}"
11
- @source = source
12
- FileUtils.rm_f(@path) if Holdify.reconcile
13
- @data = File.exist?(@path) ? load_and_align : {}
14
- end
15
-
16
- def get(line) = @data[line]
17
-
18
- def set(line, values) = (@data[line] = values)
19
-
20
- def persist
21
- return FileUtils.rm_f(@path) if @data.empty?
22
-
23
- output = {}
24
- @data.keys.sort.each do |line|
25
- xxh = @source.xxh(line)
26
- next unless xxh
27
-
28
- output["L#{line} #{xxh}"] = @data[line]
29
- end
30
-
31
- content = YAML.dump(output, line_width: 78) # Ensure 80 columns (including pretty gutter)
32
- File.write(@path, content)
33
- end
34
-
35
- private
36
-
37
- def load_and_align
38
- {}.tap do |aligned|
39
- data = YAML.unsafe_load_file(@path) || {}
40
- data.group_by { |k, _| k.split.last }.each do |xxh, entries|
41
- lines = @source.lines(xxh)
42
- next if lines.empty?
43
-
44
- # 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] }
48
-
49
- # Align lines
50
- lines.each do |line|
51
- match = stayed.find { |c| c[:line] == line } || moved.shift
52
- aligned[line] = match[:values] if match
53
- end
54
- end
55
- end
56
- end
57
- end
58
- end
@@ -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