Class: Cukedep::FeatureModel

Inherits:
Object
  • Object
show all
Defined in:
lib/cukedep/feature-model.rb

Overview

The internal representation of a set of feature files. Dependencies: use topological sort TSort module http://ruby-doc.org/stdlib-2.6/libdoc/tsort/rdoc/index.html See also: Is this topological sort in Ruby flawed?

Defined Under Namespace

Classes: DepGraph, FeatureDependencies

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(theFeatureFiles) ⇒ FeatureModel

Returns a new instance of FeatureModel.



55
56
57
# File 'lib/cukedep/feature-model.rb', line 55

def initialize(theFeatureFiles)
  @feature_files = validated_model(theFeatureFiles)
end

Instance Attribute Details

#dependenciesObject (readonly)

An Array of FeatureDependencies



53
54
55
# File 'lib/cukedep/feature-model.rb', line 53

def dependencies
  @dependencies
end

#feature_filesObject (readonly)

Returns the value of attribute feature_files.



50
51
52
# File 'lib/cukedep/feature-model.rb', line 50

def feature_files
  @feature_files
end

Instance Method Details

#anonymous_featuresObject

The list of feature files without identifiers



76
77
78
# File 'lib/cukedep/feature-model.rb', line 76

def anonymous_features
  feature_files.select { |ff| ff.feature.anonymous? }
end

Build an array of FileDependencies objects.



81
82
83
84
85
86
87
88
89
90
91
# File 'lib/cukedep/feature-model.rb', line 81

def dependency_links
  if @dependencies.nil?
    # Build the mapping: feature identifier => feature
    features_by_id = id2features

    # Resolve the dependency tags
    resolve_dependencies(features_by_id)
  end

  return @dependencies
end

#draw_dependency_graph(theDOTfile, isVerbose = false) ⇒ Object

Create a graphical representation of the dependencies. The result is a DOT file that can be rendered via the DOT application from the GraphViz distribution.



132
133
134
135
136
137
138
# File 'lib/cukedep/feature-model.rb', line 132

def draw_dependency_graph(theDOTfile, isVerbose = false)
  puts "  #{theDOTfile}" if isVerbose
  dot_file = File.open(theDOTfile, 'w')
  emit_heading(dot_file)
  emit_body(dot_file)
  emit_trailing(dot_file)
end

#draw_edge(anIO, aDependency) ⇒ Object

Draw an edge between feature files having dependencies.



207
208
209
210
211
212
213
214
215
216
# File 'lib/cukedep/feature-model.rb', line 207

def draw_edge(anIO, aDependency)
  source_id = feature_files.find_index(aDependency.dependee)
  target_ids = aDependency.dependents.map do |a_target|
    feature_files.find_index(a_target)
  end

  target_ids.each do |t_id|
    anIO.puts "\tnode_#{source_id} -> node_#{t_id};"
  end
end

#draw_node(anIO, aFeatureFile, anIndex) ⇒ Object

Draw a refinement node in DOT format



195
196
197
198
199
200
201
202
203
204
# File 'lib/cukedep/feature-model.rb', line 195

def draw_node(anIO, aFeatureFile, anIndex)
  basename = File.basename(aFeatureFile.filepath, '.feature')
  its_feature = aFeatureFile.feature
  if its_feature.anonymous?
    id_suffix = ''
  else
    id_suffix = " -- #{its_feature.identifier}"
  end
  anIO.puts %Q(    node_#{anIndex} [label = "#{basename}#{id_suffix}"];)
end

#emit_body(anIO) ⇒ Object

Output the nodes as graph vertices + their edges with parent node



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/cukedep/feature-model.rb', line 161

def emit_body(anIO)
  anIO.puts <<-DOT
subgraph island {
  node [shape = box, style=filled, color=lightgray];
DOT
  feature_files.each_with_index do |ff, i|
    draw_node(anIO, ff, i) if ff.feature.anonymous?
  end

  anIO.puts <<-DOT
  label = "Isolated features";
  }

subgraph dependencies {
  node [shape = box, fillcolor = none];
DOT
  feature_files.each_with_index do |ff, i|
    draw_node(anIO, ff, i) unless ff.feature.anonymous?
  end
  anIO.puts <<-DOT
  label = "Dependencies";
}

// The edges represent dependencies
DOT
  dependencies.each { |a_dep| draw_edge(anIO, a_dep) }
end

#emit_heading(anIO) ⇒ Object



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/cukedep/feature-model.rb', line 140

def emit_heading(anIO)
  dir = File.dirname(File.absolute_path(feature_files[0].filepath))
  heading = <<-DOT
// Graph of dependencies of feature files in directory:
// '#{dir}'
// This file uses the DOT syntax, a free utility from the Graphviz toolset.
// Graphviz is available at: www.graphviz.org
// File generated on #{Time.now.asctime}.

digraph g {
size = "7, 11"; // Dimensions in inches...
center = true;
rankdir = BT; // Draw from bottom to top
label = "\\nDependency graph of '#{dir}'";

// Nodes represent feature files
DOT
  anIO.write heading
end

#emit_trailing(anIO) ⇒ Object

Output the closing part of the graph drawing



190
191
192
# File 'lib/cukedep/feature-model.rb', line 190

def emit_trailing(anIO)
  anIO.puts '} // End of graph'
end

#generate_rake_tasks(rakefile, theProjDir) ⇒ Object



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/cukedep/feature-model.rb', line 218

def generate_rake_tasks(rakefile, theProjDir)
  puts "  #{rakefile}"
  grandparent_path = Pathname.new(File.dirname(__FILE__)).parent.parent
  template_source = File.read(grandparent_path + './templates/rake.erb')

  # Create one template engine instance
  engine = ERB.new(template_source)

  source_dir = File.absolute_path(Dir.getwd)
  proj_dir = File.absolute_path(theProjDir)
  anonymous = anonymous_features.map(&:basename)
  feature_ids = feature_files.map { |ff| ff.feature.identifier }
  feature_ids.compact!
  deps = dependencies.reject { |dep| dep.dependee.feature.anonymous? }

  # Generate the text representation with given context
  file_source = engine.result(binding)
  File.open(rakefile, 'w') { |f| f.write(file_source) }
end

#mapping_reports(fileFeature2id, fileId2Feature, isVerbose = false) ⇒ Object

Generate CSV files detailing the feature to identifier mapping and vise versa



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/cukedep/feature-model.rb', line 105

def mapping_reports(fileFeature2id, fileId2Feature, isVerbose = false)
  puts "  #{fileFeature2id}" if isVerbose
  # Generate the feature file name => feature identifier report
  CSV.open(fileFeature2id, 'wb') do |f|
    f << ['Feature file', 'Identifier']
    feature_files.each do |ff|
      identifier = ff.feature.identifier
      filename = File.basename(ff.filepath)
      f << [filename, identifier.nil? ? 'nil' : identifier]
    end
  end

  # Generate the feature file name => feature identifier report
  puts "  #{fileId2Feature}" if isVerbose
  CSV.open(fileId2Feature, 'wb') do |f|
    f << ['identifier', 'feature file']
    feature_files.each do |ff|
      identifier = ff.feature.identifier
      filename = File.basename(ff.filepath)
      f << [identifier, filename] unless identifier.nil?
    end
  end
end

#select_by_ids(*theIds) ⇒ Object

Retrieve the feature file matching the given feature identifiers theIds one or more Strings, each being one feature identifier



61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/cukedep/feature-model.rb', line 61

def select_by_ids(*theIds)
  features_by_ids = id2features
  selection = theIds.each_with_object([]) do |an_id, sub_result|
    found_feature = features_by_ids[an_id]
    if found_feature.nil?
      raise StandardError, "No feature file with identifier '#{an_id}'."
    end

    sub_result << found_feature
  end

  return selection
end

#sort_features_by_depObject

Sort the feature files by dependency order.



94
95
96
97
98
99
100
101
# File 'lib/cukedep/feature-model.rb', line 94

def sort_features_by_dep
  dep_links = dependency_links
  graph = DepGraph.new(dep_links)
  sorted_deps = graph.tsort

  all_sorted = sorted_deps.map(&:dependee)
  @sorted_features = all_sorted.reject { |f| f.feature.anonymous? }
end