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
-
.build(result, actionable_only: false) ⇒ Object
Returns an array of TierPlan in review order.
- .cluster_for(keeper_id, group, keepers, approved, root) ⇒ Object
- .index_footprints(result) ⇒ Object
- .loc_for(id, file, line, unit_count, root) ⇒ Object
- .relpath(file, root) ⇒ Object
-
.short_method(id) ⇒ Object
‘ClassName#method` -> `#method`; leaves bare ids untouched.
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 |