skillnad 1.2.2

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.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +20 -0
  3. data/README.md +172 -0
  4. data/Rakefile +17 -0
  5. data/lib/rubygems/commands/compare_command.rb +105 -0
  6. data/lib/rubygems/comparator/base.rb +45 -0
  7. data/lib/rubygems/comparator/dependency_comparator.rb +110 -0
  8. data/lib/rubygems/comparator/dir_utils.rb +51 -0
  9. data/lib/rubygems/comparator/file_list_comparator.rb +212 -0
  10. data/lib/rubygems/comparator/gemfile_comparator.rb +149 -0
  11. data/lib/rubygems/comparator/monitor.rb +140 -0
  12. data/lib/rubygems/comparator/report/entry.rb +40 -0
  13. data/lib/rubygems/comparator/report.rb +131 -0
  14. data/lib/rubygems/comparator/spec_comparator.rb +66 -0
  15. data/lib/rubygems/comparator/utils.rb +134 -0
  16. data/lib/rubygems/comparator/version.rb +5 -0
  17. data/lib/rubygems/comparator.rb +308 -0
  18. data/lib/rubygems_plugin.rb +4 -0
  19. data/test/gemfiles/lorem-0.0.1/Gemfile +4 -0
  20. data/test/gemfiles/lorem-0.0.1/LICENSE.txt +22 -0
  21. data/test/gemfiles/lorem-0.0.1/README.md +29 -0
  22. data/test/gemfiles/lorem-0.0.1/Rakefile +2 -0
  23. data/test/gemfiles/lorem-0.0.1/lib/lorem/version.rb +3 -0
  24. data/test/gemfiles/lorem-0.0.1/lib/lorem.rb +7 -0
  25. data/test/gemfiles/lorem-0.0.1/lorem.gemspec +24 -0
  26. data/test/gemfiles/lorem-0.0.1.gem +0 -0
  27. data/test/gemfiles/lorem-0.0.2/CHANGELOG.md +6 -0
  28. data/test/gemfiles/lorem-0.0.2/Gemfile +4 -0
  29. data/test/gemfiles/lorem-0.0.2/LICENSE.txt +3 -0
  30. data/test/gemfiles/lorem-0.0.2/README.md +29 -0
  31. data/test/gemfiles/lorem-0.0.2/Rakefile +2 -0
  32. data/test/gemfiles/lorem-0.0.2/lib/lorem/version.rb +3 -0
  33. data/test/gemfiles/lorem-0.0.2/lib/lorem.rb +11 -0
  34. data/test/gemfiles/lorem-0.0.2/lorem.gemspec +24 -0
  35. data/test/gemfiles/lorem-0.0.2.gem +0 -0
  36. data/test/gemfiles/lorem-0.0.3/CHANGELOG.md +14 -0
  37. data/test/gemfiles/lorem-0.0.3/Gemfile +7 -0
  38. data/test/gemfiles/lorem-0.0.3/LICENSE.txt +3 -0
  39. data/test/gemfiles/lorem-0.0.3/README.md +29 -0
  40. data/test/gemfiles/lorem-0.0.3/Rakefile +2 -0
  41. data/test/gemfiles/lorem-0.0.3/bin/lorem +3 -0
  42. data/test/gemfiles/lorem-0.0.3/lib/lorem/utils.rb +7 -0
  43. data/test/gemfiles/lorem-0.0.3/lib/lorem/version.rb +3 -0
  44. data/test/gemfiles/lorem-0.0.3/lib/lorem.rb +11 -0
  45. data/test/gemfiles/lorem-0.0.3/lorem.gemspec +25 -0
  46. data/test/gemfiles/lorem-0.0.3.gem +0 -0
  47. data/test/gemfiles/lorem-0.0.4/CHANGELOG.md +21 -0
  48. data/test/gemfiles/lorem-0.0.4/Gemfile +6 -0
  49. data/test/gemfiles/lorem-0.0.4/LICENSE.txt +3 -0
  50. data/test/gemfiles/lorem-0.0.4/README.md +29 -0
  51. data/test/gemfiles/lorem-0.0.4/Rakefile +2 -0
  52. data/test/gemfiles/lorem-0.0.4/bin/lorem +3 -0
  53. data/test/gemfiles/lorem-0.0.4/lib/lorem/utils.rb +7 -0
  54. data/test/gemfiles/lorem-0.0.4/lib/lorem/version.rb +3 -0
  55. data/test/gemfiles/lorem-0.0.4/lib/lorem.rb +11 -0
  56. data/test/gemfiles/lorem-0.0.4/lorem.gemspec +25 -0
  57. data/test/gemfiles/lorem-0.0.4.gem +0 -0
  58. data/test/rubygems/comparator/test_dependency_comparator.rb +32 -0
  59. data/test/rubygems/comparator/test_dir_utils.rb +47 -0
  60. data/test/rubygems/comparator/test_file_list_comparator.rb +29 -0
  61. data/test/rubygems/comparator/test_gemfile_comparator.rb +16 -0
  62. data/test/rubygems/comparator/test_monitor.rb +183 -0
  63. data/test/rubygems/comparator/test_report.rb +25 -0
  64. data/test/rubygems/comparator/test_spec_comparator.rb +150 -0
  65. data/test/rubygems/comparator/test_utils.rb +35 -0
  66. data/test/rubygems/test_gem_commands_compare_command.rb +41 -0
  67. data/test/test_helper.rb +53 -0
  68. metadata +165 -0
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'diffy'
4
+ require 'rubygems/comparator/base'
5
+ require 'rubygems/comparator/dir_utils'
6
+ require 'rubygems/comparator/monitor'
7
+
8
+ class Gem::Comparator
9
+
10
+ ##
11
+ # Gem::Comparator::FileListComparator can
12
+ # compare file lists from gem's specs
13
+ # based on the given Gem::Package objects
14
+ #
15
+ # To compare the files it needs to extract
16
+ # gem packages to +options[:output]+
17
+
18
+ class FileListComparator < Gem::Comparator::Base
19
+
20
+ def initialize(ignore_group_writable=true)
21
+ expect(:packages)
22
+
23
+ # We need diff
24
+ begin
25
+ IO.popen('diff --version')
26
+ rescue Exception
27
+ error('Calling `diff` command failed. Do you have it installed?')
28
+ end
29
+
30
+ @ignore_group_writable = ignore_group_writable
31
+ end
32
+
33
+ ##
34
+ # Compare file lists for gem's Gem::Package objects
35
+ # in +packages+ and writes the changes to the +report+
36
+ #
37
+ # If +options[:param]+ is set, it compares only
38
+ # that file list
39
+
40
+ def compare(packages, report, options = {})
41
+ info 'Checking file lists...'
42
+
43
+ @packages = packages
44
+
45
+ # Check file lists from older versions to newer
46
+ filter_params(SPEC_FILES_PARAMS, options[:param]).each do |param|
47
+ all_same = true
48
+
49
+ packages.each_with_index do |pkg, index|
50
+ unpacked_gem_dirs[packages[index].spec.version] = extract_gem(pkg, options[:output])
51
+ next if index == 0
52
+
53
+ # File lists as arrays
54
+ previous = value_from_spec(param, packages[index-1].spec)
55
+ current = value_from_spec(param, pkg.spec)
56
+ next unless (previous && current)
57
+
58
+ vers = "#{packages[index-1].spec.version}->#{packages[index].spec.version}"
59
+
60
+ deleted = previous - current
61
+ added = current - previous
62
+ same = current - added
63
+
64
+ if options[:brief]
65
+ deleted, dirs_added = dir_changed(previous, current)
66
+ end
67
+
68
+ report[param].set_header "#{different} #{param}:"
69
+
70
+ report[param][vers].section do
71
+ set_header "#{Rainbow(packages[index-1].spec.version).cyan}->" +
72
+ "#{Rainbow(packages[index].spec.version).cyan}:"
73
+ nest('deleted').section do
74
+ set_header '* Deleted:'
75
+ puts deleted unless deleted.empty?
76
+ end
77
+
78
+ nest('added').section do
79
+ set_header '* Added:'
80
+ puts dirs_added if options[:brief]
81
+ end
82
+ end
83
+ # Add information about permissions, shebangs etc.
84
+ report = check_added_files(param, vers, index, added, report, options[:brief], options[:diff])
85
+
86
+ report[param][vers]['changed'].set_header '* Changed:'
87
+ report = check_same_files(param, vers, index, same, report, options[:brief], options[:diff])
88
+ same_files = report[param][vers]['changed'].messages.empty?
89
+ all_same = false unless same_files
90
+
91
+ if previous == current && same_files && !all_same
92
+ report[param][vers] << "#{Rainbow(packages[index-1].spec.version).cyan}->" + \
93
+ "#{Rainbow(packages[index].spec.version).cyan}: No change"
94
+ end
95
+
96
+ end
97
+
98
+ if all_same && options[:log_all]
99
+ report[param].set_header "#{same} #{param}:"
100
+ value = value_from_spec(param, @packages[0].spec)
101
+ value = '[]' if value.empty?
102
+ report[param] << value
103
+ end
104
+ end
105
+ report
106
+ end
107
+
108
+ private
109
+
110
+ ##
111
+ # Access @unpacked_gem_dirs hash that stores
112
+ # paths to the unpacked gem dirs
113
+ #
114
+ # Keys of the hash are gem's versions
115
+
116
+ def unpacked_gem_dirs
117
+ @unpacked_gem_dirs ||= {}
118
+ end
119
+
120
+ ##
121
+ # This returns [deleted, added] directories between
122
+ # +previous+ and +current+ file lists
123
+ #
124
+ # For top level (.) it compares files themselves
125
+
126
+ def dir_changed(previous, current)
127
+ prev_dirs = DirUtils.dirs_of_files(previous)
128
+ curr_dirs = DirUtils.dirs_of_files(current)
129
+ deleted = DirUtils.remove_subdirs(prev_dirs - curr_dirs)
130
+ added = DirUtils.remove_subdirs(curr_dirs - prev_dirs)
131
+ [deleted, added]
132
+ end
133
+
134
+ def check_added_files(param, vers, index, files, report, brief_mode, diff_mode)
135
+ files.each do |file|
136
+ added_file = File.join(unpacked_gem_dirs[@packages[index].spec.version], file)
137
+
138
+ line_changes = if diff_mode
139
+ Monitor.files_diff(nil, added_file)
140
+ else
141
+ Monitor.lines_changed(nil, added_file)
142
+ end
143
+
144
+ changes = Monitor.new_file_permissions(added_file, @ignore_group_writable),
145
+ Monitor.new_file_executability(added_file),
146
+ Monitor.new_file_shebang(added_file)
147
+
148
+ if(!changes.join.empty? || (!brief_mode && !line_changes.empty?))
149
+ if diff_mode
150
+ report[param][vers]['added'][file].set_header file
151
+ report[param][vers]['added'][file] << line_changes.split("\n")
152
+ else
153
+ report[param][vers]['added'] << "#{file} #{line_changes}"
154
+ end
155
+ end
156
+
157
+ changes.each do |change|
158
+ report[param][vers]['added'] << change unless change.empty?
159
+ end
160
+ end
161
+ report
162
+ end
163
+
164
+ def check_same_files(param, vers, index, files, report, brief_mode, diff_mode)
165
+ files.each do |file|
166
+ prev_file = File.join(unpacked_gem_dirs[@packages[index-1].spec.version], file)
167
+ curr_file = File.join(unpacked_gem_dirs[@packages[index].spec.version], file)
168
+
169
+ next unless check_files([prev_file, curr_file])
170
+
171
+ line_changes = if diff_mode
172
+ Monitor.files_diff(prev_file, curr_file)
173
+ else
174
+ Monitor.lines_changed(prev_file, curr_file)
175
+ end
176
+
177
+ changes = Monitor.files_permissions_changes(prev_file, curr_file, @ignore_group_writable),
178
+ Monitor.files_executability_changes(prev_file, curr_file),
179
+ Monitor.files_shebang_changes(prev_file, curr_file)
180
+
181
+ if(!changes.join.empty? || (!brief_mode && !line_changes.empty?))
182
+ if diff_mode
183
+ report[param][vers]['changed'][file].set_header file
184
+ report[param][vers]['changed'][file] << line_changes.split("\n")
185
+ else
186
+ report[param][vers]['changed'] << "#{file} #{line_changes}"
187
+ end
188
+ end
189
+
190
+ changes.each do |change|
191
+ report[param][vers]['changed'] << change unless change.empty?
192
+ end
193
+ end
194
+ report
195
+ end
196
+
197
+ ##
198
+ # Check that files exist
199
+
200
+ def check_files(files)
201
+ files.each do |file|
202
+ unless File.exist? file
203
+ warn "#{file} mentioned in spec does not exist " +
204
+ "in the gem package, skipping check"
205
+ return false
206
+ end
207
+ end
208
+ true
209
+ end
210
+
211
+ end
212
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gemnasium/parser'
4
+ require 'rubygems/comparator/base'
5
+
6
+ class Gem::Comparator
7
+
8
+ ##
9
+ # Gem::Comparator::GemfileComparator can
10
+ # compare dependencies between gem's Gemfiles
11
+ # based on the given Gem::Package objects
12
+ #
13
+ # To compare Gemfiles it needs to extract
14
+ # gem packages to +options[:output]+
15
+
16
+ class GemfileComparator < Gem::Comparator::Base
17
+
18
+ def initialize
19
+ expect(:packages)
20
+ end
21
+
22
+ ##
23
+ # Compare Gemfiles using gem's +packages+
24
+ # and write the changes to the +report+
25
+
26
+ def compare(packages, report, options = {})
27
+ info 'Checking Gemfiles for dependencies...'
28
+ return report if options[:param] && options[:param] != 'gemfiles'
29
+
30
+ @packages = packages
31
+ all_same = true
32
+
33
+ # Check Gemfiles from older versions to newer
34
+ packages.each_with_index do |pkg, index|
35
+ unpacked_gem_dirs[@packages[index].spec.version] = extract_gem(pkg, options[:output])
36
+ next if index == 0
37
+
38
+ prev_gemfile = File.join(unpacked_gem_dirs[@packages[index-1].spec.version], 'Gemfile')
39
+ curr_gemfile = File.join(unpacked_gem_dirs[@packages[index].spec.version], 'Gemfile')
40
+
41
+ vers = "#{@packages[index-1].spec.version}->#{@packages[index].spec.version}"
42
+ report['gemfiles'][vers].set_header "#{Rainbow(packages[index-1].spec.version).cyan}->" +
43
+ "#{Rainbow(packages[index].spec.version).cyan}:"
44
+
45
+ added, deleted, updated = compare_gemfiles(prev_gemfile, curr_gemfile)
46
+
47
+ report['gemfiles'][vers]['added'].section do
48
+ set_header '* Added:'
49
+ puts added.map { |x| "#{x.name} #{x.requirements_list} (#{x.type})" } unless added.empty?
50
+ end
51
+ report['gemfiles'][vers]['deleted'].section do
52
+ set_header '* Deleted'
53
+ puts deleted.map { |x| "#{x.name} #{x.requirements_list} (#{x.type})" } unless deleted.empty?
54
+ end
55
+ report['gemfiles'][vers]['updated'].section do
56
+ set_header '* Updated'
57
+ puts updated unless updated.empty?
58
+ end
59
+ all_same = false if !added.empty? || !deleted.empty?
60
+ end
61
+ if all_same && options[:log_all]
62
+ report['gemfiles'].set_header "#{same} Gemfiles:"
63
+ gemfile = File.join(unpacked_gem_dirs[@packages[1].spec.version], 'Gemfile')
64
+ if File.exist? gemfile
65
+ deps = gemfile_deps(gemfile)
66
+ deps = '[]' if deps.empty?
67
+ report['gemfiles'] << deps
68
+ else
69
+ report['gemfiles'] << 'No Gemfiles'
70
+ end
71
+ elsif !all_same
72
+ report['gemfiles'].set_header "#{different} Gemfile dependencies"
73
+ end
74
+
75
+ report
76
+ end
77
+
78
+ private
79
+
80
+ ##
81
+ # Access @unpacked_gem_dirs hash that stores
82
+ # paths to the unpacked gem dirs
83
+ #
84
+ # Keys of the hash are gem's versions
85
+
86
+ def unpacked_gem_dirs
87
+ @unpacked_gem_dirs ||= {}
88
+ end
89
+
90
+ ##
91
+ # Compare two Gemfiles for dependencies
92
+ #
93
+ # Return [added, deleted, updated] deps
94
+
95
+ def compare_gemfiles(prev_gemfile, curr_gemfile)
96
+ prev_deps = gemfile_deps(prev_gemfile)
97
+ curr_deps = gemfile_deps(curr_gemfile)
98
+ added = curr_deps - prev_deps
99
+ deleted = prev_deps - curr_deps
100
+
101
+ split_dependencies(added, deleted)
102
+ end
103
+
104
+ ##
105
+ # Get the Gemfile dependencies from +gemfile+
106
+
107
+ def gemfile_deps(gemfile)
108
+ if File.exist?(gemfile)
109
+ parse_gemfile(gemfile).dependencies
110
+ else
111
+ []
112
+ end
113
+ end
114
+
115
+ ##
116
+ # Parse +gemfile+ using Gemnasium::Parser
117
+ #
118
+ # Return Gemnasium::Parser::Gemfile
119
+
120
+ def parse_gemfile(gemfile)
121
+ Gemnasium::Parser.gemfile File.open(gemfile).read
122
+ end
123
+
124
+ ##
125
+ # Find updated dependencies between +added+ and
126
+ # +deleted+ deps and move them out to +updated+.
127
+ #
128
+ # Return [added, deleted, updated] deps
129
+
130
+ def split_dependencies(added, deleted)
131
+ updated = []
132
+ added.dup.each do |ad|
133
+ deleted.dup.each do |dd|
134
+ if ad.name == dd.name && ad.type == dd.type
135
+ unless ad.requirements_list == dd.requirements_list
136
+ updated << "#{ad.name} " +
137
+ "from: #{dd.requirements_list} " +
138
+ "to: #{ad.requirements_list}"
139
+ end
140
+ added.delete ad
141
+ deleted.delete dd
142
+ end
143
+ end
144
+ end
145
+ [added, deleted, updated]
146
+ end
147
+
148
+ end
149
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'diffy'
4
+ require 'rubygems/comparator/base'
5
+ require 'rubygems/comparator/dir_utils'
6
+
7
+ class Gem::Comparator
8
+ module Monitor
9
+
10
+ def self.lines_changed(prev_file, curr_file)
11
+ line = compact_files_diff(prev_file, curr_file)
12
+ return '' if line.empty?
13
+ plus = "+#{line.count('+')}"
14
+ minus = "-#{line.count('-')}"
15
+ "#{Rainbow(plus).green}/#{Rainbow(minus).red}"
16
+ end
17
+
18
+ def self.compact_files_diff(prev_file, curr_file)
19
+ prev_file = prev_file.nil? ? Tempfile.new.path : prev_file
20
+ changes = +''
21
+ Diffy::Diff.new(
22
+ prev_file, curr_file, :source => 'files', :context => 0
23
+ ).each do |line|
24
+ case line
25
+ when /^\+/ then changes << Rainbow('+').green
26
+ when /^-/ then changes << Rainbow('-').red
27
+ end
28
+ end
29
+ changes
30
+ end
31
+
32
+ def self.files_diff(prev_file, curr_file)
33
+ prev_file = prev_file.nil? ? Tempfile.new.path : prev_file
34
+ changes = +''
35
+ Diffy::Diff.new(
36
+ prev_file, curr_file, :source => 'files', :context => 0, :include_diff_info => true
37
+ ).each do |line|
38
+ case line
39
+ when /^\+/ then changes << Rainbow(line).green
40
+ when /^-/ then changes << Rainbow(line).red
41
+ else changes << line
42
+ end
43
+ end
44
+ changes
45
+ end
46
+
47
+ def self.files_permissions_changes(prev_file, curr_file, ignore_group_writable=false)
48
+ prev_permissions = File.stat(prev_file).mode
49
+ curr_permissions = File.stat(curr_file).mode
50
+
51
+ diff = prev_permissions ^ curr_permissions
52
+ diff ^= 020 if diff != 0 && ignore_group_writable
53
+
54
+ if diff != 0
55
+ " (!) New permissions: " +
56
+ "#{format_permissions(prev_permissions)} -> #{format_permissions(curr_permissions)}"
57
+ else
58
+ ''
59
+ end
60
+ end
61
+
62
+ def self.new_file_permissions(file, ignore_group_writable=false)
63
+ file_permissions = File.stat(file).mode
64
+
65
+ expected_permissions = if DirUtils.gem_bin_file?(file)
66
+ 0100755
67
+ else
68
+ 0100644
69
+ end
70
+
71
+ permissions_to_compare = if ignore_group_writable && file_permissions != expected_permissions
72
+ file_permissions ^ 020
73
+ else
74
+ file_permissions
75
+ end
76
+
77
+ unless permissions_to_compare == expected_permissions
78
+ return " (!) Unexpected permissions: #{format_permissions(file_permissions)}"
79
+ end
80
+ ''
81
+ end
82
+
83
+ def self.files_executability_changes(prev_file, curr_file)
84
+ prev_executable = File.stat(prev_file).executable?
85
+ curr_executable = File.stat(curr_file).executable?
86
+
87
+ if !prev_executable && curr_executable
88
+ " (!) File is now executable!"
89
+ elsif prev_executable && !curr_executable
90
+ " (!) File is no longer executable!"
91
+ else
92
+ ''
93
+ end
94
+ end
95
+
96
+ def self.new_file_executability(file)
97
+ file_executable = File.stat(file).executable?
98
+
99
+ if file_executable && !DirUtils.gem_bin_file?(file)
100
+ " (!) File is executable"
101
+ elsif !file_executable && DirUtils.gem_bin_file?(file)
102
+ " (!) File is not executable"
103
+ else
104
+ ''
105
+ end
106
+ end
107
+
108
+ def self.files_shebang_changes(prev_file, curr_file)
109
+ return '' if DirUtils.files_same_first_line?(prev_file, curr_file)
110
+
111
+ prev_has_shebang = DirUtils.file_has_shebang? prev_file
112
+ curr_has_shebang = DirUtils.file_has_shebang? curr_file
113
+
114
+ if prev_has_shebang && !curr_has_shebang
115
+ " (!) Shebang probably lost: #{DirUtils.file_first_line(prev_file)}"
116
+ elsif !prev_has_shebang && curr_has_shebang
117
+ " (!) Shebang probably added: #{DirUtils.file_first_line(curr_file)}"
118
+ elsif prev_has_shebang && curr_has_shebang
119
+ " (!) Shebang probably changed: " +
120
+ "#{first_lines[prev_file]} -> #{DirUtils.file_first_line(curr_file)}"
121
+ else
122
+ ''
123
+ end
124
+ end
125
+
126
+ def self.new_file_shebang(file)
127
+ file_has_shebang = DirUtils.file_has_shebang? file
128
+
129
+ if file_has_shebang
130
+ " (!) Shebang found: #{DirUtils.file_first_line(file)}"
131
+ else
132
+ ''
133
+ end
134
+ end
135
+
136
+ def self.format_permissions(permissions)
137
+ sprintf("%o", permissions)
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Gem::Comparator
4
+ class Report
5
+ class Entry
6
+ include Gem::UserInteraction
7
+
8
+ attr_accessor :data, :indent
9
+
10
+ def initialize(data = '', indent = '')
11
+ @data = data
12
+ @indent = indent
13
+ end
14
+
15
+ def set_indent!(indent)
16
+ @indent = indent
17
+ self
18
+ end
19
+
20
+ def empty?
21
+ case @data
22
+ when String, Array
23
+ @data.empty?
24
+ end
25
+ end
26
+
27
+ def print
28
+ printed = case @data
29
+ when String
30
+ "#{@indent}#{@data}"
31
+ when Array
32
+ @indent + @data.join("\n#{@indent}")
33
+ else
34
+ "#{@indent}#{@data}"
35
+ end
36
+ say printed
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubygems/comparator/report/entry'
4
+
5
+ class Gem::Comparator
6
+
7
+ ##
8
+ # Gem::Comparator::Report can nest sections and print only those that
9
+ # contain some messages.
10
+ #
11
+ # Usage:
12
+ # report = Gem::Comparator::Report.new
13
+ # report['section1'] << "Message 1"
14
+ # report['section1']['subsection1'].set_header 'Message 2 Header'
15
+ # report['section1']['subsection1'] << "Message 2" if false
16
+ # report['section1'].section
17
+ # nest('subsection2').section
18
+ # puts "Message 3"
19
+ # end
20
+ # end
21
+ # report.print
22
+ #
23
+ # This won't print Message 2 nor its header saving a lot of if/else.
24
+
25
+ class Report
26
+
27
+ module Signs
28
+ def same
29
+ Rainbow('SAME').green.bright
30
+ end
31
+
32
+ def different
33
+ Rainbow('DIFFERENT').red.bright
34
+ end
35
+ end
36
+
37
+ def self.new(name = 'main')
38
+ Gem::Comparator::Report::NestedSection.new(name)
39
+ end
40
+
41
+ class NestedSection
42
+ include Report::Signs
43
+ include Gem::UserInteraction
44
+
45
+ DEFAULT_INDENT = ' '
46
+
47
+ attr_reader :header, :messages, :parent_section, :level
48
+ attr_accessor :name, :sections
49
+
50
+ def initialize(name, parent_section = nil)
51
+ @name = name
52
+ @header = Entry.new
53
+ @messages = []
54
+ @sections = []
55
+ @level = 0
56
+
57
+ set_parent parent_section if parent_section
58
+ end
59
+
60
+ def section(&block)
61
+ instance_eval(&block)
62
+ end
63
+
64
+ def set_header(message)
65
+ @header = Entry.new(message)
66
+ end
67
+
68
+ def puts(message)
69
+ case message
70
+ when String, Array
71
+ @messages << Entry.new(message) unless message.empty?
72
+ else
73
+ @messages << Entry.new(message) unless message
74
+ end
75
+ end
76
+ alias_method :<<, :puts
77
+
78
+ def nest(name)
79
+ @sections.each do |s|
80
+ return s if s.name == name
81
+ end
82
+ NestedSection.new(name, self)
83
+ end
84
+ alias_method :[], :nest
85
+
86
+ def print
87
+ all_messages.each { |m| m.print }
88
+ end
89
+
90
+ def all_messages
91
+ indent = DEFAULT_INDENT*@level
92
+
93
+ if @header.empty?
94
+ @messages.map do |m|
95
+ m.set_indent!(indent)
96
+ end + nested_messages
97
+ else
98
+ nested = @messages.map do |m|
99
+ m.set_indent!(indent * 2)
100
+ end + nested_messages
101
+ return [] if nested.empty?
102
+
103
+ @header.set_indent!(indent)
104
+ nested.unshift(@header)
105
+ end
106
+ end
107
+
108
+ def lines(line_num)
109
+ all_messages[line_num]&.data
110
+ end
111
+
112
+ def nested_messages
113
+ nested_messages = []
114
+ @sections.each do |section|
115
+ section.all_messages.each do |m|
116
+ nested_messages << m
117
+ end
118
+ end
119
+ nested_messages
120
+ end
121
+
122
+ private
123
+
124
+ def set_parent(parent)
125
+ parent.sections << self
126
+ @level = parent.level + 1
127
+ end
128
+
129
+ end
130
+ end
131
+ end