inspec 0.12.0 → 0.14.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 +4 -4
- data/CHANGELOG.md +39 -2
- data/bin/inspec +11 -9
- data/docs/matchers.rst +129 -0
- data/docs/resources.rst +64 -37
- data/inspec.gemspec +1 -1
- data/lib/bundles/inspec-compliance/cli.rb +1 -1
- data/lib/bundles/inspec-compliance/configuration.rb +1 -0
- data/lib/bundles/inspec-compliance/target.rb +16 -32
- data/lib/bundles/inspec-init/cli.rb +2 -0
- data/lib/bundles/inspec-supermarket.rb +13 -0
- data/lib/bundles/inspec-supermarket/api.rb +2 -0
- data/lib/bundles/inspec-supermarket/cli.rb +2 -2
- data/lib/bundles/inspec-supermarket/target.rb +11 -15
- data/lib/fetchers/local.rb +31 -0
- data/lib/fetchers/tar.rb +48 -0
- data/lib/fetchers/url.rb +100 -0
- data/lib/fetchers/zip.rb +47 -0
- data/lib/inspec.rb +2 -3
- data/lib/inspec/fetcher.rb +22 -0
- data/lib/inspec/metadata.rb +4 -2
- data/lib/inspec/plugins.rb +2 -0
- data/lib/inspec/plugins/fetcher.rb +97 -0
- data/lib/inspec/plugins/source_reader.rb +36 -0
- data/lib/inspec/profile.rb +92 -81
- data/lib/inspec/resource.rb +1 -0
- data/lib/inspec/runner.rb +15 -35
- data/lib/inspec/source_reader.rb +32 -0
- data/lib/inspec/version.rb +1 -1
- data/lib/matchers/matchers.rb +5 -6
- data/lib/resources/file.rb +8 -2
- data/lib/resources/passwd.rb +71 -45
- data/lib/resources/service.rb +13 -9
- data/lib/resources/shadow.rb +135 -0
- data/lib/source_readers/flat.rb +38 -0
- data/lib/source_readers/inspec.rb +78 -0
- data/lib/utils/base_cli.rb +2 -2
- data/lib/utils/parser.rb +1 -1
- data/lib/utils/plugin_registry.rb +93 -0
- data/test/docker_test.rb +1 -1
- data/test/helper.rb +62 -2
- data/test/integration/cookbooks/os_prepare/recipes/service.rb +4 -2
- data/test/integration/test/integration/default/compare_matcher_spec.rb +11 -0
- data/test/integration/test/integration/default/service_spec.rb +16 -1
- data/test/unit/fetchers.rb +61 -0
- data/test/unit/fetchers/local_test.rb +67 -0
- data/test/unit/fetchers/tar_test.rb +36 -0
- data/test/unit/fetchers/url_test.rb +152 -0
- data/test/unit/fetchers/zip_test.rb +36 -0
- data/test/unit/mock/files/passwd +1 -1
- data/test/unit/mock/files/shadow +2 -0
- data/test/unit/mock/profiles/complete-profile/libraries/testlib.rb +1 -0
- data/test/unit/plugin_test.rb +0 -1
- data/test/unit/profile_test.rb +32 -53
- data/test/unit/resources/passwd_test.rb +69 -14
- data/test/unit/resources/shadow_test.rb +67 -0
- data/test/unit/source_reader_test.rb +17 -0
- data/test/unit/source_readers/flat_test.rb +61 -0
- data/test/unit/source_readers/inspec_test.rb +38 -0
- data/test/unit/utils/passwd_parser_test.rb +1 -1
- metadata +40 -21
- data/lib/inspec/targets.rb +0 -10
- data/lib/inspec/targets/archive.rb +0 -33
- data/lib/inspec/targets/core.rb +0 -56
- data/lib/inspec/targets/dir.rb +0 -144
- data/lib/inspec/targets/file.rb +0 -33
- data/lib/inspec/targets/folder.rb +0 -38
- data/lib/inspec/targets/tar.rb +0 -61
- data/lib/inspec/targets/url.rb +0 -78
- data/lib/inspec/targets/zip.rb +0 -55
- data/test/unit/targets.rb +0 -132
@@ -0,0 +1,38 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# author: Dominik Richter
|
3
|
+
# author: Christoph Hartmann
|
4
|
+
|
5
|
+
require 'inspec/fetcher'
|
6
|
+
require 'inspec/metadata'
|
7
|
+
|
8
|
+
module SourceReaders
|
9
|
+
class Flat < Inspec.source_reader(1)
|
10
|
+
name 'flat'
|
11
|
+
priority 5
|
12
|
+
|
13
|
+
def self.resolve(target)
|
14
|
+
# TODO: eventually remove the metadata.rb exception here
|
15
|
+
# when we have fully phased out metadata.rb in 1.0
|
16
|
+
files = target.files.find_all { |x|
|
17
|
+
x.end_with?('.rb') && !x.include?('/') && x != 'metadata.rb'
|
18
|
+
}
|
19
|
+
return nil if files.empty?
|
20
|
+
new(target, files)
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :metadata, :tests, :libraries
|
24
|
+
|
25
|
+
def initialize(target, files)
|
26
|
+
@target = target
|
27
|
+
@metadata = ::Inspec::Metadata.new(nil)
|
28
|
+
@tests = load_tests(files)
|
29
|
+
@libraries = {}
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def load_tests(files)
|
35
|
+
Hash[files.map { |x| [x, @target.read(x)] }]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# author: Dominik Richter
|
3
|
+
# author: Christoph Hartmann
|
4
|
+
|
5
|
+
require 'inspec/fetcher'
|
6
|
+
require 'inspec/metadata'
|
7
|
+
|
8
|
+
module SourceReaders
|
9
|
+
class InspecReader < Inspec.source_reader(1)
|
10
|
+
name 'inspec'
|
11
|
+
priority 10
|
12
|
+
|
13
|
+
def self.resolve(target)
|
14
|
+
return new(target, 'inspec.yml') if target.files.include?('inspec.yml')
|
15
|
+
# TODO: deprecated for 1.0.0 release
|
16
|
+
if target.files.include?('metadata.rb') &&
|
17
|
+
(
|
18
|
+
target.files.include?('controls') ||
|
19
|
+
target.files.include?('test')
|
20
|
+
)
|
21
|
+
return new(target, 'metadata.rb')
|
22
|
+
end
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_reader :metadata, :tests, :libraries
|
27
|
+
|
28
|
+
def initialize(target, metadata_source)
|
29
|
+
@target = target
|
30
|
+
@metadata = Inspec::Metadata.from_ref(
|
31
|
+
metadata_source,
|
32
|
+
@target.read(metadata_source),
|
33
|
+
nil)
|
34
|
+
|
35
|
+
@tests = load_tests
|
36
|
+
@libraries = load_libs
|
37
|
+
prepare_load_paths
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def load_tests
|
43
|
+
tests = @target.files.find_all do |path|
|
44
|
+
path.start_with?('controls', 'test') && path.end_with?('.rb')
|
45
|
+
end
|
46
|
+
Hash[tests.map { |x| [x, @target.read(x)] }]
|
47
|
+
end
|
48
|
+
|
49
|
+
def load_libs
|
50
|
+
tests = @target.files.find_all do |path|
|
51
|
+
path.start_with?('libraries') && path.end_with?('.rb')
|
52
|
+
end
|
53
|
+
Hash[tests.map { |x| [x, @target.read(x)] }]
|
54
|
+
end
|
55
|
+
|
56
|
+
# Ensure each test directory exists on the $LOAD_PATH. This
|
57
|
+
# will ensure traditional RSpec-isms like `require 'spec_helper'`
|
58
|
+
# continue to work. The method outlined here is only meant to be temporary!
|
59
|
+
def prepare_load_paths
|
60
|
+
t = @target
|
61
|
+
t = @target.parent unless @target.parent.nil?
|
62
|
+
unless t.is_a?(Fetchers::Local)
|
63
|
+
return # no need to mess with load-paths if this is not on disk
|
64
|
+
end
|
65
|
+
|
66
|
+
rel_dirs = (@libraries.keys + @tests.keys)
|
67
|
+
.map { |x| File.dirname(x) }.uniq
|
68
|
+
|
69
|
+
abs_dirs = rel_dirs.map { |x| @target.abs_path(x) }
|
70
|
+
.find_all { |x| File.directory?(x) }
|
71
|
+
.map { |x| File.expand_path(x) }
|
72
|
+
|
73
|
+
abs_dirs.each do |dir|
|
74
|
+
$LOAD_PATH.unshift dir unless $LOAD_PATH.include?(dir)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/utils/base_cli.rb
CHANGED
@@ -42,13 +42,13 @@ module Inspec
|
|
42
42
|
private
|
43
43
|
|
44
44
|
# helper method to run tests
|
45
|
-
def run_tests(
|
45
|
+
def run_tests(targets, opts)
|
46
46
|
o = opts.dup
|
47
47
|
o[:logger] = Logger.new(opts['format'] == 'json' ? nil : STDOUT)
|
48
48
|
o[:logger].level = get_log_level(o.log_level)
|
49
49
|
|
50
50
|
runner = Inspec::Runner.new(o)
|
51
|
-
runner.
|
51
|
+
targets.each { |target| runner.add_target(target, opts) }
|
52
52
|
exit runner.run
|
53
53
|
rescue RuntimeError => e
|
54
54
|
puts e.message
|
data/lib/utils/parser.rb
CHANGED
@@ -0,0 +1,93 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# author: Dominik Richter
|
3
|
+
# author: Christoph Hartmann
|
4
|
+
|
5
|
+
class PluginRegistry
|
6
|
+
attr_reader :registry
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@registry = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
# Resolve a target via available plugins.
|
13
|
+
#
|
14
|
+
# @param [String] target to resolve
|
15
|
+
# @return [Plugin] plugin instance if it can be resolved, nil otherwise
|
16
|
+
def resolve(target)
|
17
|
+
modules.each do |m|
|
18
|
+
res = m.resolve(target)
|
19
|
+
return res unless res.nil?
|
20
|
+
end
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# Get all registered plugins sorted by priority, with highest first
|
27
|
+
#
|
28
|
+
# @return [Array[Plugin]] sorted list of plugins
|
29
|
+
def modules
|
30
|
+
@registry.values
|
31
|
+
.sort_by { |x| x.respond_to?(:priority) ? x.priority : 0 }
|
32
|
+
.reverse
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class PluginRegistry
|
37
|
+
class Plugin
|
38
|
+
# Retrieve the plugin registry associated with this plugin
|
39
|
+
#
|
40
|
+
# @return [PluginRegistry] plugin registry for this plugin
|
41
|
+
def self.plugin_registry
|
42
|
+
fail "Plugin #{self} does not implement `self.plugin_registry()`. This method is required"
|
43
|
+
end
|
44
|
+
|
45
|
+
# Register a new plugin by name
|
46
|
+
#
|
47
|
+
# @param [String] the unique name of this plugin
|
48
|
+
# @return [nil] disregard
|
49
|
+
def self.name(name)
|
50
|
+
fail "Trying to register #{self} with name == nil" if name.nil?
|
51
|
+
@name = name
|
52
|
+
plugin_registry.registry[name] = self
|
53
|
+
end
|
54
|
+
|
55
|
+
# This plugin's priority. Set it by providing the priority as an
|
56
|
+
# argument. Higher numbers ensure that the plugin is
|
57
|
+
# called early to check if a target belongs to it. When called without
|
58
|
+
# an argument, it retrieves this plugin's priority. Defaults to 0.
|
59
|
+
#
|
60
|
+
# @param [Numeric] Priority as a number. Will only be set if != nil
|
61
|
+
# @return [Numeric] This plugin's priority
|
62
|
+
def self.priority(x = nil)
|
63
|
+
@priority = x unless x.nil?
|
64
|
+
@priority || 0
|
65
|
+
end
|
66
|
+
|
67
|
+
# Try to resolve the target. If this plugin cannot handle it, the result
|
68
|
+
# will be nil. If, however, the plugin can resolve it, the resulting
|
69
|
+
# object will be an instance of this plugin. This means, that the interface
|
70
|
+
# that this base class provides, is the basis for the returned type.
|
71
|
+
#
|
72
|
+
# @param [String] target to try to resolve
|
73
|
+
# @return [Plugin] instance if it can be resolved, nil otherwise
|
74
|
+
def self.resolve(_target)
|
75
|
+
fail "Plugin #{self} does not implement `self.resolve(target)`. This method is required"
|
76
|
+
end
|
77
|
+
|
78
|
+
# When a plugin's resolve doesn't lead to the final state, it can
|
79
|
+
# use this method to hand it back for another resolver to handle.
|
80
|
+
#
|
81
|
+
# @param [Any] the current target that needs resolving
|
82
|
+
# @param [Plugin] an instance of the calling resolver
|
83
|
+
# @return [Plugin] instance if it can be resolved, nil otherwise
|
84
|
+
def self.resolve_next(target, parent)
|
85
|
+
res = plugin_registry.resolve(target)
|
86
|
+
res.parent = parent
|
87
|
+
res
|
88
|
+
end
|
89
|
+
|
90
|
+
attr_reader :target
|
91
|
+
attr_accessor :parent
|
92
|
+
end
|
93
|
+
end
|
data/test/docker_test.rb
CHANGED
@@ -42,7 +42,7 @@ class DockerTester
|
|
42
42
|
puts "--> run test on docker #{container.id}"
|
43
43
|
opts = { 'target' => "docker://#{container.id}" }
|
44
44
|
runner = Inspec::Runner.new(opts)
|
45
|
-
runner.
|
45
|
+
@tests.each { |test| runner.add_target(test, opts) }
|
46
46
|
runner.tests.map { |g| g.run(report) }
|
47
47
|
end
|
48
48
|
end
|
data/test/helper.rb
CHANGED
@@ -14,12 +14,19 @@ SimpleCov.start do
|
|
14
14
|
add_group 'Backends', 'lib/inspec/backend'
|
15
15
|
end
|
16
16
|
|
17
|
+
require 'fileutils'
|
18
|
+
require 'pathname'
|
19
|
+
require 'tempfile'
|
20
|
+
require 'zip'
|
21
|
+
|
17
22
|
require 'utils/base_cli'
|
18
|
-
require 'inspec/
|
23
|
+
require 'inspec/fetcher'
|
24
|
+
require 'inspec/source_reader'
|
19
25
|
require 'inspec/resource'
|
20
26
|
require 'inspec/backend'
|
21
27
|
require 'inspec/profile'
|
22
|
-
|
28
|
+
require 'inspec/runner'
|
29
|
+
require 'inspec/runner_mock'
|
23
30
|
|
24
31
|
class MockLoader
|
25
32
|
# collects emulation operating systems
|
@@ -44,6 +51,8 @@ class MockLoader
|
|
44
51
|
undefined: { family: nil, release: nil, arch: nil },
|
45
52
|
}
|
46
53
|
|
54
|
+
@archives = {}
|
55
|
+
|
47
56
|
# pass the os identifier to emulate a specific operating system
|
48
57
|
def initialize(os = nil)
|
49
58
|
# selects operating system
|
@@ -86,6 +95,7 @@ class MockLoader
|
|
86
95
|
'/etc/ssh/ssh_config' => mockfile.call('ssh_config'),
|
87
96
|
'/etc/ssh/sshd_config' => mockfile.call('sshd_config'),
|
88
97
|
'/etc/passwd' => mockfile.call('passwd'),
|
98
|
+
'/etc/shadow' => mockfile.call('shadow'),
|
89
99
|
'/etc/ntp.conf' => mockfile.call('ntp.conf'),
|
90
100
|
'/etc/login.defs' => mockfile.call('login.defs'),
|
91
101
|
'/etc/security/limits.conf' => mockfile.call('limits.conf'),
|
@@ -238,6 +248,56 @@ class MockLoader
|
|
238
248
|
resource.inspec.backend
|
239
249
|
.mock_command(cmd, res[:stdout], res[:stderr], res[:exit_status])
|
240
250
|
end
|
251
|
+
|
252
|
+
def self.home
|
253
|
+
File.join(File.dirname(__FILE__), 'unit')
|
254
|
+
end
|
255
|
+
|
256
|
+
def self.profile_path(name)
|
257
|
+
dst = name
|
258
|
+
dst = "#{home}/mock/profiles/#{name}" unless name.start_with?(home)
|
259
|
+
dst
|
260
|
+
end
|
261
|
+
|
262
|
+
def self.load_profile(name, opts = {})
|
263
|
+
opts[:test_collector] = Inspec::RunnerMock.new
|
264
|
+
Inspec::Profile.for_target(profile_path(name), opts)
|
265
|
+
end
|
266
|
+
|
267
|
+
def self.profile_tgz(name)
|
268
|
+
path = File.join(home, 'mock', 'profiles', name)
|
269
|
+
archive = Tempfile.new([name, '.tar.gz'])
|
270
|
+
dst = archive.path
|
271
|
+
archive.close
|
272
|
+
|
273
|
+
# generate relative paths
|
274
|
+
files = Dir.glob("#{path}/**/*")
|
275
|
+
relatives = files.map { |e| Pathname.new(e).relative_path_from(Pathname.new(path)).to_s }
|
276
|
+
|
277
|
+
require 'inspec/archive/tar'
|
278
|
+
tag = Inspec::Archive::TarArchiveGenerator.new
|
279
|
+
tag.archive(path, relatives, dst)
|
280
|
+
@archives[dst] = archive
|
281
|
+
|
282
|
+
dst
|
283
|
+
end
|
284
|
+
|
285
|
+
def self.profile_zip(name, opts = {})
|
286
|
+
path = File.join(home, 'mock', 'profiles', name)
|
287
|
+
archive = Tempfile.new([name, '.zip'])
|
288
|
+
dst = archive.path
|
289
|
+
archive.close
|
290
|
+
|
291
|
+
# rubyzip only works relative paths
|
292
|
+
files = Dir.glob("#{path}/**/*")
|
293
|
+
relatives = files.map { |e| Pathname.new(e).relative_path_from(Pathname.new(path)).to_s }
|
294
|
+
|
295
|
+
require 'inspec/archive/zip'
|
296
|
+
zag = Inspec::Archive::ZipArchiveGenerator.new
|
297
|
+
zag.archive(path, relatives, dst)
|
298
|
+
@archives[dst] = archive
|
299
|
+
dst
|
300
|
+
end
|
241
301
|
end
|
242
302
|
|
243
303
|
def load_resource(*args)
|
@@ -12,6 +12,8 @@ when 'ubuntu'
|
|
12
12
|
|
13
13
|
when 'centos'
|
14
14
|
# install runit for alternative service mgmt
|
15
|
-
|
16
|
-
|
15
|
+
if node['platform_version'].to_i >= 6
|
16
|
+
include_recipe 'os_prepare::_runit_service_centos'
|
17
|
+
include_recipe 'os_prepare::_upstart_service_centos'
|
18
|
+
end
|
17
19
|
end
|
@@ -18,4 +18,15 @@ if os.linux?
|
|
18
18
|
its('LogLevel') { should cmp 'info' }
|
19
19
|
its('LogLevel') { should cmp 'InfO' }
|
20
20
|
end
|
21
|
+
|
22
|
+
describe passwd.passwords.uniq do
|
23
|
+
it { should eq ['x'] }
|
24
|
+
it { should cmp ['x'] }
|
25
|
+
it { should cmp 'x' }
|
26
|
+
end
|
27
|
+
|
28
|
+
describe passwd.usernames do
|
29
|
+
it { should include 'root' }
|
30
|
+
it { should_not cmp 'root' }
|
31
|
+
end
|
21
32
|
end
|
@@ -47,7 +47,7 @@ if os[:family] == 'ubuntu'
|
|
47
47
|
end
|
48
48
|
|
49
49
|
# extra tests for alt. runit on centos with runit_service
|
50
|
-
if os[:family] == 'centos'
|
50
|
+
if os[:family] == 'centos' && os[:release].to_i >= 6
|
51
51
|
describe runit_service('running-runit-service') do
|
52
52
|
it { should be_enabled }
|
53
53
|
it { should be_installed }
|
@@ -103,3 +103,18 @@ if os[:family] == 'centos'
|
|
103
103
|
it { should_not be_running }
|
104
104
|
end
|
105
105
|
end
|
106
|
+
|
107
|
+
# extra tests for sys-v runlevels
|
108
|
+
if os[:family] == 'centos' && os[:release].to_i <= 6
|
109
|
+
describe service('sshd').runlevels do
|
110
|
+
its('keys') { should include(2) }
|
111
|
+
end
|
112
|
+
|
113
|
+
describe service('sshd').runlevels(2, 4) do
|
114
|
+
it { should be_enabled }
|
115
|
+
end
|
116
|
+
|
117
|
+
describe service('sshd').runlevels(0, 1) do
|
118
|
+
it { should_not be_enabled }
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# author: Dominik Richter
|
3
|
+
# author: Christoph Hartmann
|
4
|
+
|
5
|
+
require 'helper'
|
6
|
+
|
7
|
+
describe Inspec::Fetcher do
|
8
|
+
it 'loads the local fetcher for this file' do
|
9
|
+
res = Inspec::Fetcher.resolve(__FILE__)
|
10
|
+
res.must_be_kind_of Fetchers::Local
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe Inspec::Plugins::RelFetcher do
|
15
|
+
def fetcher
|
16
|
+
src_fetcher.expects(:files).returns(in_files).at_least_once
|
17
|
+
Inspec::Plugins::RelFetcher.new(src_fetcher)
|
18
|
+
end
|
19
|
+
|
20
|
+
let(:src_fetcher) { mock() }
|
21
|
+
|
22
|
+
IN_AND_OUT = {
|
23
|
+
[] => [],
|
24
|
+
%w{file} => %w{file},
|
25
|
+
# don't prefix just by filename
|
26
|
+
%w{file file_a} => %w{file file_a},
|
27
|
+
%w{path/file path/file_a} => %w{file file_a},
|
28
|
+
%w{path/to/file} => %w{file},
|
29
|
+
%w{/path/to/file} => %w{file},
|
30
|
+
%w{alice bob} => %w{alice bob},
|
31
|
+
# mixed paths
|
32
|
+
%w{x/a y/b} => %w{x/a y/b},
|
33
|
+
%w{/x/a /y/b} => %w{x/a y/b},
|
34
|
+
%w{z/x/a z/y/b} => %w{x/a y/b},
|
35
|
+
%w{/z/x/a /z/y/b} => %w{x/a y/b},
|
36
|
+
# mixed with relative path
|
37
|
+
%w{a path/to/b} => %w{a path/to/b},
|
38
|
+
%w{path/to/b a} => %w{path/to/b a},
|
39
|
+
%w{path/to/b path/a} => %w{to/b a},
|
40
|
+
%w{path/to/b path/a c} => %w{path/to/b path/a c},
|
41
|
+
# mixed with absolute paths
|
42
|
+
%w{/path/to/b /a} => %w{path/to/b a},
|
43
|
+
%w{/path/to/b /path/a} => %w{to/b a},
|
44
|
+
%w{/path/to/b /path/a /c} => %w{path/to/b path/a c},
|
45
|
+
# mixing absolute and relative paths
|
46
|
+
%w{path/a /path/b} => %w{path/a /path/b},
|
47
|
+
%w{/path/a path/b} => %w{/path/a path/b},
|
48
|
+
# extract folder structure buildup
|
49
|
+
%w{/a /a/b /a/b/c} => %w{c},
|
50
|
+
%w{/a /a/b /a/b/c/d/e} => %w{e},
|
51
|
+
}.each do |ins, outs|
|
52
|
+
describe 'empty profile' do
|
53
|
+
let(:in_files) { ins }
|
54
|
+
|
55
|
+
it 'also has no files' do
|
56
|
+
fetcher.files.must_equal outs
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|