Skip to content

Commit 8774031

Browse files
authored
Merge pull request #184 from github/kpaulisse-regexp-attributes
Better regular expression attribute handling
2 parents cb71fb2 + 178f7cb commit 8774031

File tree

5 files changed

+208
-3
lines changed

5 files changed

+208
-3
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
language: ruby
44
install: true
55
script: "script/cibuild"
6+
before_install: gem install bundler
67

78
matrix:
89
include:

doc/dev/api/v1/calls/catalog-diff.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,12 +167,16 @@ In this case, "owner", "notify", and "content" are nested under "parameters". In
167167

168168
```
169169
# Ignore all changes to the `owner` attribute of a file.
170-
[ { type: Regexp.new('\AFile\z'), attr: Regexp.new("\Aparameters\fowner\z" } ]
170+
[ { type: Regexp.new('\AFile\z'), attr: Regexp.new("\\Aparameters\fowner\\z" } ]
171171
172172
# Ignore changes to `owner` or `group` for a file or an exec.
173-
[ { type: Regexp.new('\A(File|Exec)\z'), attr: Regexp.new("\Aparameters\f(owner|group)\z" } ]
173+
[ { type: Regexp.new('\A(File|Exec)\z'), attr: Regexp.new("\\Aparameters\f(owner|group)\\z" } ]
174174
```
175175

176+
When using regular expressions, `\f` (form feed character) is used to separate the structure (e.g. `parameters\fowner` refers to the `parameters` hash, `owner` key).
177+
178+
:bulb: Note that `\A` in Ruby matches the beginning of the string and `\z` matches the end, but these are not actual characters. Therefore, if you are using `\A` or `\z` in double quotes (`"`), be sure to heed the examples above and write your expression like: `Regexp.new("\\Aparameters\fowner\\z")`.
179+
176180
#### `:validate_references` (Array<String>, Optional)
177181

178182
Invoke the [catalog validation](/doc/advanced-catalog-validation.md) feature to ensure resources targeted by `before`, `notify`, `require`, and/or `subscribe` exist in the catalog. If this parameter is not defined, no reference validation occurs.

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,10 +294,13 @@ def regexp_operator_match?(operator, regex, old_val, new_val)
294294
# Use diffy to get only the lines that have changed in a text object.
295295
# As we iterate through the diff, jump out if we have our answer: either
296296
# true if '=~>' finds ANY match, or false if '=&>' fails to find a match.
297-
Diffy::Diff.new(old_val, new_val, context: 0).each do |line|
297+
diffy_result = Diffy::Diff.new(old_val, new_val, context: 0)
298+
newline_alerts = diffy_result.count { |line| line.strip == '\\ No newline at end of file' }
299+
diffy_result.each do |line|
298300
if regex.match(line.strip)
299301
return true if operator == '=~>'
300302
elsif operator == '=&>'
303+
next if line.strip == '\\ No newline at end of file' && newline_alerts == 2
301304
return false
302305
end
303306
end
@@ -394,6 +397,13 @@ def ignore_match?(rule_in, diff_type, hsh, old_val, new_val)
394397
return false unless rule[:title].casecmp(hsh[:title]).zero?
395398
end
396399

400+
# If rule[:attr] is a regular expression, handle that case here.
401+
if rule[:attr].is_a?(Regexp)
402+
return false unless hsh[:attr].is_a?(String)
403+
return false unless rule[:attr].match(hsh[:attr])
404+
return ignore_match_true(hsh, rule)
405+
end
406+
397407
# Special 'attributes': Ignore specific diff types (+ add, - remove, ~ and ! change)
398408
if rule[:attr] =~ /\A[\-\+~!]+\Z/
399409
return ignore_match_true(hsh, rule) if rule[:attr].include?(diff_type)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# frozen_string_literal: true
2+
3+
require_relative 'integration_helper'
4+
5+
describe 'ignore regexp integration' do
6+
before(:all) do
7+
@result = OctocatalogDiff::API::V1.catalog_diff(
8+
to_catalog: OctocatalogDiff::Spec.fixture_path('catalogs/default-catalog-changed.json'),
9+
from_catalog: OctocatalogDiff::Spec.fixture_path('catalogs/default-catalog-v4.json'),
10+
ignore: [
11+
{
12+
type: Regexp.new('\ASsh_authorized_key\z'),
13+
attr: Regexp.new("\\Aparameters\f(foo|bar)\\z")
14+
}
15+
]
16+
)
17+
end
18+
19+
it 'should succeed' do
20+
expect(@result.diffs).to be_a_kind_of(Array)
21+
end
22+
23+
it 'should contain a non-ignored diff in another type' do
24+
lookup = { diff_type: '+', type: 'Group', title: 'bill' }
25+
expect(OctocatalogDiff::Spec.diff_match?(@result.diffs, lookup)).to eq(true), @result.diffs.inspect
26+
end
27+
28+
it 'should contain a non-ignored removal in the same type' do
29+
lookup = { type: 'Ssh_authorized_key', title: 'root@6def27049c06f48eea8b8f37329f40799d07dc84' }
30+
expect(OctocatalogDiff::Spec.diff_match?(@result.diffs, lookup)).to eq(true), @result.diffs.inspect
31+
end
32+
33+
it 'should contain a non-ignored diff in the same type' do
34+
lookup = { type: 'Ssh_authorized_key', title: 'bob@local', structure: %w[parameters type] }
35+
expect(OctocatalogDiff::Spec.diff_match?(@result.diffs, lookup)).to eq(true), @result.diffs.inspect
36+
end
37+
38+
it 'should not contain an ignored diff in the same type' do
39+
lookup = { type: 'Ssh_authorized_key', title: 'bob@local', structure: %w[parameters foo] }
40+
expect(OctocatalogDiff::Spec.diff_match?(@result.diffs, lookup)).to eq(false), @result.diffs.inspect
41+
end
42+
end

spec/octocatalog-diff/tests/catalog-diff/differ_spec.rb

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1285,6 +1285,24 @@
12851285
expect(logger_str.string).not_to match(/Ignoring .+ matches {:type=>"\*", :title=>"dell", :attr=>"\*"}/)
12861286
end
12871287
end
1288+
1289+
context 'attrs regexp' do
1290+
it 'should filter on regular expression match' do
1291+
rule = { type: 'Apple', title: 'delicious', attr: Regexp.new("\\Aparameters\f(color|taste)\\z") }
1292+
logger, logger_str = OctocatalogDiff::Spec.setup_logger
1293+
testobj.instance_variable_set('@logger', logger)
1294+
expect(testobj.send(:"ignore_match?", rule, '+', resource, 'old_value', 'new_value')).to eq(true)
1295+
expect(logger_str.string).to match(/Ignoring .+ matches {:type=>"Apple", :title=>"delicious", :attr=>/)
1296+
end
1297+
1298+
it 'should not filter on regular expression non-match' do
1299+
rule = { type: 'Apple', title: 'delicious', attr: Regexp.new("\\Aparameters\f(odor|has_worms)\\z") }
1300+
logger, logger_str = OctocatalogDiff::Spec.setup_logger
1301+
testobj.instance_variable_set('@logger', logger)
1302+
expect(testobj.send(:"ignore_match?", rule, '+', resource, 'old_value', 'new_value')).to eq(false)
1303+
expect(logger_str.string).not_to match(/Ignoring .+ matches/)
1304+
end
1305+
end
12881306
end
12891307

12901308
describe '#ignore_tags' do
@@ -1373,4 +1391,134 @@
13731391
expect(result[0]).to eq(['!', "Class\fOpenssl::Package\fparameters\fcommon-array", [1, 2, 3], [1, 5, 25], fileref, fileref])
13741392
end
13751393
end
1394+
1395+
describe '#regexp_operator_match?' do
1396+
let(:subject) { described_class.allocate }
1397+
1398+
context 'for a multi-line diff' do
1399+
context 'for operator =~>' do
1400+
let(:operator) { '=~>' }
1401+
let(:regex) { Regexp.new('\\A.(kittens|cats)\\z') }
1402+
1403+
it 'should return true when at least one line matches' do
1404+
old_val = "kittens\nkittens\ncats\n"
1405+
new_val = "puppies\ndogs\n"
1406+
expect(subject.send(:regexp_operator_match?, operator, regex, old_val, new_val)).to eq(true)
1407+
end
1408+
1409+
it 'should return false when neither line matches' do
1410+
old_val = "puppies\ndogs\ndonkeys\n"
1411+
new_val = "puppies\ndogs\n"
1412+
expect(subject.send(:regexp_operator_match?, operator, regex, old_val, new_val)).to eq(false)
1413+
end
1414+
end
1415+
1416+
context 'for operator =&>' do
1417+
let(:operator) { '=&>' }
1418+
let(:regex) { Regexp.new('\\A(-|\\+)(kittens )*(kittens|cats)\\z') }
1419+
1420+
it 'should return true when all lines match' do
1421+
old_val = "kittens\nkittens\ncats\n"
1422+
new_val = "kittens\nkittens\n"
1423+
expect(subject.send(:regexp_operator_match?, operator, regex, old_val, new_val)).to eq(true)
1424+
end
1425+
1426+
it 'should return false when the regex does not match line' do
1427+
old_val = "kittens\nkittens\ncats\n"
1428+
new_val = "kittens\nkittens\ndogs\n"
1429+
expect(subject.send(:regexp_operator_match?, operator, regex, old_val, new_val)).to eq(false)
1430+
end
1431+
1432+
it 'should return true if both old and new do not end in a newline' do
1433+
old_val = "kittens\nkittens\ncats"
1434+
new_val = "kittens\nkittens\nkittens"
1435+
expect(subject.send(:regexp_operator_match?, operator, regex, old_val, new_val)).to eq(true)
1436+
end
1437+
1438+
it 'should return false if one ends in a newline and the other does not' do
1439+
old_val = "kittens\nkittens\ncats\n"
1440+
new_val = "kittens\nkittens\nkittens"
1441+
expect(subject.send(:regexp_operator_match?, operator, regex, old_val, new_val)).to eq(false)
1442+
end
1443+
end
1444+
end
1445+
1446+
context 'for a single-line diff' do
1447+
context 'for operator =~>' do
1448+
let(:operator) { '=~>' }
1449+
let(:regex) { Regexp.new('\\A(-|\\+)(kittens )+(kittens|cats)\\z') }
1450+
1451+
it 'should return true when at least one line matches' do
1452+
old_val = 'kittens kittens kittens'
1453+
new_val = 'kittens cats'
1454+
expect(subject.send(:regexp_operator_match?, operator, regex, old_val, new_val)).to eq(true)
1455+
end
1456+
1457+
it 'should return false when neither line matches' do
1458+
old_val = 'kittens dogs cats kittens kittens'
1459+
new_val = 'kittens cats dogs'
1460+
expect(subject.send(:regexp_operator_match?, operator, regex, old_val, new_val)).to eq(false)
1461+
end
1462+
end
1463+
1464+
context 'for operator =&>' do
1465+
let(:operator) { '=&>' }
1466+
let(:regex) { Regexp.new('\\A(-|\\+)(kittens )+(kittens|cats)\\z') }
1467+
1468+
it 'should return true when both lines match' do
1469+
old_val = 'kittens kittens kittens'
1470+
new_val = 'kittens cats'
1471+
expect(subject.send(:regexp_operator_match?, operator, regex, old_val, new_val)).to eq(true)
1472+
end
1473+
1474+
it 'should return false when the regex does not match line' do
1475+
old_val = 'kittens kittens dogs kittens'
1476+
new_val = 'kittens cats'
1477+
expect(subject.send(:regexp_operator_match?, operator, regex, old_val, new_val)).to eq(false)
1478+
end
1479+
end
1480+
end
1481+
1482+
context 'for a multi-line versus a single-line diff' do
1483+
context 'for operator =~>' do
1484+
let(:operator) { '=~>' }
1485+
let(:regex) { Regexp.new('\\A.(kittens|cats)\\z') }
1486+
1487+
it 'should return true when at least one line matches' do
1488+
old_val = "kittens\nkittens\ncats\n"
1489+
new_val = 'puppies'
1490+
expect(subject.send(:regexp_operator_match?, operator, regex, old_val, new_val)).to eq(true)
1491+
end
1492+
1493+
it 'should return false when neither line matches' do
1494+
old_val = "puppies\ndogs\ndonkeys\n"
1495+
new_val = 'puppies'
1496+
expect(subject.send(:regexp_operator_match?, operator, regex, old_val, new_val)).to eq(false)
1497+
end
1498+
end
1499+
1500+
context 'for operator =&>' do
1501+
let(:operator) { '=&>' }
1502+
let(:regex) { Regexp.new('\\A(-|\\+)(kittens )*(kittens|cats)\\z') }
1503+
1504+
it 'should return true when all lines match and both old and new do not end in newline' do
1505+
old_val = "kittens\nkittens\ncats"
1506+
new_val = 'kittens'
1507+
expect(subject.send(:regexp_operator_match?, operator, regex, old_val, new_val)).to eq(true)
1508+
end
1509+
1510+
it 'should return false when all lines match but ending in newlines differs' do
1511+
old_val = "kittens\nkittens\ncats\n"
1512+
new_val = 'kittens'
1513+
expect(subject.send(:regexp_operator_match?, operator, regex, old_val, new_val)).to eq(false)
1514+
end
1515+
1516+
it 'should return false when the regex does not match line' do
1517+
old_val = "kittens\nkittens\ncats"
1518+
new_val = 'dogs'
1519+
expect(subject.send(:regexp_operator_match?, operator, regex, old_val, new_val)).to eq(false)
1520+
end
1521+
end
1522+
end
1523+
end
13761524
end

0 commit comments

Comments
 (0)