Skip to content

Commit 5071b97

Browse files
authored
Merge pull request #254 from kpaulisse/kpaulisse-equivalent-array-filter
Add EquivalentArrayNoDatatypes filter
2 parents d2a25a3 + c58ff56 commit 5071b97

File tree

7 files changed

+370
-3
lines changed

7 files changed

+370
-3
lines changed

doc/advanced-filter.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Here is the list of available filters and an explanation of each:
2424

2525
#### Description
2626

27-
When the `AbsentFile` filter is enabled, if any file is `ensure => absent` in the *new* catalog, then changes to any other parameters will be suppressed.
27+
When the `AbsentFile` filter is enabled, if any file is `ensure => absent` in the _new_ catalog, then changes to any other parameters will be suppressed.
2828

2929
Consider that a file resource is declared as follows in two catalogs:
3030

@@ -71,6 +71,25 @@ Wouldn't it be nice if the meaningless information didn't appear, and all you sa
7171
+ absent
7272
```
7373

74+
## Equivalent Array (not considering datatypes)
75+
76+
#### Usage
77+
78+
```
79+
--filters EquivalentArrayNoDatatypes
80+
```
81+
82+
#### Description
83+
84+
In an array, ignore changes where the old and new arrays are "equivalent" as described below. This is useful when octocatalog-diff is comparing changes between a catalog with stringified values and a catalog with non-stringified values.
85+
86+
The following are considered equivalent when this filter is engaged:
87+
88+
- Stringified integers (`[0, 1]` and `['0', '1']`)
89+
- Stringified floats (`[0.0, 1.0]` and `['0.0', '1.0']`)
90+
- Numerically-equal integers and floats (`[0, 1]` and `[0.0, 1.0]`)
91+
- Symbols and corresponding strings (`[:foo, :bar]` and `[':foo', ':bar']`)
92+
7493
## JSON
7594

7695
#### Usage
@@ -105,7 +124,7 @@ New: { "notify": [ "Service[foo]" ] }
105124
This filter will suppress differences for the value of a parameter when:
106125

107126
- The value in one catalog is an object, AND
108-
- The value in the other catalog is an array containing *only* that same object
127+
- The value in the other catalog is an array containing _only_ that same object
109128

110129
## YAML
111130

lib/octocatalog-diff/catalog-diff/filter.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
require_relative '../api/v1/diff'
22
require_relative 'filter/absent_file'
33
require_relative 'filter/compilation_dir'
4+
require_relative 'filter/equivalent_array_no_datatypes'
45
require_relative 'filter/json'
56
require_relative 'filter/single_item_array'
67
require_relative 'filter/yaml'
@@ -14,7 +15,7 @@ class Filter
1415
attr_accessor :logger
1516

1617
# List the available filters here (by class name) for use in the validator method.
17-
AVAILABLE_FILTERS = %w(AbsentFile CompilationDir JSON SingleItemArray YAML).freeze
18+
AVAILABLE_FILTERS = %w(AbsentFile CompilationDir EquivalentArrayNoDatatypes JSON SingleItemArray YAML).freeze
1819

1920
# Public: Determine whether a particular filter exists. This can be used to validate
2021
# a user-submitted filter.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# frozen_string_literal: true
2+
3+
require_relative '../filter'
4+
5+
module OctocatalogDiff
6+
module CatalogDiff
7+
class Filter
8+
# Filter out changes in parameters where the elements of an array are the
9+
# same values but different data types. For example, this would filter out
10+
# the following diffs:
11+
# Exec[some command] =>
12+
# parameters =>
13+
# returns =>
14+
# - ["0", "1"]
15+
# + [0, 1]
16+
class EquivalentArrayNoDatatypes < OctocatalogDiff::CatalogDiff::Filter
17+
# Public: Implement the filter for arrays that have the same elements
18+
# but possibly different data types.
19+
#
20+
# @param diff [OctocatalogDiff::API::V1::Diff] Difference
21+
# @param _options [Hash] Additional options (there are none for this filter)
22+
# @return [Boolean] true if this should be filtered out, false otherwise
23+
def filtered?(diff, _options = {})
24+
# Skip additions or removals - focus only on changes
25+
return false unless diff.change?
26+
old_value = diff.old_value
27+
new_value = diff.new_value
28+
29+
# Skip unless both the old and new values are arrays.
30+
return false unless old_value.is_a?(Array) && new_value.is_a?(Array)
31+
32+
# Avoid generating comparable values if the arrays are a different
33+
# size, because there's no possible way that they are equivalent.
34+
return false unless old_value.size == new_value.size
35+
36+
# Generate and then compare the comparable arrays.
37+
old_value.map { |x| comparable_value(x) } == new_value.map { |x| comparable_value(x) }
38+
end
39+
40+
# Private: Get a more easily comparable value for an array element.
41+
# Integers, floats, and strings that look like integers or floats become
42+
# floats, and symbols are converted to string representation.
43+
#
44+
# @param input [any] Value to convert
45+
# @return [any] "Comparable" value
46+
def comparable_value(input)
47+
# Any string that looks like a number is converted to a float.
48+
if input.is_a?(String) && input =~ /\A-?(([0-9]*\.[0-9]+)|([0-9]+))\z/
49+
return input.to_f
50+
end
51+
52+
# Any number is converted to a float
53+
return input.to_f if input.is_a?(Integer) || input.is_a?(Float)
54+
55+
# Symbols are converted to ":xxx" strings.
56+
return ":#{input}" if input.is_a?(Symbol)
57+
58+
# Everything else is unconverted.
59+
input
60+
end
61+
end
62+
end
63+
end
64+
end
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
{
2+
"document_type": "Catalog",
3+
"data": {
4+
"tags": [
5+
"settings"
6+
],
7+
"name": "my.rspec.node",
8+
"version": "production",
9+
"environment": "production",
10+
"resources": [
11+
{
12+
"type": "Exec",
13+
"title": "run-my-command 1",
14+
"file": "/environments/production/modules/foo/manifests/init.pp",
15+
"line": 10,
16+
"exported": false,
17+
"parameters": {
18+
"path": "/usr/bin",
19+
"command": "id",
20+
"returns": "0"
21+
}
22+
},
23+
{
24+
"type": "Exec",
25+
"title": "run-my-command 2",
26+
"file": "/environments/production/modules/foo/manifests/init.pp",
27+
"line": 10,
28+
"exported": false,
29+
"parameters": {
30+
"path": "/usr/bin",
31+
"command": "id",
32+
"returns": ["0", "1"]
33+
}
34+
},
35+
{
36+
"type": "Exec",
37+
"title": "run-my-command 3",
38+
"file": "/environments/production/modules/foo/manifests/init.pp",
39+
"line": 10,
40+
"exported": false,
41+
"parameters": {
42+
"path": "/usr/bin",
43+
"command": "id",
44+
"returns": ["0", "1", "2"]
45+
}
46+
}
47+
],
48+
"classes": [
49+
"settings"
50+
]
51+
},
52+
"metadata": {
53+
"api_version": 1
54+
}
55+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
{
2+
"document_type": "Catalog",
3+
"data": {
4+
"tags": [
5+
"settings"
6+
],
7+
"name": "my.rspec.node",
8+
"version": "production",
9+
"environment": "production",
10+
"resources": [
11+
{
12+
"type": "Exec",
13+
"title": "run-my-command 1",
14+
"file": "/environments/production/modules/foo/manifests/init.pp",
15+
"line": 10,
16+
"exported": false,
17+
"parameters": {
18+
"path": "/usr/bin",
19+
"command": "id",
20+
"returns": 0
21+
}
22+
},
23+
{
24+
"type": "Exec",
25+
"title": "run-my-command 2",
26+
"file": "/environments/production/modules/foo/manifests/init.pp",
27+
"line": 10,
28+
"exported": false,
29+
"parameters": {
30+
"path": "/usr/bin",
31+
"command": "id",
32+
"returns": [0, 1]
33+
}
34+
},
35+
{
36+
"type": "Exec",
37+
"title": "run-my-command 3",
38+
"file": "/environments/production/modules/foo/manifests/init.pp",
39+
"line": 10,
40+
"exported": false,
41+
"parameters": {
42+
"path": "/usr/bin",
43+
"command": "id",
44+
"returns": [0, 1, 2, 3]
45+
}
46+
}
47+
],
48+
"classes": [
49+
"settings"
50+
]
51+
},
52+
"metadata": {
53+
"api_version": 1
54+
}
55+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# frozen_string_literal: true
2+
3+
require_relative 'integration_helper'
4+
5+
describe 'equivalent array no datatypes filter integration' do
6+
context 'with default behavior' do
7+
before(:all) do
8+
@result = OctocatalogDiff::Integration.integration(
9+
spec_catalog_old: 'filter-equivalent-array-1.json',
10+
spec_catalog_new: 'filter-equivalent-array-2.json'
11+
)
12+
end
13+
14+
it 'should succeed' do
15+
expect(@result[:exitcode]).not_to eq(-1), "Internal error: #{@result[:exception]}\n#{@result[:logs]}"
16+
expect(@result[:exitcode]).to eq(2), "Runtime error: #{@result[:logs]}"
17+
end
18+
19+
it 'should not suppress equivalent-but-for-data-type arrays' do
20+
diffs = OctocatalogDiff::Spec.remove_file_and_line(@result[:diffs])
21+
expect(diffs.size).to eq(3), diffs.inspect
22+
expect(diffs[0][1..3]).to eq(["Exec\frun-my-command 1\fparameters\freturns", '0'.inspect, 0])
23+
expect(diffs[1][1..3]).to eq(["Exec\frun-my-command 2\fparameters\freturns", %w[0 1], [0, 1]])
24+
expect(diffs[2][1..3]).to eq(["Exec\frun-my-command 3\fparameters\freturns", %w[0 1 2], [0, 1, 2, 3]])
25+
end
26+
end
27+
28+
context 'with equivalent array no datatypes filter engaged' do
29+
before(:all) do
30+
@result = OctocatalogDiff::Integration.integration(
31+
spec_catalog_old: 'filter-equivalent-array-1.json',
32+
spec_catalog_new: 'filter-equivalent-array-2.json',
33+
argv: ['--filters', 'EquivalentArrayNoDatatypes']
34+
)
35+
end
36+
37+
it 'should succeed' do
38+
expect(@result[:exitcode]).not_to eq(-1), "Internal error: #{@result[:exception]}\n#{@result[:logs]}"
39+
expect(@result[:exitcode]).to eq(2), "Runtime error: #{@result[:logs]}"
40+
end
41+
42+
it 'should suppress equivalent-but-for-data-type arrays' do
43+
diffs = OctocatalogDiff::Spec.remove_file_and_line(@result[:diffs])
44+
expect(diffs.size).to eq(2), diffs.inspect
45+
# '0' => 0 is not suppressed because it's not an array
46+
expect(diffs[0][1..3]).to eq(["Exec\frun-my-command 1\fparameters\freturns", '0'.inspect, 0])
47+
# %w[0 1] => [0, 1] is suppressed
48+
# %w[0 1 2] => [0, 1, 2, 3] is not suppressed because it's not equivalent
49+
expect(diffs[1][1..3]).to eq(["Exec\frun-my-command 3\fparameters\freturns", %w[0 1 2], [0, 1, 2, 3]])
50+
end
51+
end
52+
end

0 commit comments

Comments
 (0)