Module: RspecSprint::Fix

Defined in:
lib/rspec_sprint/fix.rb

Overview

‘fix –dry-run let-it-be` の orchestrator。suite は CLI 側で 1 回走らせ済みで、その Result(rspec JSON + FactoryProf JSON のパス)を受け取る。FactoryProf はhard precondition(設計): 無ければ診断不能としてヒントを返す。

Class Method Summary collapse

Class Method Details

.apply(result, dir: ".", limit: 10, all: false) ⇒ Object

let/let! 候補を verify-and-revert で適用する。limit: 処理するファイル上限(ROI 降順)。all: true なら全ファイルを処理。



49
50
51
52
53
54
55
56
57
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
85
86
# File 'lib/rspec_sprint/fix.rb', line 49

def apply(result, dir: ".", limit: 10, all: false)
  return Doctor.factory_prof_missing_hint unless result.factory_prof?
  return git_not_repo_hint unless git_repo?(dir)
  return recipe_not_loaded_hint(dir) unless let_it_be_recipe_loaded?(dir)

  snapshot = Normalizer.new(
    rspec_json: result.rspec_json_path,
    factory_prof_json: result.factory_prof_path
  ).call

  factory_time = snapshot.factories.each_with_object({}) do |f, h|
    h[f.name.to_s] = f.top_level_time.to_f
  end

  files_with_candidates = {}
  spec_files(dir).each do |abs_path|
    rel_path = relative(abs_path, dir)
    source = File.read(abs_path)
    cands = Fixers::LetItBe::Detector.scan_all(source, file_path: rel_path)
    files_with_candidates[abs_path] = cands unless cands.empty?
  end

  return "変換可能な候補が見つかりませんでした(let/let! の単一 create 本体が対象)。" \
         " まず `rspec-sprint fix let-it-be --dry-run` で確認してください。" if files_with_candidates.empty?

  sorted = files_with_candidates.sort_by do |_path, cands|
    -cands.map { |c| factory_time[c.factory_name.to_s].to_f }.sum
  end

  target_files = all ? sorted : sorted.first(limit)

  verifier = Fixers::LetItBe::Verifier.new(runner: rspec_runner(dir), dir: dir)
  file_results = target_files.map do |abs_path, cands|
    verifier.verify_file(abs_path, cands)
  end

  Fixers::LetItBe::ApplyFormatter.format(file_results)
end

.dry_run(result, dir: ".") ⇒ Object



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/rspec_sprint/fix.rb', line 23

def dry_run(result, dir: ".")
  return Doctor.factory_prof_missing_hint unless result.factory_prof?

  snapshot = Normalizer.new(
    rspec_json: result.rspec_json_path,
    factory_prof_json: result.factory_prof_path
  ).call

  candidates = []
  let_bang_count = 0
  spec_files(dir).each do |path|
    source = File.read(path)
    candidates.concat(Fixers::LetItBe::Detector.scan(source, file_path: relative(path, dir)))
    let_bang_count += Fixers::LetItBe::Detector.count_let_bang_create(source)
  end

  report = Fixers::LetItBe::Report.build(
    candidates: candidates,
    factories: snapshot.factories,
    let_bang_count: let_bang_count
  )
  Fixers::LetItBe::Formatter.format(report)
end

.git_not_repo_hintObject



102
103
104
# File 'lib/rspec_sprint/fix.rb', line 102

def git_not_repo_hint
  "エラー: git リポジトリが見つかりません。`fix` 適用は git が必要です(復元保証のため)。"
end

.git_repo?(dir) ⇒ Boolean

Returns:

  • (Boolean)


97
98
99
100
# File 'lib/rspec_sprint/fix.rb', line 97

def git_repo?(dir)
  _, status = Open3.capture2e("git", "-C", dir, "rev-parse", "--git-dir")
  status.success?
end

.let_it_be_recipe_loaded?(dir) ⇒ Boolean

Returns:

  • (Boolean)


106
107
108
109
110
111
112
113
114
115
# File 'lib/rspec_sprint/fix.rb', line 106

def let_it_be_recipe_loaded?(dir)
  helper_files = Dir.glob(File.join(dir, "spec/{spec_helper,rails_helper}.rb"))
  helper_files.any? do |f|
    content = File.read(f)
    content.include?("test_prof/recipes/rspec/let_it_be") ||
      content.include?("RSpecLetItBe")
  end
rescue StandardError
  false
end

.parse_verify_duration(out_path) ⇒ Object



145
146
147
148
149
150
151
152
# File 'lib/rspec_sprint/fix.rb', line 145

def parse_verify_duration(out_path)
  return 0.0 unless File.exist?(out_path) && !File.zero?(out_path)

  data = JSON.parse(File.read(out_path))
  data.dig("summary", "duration")&.to_f || 0.0
rescue JSON::ParserError
  0.0
end

.recipe_not_loaded_hint(dir) ⇒ Object



117
118
119
120
121
122
123
124
125
126
127
# File 'lib/rspec_sprint/fix.rb', line 117

def recipe_not_loaded_hint(dir)
  <<~MSG
    エラー: `let_it_be` recipe がロードされていません。

    spec/spec_helper.rb または spec/rails_helper.rb に以下を追加してください:

      require "test_prof/recipes/rspec/let_it_be"

    追加後、もう一度 `rspec-sprint fix let-it-be` を実行してください。
  MSG
end

.relative(path, dir) ⇒ Object



92
93
94
95
# File 'lib/rspec_sprint/fix.rb', line 92

def relative(path, dir)
  base = File.expand_path(dir)
  File.expand_path(path).delete_prefix("#{base}/")
end

.rspec_runner(dir) ⇒ Object



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/rspec_sprint/fix.rb', line 129

def rspec_runner(dir)
  out_dir = File.join(dir, "tmp", "rspec_sprint", "verify")
  FileUtils.mkdir_p(out_dir)
  ->(path) {
    out_path = File.join(out_dir, "#{Digest::MD5.hexdigest(path)[0..7]}_verify.json")
    _, status = Open3.capture2e(
      "bundle", "exec", "rspec", path,
      "--format", "json", "--out", out_path,
      chdir: dir
    )
    green = status.exitstatus&.zero? || false
    duration = parse_verify_duration(out_path)
    {green: green, duration: duration}
  }
end

.spec_files(dir) ⇒ Object



88
89
90
# File 'lib/rspec_sprint/fix.rb', line 88

def spec_files(dir)
  Dir.glob(File.join(dir, "spec/**/*_spec.rb")).sort
end