cukedep 0.1.11 → 0.2.00

Sign up to get free protection for your applications and to get access to all the features.
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