cookbooks/fb_helpers/spec/default_spec.rb (384 lines of code) (raw):
# vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2
#
# Copyright (c) 2020-present, Facebook, Inc.
# All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require_relative '../libraries/fb_helpers'
describe FB::Helpers do
context 'parse_json' do
it 'parses basic JSON' do
expect(FB::Helpers.parse_json('{}')).to eq({})
end
it 'parses complex JSON' do
json_str = '{"fb_sysctl": {"kernel.core_uses_pid": 0}}'
json_hash = {
'fb_sysctl' => {
'kernel.core_uses_pid' => 0,
},
}
expect(FB::Helpers.parse_json(json_str)).to eq(json_hash)
end
it 'ignores empty JSON' do
expect(FB::Helpers.parse_json('', Hash, true)).to eq({})
end
it 'ignores invalid JSON' do
expect(FB::Helpers.parse_json('"foo"', Hash, true)).to eq({})
end
it 'ignores broken JSON' do
expect(FB::Helpers.parse_json('{bar}', Hash, true)).to eq({})
end
end
context 'parse_json_file' do
PATH = 'json_file.json'.freeze
it 'ignores a file that cannot be read' do
allow(File).to receive(:read).with(PATH).and_raise(IOError)
expect(FB::Helpers.parse_json_file(PATH, Hash, true)).to eq({})
end
it 'parses a basic JSON file' do
allow(File).to receive(:read).with(PATH).and_return('{}')
expect(FB::Helpers.parse_json_file(PATH)).to eq({})
end
it 'parses a basic JSON file, and ignores empty JSON' do
allow(File).to receive(:read).with(PATH).and_return('')
expect(FB::Helpers.parse_json_file(PATH, Hash, true)).to eq({})
end
it 'parses a basic JSON file, and ignores invalid JSON' do
allow(File).to receive(:read).with(PATH).and_return('"foo"')
expect(FB::Helpers.parse_json_file(PATH, Hash, true)).to eq({})
end
it 'parses a basic JSON file, and ignores broken JSON' do
allow(File).to receive(:read).with(PATH).and_return('{bar}')
expect(FB::Helpers.parse_json_file(PATH, Hash, true)).to eq({})
end
end
context 'parse a time for timeshard computation' do
it 'should succeed with a valid timestamp - 2020-1-1 9:00:00' do
expect(FB::Helpers.parse_timeshard_start(
'2020-1-1 9:00:00 PST',
)).to eq(1577898000)
end
it 'should fail if start_time is an invalid date - 2018-14-1 9:00:00' do
expect do
FB::Helpers.parse_timeshard_start('2018-14-1 9:00:00')
end.to raise_error(RuntimeError)
end
end
context 'parse a time for timeshard computation' do
{
'24h' => (24 * 60 * 60),
'1h' => (60 * 60),
'7d' => (7 * 24 * 60 * 60),
}.each do |duration, seconds|
it "should successfully parse #{duration}" do
expect(FB::Helpers.parse_timeshard_duration(
duration,
)).to eq(seconds)
end
end
it 'should fail if duration is invalid - 43min' do
expect do
FB::Helpers.parse_timeshard_duration('43min')
end.to raise_error(RuntimeError)
end
end
context 'filter_hash' do
it 'returns a passing hash unchanged' do
hash = {
'fb_sysctl' => {
'kernel.core_uses_pid' => 0,
},
}
filter = ['fb_sysctl']
expect(FB::Helpers.filter_hash(hash, filter)).to eq(hash)
end
it 'filters a failing hash' do
hash = {
'fb_sysctl' => {
'kernel.core_uses_pid' => 0,
},
}
expect(FB::Helpers.filter_hash(hash, [])).to eq({})
end
it 'returns a passing deep hash unchanged' do
hash = {
'fb_network_scripts' => {
'ifup' => {
'ethtool' => 'cookie',
},
},
}
filter = ['fb_network_scripts/ifup/ethtool']
expect(FB::Helpers.filter_hash(hash, filter)).to eq(hash)
end
it 'filters a failing deep hash' do
hash = {
'fb_network_scripts' => {
'ifup' => {
'extra_commands' => 'cookie',
},
},
}
filter = ['fb_network_scripts/ifup/ethtool']
expect(FB::Helpers.filter_hash(hash, filter)).to eq({})
end
it 'handles compound hashes and filters' do
hash = {
'fb_sysctl' => {
'kernel.core_uses_pid' => 0,
},
'fb_network_scripts' => {
'ifup' => {
'ethtool' => 'cookie',
'boo' => 123,
},
},
'fb_foo' => {
'bar' => 4,
},
}
filtered_hash = {
'fb_sysctl' => {
'kernel.core_uses_pid' => 0,
},
'fb_network_scripts' => {
'ifup' => {
'ethtool' => 'cookie',
},
},
}
filter = ['fb_sysctl', 'fb_network_scripts/ifup/ethtool']
expect(FB::Helpers.filter_hash(hash, filter)).to eq(filtered_hash)
end
it 'handles hashes with empty values' do
hash = {
'fb_network_scripts' => {
'ifup' => {
'extra_commands' => {},
},
},
}
filter = ['fb_network_scripts/ifup/extra_commands']
expect(FB::Helpers.filter_hash(hash, filter)).to eq(hash)
end
it 'handles multiple filters on the same hash' do
hash = {
'foo' => {
'bar' => 1,
'baz' => 2,
},
}
hash2 = {
'foo' => {
'bar' => 1,
'baz' => 2,
'boo' => 3,
},
}
filter = ['foo/bar', 'foo/baz']
expect(FB::Helpers.filter_hash(hash, filter)).to eq(hash)
expect(FB::Helpers.filter_hash(hash2, filter)).to eq(hash)
end
end
# These tests are based on spec/unit/mixin/deep_merge_spec.rb from
# https://github.com/chef/chef at revision
# 5c8383fedd13b07f13d64a58f7cc78664a235ced
context 'merge_hash' do
it 'merges Hashes like normal deep merge' do
merge_ee_hash = {
'top_level_a' => {
'1_deep_a' => '1-a-merge-ee',
'1_deep_b' => '1-deep-b-merge-ee',
},
'top_level_b' => 'top-level-b-merge-ee',
}
merge_with_hash = {
'top_level_a' => {
'1_deep_b' => '1-deep-b-merged-onto',
'1_deep_c' => '1-deep-c-merged-onto',
},
'top_level_b' => 'top-level-b-merged-onto',
}
merged_result = FB::Helpers.merge_hash(merge_ee_hash, merge_with_hash)
expect(merged_result['top_level_b']).to eq('top-level-b-merged-onto')
expect(merged_result['top_level_a']['1_deep_a']).to eq('1-a-merge-ee')
expect(merged_result['top_level_a']['1_deep_b']).
to eq('1-deep-b-merged-onto')
expect(merged_result['top_level_a']['1_deep_c']).
to eq('1-deep-c-merged-onto')
end
it 'replaces arrays rather than merging them' do
merge_ee_hash = {
'top_level_a' => {
'1_deep_a' => '1-a-merge-ee',
'1_deep_b' => %w{A A A},
},
'top_level_b' => 'top-level-b-merge-ee',
}
merge_with_hash = {
'top_level_a' => {
'1_deep_b' => %w{B B B},
'1_deep_c' => '1-deep-c-merged-onto',
},
'top_level_b' => 'top-level-b-merged-onto',
}
merged_result = FB::Helpers.merge_hash(merge_ee_hash, merge_with_hash)
expect(merged_result['top_level_b']).to eq('top-level-b-merged-onto')
expect(merged_result['top_level_a']['1_deep_a']).to eq('1-a-merge-ee')
expect(merged_result['top_level_a']['1_deep_b']).to eq(%w{B B B})
end
it 'replaces non-hash items with hashes when there is a conflict' do
merge_ee_hash = {
'top_level_a' => 'top-level-a-mergee',
'top_level_b' => 'top-level-b-merge-ee',
}
merge_with_hash = {
'top_level_a' => {
'1_deep_b' => %w{B B B},
'1_deep_c' => '1-deep-c-merged-onto',
},
'top_level_b' => 'top-level-b-merged-onto',
}
merged_result = FB::Helpers.merge_hash(merge_ee_hash, merge_with_hash)
expect(merged_result['top_level_a']).to be_a(Hash)
expect(merged_result['top_level_a']['1_deep_a']).to be_nil
expect(merged_result['top_level_a']['1_deep_b']).to eq(%w{B B B})
end
it 'does not mutate deeply-nested original hashes by default' do
merge_ee_hash = {
'top_level_a' => {
'1_deep_a' => {
'2_deep_a' => {
'3_deep_a' => 'foo',
},
},
},
}
merge_with_hash = {
'top_level_a' => {
'1_deep_a' => {
'2_deep_a' => {
'3_deep_b' => 'bar',
},
},
},
}
FB::Helpers.merge_hash(merge_ee_hash, merge_with_hash)
expect(merge_ee_hash).to eq({
'top_level_a' => {
'1_deep_a' => {
'2_deep_a' => {
'3_deep_a' => 'foo',
},
},
},
})
expect(merge_with_hash).to eq({
'top_level_a' => {
'1_deep_a' => {
'2_deep_a' => {
'3_deep_b' => 'bar',
},
},
},
})
end
it 'does not error merging un-dupable items' do
merge_ee_hash = {
'top_level_a' => 1,
'top_level_b' => false,
}
merge_with_hash = {
'top_level_a' => 2,
'top_level_b' => true,
}
FB::Helpers.merge_hash(merge_ee_hash, merge_with_hash)
end
it 'merges leaf Hashes by default' do
merge_ee_hash = {
'top_level_a' => {
'1_deep_a' => {
'2_deep_a' => {
'3_deep_a' => 'foo',
},
},
},
}
merge_with_hash = {
'top_level_a' => {
'1_deep_a' => {
'2_deep_a' => {
'3_deep_b' => 'bar',
},
},
},
}
merged_result = FB::Helpers.merge_hash(merge_ee_hash, merge_with_hash)
expect(merged_result).to eq({
'top_level_a' => {
'1_deep_a' => {
'2_deep_a' => {
'3_deep_a' => 'foo',
'3_deep_b' => 'bar',
},
},
},
})
end
it 'overwrites leaf Hashes if requested' do
merge_ee_hash = {
'top_level_a' => {
'1_deep_a' => {
'2_deep_a' => {
'3_deep_a' => 'foo',
},
},
},
}
merge_with_hash = {
'top_level_a' => {
'1_deep_a' => {
'2_deep_a' => {
'3_deep_b' => 'bar',
},
},
},
}
merged_result = FB::Helpers.merge_hash(merge_ee_hash, merge_with_hash,
true)
expect(merged_result).to eq({
'top_level_a' => {
'1_deep_a' => {
'2_deep_a' => {
'3_deep_b' => 'bar',
},
},
},
})
end
it 'does not clobber top-level Hashes when overwriting leaves' do
merge_ee_hash = {
'top_level_a' => {
'1_deep_a' => {
'2_deep_a' => {
'3_deep_a' => 'foo',
},
},
'1_deep_b' => 1,
},
}
merge_with_hash = {
'top_level_a' => {
'1_deep_a' => {
'2_deep_a' => {
'3_deep_b' => 'bar',
},
},
},
}
merged_result = FB::Helpers.merge_hash(merge_ee_hash, merge_with_hash,
true)
expect(merged_result).to eq({
'top_level_a' => {
'1_deep_a' => {
'2_deep_a' => {
'3_deep_b' => 'bar',
},
},
'1_deep_b' => 1,
},
})
end
end
end