Merge pull request #184 from github/kpaulisse-regexp-attributes

Better regular expression attribute handling
This commit is contained in:
Kevin Paulisse 2018-04-02 16:56:11 -05:00 коммит произвёл GitHub
Родитель cb71fb2a24 178f7cbdf5
Коммит 8774031d5e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
5 изменённых файлов: 208 добавлений и 3 удалений

Просмотреть файл

@ -3,6 +3,7 @@
language: ruby language: ruby
install: true install: true
script: "script/cibuild" script: "script/cibuild"
before_install: gem install bundler
matrix: matrix:
include: include:

Просмотреть файл

@ -167,12 +167,16 @@ In this case, "owner", "notify", and "content" are nested under "parameters". In
``` ```
# Ignore all changes to the `owner` attribute of a file. # Ignore all changes to the `owner` attribute of a file.
[ { type: Regexp.new('\AFile\z'), attr: Regexp.new("\Aparameters\fowner\z" } ] [ { type: Regexp.new('\AFile\z'), attr: Regexp.new("\\Aparameters\fowner\\z" } ]
# Ignore changes to `owner` or `group` for a file or an exec. # Ignore changes to `owner` or `group` for a file or an exec.
[ { type: Regexp.new('\A(File|Exec)\z'), attr: Regexp.new("\Aparameters\f(owner|group)\z" } ] [ { type: Regexp.new('\A(File|Exec)\z'), attr: Regexp.new("\\Aparameters\f(owner|group)\\z" } ]
``` ```
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).
: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")`.
#### `:validate_references` (Array<String>, Optional) #### `:validate_references` (Array<String>, Optional)
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. 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.

Просмотреть файл

@ -294,10 +294,13 @@ module OctocatalogDiff
# Use diffy to get only the lines that have changed in a text object. # Use diffy to get only the lines that have changed in a text object.
# As we iterate through the diff, jump out if we have our answer: either # As we iterate through the diff, jump out if we have our answer: either
# true if '=~>' finds ANY match, or false if '=&>' fails to find a match. # true if '=~>' finds ANY match, or false if '=&>' fails to find a match.
Diffy::Diff.new(old_val, new_val, context: 0).each do |line| diffy_result = Diffy::Diff.new(old_val, new_val, context: 0)
newline_alerts = diffy_result.count { |line| line.strip == '\\ No newline at end of file' }
diffy_result.each do |line|
if regex.match(line.strip) if regex.match(line.strip)
return true if operator == '=~>' return true if operator == '=~>'
elsif operator == '=&>' elsif operator == '=&>'
next if line.strip == '\\ No newline at end of file' && newline_alerts == 2
return false return false
end end
end end
@ -394,6 +397,13 @@ module OctocatalogDiff
return false unless rule[:title].casecmp(hsh[:title]).zero? return false unless rule[:title].casecmp(hsh[:title]).zero?
end end
# If rule[:attr] is a regular expression, handle that case here.
if rule[:attr].is_a?(Regexp)
return false unless hsh[:attr].is_a?(String)
return false unless rule[:attr].match(hsh[:attr])
return ignore_match_true(hsh, rule)
end
# Special 'attributes': Ignore specific diff types (+ add, - remove, ~ and ! change) # Special 'attributes': Ignore specific diff types (+ add, - remove, ~ and ! change)
if rule[:attr] =~ /\A[\-\+~!]+\Z/ if rule[:attr] =~ /\A[\-\+~!]+\Z/
return ignore_match_true(hsh, rule) if rule[:attr].include?(diff_type) return ignore_match_true(hsh, rule) if rule[:attr].include?(diff_type)

Просмотреть файл

@ -0,0 +1,42 @@
# frozen_string_literal: true
require_relative 'integration_helper'
describe 'ignore regexp integration' do
before(:all) do
@result = OctocatalogDiff::API::V1.catalog_diff(
to_catalog: OctocatalogDiff::Spec.fixture_path('catalogs/default-catalog-changed.json'),
from_catalog: OctocatalogDiff::Spec.fixture_path('catalogs/default-catalog-v4.json'),
ignore: [
{
type: Regexp.new('\ASsh_authorized_key\z'),
attr: Regexp.new("\\Aparameters\f(foo|bar)\\z")
}
]
)
end
it 'should succeed' do
expect(@result.diffs).to be_a_kind_of(Array)
end
it 'should contain a non-ignored diff in another type' do
lookup = { diff_type: '+', type: 'Group', title: 'bill' }
expect(OctocatalogDiff::Spec.diff_match?(@result.diffs, lookup)).to eq(true), @result.diffs.inspect
end
it 'should contain a non-ignored removal in the same type' do
lookup = { type: 'Ssh_authorized_key', title: 'root@6def27049c06f48eea8b8f37329f40799d07dc84' }
expect(OctocatalogDiff::Spec.diff_match?(@result.diffs, lookup)).to eq(true), @result.diffs.inspect
end
it 'should contain a non-ignored diff in the same type' do
lookup = { type: 'Ssh_authorized_key', title: 'bob@local', structure: %w[parameters type] }
expect(OctocatalogDiff::Spec.diff_match?(@result.diffs, lookup)).to eq(true), @result.diffs.inspect
end
it 'should not contain an ignored diff in the same type' do
lookup = { type: 'Ssh_authorized_key', title: 'bob@local', structure: %w[parameters foo] }
expect(OctocatalogDiff::Spec.diff_match?(@result.diffs, lookup)).to eq(false), @result.diffs.inspect
end
end

Просмотреть файл

@ -1285,6 +1285,24 @@ describe OctocatalogDiff::CatalogDiff::Differ do
expect(logger_str.string).not_to match(/Ignoring .+ matches {:type=>"\*", :title=>"dell", :attr=>"\*"}/) expect(logger_str.string).not_to match(/Ignoring .+ matches {:type=>"\*", :title=>"dell", :attr=>"\*"}/)
end end
end end
context 'attrs regexp' do
it 'should filter on regular expression match' do
rule = { type: 'Apple', title: 'delicious', attr: Regexp.new("\\Aparameters\f(color|taste)\\z") }
logger, logger_str = OctocatalogDiff::Spec.setup_logger
testobj.instance_variable_set('@logger', logger)
expect(testobj.send(:"ignore_match?", rule, '+', resource, 'old_value', 'new_value')).to eq(true)
expect(logger_str.string).to match(/Ignoring .+ matches {:type=>"Apple", :title=>"delicious", :attr=>/)
end
it 'should not filter on regular expression non-match' do
rule = { type: 'Apple', title: 'delicious', attr: Regexp.new("\\Aparameters\f(odor|has_worms)\\z") }
logger, logger_str = OctocatalogDiff::Spec.setup_logger
testobj.instance_variable_set('@logger', logger)
expect(testobj.send(:"ignore_match?", rule, '+', resource, 'old_value', 'new_value')).to eq(false)
expect(logger_str.string).not_to match(/Ignoring .+ matches/)
end
end
end end
describe '#ignore_tags' do describe '#ignore_tags' do
@ -1373,4 +1391,134 @@ describe OctocatalogDiff::CatalogDiff::Differ do
expect(result[0]).to eq(['!', "Class\fOpenssl::Package\fparameters\fcommon-array", [1, 2, 3], [1, 5, 25], fileref, fileref]) expect(result[0]).to eq(['!', "Class\fOpenssl::Package\fparameters\fcommon-array", [1, 2, 3], [1, 5, 25], fileref, fileref])
end end
end end
describe '#regexp_operator_match?' do
let(:subject) { described_class.allocate }
context 'for a multi-line diff' do
context 'for operator =~>' do
let(:operator) { '=~>' }
let(:regex) { Regexp.new('\\A.(kittens|cats)\\z') }
it 'should return true when at least one line matches' do
old_val = "kittens\nkittens\ncats\n"
new_val = "puppies\ndogs\n"
expect(subject.send(:regexp_operator_match?, operator, regex, old_val, new_val)).to eq(true)
end
it 'should return false when neither line matches' do
old_val = "puppies\ndogs\ndonkeys\n"
new_val = "puppies\ndogs\n"
expect(subject.send(:regexp_operator_match?, operator, regex, old_val, new_val)).to eq(false)
end
end
context 'for operator =&>' do
let(:operator) { '=&>' }
let(:regex) { Regexp.new('\\A(-|\\+)(kittens )*(kittens|cats)\\z') }
it 'should return true when all lines match' do
old_val = "kittens\nkittens\ncats\n"
new_val = "kittens\nkittens\n"
expect(subject.send(:regexp_operator_match?, operator, regex, old_val, new_val)).to eq(true)
end
it 'should return false when the regex does not match line' do
old_val = "kittens\nkittens\ncats\n"
new_val = "kittens\nkittens\ndogs\n"
expect(subject.send(:regexp_operator_match?, operator, regex, old_val, new_val)).to eq(false)
end
it 'should return true if both old and new do not end in a newline' do
old_val = "kittens\nkittens\ncats"
new_val = "kittens\nkittens\nkittens"
expect(subject.send(:regexp_operator_match?, operator, regex, old_val, new_val)).to eq(true)
end
it 'should return false if one ends in a newline and the other does not' do
old_val = "kittens\nkittens\ncats\n"
new_val = "kittens\nkittens\nkittens"
expect(subject.send(:regexp_operator_match?, operator, regex, old_val, new_val)).to eq(false)
end
end
end
context 'for a single-line diff' do
context 'for operator =~>' do
let(:operator) { '=~>' }
let(:regex) { Regexp.new('\\A(-|\\+)(kittens )+(kittens|cats)\\z') }
it 'should return true when at least one line matches' do
old_val = 'kittens kittens kittens'
new_val = 'kittens cats'
expect(subject.send(:regexp_operator_match?, operator, regex, old_val, new_val)).to eq(true)
end
it 'should return false when neither line matches' do
old_val = 'kittens dogs cats kittens kittens'
new_val = 'kittens cats dogs'
expect(subject.send(:regexp_operator_match?, operator, regex, old_val, new_val)).to eq(false)
end
end
context 'for operator =&>' do
let(:operator) { '=&>' }
let(:regex) { Regexp.new('\\A(-|\\+)(kittens )+(kittens|cats)\\z') }
it 'should return true when both lines match' do
old_val = 'kittens kittens kittens'
new_val = 'kittens cats'
expect(subject.send(:regexp_operator_match?, operator, regex, old_val, new_val)).to eq(true)
end
it 'should return false when the regex does not match line' do
old_val = 'kittens kittens dogs kittens'
new_val = 'kittens cats'
expect(subject.send(:regexp_operator_match?, operator, regex, old_val, new_val)).to eq(false)
end
end
end
context 'for a multi-line versus a single-line diff' do
context 'for operator =~>' do
let(:operator) { '=~>' }
let(:regex) { Regexp.new('\\A.(kittens|cats)\\z') }
it 'should return true when at least one line matches' do
old_val = "kittens\nkittens\ncats\n"
new_val = 'puppies'
expect(subject.send(:regexp_operator_match?, operator, regex, old_val, new_val)).to eq(true)
end
it 'should return false when neither line matches' do
old_val = "puppies\ndogs\ndonkeys\n"
new_val = 'puppies'
expect(subject.send(:regexp_operator_match?, operator, regex, old_val, new_val)).to eq(false)
end
end
context 'for operator =&>' do
let(:operator) { '=&>' }
let(:regex) { Regexp.new('\\A(-|\\+)(kittens )*(kittens|cats)\\z') }
it 'should return true when all lines match and both old and new do not end in newline' do
old_val = "kittens\nkittens\ncats"
new_val = 'kittens'
expect(subject.send(:regexp_operator_match?, operator, regex, old_val, new_val)).to eq(true)
end
it 'should return false when all lines match but ending in newlines differs' do
old_val = "kittens\nkittens\ncats\n"
new_val = 'kittens'
expect(subject.send(:regexp_operator_match?, operator, regex, old_val, new_val)).to eq(false)
end
it 'should return false when the regex does not match line' do
old_val = "kittens\nkittens\ncats"
new_val = 'dogs'
expect(subject.send(:regexp_operator_match?, operator, regex, old_val, new_val)).to eq(false)
end
end
end
end
end end