clintegracon 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.travis.yml +12 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +158 -0
- data/Rakefile +60 -0
- data/clintegracon.gemspec +34 -0
- data/lib/CLIntegracon.rb +9 -0
- data/lib/CLIntegracon/adapter/bacon.rb +208 -0
- data/lib/CLIntegracon/configuration.rb +67 -0
- data/lib/CLIntegracon/diff.rb +81 -0
- data/lib/CLIntegracon/file_tree_spec.rb +213 -0
- data/lib/CLIntegracon/file_tree_spec_context.rb +180 -0
- data/lib/CLIntegracon/formatter.rb +77 -0
- data/lib/CLIntegracon/subject.rb +128 -0
- data/lib/CLIntegracon/version.rb +3 -0
- data/spec/bacon/execution_output.txt +72 -0
- data/spec/bacon/spec_helper.rb +60 -0
- data/spec/fixtures/bin/coffeemaker.rb +58 -0
- data/spec/integration/coffeemaker_help/after/execution_output.txt +23 -0
- data/spec/integration/coffeemaker_help/before/.gitkeep +0 -0
- data/spec/integration/coffeemaker_no_milk/after/BlackEye.brewed-coffee +1 -0
- data/spec/integration/coffeemaker_no_milk/after/CaPheSuaDa.brewed-coffee +1 -0
- data/spec/integration/coffeemaker_no_milk/after/Coffeemakerfile.yml +5 -0
- data/spec/integration/coffeemaker_no_milk/after/RedTux.brewed-coffee +1 -0
- data/spec/integration/coffeemaker_no_milk/after/execution_output.txt +6 -0
- data/spec/integration/coffeemaker_no_milk/before/Coffeemakerfile.yml +5 -0
- data/spec/integration/coffeemaker_sweetner_honey/after/Affogato.brewed-coffee +2 -0
- data/spec/integration/coffeemaker_sweetner_honey/after/BlackEye.brewed-coffee +2 -0
- data/spec/integration/coffeemaker_sweetner_honey/after/Coffeemakerfile.yml +3 -0
- data/spec/integration/coffeemaker_sweetner_honey/after/RedTux.brewed-coffee +2 -0
- data/spec/integration/coffeemaker_sweetner_honey/after/execution_output.txt +4 -0
- data/spec/integration/coffeemaker_sweetner_honey/before/Coffeemakerfile.yml +3 -0
- data/spec/unit/adapter/bacon_spec.rb +187 -0
- data/spec/unit/configuration_spec.rb +72 -0
- metadata +176 -0
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
module CLIntegracon
|
4
|
+
|
5
|
+
class << self
|
6
|
+
|
7
|
+
# @return [Configuration]
|
8
|
+
# Get the shared configuration, set by {self.configure}.
|
9
|
+
attr_accessor :shared_config
|
10
|
+
|
11
|
+
# Set a new shared configuration
|
12
|
+
#
|
13
|
+
# @param [Block<() -> ()>] block
|
14
|
+
# the block which is evaluated on the new shared configuration
|
15
|
+
#
|
16
|
+
def configure(&block)
|
17
|
+
self.shared_config ||= Configuration.new
|
18
|
+
shared_config.instance_eval &block
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
class Configuration
|
24
|
+
|
25
|
+
# Get the subject to configure it
|
26
|
+
#
|
27
|
+
# @return [Subject]
|
28
|
+
#
|
29
|
+
def subject
|
30
|
+
@subject ||= Subject.new()
|
31
|
+
end
|
32
|
+
|
33
|
+
# Get the context to configure it
|
34
|
+
#
|
35
|
+
# @return [FileTreeSpecContext]
|
36
|
+
#
|
37
|
+
def context
|
38
|
+
@context ||= FileTreeSpecContext.new()
|
39
|
+
end
|
40
|
+
|
41
|
+
# Hook this gem in a test framework by a supported adapter
|
42
|
+
#
|
43
|
+
# @param [Symbol] test_framework
|
44
|
+
# the test framework
|
45
|
+
#
|
46
|
+
def hook_into test_framework
|
47
|
+
adapter = self.class.adapters[test_framework]
|
48
|
+
raise ArgumentError.new "No adapter for test framework #{test_framework}" if adapter.nil?
|
49
|
+
require adapter
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
# Get the file paths of supported adapter implementations by test framework
|
55
|
+
#
|
56
|
+
# @return [Hash<Symbol, String>]
|
57
|
+
# test framework to adapter implementation files
|
58
|
+
#
|
59
|
+
def self.adapters
|
60
|
+
adapter_dir = Pathname('../adapter').expand_path(__FILE__)
|
61
|
+
@adapters ||= Dir.chdir(adapter_dir) do
|
62
|
+
Hash[Dir['*.rb'].map { |path| [path.gsub(/\.rb$/, '').to_sym, adapter_dir + path] }]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'colored'
|
3
|
+
require 'diffy'
|
4
|
+
|
5
|
+
module CLIntegracon
|
6
|
+
class Diff
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
# @return [Pathname]
|
10
|
+
# the expected file
|
11
|
+
attr_reader :expected
|
12
|
+
|
13
|
+
# @return [Pathname]
|
14
|
+
# the produced file
|
15
|
+
attr_reader :produced
|
16
|
+
|
17
|
+
# @return [Pathname]
|
18
|
+
# the relative path to the expected file
|
19
|
+
attr_reader :relative_path
|
20
|
+
|
21
|
+
# @return [Proc<(Pathname)->(to_s)>]
|
22
|
+
# the proc, which transforms the files in a better comparable form
|
23
|
+
attr_accessor :preparator
|
24
|
+
|
25
|
+
# Init a new diff
|
26
|
+
#
|
27
|
+
# @param [Pathname] expected
|
28
|
+
# the expected file
|
29
|
+
#
|
30
|
+
# @param [Pathname] produced
|
31
|
+
# the produced file
|
32
|
+
#
|
33
|
+
# @param [Pathname] relative_path
|
34
|
+
# the relative path to the expected file
|
35
|
+
#
|
36
|
+
# @param [Block<(Pathname)->(to_s)>] preparator
|
37
|
+
# the block, which transforms the files in a better comparable form
|
38
|
+
#
|
39
|
+
def initialize(expected, produced, relative_path=nil, &preparator)
|
40
|
+
@expected = expected
|
41
|
+
@produced = produced
|
42
|
+
@relative_path = relative_path
|
43
|
+
preparator ||= Proc.new { |x| x } #id
|
44
|
+
self.preparator = preparator
|
45
|
+
end
|
46
|
+
|
47
|
+
def prepared_expected
|
48
|
+
@prepared_expected ||= preparator.call(expected)
|
49
|
+
end
|
50
|
+
|
51
|
+
def prepared_produced
|
52
|
+
@prepared_produced ||= preparator.call(produced)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Check if the produced output equals the expected
|
56
|
+
#
|
57
|
+
# @return [Bool]
|
58
|
+
# whether the expected is equal to the produced
|
59
|
+
#
|
60
|
+
def is_equal?
|
61
|
+
@is_equal ||= if prepared_expected.is_a? Pathname
|
62
|
+
FileUtils.compare_file(prepared_expected, prepared_produced)
|
63
|
+
else
|
64
|
+
prepared_expected == prepared_produced
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Enumerate all lines which differ.
|
69
|
+
#
|
70
|
+
# @param [Hash] options
|
71
|
+
# see Diffy#initialize for help.
|
72
|
+
#
|
73
|
+
# @return [Diffy::Diff]
|
74
|
+
#
|
75
|
+
def each(options = {}, &block)
|
76
|
+
options = { :source => 'files', :context => 3 }.merge options
|
77
|
+
Diffy::Diff.new(prepared_expected.to_s, prepared_produced.to_s, options).each &block
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,213 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'CLIntegracon/diff'
|
3
|
+
require 'CLIntegracon/formatter'
|
4
|
+
|
5
|
+
module CLIntegracon
|
6
|
+
class FileTreeSpec
|
7
|
+
|
8
|
+
# @return [FileTreeSpecContext]
|
9
|
+
# The context, which configures path and file behaviors
|
10
|
+
attr_reader :context
|
11
|
+
|
12
|
+
# @return [String]
|
13
|
+
# The concrete spec folder
|
14
|
+
attr_reader :spec_folder
|
15
|
+
|
16
|
+
# @return [Pathname]
|
17
|
+
# The concrete spec path
|
18
|
+
def spec_path
|
19
|
+
context.spec_path + spec_folder
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [Pathname]
|
23
|
+
# The concrete before directory for this spec
|
24
|
+
def before_path
|
25
|
+
spec_path + context.before_dir
|
26
|
+
end
|
27
|
+
|
28
|
+
# @return [Pathname]
|
29
|
+
# The concrete after directory for this spec
|
30
|
+
def after_path
|
31
|
+
spec_path + context.after_dir
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [Pathname]
|
35
|
+
# The concrete temp directory for this spec
|
36
|
+
def temp_path
|
37
|
+
context.temp_path + spec_folder
|
38
|
+
end
|
39
|
+
|
40
|
+
# Init a spec with a given context
|
41
|
+
#
|
42
|
+
# @param [FileTreeSpecContext] context
|
43
|
+
# The context, which configures path and file behaviors
|
44
|
+
#
|
45
|
+
# @param [String] spec_folder
|
46
|
+
# The concrete spec folder
|
47
|
+
#
|
48
|
+
def initialize(context, spec_folder)
|
49
|
+
@context = context
|
50
|
+
@spec_folder = spec_folder
|
51
|
+
end
|
52
|
+
|
53
|
+
# Run this spec
|
54
|
+
#
|
55
|
+
# @param [Block<(FileTreeSpec)->()>] block
|
56
|
+
# The block, which will be executed after chdir into the created temporary
|
57
|
+
# directory. In this block you will likely run your modifications to the
|
58
|
+
# file system and use the received FileTreeSpec instance to make asserts
|
59
|
+
# with the test framework of your choice.
|
60
|
+
#
|
61
|
+
def run(&block)
|
62
|
+
prepare!
|
63
|
+
|
64
|
+
copy_files!
|
65
|
+
|
66
|
+
Dir.chdir(temp_path) do
|
67
|
+
block.call self
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Compares the expected and produced directory by using the rules
|
72
|
+
# defined in the context
|
73
|
+
#
|
74
|
+
# @param [Block<(Diff)->()>] diff_block
|
75
|
+
# The block, where you will likely define a test for each file to compare.
|
76
|
+
# It will receive a Diff of each of the expected and produced files.
|
77
|
+
#
|
78
|
+
def compare(&diff_block)
|
79
|
+
transform_paths!
|
80
|
+
|
81
|
+
glob_all(after_path).each do |relative_path|
|
82
|
+
expected = after_path + relative_path
|
83
|
+
|
84
|
+
next unless expected.file?
|
85
|
+
|
86
|
+
block = special_behavior_for_path relative_path
|
87
|
+
next if block == context.class.nop
|
88
|
+
|
89
|
+
diff = diff_files(expected, relative_path)
|
90
|
+
diff.preparator = block unless block.nil?
|
91
|
+
|
92
|
+
diff_block.call diff
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Compares the expected and produced directory by using the rules
|
97
|
+
# defined in the context for unexpected files.
|
98
|
+
#
|
99
|
+
# This is separate because you probably don't want to define an extra
|
100
|
+
# test case for each file, which wasn't expected at all. So you can
|
101
|
+
# keep your test cases consistent.
|
102
|
+
#
|
103
|
+
# @param [Block<(Array)->()>] diff_block
|
104
|
+
# The block, where you will likely define a test that no unexpected files exists.
|
105
|
+
# It will receive an Array.
|
106
|
+
#
|
107
|
+
def check_unexpected_files(&block)
|
108
|
+
expected_files = glob_all after_path
|
109
|
+
produced_files = glob_all
|
110
|
+
unexpected_files = produced_files - expected_files
|
111
|
+
|
112
|
+
# Select only files
|
113
|
+
unexpected_files.reject! { |path| !path.file? }
|
114
|
+
|
115
|
+
# Filter ignored paths
|
116
|
+
unexpected_files.reject! { |path| special_behavior_for_path(path) == context.class.nop }
|
117
|
+
|
118
|
+
block.call unexpected_files
|
119
|
+
end
|
120
|
+
|
121
|
+
# Return a Formatter
|
122
|
+
#
|
123
|
+
# @return [Formatter]
|
124
|
+
#
|
125
|
+
def formatter
|
126
|
+
@formatter ||= Formatter.new(self)
|
127
|
+
end
|
128
|
+
|
129
|
+
protected
|
130
|
+
|
131
|
+
# Prepare the temporary directory
|
132
|
+
#
|
133
|
+
def prepare!
|
134
|
+
context.prepare!
|
135
|
+
|
136
|
+
temp_path.rmtree if temp_path.exist?
|
137
|
+
temp_path.mkdir
|
138
|
+
end
|
139
|
+
|
140
|
+
# Copies the before subdirectory of the given tests folder in the temporary
|
141
|
+
# directory.
|
142
|
+
#
|
143
|
+
def copy_files!
|
144
|
+
source = before_path
|
145
|
+
destination = temp_path
|
146
|
+
FileUtils.cp_r("#{source}/.", destination)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Applies the in the context configured transformations.
|
150
|
+
#
|
151
|
+
def transform_paths!
|
152
|
+
context.transform_paths.each do |path, block|
|
153
|
+
Dir.glob(path) do |produced_path|
|
154
|
+
produced = Pathname(produced_path)
|
155
|
+
block.call(produced)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Searches recursively for all files and take care for including hidden files
|
161
|
+
# if this is configured in the context.
|
162
|
+
#
|
163
|
+
# @param [String] path
|
164
|
+
# The relative or absolute path to search in (optional)
|
165
|
+
#
|
166
|
+
# @return [Array<Pathname>]
|
167
|
+
#
|
168
|
+
def glob_all(path=nil)
|
169
|
+
Dir.chdir path || '.' do
|
170
|
+
Dir.glob("**/*", context.include_hidden_files? ? File::FNM_DOTMATCH : 0).map { |path|
|
171
|
+
Pathname(path)
|
172
|
+
}
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# Find the special behavior for a given path
|
177
|
+
#
|
178
|
+
# @return [Block<(Pathname) -> to_s>]
|
179
|
+
# This block takes the Pathname and transforms the file in a better comparable
|
180
|
+
# state. If it returns nil, the file is ignored.
|
181
|
+
#
|
182
|
+
def special_behavior_for_path(path)
|
183
|
+
context.special_paths.each do |key, block|
|
184
|
+
matched = if key.is_a?(Regexp)
|
185
|
+
path.to_s.match(key)
|
186
|
+
else
|
187
|
+
File.fnmatch(key, path)
|
188
|
+
end
|
189
|
+
next unless matched
|
190
|
+
return block
|
191
|
+
end
|
192
|
+
return nil
|
193
|
+
end
|
194
|
+
|
195
|
+
# Compares two files to check if they are identical and produces a clear diff
|
196
|
+
# to highlight the differences.
|
197
|
+
#
|
198
|
+
# @param [Pathname] expected
|
199
|
+
# The file in the after directory
|
200
|
+
#
|
201
|
+
# @param [Pathname] relative_path
|
202
|
+
# The file in the temp directory
|
203
|
+
#
|
204
|
+
# @return [Diff]
|
205
|
+
# An object holding a diff
|
206
|
+
#
|
207
|
+
def diff_files(expected, relative_path)
|
208
|
+
produced = temp_path + relative_path
|
209
|
+
Diff.new(expected, produced, relative_path)
|
210
|
+
end
|
211
|
+
|
212
|
+
end
|
213
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
require 'CLIntegracon/file_tree_spec'
|
2
|
+
|
3
|
+
module CLIntegracon
|
4
|
+
class FileTreeSpecContext
|
5
|
+
|
6
|
+
#-----------------------------------------------------------------------------#
|
7
|
+
|
8
|
+
# @!group Attributes
|
9
|
+
|
10
|
+
# @return [Pathname]
|
11
|
+
# The relative path to the integration specs
|
12
|
+
attr_accessor :spec_path
|
13
|
+
|
14
|
+
# @return [Pathname]
|
15
|
+
# The relative path from a concrete spec directory to the directory containing the input files,
|
16
|
+
# which will be available at execution
|
17
|
+
attr_accessor :before_dir
|
18
|
+
|
19
|
+
# @return [Pathname]
|
20
|
+
# The relative path from a concrete spec directory to the directory containing the expected files after
|
21
|
+
# the execution
|
22
|
+
attr_accessor :after_dir
|
23
|
+
|
24
|
+
# @return [Pathname]
|
25
|
+
# The relative path to the directory containing the produced files after the
|
26
|
+
# execution. This must not be the same as the before_dir or the after_dir.
|
27
|
+
#
|
28
|
+
# @note **Attention**: This path will been deleted before running to ensure a clean sandbox for testing.
|
29
|
+
#
|
30
|
+
attr_accessor :temp_path
|
31
|
+
|
32
|
+
# @return [Hash<String,Block>]
|
33
|
+
# the special paths of files, which need to be transformed in a better comparable form
|
34
|
+
attr_accessor :transform_paths
|
35
|
+
|
36
|
+
# @return [Hash<String,Block>]
|
37
|
+
# the special paths of files, where an individual file diff handling is needed
|
38
|
+
attr_accessor :special_paths
|
39
|
+
|
40
|
+
# @return [Bool]
|
41
|
+
# whether to include hidden files, when searching directories (true by default)
|
42
|
+
attr_accessor :include_hidden_files
|
43
|
+
alias :include_hidden_files? :include_hidden_files
|
44
|
+
|
45
|
+
|
46
|
+
#-----------------------------------------------------------------------------#
|
47
|
+
|
48
|
+
# @!group Initializer
|
49
|
+
|
50
|
+
# "Designated" initializer
|
51
|
+
#
|
52
|
+
# @param [Hash<Symbol,String>] properties
|
53
|
+
# The configuration parameter (optional):
|
54
|
+
# :spec_path => see self.spec_path
|
55
|
+
# :before_dir => see self.before_dir
|
56
|
+
# :after_dir => see self.after_dir
|
57
|
+
# :temp_path => see self.temp_path
|
58
|
+
#
|
59
|
+
def initialize(properties={})
|
60
|
+
self.spec_path = properties[:spec_path] || '.'
|
61
|
+
self.temp_path = properties[:temp_path] || 'tmp'
|
62
|
+
self.before_dir = properties[:before_dir] || 'before'
|
63
|
+
self.after_dir = properties[:after_dir] || 'after'
|
64
|
+
self.transform_paths = {}
|
65
|
+
self.special_paths = {}
|
66
|
+
self.include_hidden_files = true
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
#-----------------------------------------------------------------------------#
|
71
|
+
|
72
|
+
# @!group Helper
|
73
|
+
|
74
|
+
# This value is used for ignored paths
|
75
|
+
#
|
76
|
+
# @return [Proc]
|
77
|
+
# Does nothing
|
78
|
+
def self.nop
|
79
|
+
@nop ||= Proc.new {}
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
#-----------------------------------------------------------------------------#
|
84
|
+
|
85
|
+
# @!group Setter
|
86
|
+
|
87
|
+
def spec_path=(spec_path)
|
88
|
+
# Spec dir has to exist.
|
89
|
+
@spec_path= Pathname(spec_path).realpath
|
90
|
+
end
|
91
|
+
|
92
|
+
def temp_path=(temp_path)
|
93
|
+
# Temp dir, doesn't have to exist itself, it will been created, but let's ensure
|
94
|
+
# that at least the last but one path component exist.
|
95
|
+
raise "temp_path's parent directory doesn't exist" unless (Pathname(temp_path) + '..').exist?
|
96
|
+
@temp_path = Pathname(temp_path).realpath
|
97
|
+
end
|
98
|
+
|
99
|
+
def before_dir=(before_dir)
|
100
|
+
@before_dir = Pathname(before_dir)
|
101
|
+
end
|
102
|
+
|
103
|
+
def after_dir=(after_dir)
|
104
|
+
@after_dir = Pathname(after_dir)
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
#-----------------------------------------------------------------------------#
|
109
|
+
|
110
|
+
# @!group DSL-like Setter
|
111
|
+
|
112
|
+
# Registers a block for special handling certain files, matched with globs.
|
113
|
+
# Multiple transformers can match a single file.
|
114
|
+
#
|
115
|
+
# @param [String...] file_paths
|
116
|
+
# The file path(s) of the files, which were created/changed and need transformation
|
117
|
+
#
|
118
|
+
# @param [Block<(Pathname) -> ()>] block
|
119
|
+
# The block, which takes each of the matched files, transforms it if needed
|
120
|
+
# in a better comparable form in the temporary path, so that the temporary
|
121
|
+
# will be compared to a given after file, or makes appropriate expects, which
|
122
|
+
# depend on the used test framework
|
123
|
+
#
|
124
|
+
def transform_produced(*file_paths, &block)
|
125
|
+
file_paths.each do |file_path|
|
126
|
+
self.transform_paths[file_path] = block
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Registers a block for special handling certain files, matched with globs.
|
131
|
+
# Registered file paths will be excluded from default comparison by `diff`.
|
132
|
+
# Multiple special handlers can match a single file.
|
133
|
+
#
|
134
|
+
# @param [String|Regexp...] file_paths
|
135
|
+
# The file path(s) of the files, which were created/changed and need special comparison
|
136
|
+
#
|
137
|
+
# @param [Block<(Pathname) -> (String)>] block
|
138
|
+
# The block, which takes each of the matched files, transforms it if needed
|
139
|
+
# in a better comparable form.
|
140
|
+
#
|
141
|
+
def has_special_handling_for(*file_paths, &block)
|
142
|
+
file_paths.each do |file_path|
|
143
|
+
self.special_paths[file_path] = block
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Copies the before subdirectory of the given tests folder in the temporary
|
148
|
+
# directory.
|
149
|
+
#
|
150
|
+
# @param [String|RegExp...] file_path
|
151
|
+
# the file path of the files, which were changed and need special comparison
|
152
|
+
#
|
153
|
+
def ignores(*file_path)
|
154
|
+
has_special_handling_for *file_path, &self.class.nop
|
155
|
+
end
|
156
|
+
|
157
|
+
|
158
|
+
#-----------------------------------------------------------------------------#
|
159
|
+
|
160
|
+
# @!group Interaction
|
161
|
+
|
162
|
+
# Prepare the temporary directory
|
163
|
+
#
|
164
|
+
def prepare!
|
165
|
+
temp_path.mkpath
|
166
|
+
end
|
167
|
+
|
168
|
+
# Get a specific spec with given folder to run it
|
169
|
+
#
|
170
|
+
# @param [String] folder
|
171
|
+
# The name of the folder of the tests
|
172
|
+
#
|
173
|
+
# @return [FileTreeSpec]
|
174
|
+
#
|
175
|
+
def spec(spec_folder)
|
176
|
+
FileTreeSpec.new(self, spec_folder)
|
177
|
+
end
|
178
|
+
|
179
|
+
end
|
180
|
+
end
|