Module: RspecSprint::Rules
- Defined in:
- lib/rspec_sprint/rules.rb
Overview
The judgment layer — the gem’s moat. Each rule is a plain method (design D1): it reads a normalized Snapshot and returns a Finding when its signal exceeds a threshold, or nil otherwise. Thresholds and prescription wording are the opinionated, tunable heart of the tool (premise #3).
Constant Summary collapse
- FACTORY_DOMINANCE_THRESHOLD =
— Tunable thresholds (the opinionated knobs) —————————
0.30- CASCADE_RATIO =
fires when factory_time_ratio exceeds this
0.5- PATH_GROUP_SKEW_THRESHOLD =
Defaults (tune freely — these are the gem’s IP):
0.50- SLOW_EXAMPLE_TOP_N =
one path group eating MORE than this share of suite time
5- SLOW_EXAMPLE_CONCENTRATION_THRESHOLD =
“a few” = this many slowest examples
0.30- SLOW_EXAMPLE_MIN_EXAMPLES =
top-N eating more than this share of suite time
10
Class Method Summary collapse
-
.factory_dominance(snapshot) ⇒ Object
Rule ①: factory build time dominates the suite.
- .headline_for(pct, top, cascade) ⇒ Object
-
.path_group_skew(snapshot) ⇒ Object
Rule ②: one “path group” (spec/system, spec/requests, …) dominates the suite’s wall time.
-
.slow_examples_concentration(snapshot) ⇒ Object
Rule ③: a few slow examples concentrate most of the suite time.
Class Method Details
.factory_dominance(snapshot) ⇒ Object
Rule ①: factory build time dominates the suite.
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
# File 'lib/rspec_sprint/rules.rb', line 24 def factory_dominance(snapshot) ratio = snapshot.factory_time_ratio return nil if ratio < FACTORY_DOMINANCE_THRESHOLD pct = (ratio * 100).round top = (snapshot.factories || []).max_by(&:total_count) cascade = top && top.top_level_ratio < CASCADE_RATIO Finding.new( rule_id: :factory_dominance, score: ratio, headline: headline_for(pct, top, cascade), prescriptions: [ "不要な association をデフォルト factory から外し trait へ退避", "永続化が不要な spec は build_stubbed / build に置換", "全 spec で使う共有データは let_it_be / FactoryDefault 化" ], expected_saving: "factory time の一部(目安 suite の #{(pct * 0.3).round}–#{pct}%、local実測)", doc_url: "https://test-prof.evilmartians.io/guide/profilers/factory_prof" ) end |
.headline_for(pct, top, cascade) ⇒ Object
126 127 128 129 130 131 132 133 134 135 |
# File 'lib/rspec_sprint/rules.rb', line 126 def headline_for(pct, top, cascade) base = "factory が suite の #{pct}% (local実測)" return base unless top if cascade "#{base}。最上位は :#{top.name}(#{top.total_count}回, うち直接生成 #{top.top_level_count}回 = カスケード)" else "#{base}。最上位は :#{top.name}(#{top.total_count}回)" end end |
.path_group_skew(snapshot) ⇒ Object
Rule ②: one “path group” (spec/system, spec/requests, …) dominates the suite’s wall time. NOTE: call it a path group, not RSpec ‘type:` — they can diverge (design D10⑦). Do NOT assert “split your system specs”; offer it as a candidate.
Data available on ‘snapshot`:
snapshot.path_group_durations # => { "system" => 1.5, "models" => 0.3, ... } seconds
snapshot.suite_duration # => total example-run wall time (the denominator)
Return a Finding when the biggest group’s share exceeds PATH_GROUP_SKEW_THRESHOLD, else nil. Score = that share (0.0..1.0).
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/rspec_sprint/rules.rb', line 58 def path_group_skew(snapshot) total = snapshot.suite_duration.to_f return nil if total.zero? # Drop "(root)" — specs directly under spec/ aren't a coherent group you'd # split into a CI job (dogfooding finding; cf. Codex #8 path-group risk). candidates = (snapshot.path_group_durations || {}).reject { |g, _| g == "(root)" } group, time = candidates.max_by { |_, t| t } return nil if group.nil? share = time / total return nil unless share > PATH_GROUP_SKEW_THRESHOLD pct = (share * 100).round Finding.new( rule_id: :path_group_skew, score: share, headline: "path group spec/#{group} が suite 時間の #{pct}% (local実測)", prescriptions: [ "spec/#{group} を別CIジョブに分離し fast suite から切り出す(候補)", "ロジックを下位レイヤ(model/request/service)へ寄せ #{group} spec 本数を減らせるか検討", "runtime ログで #{group} を shard 分割" ], expected_saving: "spec/#{group} を分離すれば fast suite から最大 #{pct}% 相当を外せる(local実測)", doc_url: "https://test-prof.evilmartians.io/guide/profilers/tag_prof" ) end |
.slow_examples_concentration(snapshot) ⇒ Object
Rule ③: a few slow examples concentrate most of the suite time.
Data available on ‘snapshot`:
snapshot.slow_examples # => [Example(full_description, file_path, run_time, status)] slow-first
snapshot.suite_duration # => denominator
Return a Finding when the top-10 examples’ summed run_time / suite_duration exceeds SLOW_EXAMPLE_CONCENTRATION_THRESHOLD, naming the worst few. Score = that concentration.
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
# File 'lib/rspec_sprint/rules.rb', line 96 def slow_examples_concentration(snapshot) total = snapshot.suite_duration.to_f return nil if total.zero? examples = snapshot.slow_examples || [] count = snapshot.example_count || examples.size return nil if count < SLOW_EXAMPLE_MIN_EXAMPLES # tiny suite: top-N isn't a minority (D8) top = examples.first(SLOW_EXAMPLE_TOP_N) return nil if top.empty? concentration = top.sum(&:run_time) / total return nil unless concentration > SLOW_EXAMPLE_CONCENTRATION_THRESHOLD pct = (concentration * 100).round worst = top.first Finding.new( rule_id: :slow_examples_concentration, score: concentration, headline: "上位#{top.size}例が suite 時間の #{pct}% (local実測)。最遅: \"#{worst.full_description}\" (#{(worst.run_time * 1000).round}ms)", prescriptions: [ "最遅例に EVENT_PROF='sql.active_record' / TEST_STACK_PROF を当てて原因特定", "重い setup (let! / before(:each)) を let_it_be / build_stubbed へ", "外部I/O・sleep・実ブラウザ依存を切り出す" ], expected_saving: "上位例を半減できれば最大 #{(pct * 0.5).round}% 相当(local実測)", doc_url: "https://test-prof.evilmartians.io/guide/profilers/event_prof" ) end |