Module: AppMap::RSpec

Defined in:
lib/appmap/rspec.rb,
lib/appmap/rspec/parser.rb,
lib/appmap/rspec/parse_node.rb

Overview

Integration of AppMap with RSpec. When enabled with APPMAP=true, the AppMap tracer will be activated around each scenario which has the metadata key ‘:appmap`.

Defined Under Namespace

Modules: FeatureAnnotations Classes: BlockParseNode, ParseNode, ParseNodeStruct, Parser, Recorder, ScopeExample, ScopeExampleGroup

Constant Summary collapse

APPMAP_OUTPUT_DIR =
'tmp/appmap/rspec'
LOG =
false

Class Method Summary collapse

Class Method Details

.enabled?Boolean

Returns:

  • (Boolean)


330
331
332
# File 'lib/appmap/rspec.rb', line 330

def enabled?
  ENV['APPMAP'] == 'true'
end

.generate_appmaps_from_specsObject



207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# File 'lib/appmap/rspec.rb', line 207

def generate_appmaps_from_specs
  recorder = Recorder.new
  recorder.setup

  require 'set'
  # file:lineno at which an Example block begins
  trace_block_start = Set.new
  # file:lineno at which an Example block ends
  trace_block_end = Set.new

  # value: a BlockParseNode from an RSpec file
  # key: file:lineno at which the block begins
  rspec_blocks = {}

  # value: an Example instance
  # key: file:lineno at which the Example block ends
  examples = {}

  current_tracer = nil

  TracePoint.trace(:call, :b_call, :b_return) do |tp|
    # When a new ExampleGroup is encountered, parse the source file containing it and look
    # for blocks that might be Examples. Index each BlockParseNode by the start file:lineno.
    if is_example_group_subclass_call?(tp)
      example_block = tp.binding.eval('example_group_block')
      source_path, start_line = example_block.source_location
      require 'appmap/rspec/parser'
      nodes, = AppMap::RSpec::Parser.new(file_path: source_path).parse
      nodes.each do |node|
        start_loc = [ node.file_path, node.first_line ].join(':')
        rspec_blocks[start_loc] = node
      end
    end

    # When a new Example is constructed with a block, look for the BlockParseNode that starts at the block's
    # file:lineno. If it exists, store the Example object, indexed by the file:lineno at which it ends.
    if is_example_initialize_call?(tp)
      example_block = tp.binding.eval('example_block')
      if example_block
        source_path, start_line = example_block.source_location
        start_loc = [ source_path, start_line ].join(':')
        if (rspec_block = rspec_blocks[start_loc])
          end_loc = [ source_path, rspec_block.last_line ].join(':')
          trace_block_start << start_loc.tap { |loc| puts "Start: #{loc}" if LOG }
          trace_block_end << end_loc.tap { |loc| puts "End: #{loc}" if LOG }
          examples[end_loc] = tp.binding.eval('self')
        end
      end
    end

    if i[b_call b_return].member?(tp.event)
      loc = [ tp.path, tp.lineno ].join(':')
      puts loc if LOG && (trace_block_start.member?(loc) || trace_block_end.member?(loc))

      # When a new block is started, check if an Example block is known to begin at that
      # file:lineno. If it is, enable the AppMap tracer.
      if  tp.event == :b_call && trace_block_start.member?(loc)
        puts "Starting trace on #{loc}" if LOG
        current_tracer = AppMap::Trace.tracers.trace(recorder.functions)
      end

      # When the tracer is enabled and a block is completed, check to see if there is an
      # Example stored at the file:lineno. If so, finish tracing and emit the 
      # AppMap file.
      if current_tracer && tp.event == :b_return && trace_block_end.member?(loc)
        puts "Ending trace on #{loc}" if LOG
        events = []
        AppMap::Trace.tracers.delete current_tracer

        while current_tracer.event?
          events << current_tracer.next_event.to_h
        end

        example = examples[loc]
        description = []
        leaf = scope = ScopeExample.new(example)
        feature_group = feature = nil

        labels = []
        while scope
          labels += scope.labels
          description << scope.description
          feature ||= scope.feature
          feature_group ||= scope.feature_group
          scope = scope.parent
        end

        labels = labels.map(&:to_s).map(&:strip).reject(&:blank?).map(&:downcase).uniq
        description.reject!(&:nil?).reject(&:blank?)
        default_description = description.last
        description.reverse!

        normalize = lambda do |desc|
          desc.gsub('it should behave like', '')
              .gsub(/Controller$/, '')
              .gsub(/\s+/, ' ')
              .strip
        end

        full_description = normalize.call(description.join(' '))

        compute_feature_name = lambda do
          return 'unknown' if description.empty?

          feature_description = description.dup
          num_tokens = [2, feature_description.length - 1].min
          feature_description[0...num_tokens].map(&:strip).join(' ')
        end

        feature_group ||= normalize.call(default_description).underscore.gsub('/', '_').humanize
        feature_name = feature || compute_feature_name.call if feature_group
        feature_name = normalize.call(feature_name) if feature_name

        recorder.save full_description,
                      events: events,
                      feature_name: feature_name,
                      feature_group_name: feature_group,
                      labels: labels.blank? ? nil : labels
      end
    end
  end
end

.generate_inventoryObject



201
202
203
204
205
# File 'lib/appmap/rspec.rb', line 201

def generate_inventory
  Recorder.new.tap do |recorder|
    recorder.setup
  end.save 'Inventory', labels: %w[inventory]
end

.is_example_group_subclass_call?(tp) ⇒ Boolean

Returns:

  • (Boolean)


181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/appmap/rspec.rb', line 181

def is_example_group_subclass_call?(tp)
  # Order is important here. Checking for method_id == :subclass
  # first will avoid calling defined_class.to_s in many cases,
  # some of which will fail.
  #
  # For example, ActiveRecord in Rails 4 defines #inspect (and
  # therefore #to_s) in such a way that it will fail if called
  # here.
  tp.event == :call &&
    tp.method_id == :subclass &&
    tp.defined_class.singleton_class? &&
    tp.defined_class.to_s == '#<Class:RSpec::Core::ExampleGroup>'
end

.is_example_initialize_call?(tp) ⇒ Boolean

Returns:

  • (Boolean)


195
196
197
198
199
# File 'lib/appmap/rspec.rb', line 195

def is_example_initialize_call?(tp)
  tp.event == :call &&
    tp.method_id == :initialize &&
    tp.defined_class.to_s == 'RSpec::Core::Example'
end

.runObject



334
335
336
337
# File 'lib/appmap/rspec.rb', line 334

def run
  generate_inventory
  generate_appmaps_from_specs
end