clintegracon 0.4.1

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.
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