cukedep 0.1.11 → 0.2.00

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 (45) hide show
  1. checksums.yaml +6 -14
  2. data/.ruby-version +1 -1
  3. data/.travis.yml +10 -10
  4. data/CHANGELOG.md +6 -0
  5. data/Gemfile +5 -5
  6. data/LICENSE.txt +1 -1
  7. data/README.md +1 -1
  8. data/Rakefile +30 -30
  9. data/bin/cukedep +15 -15
  10. data/lib/cukedep/application.rb +105 -112
  11. data/lib/cukedep/cli/cmd-line.rb +11 -13
  12. data/lib/cukedep/config.rb +85 -89
  13. data/lib/cukedep/constants.rb +5 -5
  14. data/lib/cukedep/cuke-runner.rb +191 -198
  15. data/lib/cukedep/customization.rb +30 -30
  16. data/lib/cukedep/feature-model.rb +43 -46
  17. data/lib/cukedep/feature-rep.rb +9 -11
  18. data/lib/cukedep/file-action.rb +11 -18
  19. data/lib/cukedep/gherkin-facade.rb +11 -6
  20. data/lib/cukedep/gherkin-listener.rb +12 -42
  21. data/lib/cukedep/hook-dsl.rb +78 -78
  22. data/lib/cukedep/sandbox.rb +15 -16
  23. data/lib/cukedep.rb +1 -2
  24. data/sample/features/step_definitions/steps.rb +2 -2
  25. data/sample/model/model.rb +19 -20
  26. data/spec/cukedep/application_spec.rb +80 -80
  27. data/spec/cukedep/cli/cmd-line_spec.rb +88 -88
  28. data/spec/cukedep/cuke-runner_spec.rb +74 -74
  29. data/spec/cukedep/customization_spec.rb +31 -31
  30. data/spec/cukedep/debug-file-action.rb +29 -29
  31. data/spec/cukedep/feature-model_spec.rb +100 -100
  32. data/spec/cukedep/feature-rep_spec.rb +2 -1
  33. data/spec/cukedep/file-action_spec.rb +365 -366
  34. data/spec/cukedep/file-parsing.rb +39 -41
  35. data/spec/cukedep/gherkin-facade_spec.rb +48 -49
  36. data/spec/cukedep/gherkin-listener_spec.rb +55 -57
  37. data/spec/cukedep/hook-dsl_spec.rb +182 -182
  38. data/spec/cukedep/sample_features/cukedep_hooks.rb +30 -30
  39. data/spec/cukedep/sample_features/standalone.feature +1 -1
  40. data/templates/rake.erb +12 -21
  41. metadata +80 -58
  42. data/sample/result.html +0 -472
  43. data/spec/cukedep/sample_features/cukedep.rake +0 -215
  44. data/spec/cukedep/sample_features/dependencies.dot +0 -38
  45. data/spec/cukedep/sample_features/feature2id.csv +0 -7
@@ -18,19 +18,19 @@ class FeatureModel
18
18
  # topological sort of the nodes.
19
19
  class DepGraph
20
20
  include TSort # Mix-in module for topological sorting
21
-
21
+
22
22
  attr_reader(:dependencies)
23
-
23
+
24
24
  # Inverse lookup: from the feature file => FeatureDependencies
25
25
  attr_reader(:lookup)
26
-
26
+
27
27
  def initialize(theDependencies)
28
28
  @dependencies = theDependencies
29
29
  @lookup = dependencies.each_with_object({}) do |f_deps, subresult|
30
30
  subresult[f_deps.dependee] = f_deps
31
31
  end
32
32
  end
33
-
33
+
34
34
  # Method required by TSort module.
35
35
  # It is used to iterate over all the nodes of the dependency graph
36
36
  def tsort_each_node(&aBlock)
@@ -48,7 +48,7 @@ class FeatureModel
48
48
 
49
49
 
50
50
  attr_reader(:feature_files)
51
-
51
+
52
52
  # An Array of FeatureDependencies
53
53
  attr_reader(:dependencies)
54
54
 
@@ -63,35 +63,34 @@ class FeatureModel
63
63
  selection = theIds.each_with_object([]) do |an_id, sub_result|
64
64
  found_feature = features_by_ids[an_id]
65
65
  if found_feature.nil?
66
- fail(StandardError, "No feature file with identifier '#{an_id}'.")
66
+ raise StandardError, "No feature file with identifier '#{an_id}'."
67
67
  end
68
68
  sub_result << found_feature
69
69
  end
70
-
70
+
71
71
  return selection
72
72
  end
73
-
73
+
74
74
  # The list of feature files without identifiers
75
- def anonymous_features()
75
+ def anonymous_features
76
76
  feature_files.select { |ff| ff.feature.anonymous? }
77
77
  end
78
78
 
79
79
  # Build an array of FileDependencies objects.
80
- def dependency_links()
80
+ def dependency_links
81
81
  if @dependencies.nil?
82
82
  # Build the mapping: feature identifier => feature
83
83
  features_by_id = id2features
84
-
84
+
85
85
  # Resolve the dependency tags
86
86
  resolve_dependencies(features_by_id)
87
87
  end
88
-
88
+
89
89
  return @dependencies
90
90
  end
91
91
 
92
-
93
92
  # Sort the feature files by dependency order.
94
- def sort_features_by_dep()
93
+ def sort_features_by_dep
95
94
  dep_links = dependency_links
96
95
  graph = DepGraph.new(dep_links)
97
96
  sorted_deps = graph.tsort
@@ -99,7 +98,7 @@ class FeatureModel
99
98
  all_sorted = sorted_deps.map(&:dependee)
100
99
  @sorted_features = all_sorted.reject { |f| f.feature.anonymous? }
101
100
  end
102
-
101
+
103
102
  # Generate CSV files detailing the feature to identifier mapping
104
103
  # and vise versa
105
104
  def mapping_reports(fileFeature2id, fileId2Feature, isVerbose = false)
@@ -113,7 +112,7 @@ class FeatureModel
113
112
  f << [filename, identifier.nil? ? 'nil' : identifier]
114
113
  end
115
114
  end
116
-
115
+
117
116
  # Generate the feature file name => feature identifier report
118
117
  puts " #{fileId2Feature}" if isVerbose
119
118
  CSV.open(fileId2Feature, 'wb') do |f|
@@ -123,7 +122,7 @@ class FeatureModel
123
122
  filename = File.basename(ff.filepath)
124
123
  f << [identifier, filename] unless identifier.nil?
125
124
  end
126
- end
125
+ end
127
126
  end
128
127
 
129
128
  # Create a graphical representation of the dependencies.
@@ -136,11 +135,10 @@ class FeatureModel
136
135
  emit_body(dot_file)
137
136
  emit_trailing(dot_file)
138
137
  end
139
-
140
-
138
+
141
139
  def emit_heading(anIO)
142
140
  dir = File.dirname(File.absolute_path(feature_files[0].filepath))
143
- heading = <<-EOS
141
+ heading = <<-DOT
144
142
  // Graph of dependencies of feature files in directory:
145
143
  // '#{dir}'
146
144
  // This file uses the DOT syntax, a free utility from the Graphviz toolset.
@@ -149,49 +147,49 @@ class FeatureModel
149
147
 
150
148
  digraph g {
151
149
  size = "7, 11"; // Dimensions in inches...
152
- center = true;
150
+ center = true;
153
151
  rankdir = BT; // Draw from bottom to top
154
- label = "\\nDependency graph of '#{dir}'";
155
-
152
+ label = "\\nDependency graph of '#{dir}'";
153
+
156
154
  // Nodes represent feature files
157
- EOS
155
+ DOT
158
156
  anIO.write heading
159
157
  end
160
-
158
+
161
159
  # Output the nodes as graph vertices + their edges with parent node
162
160
  def emit_body(anIO)
163
- anIO.puts <<-EOS
161
+ anIO.puts <<-DOT
164
162
  subgraph island {
165
163
  node [shape = box, style=filled, color=lightgray];
166
- EOS
164
+ DOT
167
165
  feature_files.each_with_index do |ff, i|
168
166
  draw_node(anIO, ff, i) if ff.feature.anonymous?
169
167
  end
170
-
171
- anIO.puts <<-EOS
168
+
169
+ anIO.puts <<-DOT
172
170
  label = "Isolated features";
173
171
  }
174
172
 
175
173
  subgraph dependencies {
176
174
  node [shape = box, fillcolor = none];
177
- EOS
178
- feature_files.each_with_index do |ff, i|
175
+ DOT
176
+ feature_files.each_with_index do |ff, i|
179
177
  draw_node(anIO, ff, i) unless ff.feature.anonymous?
180
178
  end
181
- anIO.puts <<-EOS
179
+ anIO.puts <<-DOT
182
180
  label = "Dependencies";
183
181
  }
184
182
 
185
183
  // The edges represent dependencies
186
- EOS
184
+ DOT
187
185
  dependencies.each { |a_dep| draw_edge(anIO, a_dep) }
188
186
  end
189
-
187
+
190
188
  # Output the closing part of the graph drawing
191
189
  def emit_trailing(anIO)
192
190
  anIO.puts '} // End of graph'
193
191
  end
194
-
192
+
195
193
  # Draw a refinement node in DOT format
196
194
  def draw_node(anIO, aFeatureFile, anIndex)
197
195
  basename = File.basename(aFeatureFile.filepath, '.feature')
@@ -203,7 +201,7 @@ EOS
203
201
  end
204
202
  anIO.puts %Q( node_#{anIndex} [label = "#{basename}#{id_suffix}"];)
205
203
  end
206
-
204
+
207
205
  # Draw an edge between feature files having dependencies.
208
206
  def draw_edge(anIO, aDependency)
209
207
  source_id = feature_files.find_index(aDependency.dependee)
@@ -216,13 +214,12 @@ EOS
216
214
  end
217
215
  end
218
216
 
219
-
220
217
  def generate_rake_tasks(rakefile, theProjDir)
221
218
  puts " #{rakefile}"
222
219
  grandparent_path = Pathname.new(File.dirname(__FILE__)).parent.parent
223
220
  template_source = File.read(grandparent_path + './templates/rake.erb')
224
221
 
225
- # Create one template engine instance
222
+ # Create one template engine instance
226
223
  engine = ERB.new(template_source)
227
224
 
228
225
  source_dir = File.absolute_path(Dir.getwd)
@@ -245,7 +242,7 @@ EOS
245
242
  end
246
243
 
247
244
  # Build the mapping: feature identifier => feature
248
- def id2features()
245
+ def id2features
249
246
  mapping = feature_files.each_with_object({}) do |file, mp|
250
247
  feature_id = file.feature.identifier
251
248
  mp[feature_id] = file unless feature_id.nil?
@@ -253,14 +250,14 @@ EOS
253
250
 
254
251
  return mapping
255
252
  end
256
-
253
+
257
254
  # Given a feature identifier => feature mapping,
258
255
  # resolve the dependency tags; that is,
259
256
  # Establish links between a feature file object and its
260
257
  # dependent feature file objects.
261
258
  def resolve_dependencies(aMapping)
262
259
  @dependencies = []
263
-
260
+
264
261
  feature_files.each do |feature_file|
265
262
  feature = feature_file.feature
266
263
  its_id = feature.identifier
@@ -268,22 +265,22 @@ EOS
268
265
  # Complain when self dependency detected
269
266
  if dep_tags.include?(its_id)
270
267
  msg = "Feature with identifier #{its_id} depends on itself!"
271
- fail(StandardError, msg)
268
+ raise StandardError, msg
272
269
  end
273
-
270
+
274
271
  # Complain when dependency tag refers to an unknown feature
275
272
  dependents = dep_tags.map do |a_tag|
276
273
  unless aMapping.include?(a_tag)
277
274
  msg_p1 = "Feature with identifier '#{its_id}'"
278
275
  msg_p2 = " depends on unknown feature '#{a_tag}'"
279
- fail(StandardError, msg_p1, msg_p2)
276
+ raise StandardError, msg_p1 + msg_p2
280
277
  end
281
278
  aMapping[a_tag]
282
279
  end
283
-
280
+
284
281
  @dependencies << FeatureDependencies.new(feature_file, dependents)
285
282
  end
286
-
283
+
287
284
  return @dependencies
288
285
  end
289
286
  end # class
@@ -5,39 +5,37 @@ module Cukedep # This module is used as a namespace
5
5
  class FeatureRep
6
6
  # Constant that specifies how feature identifier tags should begin
7
7
  FeatureIdPrefix = /^feature:/
8
-
8
+
9
9
  # Constant that specifies how dependency tags should begin
10
10
  DependencyPrefix = /^depends_on:/
11
-
11
+
12
12
  # The sorted list of all tags of the feature.
13
13
  # The @ prefix is stripped from each tag text.
14
14
  attr_reader(:tags)
15
-
16
- # The identifier of the feature.
15
+
16
+ # The identifier of the feature.
17
17
  # It comes from a tag with the following syntax '@feature:' + identifier.
18
18
  # Note that the @feature: prefix is removed.
19
19
  attr_reader(:identifier)
20
20
 
21
21
 
22
- # theTags the tags objects from the Gherkin parser
22
+ # @param theTags [Array<String>] the tags objects from the Gherkin parser
23
23
  def initialize(theTags)
24
24
  # Strip first character of tag literal.
25
25
  @tags = theTags.map { |t| t[1..-1] }
26
-
26
+
27
27
  @identifier = tags.find { |tg| tg =~ FeatureIdPrefix }
28
28
  @identifier = @identifier.sub(FeatureIdPrefix, '') unless identifier.nil?
29
29
  end
30
-
31
- public
32
30
 
33
31
  # The list of all feature identifiers retrieved from the dependency tags
34
- def dependency_tags()
32
+ def dependency_tags
35
33
  dep_tags = tags.select { |t| t =~ DependencyPrefix }
36
34
  return dep_tags.map { |t| t.sub(DependencyPrefix, '') }
37
35
  end
38
-
36
+
39
37
  # Return true iff the identifier of the feature is nil.
40
- def anonymous?()
38
+ def anonymous?
41
39
  return identifier.nil?
42
40
  end
43
41
  end # class
@@ -5,23 +5,22 @@ require 'fileutils'
5
5
 
6
6
  module Cukedep # This module is used as a namespace
7
7
  class FileAction
8
- attr(:patterns, true)
8
+ attr_accessor(:patterns)
9
9
  attr_reader(:delta)
10
10
 
11
11
  # Constructor.
12
12
  # [thePatterns] An array of file patterns.
13
13
  def initialize(thePatterns, aDelta = nil)
14
14
  @patterns = validate_file_patterns(thePatterns)
15
- @delta = validate_delta(aDelta)
15
+ @delta = validate_delta(aDelta)
16
16
  end
17
17
 
18
-
19
18
  # Datavalue semantic: FileActions don't have identity
20
19
  def ==(other)
21
20
  return true if object_id == other.object_id
22
21
  return false if self.class != other.class
23
22
 
24
- attrs = [:patterns, :delta]
23
+ attrs = %I[patterns delta]
25
24
  equality = true
26
25
 
27
26
  attrs.each do |accessor|
@@ -32,15 +31,14 @@ module Cukedep # This module is used as a namespace
32
31
  return equality
33
32
  end
34
33
 
35
-
36
34
  protected
37
35
 
38
36
  def validate_file_patterns(filePatterns)
39
37
  err_msg = 'Expecting a list of file patterns'
40
- fail StandardError, err_msg unless filePatterns.is_a?(Array)
41
- filePatterns.each do |filePatt|
42
- err_msg = "Invalid value in list of file patterns: #{filePatt}"
43
- fail StandardError, err_msg unless filePatt.is_a?(String)
38
+ raise StandardError, err_msg unless filePatterns.is_a?(Array)
39
+ filePatterns.each do |file_patt|
40
+ err_msg = "Invalid value in list of file patterns: #{file_patt}"
41
+ raise StandardError, err_msg unless file_patt.is_a?(String)
44
42
  end
45
43
 
46
44
  return filePatterns
@@ -52,7 +50,7 @@ module Cukedep # This module is used as a namespace
52
50
  when String
53
51
  validated = aDelta.empty? ? nil : aDelta
54
52
  else
55
- fail StandardError, 'Invalid relative path #{aDelta}'
53
+ raise StandardError, "Invalid relative path #{aDelta}"
56
54
  end
57
55
 
58
56
  return validated
@@ -74,7 +72,6 @@ module Cukedep # This module is used as a namespace
74
72
  end # class
75
73
 
76
74
 
77
-
78
75
  # A delete action object has for purpose to
79
76
  # delete files matching one of its file patterns.
80
77
  # These file are deleted from (a subdir of) a given 'target' directory.
@@ -87,7 +84,7 @@ module Cukedep # This module is used as a namespace
87
84
 
88
85
  def run!(targetDir)
89
86
  return if patterns.empty?
90
- orig_dir = Dir.getwd # Store current work directory
87
+ orig_dir = Dir.getwd # Store current work directory
91
88
  # pp orig_dir
92
89
 
93
90
  begin
@@ -109,7 +106,6 @@ module Cukedep # This module is used as a namespace
109
106
  end # class
110
107
 
111
108
 
112
-
113
109
  # A copy action object has for purpose to
114
110
  # copy files matching one of its file patterns.
115
111
  # These file are copied from a given 'source' directory
@@ -161,16 +157,14 @@ module Cukedep # This module is used as a namespace
161
157
  theActionSettings[:copy_subdir])
162
158
  end
163
159
 
164
-
165
160
  def ==(other)
166
161
  return true if object_id == other.object_id
167
162
 
168
163
  return (save_action == other.save_action) &&
169
- (delete_action == other.delete_action) &&
170
- (copy_action == other.copy_action)
164
+ (delete_action == other.delete_action) &&
165
+ (copy_action == other.copy_action)
171
166
  end
172
167
 
173
-
174
168
  # Launch the file actions in sequence.
175
169
  def run!(currentDir, projectDir)
176
170
  save_action.run!(projectDir, currentDir)
@@ -178,7 +172,6 @@ module Cukedep # This module is used as a namespace
178
172
  copy_action.run!(currentDir, projectDir)
179
173
  end
180
174
 
181
-
182
175
  # Retrieve the 'built-in' action triplet associated with the given event.
183
176
  # Return nil if no triplet was found for the event.
184
177
  def self.builtin(anEvent)
@@ -1,7 +1,6 @@
1
1
  # File: gherkin-facade.rb
2
+ require 'gherkin/parser'
2
3
 
3
- require 'gherkin'
4
- require 'gherkin/lexer/encoding'
5
4
 
6
5
  module Cukedep # This module is used as a namespace
7
6
  # Facade design pattern: A facade is an object that provides
@@ -16,7 +15,6 @@ module Cukedep # This module is used as a namespace
16
15
  # as expected by the mode argument of the IO#new method
17
16
  attr_reader(:external_encoding)
18
17
 
19
-
20
18
  def initialize(isVerbose, anExternalEncoding)
21
19
  @verbose = isVerbose
22
20
  @external_encoding = anExternalEncoding
@@ -27,25 +25,32 @@ module Cukedep # This module is used as a namespace
27
25
  # Parse events are sent to the passed listener object.
28
26
  def parse_features(aListener, file_patterns)
29
27
  # Create a Gherkin parser
30
- parser = Gherkin::Parser::Parser.new(aListener)
28
+ parser = Gherkin::Parser.new
31
29
 
32
30
  puts "\nParsing:" if verbose
33
31
  # List all .feature files in work directory that match the pattern
34
32
  filenames = []
35
33
  file_patterns.each { |patt| filenames.concat(Dir.glob(patt)) }
36
-
37
34
  # Parse them
38
35
  filenames.each do |fname|
39
36
  puts " #{fname}" if verbose
40
37
  # To prevent encoding issue, open the file
41
38
  # with an explicit external encoding
42
39
  File.open(fname, "r:#{external_encoding}") do |f|
43
- parser.parse(f.read, fname, 0)
40
+ raw = parser.parse(f.read)
41
+ parse_raw_feature(raw[:feature], fname, aListener) if raw[:feature]
44
42
  end
45
43
  end
46
44
 
47
45
  return aListener
48
46
  end
47
+
48
+ def parse_raw_feature(raw_feature, file_name, listener)
49
+ listener.uri(file_name)
50
+ raw_tags = raw_feature[:tags]
51
+ tag_names = raw_tags.map { |raw_tag| raw_tag[:name] }
52
+ listener.feature_tags(tag_names)
53
+ end
49
54
  end # class
50
55
  end # module
51
56
 
@@ -4,15 +4,15 @@ require_relative 'feature-rep'
4
4
 
5
5
 
6
6
  module Cukedep # This module is used as a namespace
7
- class FeatureFileRep
7
+ class FeatureFileRep
8
8
  attr_reader(:filepath)
9
- attr(:feature, true)
10
-
9
+ attr_accessor(:feature)
10
+
11
11
  def initialize(aFilepath)
12
12
  @filepath = aFilepath
13
13
  end
14
-
15
- def basename()
14
+
15
+ def basename
16
16
  File.basename(filepath)
17
17
  end
18
18
  end # class
@@ -27,12 +27,13 @@ module Cukedep # This module is used as a namespace
27
27
  attr_reader(:feature_files)
28
28
 
29
29
  # Internal representation of the feature being parsed
30
- attr(:current_feature, true)
31
-
32
- def initialize()
30
+ attr_accessor(:current_feature)
31
+
32
+ # Constructor
33
+ def initialize
33
34
  @feature_files = []
34
35
  end
35
-
36
+
36
37
  ######################################
37
38
  # Event listening methods
38
39
  ######################################
@@ -45,44 +46,13 @@ module Cukedep # This module is used as a namespace
45
46
  end
46
47
 
47
48
  # aFeature is a Gherkin::Formatter::Model::Feature instance
48
- def feature(aFeature)
49
- tag_names = aFeature.tags.map(&:name)
49
+ def feature_tags(tag_names)
50
50
  @current_feature = feature_files.last.feature = FeatureRep.new(tag_names)
51
51
  end
52
52
 
53
- # aBackground is a Gherkin::Formatter::Model::Background instance
54
- def background(_aBackground)
55
- ; # Do nothing
56
- end
57
-
58
- # aScenario is a Gherkin::Formatter::Model::Scenario instance
59
- def scenario(_aScenario)
60
- ; # Do nothing
61
- end
62
-
63
- # aScenarioOutline is a Gherkin::Formatter::Model::ScenarioOutline instance
64
- def scenario_outline(_aScenarioOutline)
65
- ; # Do nothing
66
- end
67
-
68
- # theExamples is a Gherkin::Formatter::Model::Examples instance
69
- def examples(_theExamples)
70
- ; # Do nothing
71
- end
72
-
73
- # aStep is a Gherkin::Formatter::Model::Step instance
74
- def step(_aStep)
75
- ; # Do nothing
76
- end
77
-
78
- # End of feature file notification.
79
- def eof()
80
- ; # Do nothing
81
- end
82
-
83
-
84
53
  # Catch all method
85
54
  def method_missing(message, *_args)
55
+ puts caller(1, 5).join("\n")
86
56
  puts "Method #{message} is not implemented (yet)."
87
57
  end
88
58
  end # class