abide_dev_utils 0.9.7 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bc24decc17e394464d7ccfe4b461e8e6a5e74792b3a6a76092d5bc8d1fc9d87f
4
- data.tar.gz: 43811951c377e43d3e6a10b9d295e807472d17b37ddda143d1a7e3f39a277a6c
3
+ metadata.gz: 4fc3820895d385ca28d6e097c1822cfaf878d41e2b282bfca55bec2e884a2dc2
4
+ data.tar.gz: de27ab8fb2021de5cd437695566e4ab0ad0531bc94659948557b3b09a05d59f5
5
5
  SHA512:
6
- metadata.gz: 8228550f80f8234306b6f47ec45d4abd62df2953826b40cd225f8431753f1d72eff7e5c4caec2d5adedeac45d77c495116e606d3d8e66cfb2f662d92306e3f79
7
- data.tar.gz: 7bd968ca178a24897184997445d36ece5e6c2bcbdd0a3bed9156b984c58cce1b4e289e9e8894a71d3a3ccab0d02c25a88b2b5005190db97e49f40173c0ed6479
6
+ metadata.gz: 7df796781a8f4e4b87cf324c83273a9d2964f44f8dc8fd9f68c65610295f7b9f5161b70c23d66ab19ff75ca610860d27245256dabf6e69633850cbd2d1f3dd96
7
+ data.tar.gz: 3cf0142c1a0f3fae3bab07f65add4a320f90ca900adc5be63b77f72e40edba4936b6ea835796d0fd6b2c3cff034b6a2dcb4f5bd1ff43a476b40bcc053c5356cb
data/Gemfile.lock CHANGED
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- abide_dev_utils (0.9.7)
4
+ abide_dev_utils (0.10.0)
5
+ amatch (~> 0.4)
5
6
  cmdparse (~> 3.0)
6
7
  google-cloud-storage (~> 1.34)
7
8
  hashdiff (~> 1.0)
@@ -21,6 +22,9 @@ GEM
21
22
  tzinfo (~> 2.0)
22
23
  addressable (2.8.0)
23
24
  public_suffix (>= 2.0.2, < 5.0)
25
+ amatch (0.4.0)
26
+ mize
27
+ tins (~> 1.0)
24
28
  ast (2.4.2)
25
29
  async (1.30.1)
26
30
  console (~> 1.10)
@@ -53,7 +57,7 @@ GEM
53
57
  diff-lcs (1.5.0)
54
58
  digest-crc (0.6.4)
55
59
  rake (>= 12.0.0, < 14.0.0)
56
- facter (4.2.7)
60
+ facter (4.2.9)
57
61
  hocon (~> 1.3)
58
62
  thor (>= 1.0.1, < 2.0)
59
63
  faraday (1.10.0)
@@ -104,13 +108,13 @@ GEM
104
108
  webrick
105
109
  google-apis-iamcredentials_v1 (0.10.0)
106
110
  google-apis-core (>= 0.4, < 2.a)
107
- google-apis-storage_v1 (0.11.0)
111
+ google-apis-storage_v1 (0.13.0)
108
112
  google-apis-core (>= 0.4, < 2.a)
109
113
  google-cloud-core (1.6.0)
110
114
  google-cloud-env (~> 1.0)
111
115
  google-cloud-errors (~> 1.0)
112
- google-cloud-env (1.5.0)
113
- faraday (>= 0.17.3, < 2.0)
116
+ google-cloud-env (1.6.0)
117
+ faraday (>= 0.17.3, < 3.0)
114
118
  google-cloud-errors (1.2.0)
115
119
  google-cloud-storage (1.36.1)
116
120
  addressable (~> 2.8)
@@ -128,7 +132,7 @@ GEM
128
132
  os (>= 0.9, < 2.0)
129
133
  signet (>= 0.16, < 2.a)
130
134
  hashdiff (1.0.1)
131
- hiera (3.8.0)
135
+ hiera (3.9.0)
132
136
  hocon (1.3.1)
133
137
  httpclient (2.8.3)
134
138
  i18n (1.10.0)
@@ -143,11 +147,15 @@ GEM
143
147
  memoist (0.16.2)
144
148
  method_source (1.0.0)
145
149
  mini_mime (1.1.2)
150
+ mini_portile2 (2.8.0)
146
151
  minitest (5.15.0)
152
+ mize (0.4.0)
153
+ protocol (~> 2.0)
147
154
  multi_json (1.15.0)
148
155
  multipart-post (2.1.1)
149
156
  nio4r (2.5.8)
150
- nokogiri (1.13.3-x86_64-darwin)
157
+ nokogiri (1.13.6)
158
+ mini_portile2 (~> 2.8.0)
151
159
  racc (~> 1.4)
152
160
  oauth (0.5.8)
153
161
  octokit (4.22.0)
@@ -157,6 +165,8 @@ GEM
157
165
  parallel (1.21.0)
158
166
  parser (3.1.1.0)
159
167
  ast (~> 2.4.1)
168
+ protocol (2.0.0)
169
+ ruby_parser (~> 3.0)
160
170
  protocol-hpack (1.4.2)
161
171
  protocol-http (0.22.5)
162
172
  protocol-http1 (0.14.2)
@@ -168,7 +178,7 @@ GEM
168
178
  coderay (~> 1.1)
169
179
  method_source (~> 1.0)
170
180
  public_suffix (4.0.6)
171
- puppet (7.14.0)
181
+ puppet (7.16.0)
172
182
  concurrent-ruby (~> 1.0)
173
183
  deep_merge (~> 1.0)
174
184
  facter (> 2.0.1, < 5)
@@ -224,6 +234,8 @@ GEM
224
234
  rubocop (~> 1.19)
225
235
  ruby-progressbar (1.11.0)
226
236
  ruby2_keywords (0.0.5)
237
+ ruby_parser (3.19.1)
238
+ sexp_processor (~> 4.16)
227
239
  rubyzip (2.3.2)
228
240
  sawyer (0.8.2)
229
241
  addressable (>= 2.3.5)
@@ -234,13 +246,17 @@ GEM
234
246
  rexml (~> 3.2, >= 3.2.5)
235
247
  rubyzip (>= 1.2.2)
236
248
  semantic_puppet (1.0.4)
249
+ sexp_processor (4.16.1)
237
250
  signet (0.16.1)
238
251
  addressable (~> 2.8)
239
252
  faraday (>= 0.17.5, < 3.0)
240
253
  jwt (>= 1.5, < 3.0)
241
254
  multi_json (~> 1.10)
255
+ sync (0.5.0)
242
256
  thor (1.2.1)
243
257
  timers (4.3.3)
258
+ tins (1.31.0)
259
+ sync
244
260
  trailblazer-option (0.1.2)
245
261
  tzinfo (2.0.4)
246
262
  concurrent-ruby (~> 1.0)
@@ -40,6 +40,7 @@ Gem::Specification.new do |spec|
40
40
  spec.add_dependency 'selenium-webdriver', '~> 4.0.0.beta4'
41
41
  spec.add_dependency 'google-cloud-storage', '~> 1.34'
42
42
  spec.add_dependency 'hashdiff', '~> 1.0'
43
+ spec.add_dependency 'amatch', '~> 0.4'
43
44
 
44
45
  # Dev dependencies
45
46
  spec.add_development_dependency 'bundler'
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'abide_dev_utils/xccdf'
4
+
5
+ module AbideDevUtils
6
+ # Methods for working with Compliance Enforcement Modules (CEM)
7
+ module CEM
8
+ def self.xccdf
9
+ return @xccdf if defined?(@xccdf)
10
+
11
+ xccdf = Object.new
12
+ xccdf.extend AbideDevUtils::XCCDF::Common
13
+ @xccdf = xccdf
14
+ @xccdf
15
+ end
16
+
17
+ def self.rule_id_format(rule_id)
18
+ case rule_id
19
+ when /^c[0-9_]+$/
20
+ :hiera_title_num
21
+ when /^[a-z][a-z0-9_]+$/
22
+ :hiera_title
23
+ when /^[0-9.]+$/
24
+ :number
25
+ else
26
+ :title
27
+ end
28
+ end
29
+
30
+ def self.rule_identifiers(rule_id)
31
+ {
32
+ number: xccdf.control_parts(rule_id).first,
33
+ hiera_title: xccdf.name_normalize_control(rule_id),
34
+ hiera_title_num: xccdf.number_normalize_control(rule_id),
35
+ }
36
+ end
37
+
38
+ def self.update_legacy_config_from_diff(config_hiera, diff)
39
+ new_config_hiera = config_hiera.dup
40
+ new_control_configs = {}
41
+ change_report = []
42
+ changes = diff.select { |d| d[:type][0] == :number }
43
+ config_hiera['config']['control_configs'].each do |key, val_hash|
44
+ key_id_format = rule_id_format(key)
45
+ changed = false
46
+ changes.each do |change|
47
+ if key_id_format == :title
48
+ next unless change[:title] == key
49
+ else
50
+ next unless rule_identifiers(change[:self].id)[key_id_format] == key
51
+ end
52
+
53
+ changed = true
54
+ new_key = if key_id_format == :title
55
+ change[:other_title]
56
+ else
57
+ rule_identifiers(change[:other].id)[key_id_format]
58
+ end
59
+ new_control_configs[new_key] = val_hash
60
+ change_report << {
61
+ type: :identifier_update,
62
+ from: key,
63
+ to: new_key,
64
+ }
65
+ end
66
+ new_control_configs[key] = val_hash unless changed
67
+ end
68
+ new_config_hiera['config']['control_configs'] = new_control_configs
69
+ [new_config_hiera, change_report]
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'abide_dev_utils/cem'
4
+ require 'abide_dev_utils/files'
5
+ require 'abide_dev_utils/output'
6
+ require 'abide_dev_utils/validate'
7
+ require 'abide_dev_utils/xccdf/diff/benchmark'
8
+ require 'abide_dev_utils/cli/abstract'
9
+
10
+ module Abide
11
+ module CLI
12
+ class CemCommand < AbideCommand
13
+ CMD_NAME = 'cem'
14
+ CMD_SHORT = 'Commands related to Puppet CEM'
15
+ CMD_LONG = 'Namespace for commands related to Puppet CEM'
16
+ def initialize
17
+ super(CMD_NAME, CMD_SHORT, CMD_LONG, takes_commands: true)
18
+ add_command(CemUpdateConfig.new)
19
+ end
20
+ end
21
+
22
+ class CemUpdateConfig < AbideCommand
23
+ CMD_NAME = 'update-config'
24
+ CMD_SHORT = 'Updates the Puppet CEM config'
25
+ CMD_LONG = 'Updates the Puppet CEM config'
26
+ def initialize
27
+ super(CMD_NAME, CMD_SHORT, CMD_LONG, takes_commands: true)
28
+ add_command(CemUpdateConfigFromDiff.new)
29
+ end
30
+ end
31
+
32
+ class CemUpdateConfigFromDiff < AbideCommand
33
+ CMD_NAME = 'from-diff'
34
+ CMD_SHORT = 'Update by diffing two XCCDF files'
35
+ CMD_LONG = 'Update by diffing two XCCDF files'
36
+ CMD_CONFIG_FILE = 'Path to the Puppet CEM config file'
37
+ CMD_CURRENT_XCCDF = 'Path to the current XCCDF file'
38
+ CMD_NEW_XCCDF = 'Path to the new XCCDF file'
39
+ def initialize
40
+ super(CMD_NAME, CMD_SHORT, CMD_LONG, takes_commands: false)
41
+ argument_desc(CONFIG_FILE: CMD_CONFIG_FILE, CURRENT_XCCDF: CMD_CURRENT_XCCDF, NEW_XCCDF: CMD_NEW_XCCDF)
42
+ options.on('-o [FILE]', '--out-file [FILE]', 'Path to save the updated config file') do |o|
43
+ @data[:out_file] = o
44
+ end
45
+ options.on('-v', '--verbose', 'Verbose output') do
46
+ @data[:verbose] = true
47
+ end
48
+ options.on('-q', '--quiet', 'Quiet output') do
49
+ @data[:quiet] = true
50
+ end
51
+ end
52
+
53
+ def help_arguments
54
+ <<~ARGHELP
55
+ Arguments:
56
+ CONFIG_FILE: #{CMD_CONFIG_FILE}
57
+ CURRENT_XCCDF: #{CMD_CURRENT_XCCDF}
58
+ NEW_XCCDF: #{CMD_NEW_XCCDF}
59
+ ARGHELP
60
+ end
61
+
62
+ def execute(config_file, cur_xccdf, new_xccdf)
63
+ AbideDevUtils::Validate.file(config_file, extension: 'yaml')
64
+ AbideDevUtils::Validate.file(cur_xccdf, extension: 'xml')
65
+ config_hiera = AbideDevUtils::Files::Reader.read(config_file, safe: true)
66
+ diff = AbideDevUtils::XCCDF::Diff::BenchmarkDiff.new(cur_xccdf, new_xccdf).diff[:diff][:number_title]
67
+ new_config_hiera, change_report = AbideDevUtils::CEM.update_legacy_config_from_diff(config_hiera, diff)
68
+ AbideDevUtils::Output.yaml(new_config_hiera, console: @data[:verbose], file: @data[:out_file])
69
+ AbideDevUtils::Output.simple(change_report) unless @data[:quiet]
70
+ end
71
+ end
72
+ end
73
+ end
@@ -104,13 +104,24 @@ module Abide
104
104
  options.on('-p [PROFILE]', '--profile', 'Only diff and specific profile in the benchmarks') do |x|
105
105
  @data[:profile] = x
106
106
  end
107
+ options.on('-l [LEVEL]', '--level', 'Only diff the specific level in the benchmarks') do |x|
108
+ @data[:level] = x
109
+ end
110
+ options.on('-r', '--raw', 'Output the diff in raw hash format') { @data[:raw] = true }
107
111
  options.on('-q', '--quiet', 'Show no output in the terminal') { @data[:quiet] = false }
108
112
  options.on('--no-diff-profiles', 'Do not diff the profiles in the XCCDF files') { @data[:diff_profiles] = false }
109
113
  options.on('--no-diff-controls', 'Do not diff the controls in the XCCDF files') { @data[:diff_controls] = false }
114
+ options.on('--old-style', 'Use old-style diffs') { @data[:old_style] = true }
110
115
  end
111
116
 
112
117
  def execute(file1, file2)
113
- diffreport = AbideDevUtils::XCCDF.diff(file1, file2, @data)
118
+ diffreport = if @data[:old_style]
119
+ AbideDevUtils::XCCDF.diff(file1, file2, @data)
120
+ else
121
+ dr = AbideDevUtils::XCCDF.new_style_diff(file1, file2, @data)
122
+ dr[:diff][:number_title].map! { |d| d[:text] }
123
+ dr
124
+ end
114
125
  AbideDevUtils::Output.yaml(diffreport, console: @data.fetch(:quiet, true), file: @data.fetch(:outfile, nil))
115
126
  end
116
127
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'cmdparse'
4
4
  require 'abide_dev_utils/version'
5
+ require 'abide_dev_utils/cli/cem'
5
6
  require 'abide_dev_utils/constants'
6
7
  require 'abide_dev_utils/cli/comply'
7
8
  require 'abide_dev_utils/cli/puppet'
@@ -22,6 +23,7 @@ module Abide
22
23
  parser.main_options.banner = ROOT_CMD_BANNER
23
24
  parser.add_command(CmdParse::HelpCommand.new, default: true)
24
25
  parser.add_command(CmdParse::VersionCommand.new(add_switches: true))
26
+ parser.add_command(CemCommand.new)
25
27
  parser.add_command(ComplyCommand.new)
26
28
  parser.add_command(PuppetCommand.new)
27
29
  parser.add_command(XccdfCommand.new)
@@ -1,7 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'abide_dev_utils/validate'
4
+
3
5
  module AbideDevUtils
4
6
  module Files
7
+ class Reader
8
+ def self.read(path, raw: false, safe: true, opts: {})
9
+ AbideDevUtils::Validate.file(path)
10
+ return File.read(path) if raw
11
+
12
+ extension = File.extname(path)
13
+ case extension
14
+ when /\.yaml|\.yml/
15
+ require 'yaml'
16
+ if safe
17
+ YAML.safe_load(File.read(path))
18
+ else
19
+ YAML.load_file(path)
20
+ end
21
+ when '.json'
22
+ require 'json'
23
+ return JSON.parse(File.read(path), opts) if safe
24
+
25
+ JSON.parse!(File.read(path), opts)
26
+ when '.xml'
27
+ require 'nokogiri'
28
+ File.open(path, 'r') do |file|
29
+ Nokogiri::XML.parse(file) do |config|
30
+ config.strict.noblanks.norecover
31
+ end
32
+ end
33
+ else
34
+ File.read(path)
35
+ end
36
+ end
37
+ end
38
+
5
39
  class Writer
6
40
  MSG_EXT_APPEND = 'Appending %s extension to file'
7
41
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AbideDevUtils
4
- VERSION = "0.9.7"
4
+ VERSION = "0.10.0"
5
5
  end
@@ -0,0 +1,270 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'abide_dev_utils/xccdf/diff/benchmark/property_existence'
4
+
5
+ module AbideDevUtils
6
+ module XCCDF
7
+ module Diff
8
+ # Diffs two XCCDF benchmarks using the title / number of the items as the primary
9
+ # diff properties.
10
+ class NumberTitleDiff
11
+ SKIP_DIFF_TYPES = %i[equal both].freeze
12
+
13
+ def initialize(numbered_children, other_numbered_children)
14
+ new_number_title_objs(numbered_children, other_numbered_children)
15
+ end
16
+
17
+ def diff
18
+ @diff ||= find_diffs(@number_title_objs, @other_number_title_objs)
19
+ end
20
+
21
+ def to_s
22
+ parts = []
23
+ @diff.each do |_, diffs|
24
+ diffs.each do |dh|
25
+ parts << dh[:diff_text]
26
+ end
27
+ end
28
+ parts.join("\n")
29
+ end
30
+
31
+ private
32
+
33
+ attr_writer :diff
34
+
35
+ def added_number_title_objs
36
+ added_titles = @self_prop_checker.added_titles
37
+ @other_number_title_objs.select do |nto|
38
+ added_titles.include?(nto.title)
39
+ end
40
+ end
41
+
42
+ def removed_number_title_objs
43
+ removed_titles = @self_prop_checker.removed_titles
44
+ @number_title_objs.select do |nto|
45
+ removed_titles.include?(nto.title)
46
+ end
47
+ end
48
+
49
+ def find_diffs(objs, other_objs)
50
+ diffs = []
51
+ added_number_title_objs.each do |nto|
52
+ change_type = %i[both added]
53
+ stand_in = NumberTitleContainerStandIn.new(change_type)
54
+ diffs << process_diffs([diff_hash(change_type, stand_in, nto)])
55
+ end
56
+ removed_number_title_objs.each do |nto|
57
+ change_type = %i[both removed]
58
+ stand_in = NumberTitleContainerStandIn.new(change_type)
59
+ diffs << process_diffs([diff_hash(change_type, nto, stand_in)])
60
+ end
61
+ objs.each do |obj|
62
+ obj_diffs = other_objs.each_with_object([]) do |other_obj, o_ary|
63
+ obj_diff = obj.diff(other_obj)
64
+ next if SKIP_DIFF_TYPES.include?(obj_diff[0])
65
+
66
+ o_ary << diff_hash(obj_diff, obj, other_obj)
67
+ end
68
+
69
+ processed_obj_diffs = process_diffs(obj_diffs)
70
+ diffs << processed_obj_diffs unless obj_diffs.empty?
71
+ end
72
+ diffs
73
+ end
74
+
75
+ def process_diffs(diffs)
76
+ return {} if diffs.empty?
77
+
78
+ raise "Unexpected diffs: #{diffs}" if diffs.length > 2
79
+
80
+ return diffs[0] if diffs.length == 1
81
+
82
+ if diffs[0][:type][0] == PropChecker.inverse_existence_state[diffs[1][:type][0]]
83
+ diffs[0]
84
+ else
85
+ diffs[1]
86
+ end
87
+ end
88
+
89
+ def diff_hash(diff_type, obj, other_obj)
90
+ {
91
+ self: obj.child,
92
+ other: other_obj.child,
93
+ type: diff_type,
94
+ text: diff_type_text(diff_type, obj, other_obj),
95
+ number: obj.number,
96
+ other_number: other_obj.number,
97
+ title: obj.title,
98
+ other_title: other_obj.title,
99
+ }
100
+ end
101
+
102
+ def diff_type_text(diff_type, obj, other_obj)
103
+ DiffTypeText.text(diff_type, obj, other_obj)
104
+ end
105
+
106
+ def new_number_title_objs(children, other_children)
107
+ number_title_objs = children.map { |c| NumberTitleContainer.new(c) }.sort
108
+ other_number_title_objs = other_children.map { |c| NumberTitleContainer.new(c) }.sort
109
+ @self_prop_checker = PropChecker.new(number_title_objs, other_number_title_objs)
110
+ @other_prop_checker = PropChecker.new(other_number_title_objs, number_title_objs)
111
+ number_title_objs.map { |n| n.prop_checker = @self_prop_checker }
112
+ other_number_title_objs.map { |n| n.prop_checker = @other_prop_checker }
113
+ @number_title_objs = number_title_objs
114
+ @other_number_title_objs = other_number_title_objs
115
+ end
116
+ end
117
+
118
+ # Creates string representations of diff types
119
+ class DiffTypeText
120
+ def self.text(diff_type, obj, other_obj)
121
+ case diff_type[0]
122
+ when :equal
123
+ 'The objects are equal'
124
+ when :title
125
+ "Title changed: Number \"#{obj.number}\": #{obj.title} -> #{other_obj.title}"
126
+ when :number
127
+ number_diff_type_text(diff_type, obj, other_obj)
128
+ when :both
129
+ both_diff_type_text(diff_type, obj, other_obj)
130
+ when :add
131
+ "Add object with number \"#{other_obj.number}\" and title \"#{other_obj.title}\""
132
+ when :remove
133
+ "Remove object with number \"#{obj.number}\" and title \"#{obj.title}\""
134
+ else
135
+ raise ArgumentError, "Unknown diff type: #{diff_type}"
136
+ end
137
+ end
138
+
139
+ def self.number_diff_type_text(diff_type, obj, other_obj)
140
+ case diff_type[1]
141
+ when :added
142
+ "Number changed (New Number): Title \"#{obj.title}\": #{obj.number} -> #{other_obj.number}"
143
+ when :exists
144
+ "Number changed (Existing Number): Title \"#{obj.title}\": #{obj.number} -> #{other_obj.number}"
145
+ else
146
+ raise ArgumentError, "Unknown diff type for number change: #{diff_type[1]}"
147
+ end
148
+ end
149
+
150
+ def self.both_diff_type_text(diff_type, obj, other_obj)
151
+ case diff_type[1]
152
+ when :added
153
+ "Added object: Title \"#{other_obj.title}\": Number \"#{other_obj.number}\""
154
+ when :removed
155
+ "Removed object: Title \"#{obj.title}\": Number \"#{obj.number}\""
156
+ else
157
+ raise ArgumentError, "Unknown diff type for both change: #{diff_type[1]}"
158
+ end
159
+ end
160
+ end
161
+
162
+ # Checks properties for existence in both benchmarks.
163
+ class PropChecker < AbideDevUtils::XCCDF::Diff::PropertyExistenceChecker
164
+ attr_reader :all_numbers, :all_titles, :other_all_numbers, :other_all_titles
165
+
166
+ def initialize(number_title_objs, other_number_title_objs)
167
+ super
168
+ @all_numbers = number_title_objs.map(&:number)
169
+ @all_titles = number_title_objs.map(&:title)
170
+ @other_all_numbers = other_number_title_objs.map(&:number)
171
+ @other_all_titles = other_number_title_objs.map(&:title)
172
+ end
173
+
174
+ def title(title)
175
+ property_existence(title, @all_titles, @other_all_titles)
176
+ end
177
+
178
+ def number(number)
179
+ property_existence(number, @all_numbers, @other_all_numbers)
180
+ end
181
+
182
+ def added_numbers
183
+ added(@all_numbers, @other_all_numbers)
184
+ end
185
+
186
+ def removed_numbers
187
+ removed(@all_numbers, @other_all_numbers)
188
+ end
189
+
190
+ def added_titles
191
+ added(@all_titles, @other_all_titles)
192
+ end
193
+
194
+ def removed_titles
195
+ removed(@all_titles, @other_all_titles)
196
+ end
197
+ end
198
+
199
+ class NumberTitleDiffError < StandardError; end
200
+ class InconsistentDiffTypeError < StandardError; end
201
+
202
+ # Holds a number and title for a child of a benchmark
203
+ # and provides methods to compare it to another child.
204
+ class NumberTitleContainer
205
+ include ::Comparable
206
+ attr_accessor :prop_checker
207
+ attr_reader :child, :number, :title
208
+
209
+ def initialize(child, prop_checker = nil)
210
+ @child = child
211
+ @number = child.number.to_s
212
+ @title = child.title.to_s
213
+ @prop_checker = prop_checker
214
+ end
215
+
216
+ def diff(other)
217
+ return %i[equal exist] if number == other.number && title == other.title
218
+
219
+ if number == other.number && title != other.title
220
+ c_diff = correlate_prop_diff_types(@prop_checker.title(other.title),
221
+ other.prop_checker.title(other.title))
222
+ [:title, c_diff]
223
+ elsif title == other.title && number != other.number
224
+ c_diff = correlate_prop_diff_types(@prop_checker.number(other.number),
225
+ other.prop_checker.number(other.number))
226
+ [:number, c_diff]
227
+ else
228
+ %i[both exist]
229
+ end
230
+ rescue StandardError => e
231
+ err_str = [
232
+ 'Error diffing number and title',
233
+ "Number: #{number}",
234
+ "Title: #{title}",
235
+ "Other number: #{other.number}",
236
+ "Other title: #{other.title}",
237
+ e.message,
238
+ ]
239
+ raise NumberTitleDiffError, err_str.join(', ')
240
+ end
241
+
242
+ def <=>(other)
243
+ child <=> other.child
244
+ end
245
+
246
+ private
247
+
248
+ def correlate_prop_diff_types(self_type, other_type)
249
+ inverse_diff_type = PropChecker.inverse_existence_state[self_type]
250
+ return other_type if inverse_diff_type.nil?
251
+
252
+ self_type
253
+ end
254
+ end
255
+
256
+ # Stand-in object for a NumberTitleContainer when the NumberTitleContainer
257
+ # would not exist. This is used when a change is an add or remove.
258
+ class NumberTitleContainerStandIn
259
+ attr_reader :child, :number, :title
260
+
261
+ def initialize(change_type)
262
+ @change_type = change_type
263
+ @child = nil
264
+ @number = ''
265
+ @title = ''
266
+ end
267
+ end
268
+ end
269
+ end
270
+ end