cukedep 0.0.1

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