cukedep 0.0.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 (43) hide show
  1. checksums.yaml +15 -0
  2. data/.rspec +1 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. data/.simplecov +7 -0
  6. data/.travis.yml +15 -0
  7. data/.yardopts +6 -0
  8. data/CHANGELOG.md +3 -0
  9. data/Gemfile +12 -0
  10. data/LICENSE.txt +19 -0
  11. data/README.md +64 -0
  12. data/Rakefile +32 -0
  13. data/bin/cukedep +14 -0
  14. data/cucumber.yml +10 -0
  15. data/lib/cukedep.rb +9 -0
  16. data/lib/cukedep/application.rb +112 -0
  17. data/lib/cukedep/cli/application.rb +0 -0
  18. data/lib/cukedep/cli/cmd-line.rb +115 -0
  19. data/lib/cukedep/config.rb +31 -0
  20. data/lib/cukedep/constants.rb +29 -0
  21. data/lib/cukedep/feature-model.rb +285 -0
  22. data/lib/cukedep/feature-rep.rb +49 -0
  23. data/lib/cukedep/gherkin-listener.rb +98 -0
  24. data/lib/macros4cuke.rb +8 -0
  25. data/sample/features/step_definitions/steps.rb +105 -0
  26. data/sample/features/support/env.rb +12 -0
  27. data/sample/model/model.rb +216 -0
  28. data/spec/cukedep/application_spec.rb +81 -0
  29. data/spec/cukedep/cli/cmd-line_spec.rb +91 -0
  30. data/spec/cukedep/feature-model_spec.rb +103 -0
  31. data/spec/cukedep/feature-rep_spec.rb +53 -0
  32. data/spec/cukedep/file-parsing.rb +40 -0
  33. data/spec/cukedep/gherkin-listener_spec.rb +59 -0
  34. data/spec/cukedep/sample_features/a_few_tests.feature +24 -0
  35. data/spec/cukedep/sample_features/more_tests.feature +24 -0
  36. data/spec/cukedep/sample_features/other_tests.feature +15 -0
  37. data/spec/cukedep/sample_features/some_tests.feature +13 -0
  38. data/spec/cukedep/sample_features/standalone.feature +19 -0
  39. data/spec/cukedep/sample_features/still_other_tests.feature +24 -0
  40. data/spec/cukedep/sample_features/yet_other_tests.feature +19 -0
  41. data/spec/spec_helper.rb +18 -0
  42. data/templates/rake.erb +163 -0
  43. metadata +165 -0
@@ -0,0 +1,31 @@
1
+ # File: config.rb
2
+
3
+ module Cukedep # Module used as a namespace
4
+
5
+ FileMetaData = Struct.new(:name)
6
+
7
+ Config = Struct.new(
8
+ :proj_dir, # The directory of the cucumber project
9
+ :feature2id, # Meta-data about the feature => feature id report
10
+ :id2feature, # Meta-data about the feature id => feature report
11
+ :graph_file, # Meta-data about the dependency graph file
12
+ :rake_file, # Name of the output rake file
13
+ :cucumber_args # Command-line syntax to use for the cucumber application
14
+ )
15
+
16
+ # Re-open the class for further customisation
17
+
18
+ # Configuration object for the Cukedep application.
19
+ class Config
20
+ # Factory method. Build a config object with default settings.
21
+ def self.default()
22
+ Config.new(nil, FileMetaData.new('feature2id.csv'),
23
+ FileMetaData.new('feature2id.csv'), FileMetaData.new('dependencies.dot'),
24
+ 'cukedep.rake', [])
25
+ end
26
+
27
+ end # class
28
+
29
+ end # module
30
+
31
+ # End of file
@@ -0,0 +1,29 @@
1
+ # File: constants.rb
2
+ # Purpose: definition of Cukedep constants.
3
+
4
+ module Cukedep # Module used as a namespace
5
+ # The version number of the gem.
6
+ Version = '0.0.1'
7
+
8
+ # Brief description of the gem.
9
+ Description = 'Manage dependencies between Cucumber feature files'
10
+
11
+ # Constant Cukedep::RootDir contains the absolute path of Rodent's
12
+ # root directory. Note: it also ends with a slash character.
13
+ unless defined?(RootDir)
14
+ # The initialisation of constant RootDir is guarded in order
15
+ # to avoid multiple initialisation (not allowed for constants)
16
+
17
+ # The root folder of Cukedep.
18
+ RootDir = begin
19
+ require 'pathname' # Load Pathname class from standard library
20
+ rootdir = Pathname(__FILE__).dirname.parent.parent.expand_path
21
+ rootdir.to_s + '/' # Append trailing slash character to it
22
+ end
23
+
24
+ # The file name for the user's settings
25
+ YMLFilename = '.cukedep.yml'
26
+ end
27
+ end # module
28
+
29
+ # End of file
@@ -0,0 +1,285 @@
1
+ # File: feature-model.rb
2
+
3
+ require 'tsort'
4
+ require 'csv'
5
+ require 'erb'
6
+ require 'pathname'
7
+
8
+ module Cukedep # This module is used as a namespace
9
+
10
+
11
+ # The internal representation of a set of feature files.
12
+ # Dependencies: use topological sort
13
+ # TSort module http://ruby-doc.org/stdlib-1.9.3/libdoc/tsort/rdoc/TSort.html
14
+ # See also: Is this topological sort in Ruby flawed?
15
+ class FeatureModel
16
+
17
+ FeatureDependencies = Struct.new(:dependee, :dependents)
18
+
19
+
20
+ class DepGraph
21
+ include TSort # Mix-in module for topological sorting
22
+
23
+ attr_reader(:dependencies)
24
+
25
+ # Inverse lookup: from the feature file => FeatureDependencies
26
+ attr_reader(:lookup)
27
+
28
+ def initialize(theDependencies)
29
+ @dependencies = theDependencies
30
+ @lookup = dependencies.each_with_object({}) do |f_dependencies, sub_result|
31
+ sub_result[f_dependencies.dependee] = f_dependencies
32
+ end
33
+ end
34
+
35
+ # Method required by TSort module.
36
+ # It is used to iterate over all the nodes of the dependency graph
37
+ def tsort_each_node(&aBlock)
38
+ return dependencies.each(&aBlock)
39
+ end
40
+
41
+ # Method required by TSort module.
42
+ # It is used to iterate over all the children nodes of the given node.
43
+ def tsort_each_child(aDependency, &aBlock)
44
+ dependents = aDependency.dependents
45
+ children = dependents.map { |feature| lookup[feature] }
46
+ children.each(&aBlock)
47
+ end
48
+
49
+ end # class
50
+
51
+
52
+ attr_reader(:feature_files)
53
+
54
+ # An Array of FeatureDependencies
55
+ attr_reader(:dependencies)
56
+
57
+ def initialize(theFeatureFiles)
58
+ @feature_files = validated_model(theFeatureFiles)
59
+ end
60
+
61
+ # Retrieve the feature file matching the given feature identifiers
62
+ # theIds one or more Strings, each being one feature identifier
63
+ def select_by_ids(*theIds)
64
+ features_by_ids = id2features()
65
+ selection = theIds.each_with_object([]) do |an_id, sub_result|
66
+ found_feature = features_by_ids[an_id]
67
+ if found_feature.nil?
68
+ fail(StandardError, "No feature file with identifier '#{an_id}'.")
69
+ end
70
+ sub_result << found_feature
71
+ end
72
+
73
+ return selection
74
+ end
75
+
76
+ # The list of feature files without identifiers
77
+ def anonymous_features()
78
+ feature_files.select { |ff| ff.feature.anonymous? }
79
+ end
80
+
81
+ # Build an array of FileDependencies objects.
82
+ def dependency_links()
83
+ if @dependencies.nil?
84
+ # Build the mapping: feature identifier => feature
85
+ features_by_id = id2features()
86
+
87
+ # Resolve the dependency tags
88
+ resolve_dependencies(features_by_id)
89
+ end
90
+
91
+ return @dependencies
92
+ end
93
+
94
+
95
+ # Sort the feature files by dependency order.
96
+ def sort_features_by_dep()
97
+ dep_links = dependency_links()
98
+ graph = DepGraph.new(dep_links)
99
+ sorted_deps = graph.tsort()
100
+
101
+ all_sorted = sorted_deps.map(&:dependee)
102
+ @sorted_features = all_sorted.reject { |f| f.feature.anonymous? }
103
+ end
104
+
105
+ # Generate CSV files detailing the feature to identifier mapping
106
+ # and vise versa
107
+ def mapping_reports(fileFeature2id, fileId2Feature, isVerbose = false)
108
+ puts " #{fileFeature2id}" if isVerbose
109
+ # Generate the feature file name => feature identifier report
110
+ CSV.open(fileFeature2id, 'wb') do |f|
111
+ f << ['Feature file', 'Identifier']
112
+ feature_files.each do |ff|
113
+ identifier = ff.feature.identifier
114
+ filename = File.basename(ff.filepath)
115
+ f << [filename, identifier.nil? ? 'nil' : identifier]
116
+ end
117
+ end
118
+
119
+ # Generate the feature file name => feature identifier report
120
+ puts " #{fileId2Feature}" if isVerbose
121
+ CSV.open(fileId2Feature, 'wb') do |f|
122
+ f << ['identifier', 'feature file']
123
+ feature_files.each do |ff|
124
+ identifier = ff.feature.identifier
125
+ filename = File.basename(ff.filepath)
126
+ f << [identifier, filename] unless identifier.nil?
127
+ end
128
+ end
129
+ end
130
+
131
+ # Create a graphical representation of the dependencies.
132
+ # The result is a DOT file that can be rendered via the DOT
133
+ # application from the GraphViz distribution.
134
+ def draw_dependency_graph(theDOTfile, isVerbose = false)
135
+ puts " #{theDOTfile}" if isVerbose
136
+ dot_file = File.open(theDOTfile, 'w')
137
+ emit_heading(dot_file)
138
+ emit_body(dot_file)
139
+ emit_trailing(dot_file)
140
+ end
141
+
142
+
143
+ def emit_heading(anIO)
144
+ dir = File.dirname(File.absolute_path(feature_files[0].filepath))
145
+ heading =<<-EOS
146
+ // Graph of dependencies of feature files in directory:
147
+ // '#{dir}'
148
+ // This file uses the DOT syntax, a free utility from the Graphviz toolset.
149
+ // Graphviz is available at: www.graphviz.org
150
+ // File generated on #{Time.now.asctime}.
151
+
152
+ digraph g {
153
+ size = "7, 11"; // Dimensions in inches...
154
+ center = true;
155
+ rankdir = BT; // Draw from bottom to top
156
+ label = "\\nDependency graph of '#{dir}'";
157
+
158
+ // Nodes represent feature files
159
+ EOS
160
+ anIO.write heading
161
+ end
162
+
163
+ # Output the nodes as graph vertices + their edges with parent node
164
+ def emit_body(anIO)
165
+ anIO.puts <<-EOS
166
+ subgraph island {
167
+ node [shape = box, style=filled, color=lightgray];
168
+ EOS
169
+ feature_files.each_with_index { |ff, i| draw_node(anIO, ff, i) if ff.feature.anonymous? }
170
+
171
+ anIO.puts <<-EOS
172
+ label = "Isolated features";
173
+ }
174
+
175
+ subgraph dependencies {
176
+ node [shape = box, fillcolor = none];
177
+ EOS
178
+ feature_files.each_with_index { |ff, i| draw_node(anIO, ff, i) unless ff.feature.anonymous? }
179
+ anIO.puts <<-EOS
180
+ label = "Dependencies";
181
+ }
182
+
183
+ // The edges represent dependencies
184
+ EOS
185
+ dependencies.each {|a_dep| draw_edge(anIO, a_dep) }
186
+ end
187
+
188
+ # Output the closing part of the graph drawing
189
+ def emit_trailing(anIO)
190
+ anIO.puts "} // End of graph"
191
+ end
192
+
193
+ # Draw a refinement node in DOT format
194
+ def draw_node(anIO, aFeatureFile, anIndex)
195
+ basename = File.basename(aFeatureFile.filepath, '.feature')
196
+ identifier_suffix = aFeatureFile.feature.anonymous? ? '' : " -- #{aFeatureFile.feature.identifier}"
197
+ anIO.puts %Q| node_#{anIndex} [label = "#{basename}#{identifier_suffix}"];|
198
+ end
199
+
200
+ # Draw an edge between feature files having dependencies.
201
+ def draw_edge(anIO, aDependency)
202
+ source_id = feature_files.find_index(aDependency.dependee)
203
+ target_ids = aDependency.dependents.map do |a_target|
204
+ feature_files.find_index(a_target)
205
+ end
206
+
207
+ target_ids.each do |t_id|
208
+ anIO.puts "\tnode_#{source_id} -> node_#{t_id};"
209
+ end
210
+ end
211
+
212
+
213
+ def generate_rake_tasks(rakefile, theProjDir)
214
+ puts " #{rakefile}"
215
+ template_filepath = Pathname.new(File.dirname(__FILE__)).parent.parent + './templates/rake.erb'
216
+ template_source = File.read(template_filepath)
217
+
218
+ # Create one template engine instance
219
+ engine = ERB.new(template_source)
220
+
221
+ source_dir = File.absolute_path(Dir.getwd())
222
+ proj_dir = File.absolute_path(theProjDir)
223
+ anonymous = anonymous_features.map {|ff| ff.basename}
224
+ feature_ids = feature_files.map { |ff| ff.feature.identifier }
225
+ feature_ids.compact!
226
+ deps = dependencies.reject {|dep| dep.dependee.feature.anonymous?}
227
+
228
+ # Generate the text representation with given context
229
+ file_source = engine.result(binding)
230
+ File.open(rakefile, 'w') { |f| f.write(file_source) }
231
+ end
232
+
233
+
234
+ protected
235
+ def validated_model(theFeatureFiles)
236
+ return theFeatureFiles
237
+ end
238
+
239
+ # Build the mapping: feature identifier => feature
240
+ def id2features()
241
+ mapping = feature_files.each_with_object({}) do |file, mp|
242
+ feature_id = file.feature.identifier
243
+ mp[feature_id] = file unless feature_id.nil?
244
+ end
245
+
246
+ return mapping
247
+ end
248
+
249
+ # Given a feature identifier => feature mapping,
250
+ # resolve the dependency tags; that is,
251
+ # Establish links between a feature file object and its
252
+ # dependent feature file objects.
253
+ def resolve_dependencies(aMapping)
254
+ @dependencies = []
255
+
256
+ feature_files.each do |feature_file|
257
+ feature = feature_file.feature
258
+ its_id = feature.identifier
259
+ dep_tags = feature.dependency_tags
260
+ # Complain when self dependency detected
261
+ if dep_tags.include?(its_id)
262
+ msg = "Feature #{} with identifier #{its_id} depends on itself!"
263
+ fail(StandardError, msg)
264
+ end
265
+
266
+ # Complain when dependency tag refers to an unknown feature
267
+ dependents = dep_tags.map do |a_tag|
268
+ unless aMapping.include?(a_tag)
269
+ msg = "Feature with identifier '#{its_id}' depends on unknown feature '#{a_tag}'"
270
+ fail(StandardError, msg)
271
+ end
272
+ aMapping[a_tag]
273
+ end
274
+
275
+ @dependencies << FeatureDependencies.new(feature_file, dependents)
276
+ end
277
+
278
+ return @dependencies
279
+ end
280
+
281
+ end # class
282
+
283
+ end # module
284
+
285
+ # end of file
@@ -0,0 +1,49 @@
1
+ # File: feature-rep.rb
2
+
3
+ module Cukedep # This module is used as a namespace
4
+
5
+
6
+ # A FeatureRep is the internal representation of a Gherkin feature.
7
+ class FeatureRep
8
+ # Constant that specifies how feature identifier tags should begin
9
+ FeatureIdPrefix = /^feature:/
10
+
11
+ # Constant that specifies how dependency tags should begin
12
+ DependencyPrefix = /^depends_on:/
13
+
14
+ # The sorted list of all tags of the feature.
15
+ # The @ prefix is stripped from each tag text.
16
+ attr_reader(:tags)
17
+
18
+ # The identifier of the feature.
19
+ # It comes from a tag with the following syntax '@feature_' + identifier.
20
+ # Note that the @feature_ prefix is removed.
21
+ attr_reader(:identifier)
22
+
23
+
24
+ # theTags the tags objects from the Gherkin parser
25
+ def initialize(theTags)
26
+ # Strip first character of tag literal.
27
+ @tags = theTags.map { |t| t[1..-1] }
28
+
29
+ @identifier = tags.find { |tg| tg =~ FeatureIdPrefix }
30
+ @identifier = @identifier.sub(FeatureIdPrefix, '') unless identifier.nil?
31
+ end
32
+
33
+ public
34
+ # The list of all feature identifiers retrieved from the dependency tags
35
+ def dependency_tags()
36
+ dep_tags = tags.select { |t| t =~ DependencyPrefix }
37
+ return dep_tags.map { |t| t.sub(DependencyPrefix, '') }
38
+ end
39
+
40
+ # Return true iff the identifier of the feature is nil.
41
+ def anonymous?()
42
+ return identifier.nil?
43
+ end
44
+
45
+ end # class
46
+
47
+ end # module
48
+
49
+ # End of file
@@ -0,0 +1,98 @@
1
+ # File: gherkin-listener.rb
2
+
3
+ require_relative 'feature-rep'
4
+
5
+
6
+ module Cukedep # This module is used as a namespace
7
+
8
+
9
+ class FeatureFileRep
10
+ attr_reader(:filepath)
11
+ attr(:feature, true)
12
+
13
+ def initialize(aFilepath)
14
+ @filepath = aFilepath
15
+ end
16
+
17
+ def basename()
18
+ File.basename(filepath)
19
+ end
20
+ end
21
+
22
+ # A ParserListener listens to all the formatting events
23
+ # emitted by the Gherkin parser.
24
+ # It converts the received the feature file elements and builds
25
+ # a representation of the feature files that is appropriate
26
+ # for the Cukedep application.
27
+ class GherkinListener
28
+ # The list of feature files encountered so far
29
+ attr_reader(:feature_files)
30
+
31
+ # Internal representation of the feature being parsed
32
+ attr(:current_feature, true)
33
+
34
+ def initialize()
35
+ @feature_files = []
36
+ end
37
+
38
+ ######################################
39
+ # Event listening methods
40
+ ######################################
41
+
42
+ # Called when beginning the parsing of a feature file.
43
+ # featureURI: path + filename of feature file.
44
+ def uri(featureURI)
45
+ new_file = FeatureFileRep.new(featureURI)
46
+ feature_files << new_file
47
+ end
48
+
49
+ # aFeature is a Gherkin::Formatter::Model::Feature instance
50
+ def feature(aFeature)
51
+ tag_names = aFeature.tags.map(&:name)
52
+ @current_feature = feature_files.last.feature = FeatureRep.new(tag_names)
53
+ end
54
+
55
+ # aBackground is a Gherkin::Formatter::Model::Background instance
56
+ def background(aBackground)
57
+ ; # Do nothing
58
+ end
59
+
60
+ # aScenario is a Gherkin::Formatter::Model::Scenario instance
61
+ def scenario(aScenario)
62
+ ; # Do nothing
63
+ end
64
+
65
+ # aScenarioOutline is a Gherkin::Formatter::Model::ScenarioOutline instance
66
+ def scenario_outline(aScenarioOutline)
67
+ ; # Do nothing
68
+ end
69
+
70
+ # theExamples is a Gherkin::Formatter::Model::Examples instance
71
+ def examples(theExamples)
72
+ ; # Do nothing
73
+ end
74
+
75
+ # aStep is a Gherkin::Formatter::Model::Step instance
76
+ def step(aStep)
77
+ ; # Do nothing
78
+ end
79
+
80
+ # End of feature file notification.
81
+ def eof()
82
+ ; # Do nothing
83
+ end
84
+
85
+
86
+ # Catch all method
87
+ def method_missing(message, *args)
88
+ puts "Method #{message} is not implemented (yet)."
89
+ end
90
+
91
+ end # class
92
+
93
+ end # module
94
+
95
+
96
+
97
+
98
+ # End of file