abide_dev_utils 0.9.7 → 0.10.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: 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