minitest-holdify 1.0.3 → 1.1.1

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: 06f80be32b494ec786f8d873556a8046d6e051854ce38051c1f9a41ecce91fe7
4
- data.tar.gz: 95efd3e040696f2eacdbcf61b378db9c51bb33889f73b54c418d8bb0417e6244
3
+ metadata.gz: 48b28eeeaa320f75630822394f0f4b91a3025e081f085e39dee88ce264a5b2ab
4
+ data.tar.gz: fbc6c3b837a70c44d7359495c1e58809376022a6a458f504db4d7f06ff8585ae
5
5
  SHA512:
6
- metadata.gz: 3174aad64ba870444147f629ca38dddbbafce4b57d94d72d32febc1c32afdc56c6f2415337e43fab7be44a45a7d7edae7197aabc06f5ea82504bc868ca3ab123
7
- data.tar.gz: a336655be1c76832e50c00b919bbb7ecc8a735b1b49e8067a8df3506fa7a6ef2f7b1553b10356bca2fd54025a0fb22a936bdc70f390eaaa124e244f978407df9
6
+ metadata.gz: 0accd428f401a337bbdbd8f9537b952c42e40508425d3353213b9795d2b0ecd4b617087d41f0abb79af508e156e7aa7149a6fe30e9cdff7b135c062f3b3caf32
7
+ data.tar.gz: c71c3caff7e00b1f8fb40ec5e871d3697cec7925a76559b1c8064a512bcc5a9285c150836a013edfa3c33325da9e3256096dfc27161251467ca817cfffbf8c19
data/lib/holdify/hold.rb CHANGED
@@ -9,37 +9,39 @@ module Holdify
9
9
  @test = test
10
10
  @path, = test.method(test.name).source_location
11
11
  @store = Holdify.stores[@path] ||= Store.new(@path)
12
- @session = Hash.new { |h, k| h[k] = [] }
13
- @forced = []
14
- @added = []
12
+ @session = Hash.new { |h, k| h[k] = [] } # { line => [values] }
13
+ @forced = [] # [ "file:line" ]
14
+ @added = [] # [ "file:line" ]
15
15
  end
16
16
 
17
17
  def call(actual, force: false)
18
18
  location = find_location
19
- id = @store.id_at(location.lineno)
20
- raise "Could not find holdify statement at line #{location.lineno}" unless id
19
+ line = location.lineno
20
+ raise "Could not find holdify statement at line #{line}" unless @store.sha_at(line)
21
21
 
22
- @session[id] << actual
23
- @forced << "#{location.path}:#{location.lineno}" if force
22
+ @session[line] << actual
23
+ @forced << "#{@path}:#{line}" if force
24
24
 
25
25
  return actual if force || Holdify.reconcile
26
26
 
27
- stored = @store.stored(id)
28
- index = @session[id].size - 1
29
- return stored[index] if stored && index < stored.size
27
+ # Expected value
28
+ values = @store.get(line)
29
+ index = @session[line].size - 1
30
+ return values[index] if values && index < values.size
30
31
 
31
- @added << "#{location.path}:#{location.lineno}"
32
+ @added << "#{@path}:#{line}"
32
33
  actual
33
34
  end
34
35
 
35
36
  def save
36
37
  return unless @test.failures.empty?
37
38
 
38
- @added.each { |loc| warn "[holdify] Held new value for #{loc}" } unless Holdify.quiet
39
- @session.each { |id, values| @store.update(id, values) }
39
+ @added.each { |loc| warn "[holdify] Held new value for #{loc}" } unless Holdify.quiet
40
+ @session.each { |line, values| @store.set(line, values) }
40
41
  @store.save
41
42
  end
42
43
 
44
+ # Find the location in the test that triggered the hold
43
45
  def find_location
44
46
  caller_locations.find do |location|
45
47
  next unless location.path == @path
@@ -0,0 +1,60 @@
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
+ File.delete(@path) if Holdify.reconcile && File.exist?(@path)
13
+ @data = load_and_align
14
+ end
15
+
16
+ def get(line) = @data[line]
17
+
18
+ def set(line, values) = (@data[line] = values)
19
+
20
+ def save
21
+ return FileUtils.rm_f(@path) if @data.empty?
22
+
23
+ output = {}
24
+ @data.keys.sort.each do |line|
25
+ sha = @source.sha_at(line)
26
+ next unless sha
27
+
28
+ output["L#{line} #{sha}"] = @data[line]
29
+ end
30
+
31
+ content = YAML.dump(output, line_width: 78) # Ensure 80 columns (including pretty gutter)
32
+ return if File.exist?(@path) && File.read(@path) == content
33
+
34
+ File.write(@path, content)
35
+ end
36
+
37
+ private
38
+
39
+ def load_and_align
40
+ {}.tap do |aligned|
41
+ raw_data = (File.exist?(@path) && YAML.unsafe_load_file(@path)) || {}
42
+ raw_data.group_by { |k, _| k.split.last }.each do |sha, entries|
43
+ target_lines = @source.lines_with(sha)
44
+ next if target_lines.empty?
45
+
46
+ # Old data
47
+ candidates = entries.map { |key, values| { line: key[/\d+/].to_i, values: values } }
48
+ exact, moved = candidates.partition { |c| target_lines.include?(c[:line]) }
49
+ moved.sort_by! { |c| c[:line] }
50
+
51
+ # New aligned data
52
+ target_lines.each do |line|
53
+ match = exact.find { |c| c[:line] == line } || moved.shift
54
+ aligned[line] = match[:values] if match
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ 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 pretty gutter)
85
+ file.flush
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest/sha1'
4
+
5
+ module Holdify
6
+ # Represents the current state of the source file
7
+ class Source
8
+ def initialize(path) = (@line_sha, @sha_lines = parse(path))
9
+
10
+ def sha_at(line) = @line_sha[line]
11
+
12
+ def lines_with(sha) = @sha_lines[sha]
13
+
14
+ private
15
+
16
+ def parse(path)
17
+ line_sha = {}
18
+ sha_lines = Hash.new { |h, k| h[k] = [] }
19
+
20
+ File.foreach(path).with_index(1) do |text, line|
21
+ content = text.strip
22
+ next if content.empty?
23
+
24
+ sha = Digest::SHA1.hexdigest(content)
25
+ line_sha[line] = sha
26
+ sha_lines[sha] << line
27
+ end
28
+
29
+ [line_sha, sha_lines]
30
+ end
31
+ end
32
+ end
data/lib/holdify/store.rb CHANGED
@@ -1,54 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'yaml'
4
- require 'fileutils'
5
- require 'digest/sha1'
3
+ require 'forwardable'
4
+ require_relative 'source'
5
+ require_relative 'ledger'
6
6
 
7
7
  module Holdify
8
- # A simple Hash-based store that syncs with a static source map
8
+ # Interface to the Source (code) and the Ledger (data)
9
9
  class Store
10
- def initialize(source_path)
11
- @path = "#{source_path}#{CONFIG[:ext]}"
12
- File.delete(@path) if Holdify.reconcile && File.exist?(@path)
10
+ extend Forwardable
13
11
 
14
- @source = {} # { lineno => id }
15
- File.foreach(source_path).with_index(1) do |line, lineno|
16
- content = line.strip
17
- @source[lineno] = Digest::SHA1.hexdigest(content) unless content.empty?
18
- end
19
-
20
- @data = (File.exist?(@path) && YAML.unsafe_load_file(@path)) || {} # { key => [values] }
21
- @index = {} # { id => "L123 id" }
22
-
23
- valid_ids = @source.values
24
- @data.keep_if do |key, _|
25
- id = key.split.last
26
- next false unless valid_ids.include?(id)
27
-
28
- @index[id] = key
29
- true
30
- end
12
+ def initialize(path)
13
+ @source = Source.new(path)
14
+ @ledger = Ledger.new(path, @source)
31
15
  end
32
16
 
33
- def id_at(lineno) = @source[lineno]
34
- def stored(id) = @data[@index[id]]
35
-
36
- # Overwrite the entry for a given ID with a new list of values
37
- def update(id, values)
38
- new_key = "L#{@source.key(id)} #{id}"
39
- old_key = @index[id]
40
- @data.delete(old_key) if old_key && old_key != new_key
41
- @data[@index[id] = new_key] = values
42
- end
43
-
44
- def save
45
- return FileUtils.rm_f(@path) if @data.empty?
46
-
47
- sorted = @data.sort_by { |k, _| k[/\d+/].to_i }.to_h
48
- content = YAML.dump(sorted, line_width: -1)
49
- return if File.exist?(@path) && File.read(@path) == content
50
-
51
- File.write(@path, content)
52
- end
17
+ def_delegator :@source, :sha_at
18
+ def_delegators :@ledger, :get, :set, :save
53
19
  end
54
20
  end
data/lib/holdify.rb CHANGED
@@ -3,14 +3,22 @@
3
3
  require_relative 'holdify/store'
4
4
  require_relative 'holdify/hold'
5
5
 
6
- # Add description
6
+ # The container module
7
7
  module Holdify
8
- VERSION = '1.0.3'
8
+ VERSION = '1.1.1'
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 ||= {}
16
+
17
+ def pretty
18
+ return @pretty unless @pretty.nil?
19
+
20
+ @pretty = $stdout.tty? && !ENV.key?('NO_COLOR') && ENV['TERM'] != 'dumb' &&
21
+ system('git --version', out: File::NULL, err: File::NULL)
22
+ end
15
23
  end
16
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,6 +11,11 @@ module Minitest
10
11
  Holdify.reconcile = true
11
12
  Holdify.quiet = true
12
13
  end
14
+
15
+ opts.on '--holdify-pretty', 'Format the stored values with pretty print' do
16
+ Holdify.pretty = true
17
+ end
18
+
13
19
  opts.on '--holdify-quiet', 'Skip the warning on storing a new value' do
14
20
  Holdify.quiet = true
15
21
  end
@@ -40,10 +46,20 @@ module Minitest
40
46
  assertion, message = message, assertion unless assertion.nil? || assertion.is_a?(Symbol)
41
47
  expected = @hold.(actual, **)
42
48
 
43
- if actual.nil?
44
- assert_nil expected, message
45
- else
46
- send(assertion || :assert_equal, expected, actual, message)
49
+ begin
50
+ if actual.nil?
51
+ assert_nil expected, message
52
+ else
53
+ send(assertion || :assert_equal, expected, actual, message)
54
+ end
55
+ rescue Minitest::Assertion
56
+ raise unless Holdify.pretty
57
+
58
+ diff = Holdify::Pretty.call(expected, actual)
59
+ raise unless diff
60
+
61
+ msg = message ? "#{message}\n#{diff}" : diff
62
+ raise Minitest::Assertion, msg
47
63
  end
48
64
 
49
65
  if inspect
@@ -54,10 +70,10 @@ module Minitest
54
70
  expected
55
71
  end
56
72
 
57
- # Temporarily used to store the actual value, useful for reconciliation of expected changed values
73
+ # Force store the current value
58
74
  def assert_hold!(*, **) = assert_hold(*, **, force: true)
59
75
 
60
- # Temporarily used for development feedback to print to STDERR the actual value
76
+ # Print to STDERR the actual value
61
77
  def assert_hold?(*, **) = assert_hold(*, **, inspect: true)
62
78
  end
63
79
 
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.3
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Domizio Demichelis
@@ -34,6 +34,9 @@ files:
34
34
  - LICENSE.txt
35
35
  - lib/holdify.rb
36
36
  - lib/holdify/hold.rb
37
+ - lib/holdify/ledger.rb
38
+ - lib/holdify/pretty.rb
39
+ - lib/holdify/source.rb
37
40
  - lib/holdify/store.rb
38
41
  - lib/minitest-holdify.rb
39
42
  - lib/minitest/holdify_plugin.rb
@@ -59,7 +62,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
59
62
  - !ruby/object:Gem::Version
60
63
  version: '0'
61
64
  requirements: []
62
- rubygems_version: 3.6.9
65
+ rubygems_version: 4.0.3
63
66
  specification_version: 4
64
67
  summary: Hardcoded values suck! Holdify them.
65
68
  test_files: []