minitest-holdify 1.2.0 → 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: 056a8cc8b2a69276cf8adc189965b484cb9742b6c4e71efaca3b194b56bc1965
4
- data.tar.gz: 1570f826edd18de9ee3dbbe6fa39d759f597ab5f5f4ebf452b015ae9cc26f240
3
+ metadata.gz: b2fecbc870f5205d62da0200a806adc5dc5d47b99a81e53c1af4d50f3f8aaa21
4
+ data.tar.gz: 06aed82a631a3632c531ef311a53f89e339a00d122fbc75eb31c52f1723e8d24
5
5
  SHA512:
6
- metadata.gz: c6caaf67f67c9992df20d00ab2fc498365ae09f13225e7469aa370b68f0a42cbb77c97c7e68825c21b42600a606ef4c468bcec011c54e2380945bb8f9394741f
7
- data.tar.gz: 30bf7d38f0df80f1a2b581c12ee7fbd1ca6811369fe4a2977e286d1876d37daabb1eb5cd8b8d9b81ceb40103dcf430003ef1ed8240db79ce5c5579680395ca7b
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,57 +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, :store
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] }
12
+ @session = Hash.new { |h, k| h[k] = [] } # { lineno => [values] }
13
13
  @forced = [] # [ lines ]
14
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
- # The index of the current test/value
45
- def current_index(line) = @session[line].size - 1
46
-
47
- # Find the location in the test that triggered the hold
48
- def find_location
44
+ # Find the triggering LOC inside the test block/method
45
+ def find_test_loc
49
46
  caller_locations(2).find do |location|
50
47
  next unless location.path == @path
51
48
 
52
- label = location.base_label
49
+ label = location.base_label
53
50
  label == @test.name || label == '<top (required)>' || label == '<main>' || label.start_with?('<class:', '<module:')
54
51
  end
55
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
56
59
  end
57
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, :lookup
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,19 +1,23 @@
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.2.0'
9
- CONFIG = { ext: '.yaml' }.freeze
10
+ VERSION = '1.3.0'
10
11
 
11
12
  class << self
12
- attr_accessor :reconcile, :quiet
13
+ attr_accessor :reconcile, :quiet, :git, :pwd, :color, :rel_paths, :store_ext
14
+
15
+ def persist_all! = @stores&.each_value(&:persist)
13
16
 
14
- def config
15
- @config ||= { git: system('git --version', out: File::NULL, err: File::NULL),
16
- color: !ENV.key?('NO_COLOR') }
17
+ def relative(path)
18
+ return path unless rel_paths
19
+
20
+ path.sub(%r{^#{pwd}/}, '')
17
21
  end
18
22
 
19
23
  def stores(path = nil)
@@ -23,10 +27,6 @@ module Holdify
23
27
  @stores[path] ||= Store.new(path)
24
28
  end
25
29
  end
26
-
27
- def persist_all!
28
- @stores&.each_value(&:persist)
29
- end
30
30
  end
31
31
  @mutex = Mutex.new
32
32
  @stores = {}
@@ -1,25 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'holdify'
4
- require 'holdify/failure'
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
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
18
12
  end
19
-
20
13
  opts.on '--holdify-quiet', 'Skip the warning on storing a new value' do
21
- 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
22
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! }
23
41
  end
24
42
 
25
43
  # Reopen the minitest class
@@ -34,7 +52,7 @@ module Minitest
34
52
 
35
53
  path, = method(name).source_location
36
54
  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")
55
+ msg << @hold.forced.uniq.map { |lineno| " #{path}:#{lineno}" }.join("\n")
38
56
 
39
57
  raise Minitest::Assertion, msg
40
58
  end
@@ -56,18 +74,10 @@ module Minitest
56
74
  send(assertion || :assert_equal, expected, actual, message)
57
75
  end
58
76
  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
62
-
63
- msg = message ? "#{message}\n#{hold_msg}" : hold_msg
64
- raise Minitest::Assertion, msg
77
+ raise Minitest::Assertion, @hold.feedback(e.location, expected, actual, message)
65
78
  end
66
79
 
67
- if inspect
68
- location = @hold.find_location
69
- warn "[holdify] The value from #{location.path}:#{location.lineno} is:\n[holdify] => #{actual.inspect}"
70
- end
80
+ @hold.warn_for(actual) if inspect
71
81
 
72
82
  expected
73
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.2.0
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/failure.rb
50
+ - lib/holdify/feedback.rb
51
51
  - lib/holdify/hold.rb
52
- - lib/holdify/ledger.rb
53
52
  - lib/holdify/source.rb
54
53
  - lib/holdify/store.rb
55
54
  - lib/minitest-holdify.rb
@@ -1,110 +0,0 @@
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
@@ -1,82 +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)
32
- File.write(@path, content)
33
- end
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
-
59
- private
60
-
61
- def load_and_align
62
- {}.tap do |aligned|
63
- data = YAML.unsafe_load_file(@path) || {}
64
- data.group_by { |k, _| k.split.last }.each do |xxh, entries|
65
- lines = @source.lines(xxh)
66
- next if lines.empty?
67
-
68
- # Position of the held lines compared to the source lines
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] }
72
-
73
- # Align lines
74
- lines.each do |line|
75
- match = stayed.find { _1[:line] == line } || moved.shift
76
- aligned[line] = match[:values] if match
77
- end
78
- end
79
- end
80
- end
81
- end
82
- end