clintegracon 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.travis.yml +12 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +158 -0
  7. data/Rakefile +60 -0
  8. data/clintegracon.gemspec +34 -0
  9. data/lib/CLIntegracon.rb +9 -0
  10. data/lib/CLIntegracon/adapter/bacon.rb +208 -0
  11. data/lib/CLIntegracon/configuration.rb +67 -0
  12. data/lib/CLIntegracon/diff.rb +81 -0
  13. data/lib/CLIntegracon/file_tree_spec.rb +213 -0
  14. data/lib/CLIntegracon/file_tree_spec_context.rb +180 -0
  15. data/lib/CLIntegracon/formatter.rb +77 -0
  16. data/lib/CLIntegracon/subject.rb +128 -0
  17. data/lib/CLIntegracon/version.rb +3 -0
  18. data/spec/bacon/execution_output.txt +72 -0
  19. data/spec/bacon/spec_helper.rb +60 -0
  20. data/spec/fixtures/bin/coffeemaker.rb +58 -0
  21. data/spec/integration/coffeemaker_help/after/execution_output.txt +23 -0
  22. data/spec/integration/coffeemaker_help/before/.gitkeep +0 -0
  23. data/spec/integration/coffeemaker_no_milk/after/BlackEye.brewed-coffee +1 -0
  24. data/spec/integration/coffeemaker_no_milk/after/CaPheSuaDa.brewed-coffee +1 -0
  25. data/spec/integration/coffeemaker_no_milk/after/Coffeemakerfile.yml +5 -0
  26. data/spec/integration/coffeemaker_no_milk/after/RedTux.brewed-coffee +1 -0
  27. data/spec/integration/coffeemaker_no_milk/after/execution_output.txt +6 -0
  28. data/spec/integration/coffeemaker_no_milk/before/Coffeemakerfile.yml +5 -0
  29. data/spec/integration/coffeemaker_sweetner_honey/after/Affogato.brewed-coffee +2 -0
  30. data/spec/integration/coffeemaker_sweetner_honey/after/BlackEye.brewed-coffee +2 -0
  31. data/spec/integration/coffeemaker_sweetner_honey/after/Coffeemakerfile.yml +3 -0
  32. data/spec/integration/coffeemaker_sweetner_honey/after/RedTux.brewed-coffee +2 -0
  33. data/spec/integration/coffeemaker_sweetner_honey/after/execution_output.txt +4 -0
  34. data/spec/integration/coffeemaker_sweetner_honey/before/Coffeemakerfile.yml +3 -0
  35. data/spec/unit/adapter/bacon_spec.rb +187 -0
  36. data/spec/unit/configuration_spec.rb +72 -0
  37. 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