Module: Testprune::ReviewPlan

Defined in:
lib/testprune/review_plan.rb

Overview

Pure (IO-free) transformation of an Analysis::Result into an ordered, clustered review plan. Candidates are ordered by tier — identical first — and within each tier grouped by their keeper, so a reviewer can approve a whole cluster of redundant tests in one decision instead of N. File paths are relativized to the scan root here so every consumer renders short, consistent locations.

Defined Under Namespace

Classes: Cluster, Loc, Member, TierPlan

Constant Summary collapse

TIERS =

Review order. ‘actionable` tiers contain candidates that can actually be patched (HIGH-confidence, safety-verified); the rest are review-only.

[
  { tier: :identical,  title: 'Identical coverage',          actionable: true  },
  { tier: :subset,     title: 'Subset / subsumed coverage',  actionable: true  },
  { tier: :structural, title: 'Structurally duplicated body', actionable: false },
  { tier: :overlap,    title: 'High coverage overlap',        actionable: false }
].freeze

Class Method Summary collapse

Class Method Details

.build(result, actionable_only: false) ⇒ Object

Returns an array of TierPlan in review order. Empty tiers are omitted. When actionable_only: true, only safety-verified removable candidates are included (what the interactive reviewer turns into a patch).



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/testprune/review_plan.rb', line 35

def build(result, actionable_only: false)
  root      = result.run['root']
  keepers   = index_footprints(result)
  approved  = result.approved_removals.to_set

  TIERS.filter_map do |spec|
    members = result.candidates.select { |c| c.group == spec[:tier] }
    members = members.select { |c| approved.include?(c) } if actionable_only
    next if members.empty?

    clusters = members
               .group_by { |c| c.kept_by.first }
               .map { |keeper_id, group| cluster_for(keeper_id, group, keepers, approved, root) }
               .sort_by { |cl| [-cl.size, cl.keeper&.id || ''] }

    TierPlan.new(tier: spec[:tier], title: spec[:title],
                 actionable: spec[:actionable], clusters: clusters)
  end
end

.cluster_for(keeper_id, group, keepers, approved, root) ⇒ Object



55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/testprune/review_plan.rb', line 55

def cluster_for(keeper_id, group, keepers, approved, root)
  keeper_fp = keepers[keeper_id]
  keeper_loc = keeper_fp && loc_for(keeper_id, keeper_fp.file, keeper_fp.line,
                                    keeper_fp.units.size, root)
  members = group
            .sort_by { |c| [c.footprint.file.to_s, c.footprint.line.to_i, c.footprint.id] }
            .map do |c|
    fp = c.footprint
    Member.new(candidate: c, safe: approved.include?(c),
               loc: loc_for(fp.id, fp.file, fp.line, fp.units.size, root))
  end
  Cluster.new(keeper: keeper_loc, members: members)
end

.index_footprints(result) ⇒ Object



86
87
88
89
90
# File 'lib/testprune/review_plan.rb', line 86

def index_footprints(result)
  return {} unless result.respond_to?(:detector_result)

  result.detector_result.footprints.each_with_object({}) { |fp, h| h[fp.id] = fp }
end

.loc_for(id, file, line, unit_count, root) ⇒ Object



69
70
71
72
# File 'lib/testprune/review_plan.rb', line 69

def loc_for(id, file, line, unit_count, root)
  Loc.new(id: id, method: short_method(id), file: relpath(file, root),
          line: line, unit_count: unit_count)
end

.relpath(file, root) ⇒ Object



80
81
82
83
84
# File 'lib/testprune/review_plan.rb', line 80

def relpath(file, root)
  return file unless file && root && file.start_with?("#{root}/")

  file[(root.length + 1)..]
end

.short_method(id) ⇒ Object

‘ClassName#method` -> `#method`; leaves bare ids untouched.



75
76
77
78
# File 'lib/testprune/review_plan.rb', line 75

def short_method(id)
  idx = id.index('#')
  idx ? id[idx..] : id
end