Module: Testprune::TestBody

Defined in:
lib/testprune/test_body.rb

Overview

Produces a structural signature for the test defined at file:line by parsing its body with Prism and emitting a node-type sequence that ignores literal values and identifiers but keeps called method names. Two tests that differ only in their data (e.g. ‘assert_equal 3, add(1,2)` vs `assert_equal 5, add(2,3)`) get the same signature.

Class Method Summary collapse

Class Method Details

.body_of(node) ⇒ Object



42
43
44
45
46
# File 'lib/testprune/test_body.rb', line 42

def body_of(node)
  return nil unless node

  node.is_a?(Prism::DefNode) ? node.body : node.block&.body
end

.locate(root, line) ⇒ Object

Innermost def or block whose definition starts on ‘line`.



28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/testprune/test_body.rb', line 28

def locate(root, line)
  stack = [root]
  until stack.empty?
    node = stack.pop
    if node.location.start_line == line &&
       (node.is_a?(Prism::DefNode) || (node.is_a?(Prism::CallNode) && node.block))
      return node
    end

    stack.concat(node.compact_child_nodes)
  end
  nil
end

.normalize(node) ⇒ Object



48
49
50
51
52
53
54
# File 'lib/testprune/test_body.rb', line 48

def normalize(node)
  tokens = []
  walk(node) do |n|
    tokens << (n.is_a?(Prism::CallNode) ? "call:#{n.name}" : n.type.to_s)
  end
  tokens.join(',')
end

.signature(file, line) ⇒ Object



16
17
18
19
20
21
22
23
24
25
# File 'lib/testprune/test_body.rb', line 16

def signature(file, line)
  return nil unless file && line && File.exist?(file)

  tree = (@file_cache[file] ||= Prism.parse(File.read(file)).value)
  node = locate(tree, line)
  body = body_of(node)
  return nil unless body

  normalize(body)
end

.walk(node, &block) ⇒ Object



56
57
58
59
# File 'lib/testprune/test_body.rb', line 56

def walk(node, &block)
  block.call(node)
  node.compact_child_nodes.each { |child| walk(child, &block) }
end