rspec-sprint 0.1.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f16ad949645afe84a5a8ba614a5a6e2e40f609fa76f73c2e0af72557830dbd9d
4
- data.tar.gz: 8503cde0dabd4c8cccefee9d53ef55b996e2724ad21ebcb6f4f4d6b9b6eec747
3
+ metadata.gz: fff025e27cecb118a9aca49be7611c62cf40da2dddd17d827320883c6e232acd
4
+ data.tar.gz: 99e64c381ca443afea9d34bb909b9fddc9d857aadb32beec52d677040885f553
5
5
  SHA512:
6
- metadata.gz: 021d7debd206e0d543a97c71692eed732b0f3bc280e13e3262c19936ee790012d3b93588f3de91f1741f19fa26ac36da322e2fd1ad87ef81f215098231f00a66
7
- data.tar.gz: 0d93b9cb8eb129e2cc6451d8ad2607efcbd0a1a6108549d4c5f8a566c4fd69db39b481d1b40293462ed94bd0396b49da6a2cf573b9e38eb3c6357c22c73a9e43
6
+ metadata.gz: c73e4b56bc7e6a57ad70c8fec7719ed24502ffc121b99fee8d8058c13996296f68d69cf218c1d75e1da4b0078a251dda9e61769aad86354eef14fb6d8c8e0246
7
+ data.tar.gz: 7d44089f6ec20e24796892c8f395b4be545ee1ce87d4b327555b3620548afad35bb5a047a8ceec07ac391887b8ed8758d2f5b4ae1b4940389bd23590d03cb9f1
@@ -21,9 +21,35 @@ module RspecSprint
21
21
  Requires test-prof in your bundle and `require "test_prof"` in your spec
22
22
  helper for factory diagnosis (Rule①); without it, the other rules still run.
23
23
  DESC
24
+ option :no_snapshot, type: :boolean, default: false,
25
+ desc: "Skip saving a snapshot to .rspec-sprint/"
26
+ option :compare_last, type: :boolean, default: false,
27
+ desc: "Show delta vs most recent previous snapshot"
24
28
  def doctor(*rspec_args)
25
29
  result = Collector.new(rspec_args: rspec_args).run
26
- puts Doctor.diagnose(result)
30
+ puts Doctor.diagnose(result, no_snapshot: options[:no_snapshot],
31
+ compare_last: options[:compare_last])
32
+ end
33
+
34
+ desc "compare [-- RSPEC_ARGS]", "Run your suite and save results for long-term tracking"
35
+ long_desc <<~DESC
36
+ Runs your RSpec suite once and (with --save-baseline) saves the result as a
37
+ named baseline in .rspec-sprint/baseline.json for long-term comparison.
38
+
39
+ Unlike the rolling snapshots used by `doctor --compare-last`, the baseline
40
+ is a permanent reference point that is never auto-pruned.
41
+
42
+ bundle exec rspec-sprint compare --save-baseline
43
+ bundle exec rspec-sprint compare --save-baseline -- spec/models
44
+ DESC
45
+ option :save_baseline, type: :boolean, default: false,
46
+ desc: "Save results as .rspec-sprint/baseline.json"
47
+ option :no_snapshot, type: :boolean, default: false,
48
+ desc: "Skip saving a rolling snapshot to .rspec-sprint/"
49
+ def compare(*rspec_args)
50
+ result = Collector.new(rspec_args: rspec_args).run
51
+ puts Doctor.diagnose(result, no_snapshot: options[:no_snapshot],
52
+ save_baseline: options[:save_baseline])
27
53
  end
28
54
 
29
55
  default_command :doctor
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module RspecSprint
6
+ # Computes and formats the delta between a previous snapshot (raw JSON hash)
7
+ # and the current Normalizer::Snapshot.
8
+ module Comparator
9
+ NO_BASELINE_MSG = "前回のスナップショットがありません。まず `bundle exec rspec-sprint doctor` を実行してベースラインを作成してください。\n" \
10
+ "(No prior snapshot. Run doctor once to create a baseline.)"
11
+
12
+ module_function
13
+
14
+ # previous_data: Hash parsed from a saved snapshot JSON (may be nil)
15
+ # current: Normalizer::Snapshot
16
+ # Returns a formatted string to append to the doctor report.
17
+ def format_delta(previous_data, current)
18
+ return NO_BASELINE_MSG if previous_data.nil?
19
+
20
+ prev_duration = previous_data[:suite_duration].to_f
21
+ curr_duration = current.suite_duration.to_f
22
+ duration_delta = curr_duration - prev_duration
23
+ duration_pct = prev_duration > 0 ? (duration_delta / prev_duration * 100).round(1) : 0.0
24
+
25
+ prev_factory_pct = prev_duration > 0 ? (previous_data[:factory_time].to_f / prev_duration * 100).round(1) : 0.0
26
+ curr_factory_pct = (current.factory_time_ratio * 100).round(1)
27
+
28
+ prev_ts = format_timestamp(previous_data[:created_at])
29
+
30
+ sign = duration_delta >= 0 ? "+" : ""
31
+ "前回比 #{sign}#{duration_delta.round(1)}s (#{sign}#{duration_pct}%) " \
32
+ "factory time #{prev_factory_pct}% → #{curr_factory_pct}% [#{prev_ts}]"
33
+ end
34
+
35
+ def format_timestamp(iso8601)
36
+ return "unknown" if iso8601.nil?
37
+
38
+ Time.parse(iso8601).strftime("%Y-%m-%d %H:%M")
39
+ rescue ArgumentError
40
+ iso8601.to_s
41
+ end
42
+ end
43
+ end
@@ -3,6 +3,8 @@
3
3
  require_relative "normalizer"
4
4
  require_relative "ranker"
5
5
  require_relative "formatter"
6
+ require_relative "snapshot_store"
7
+ require_relative "comparator"
6
8
 
7
9
  module RspecSprint
8
10
  # Pure pipeline: profiler JSON files -> normalized snapshot -> ranked findings
@@ -11,9 +13,23 @@ module RspecSprint
11
13
  module Diagnosis
12
14
  module_function
13
15
 
14
- def from_files(rspec_json:, factory_prof_json: nil)
16
+ def from_files(rspec_json:, factory_prof_json: nil, save_snapshot: false, compare_last: false, save_baseline: false)
17
+ # Load previous BEFORE saving so "last" refers to the prior run, not this one.
18
+ previous = compare_last ? SnapshotStore.load_last : nil
19
+
15
20
  snapshot = Normalizer.new(rspec_json: rspec_json, factory_prof_json: factory_prof_json).call
16
- Formatter.format(Ranker.call(snapshot))
21
+ SnapshotStore.save(snapshot) if save_snapshot
22
+ SnapshotStore.save_baseline(snapshot) if save_baseline
23
+
24
+ sections = [Formatter.format(Ranker.call(snapshot))]
25
+ sections << Comparator.format_delta(previous, snapshot) if compare_last
26
+ sections << baseline_saved_msg if save_baseline
27
+ sections.join("\n\n")
28
+ end
29
+
30
+ def baseline_saved_msg
31
+ "ベースライン保存済み: #{SnapshotStore::BASELINE_PATH}\n" \
32
+ "(Baseline saved — run `rspec-sprint compare` to compare future runs)"
17
33
  end
18
34
  end
19
35
  end
@@ -11,7 +11,7 @@ module RspecSprint
11
11
  module Doctor
12
12
  module_function
13
13
 
14
- def diagnose(result)
14
+ def diagnose(result, no_snapshot: false, compare_last: false, save_baseline: false)
15
15
  return diagnosis_impossible(result) unless result.rspec_json?
16
16
 
17
17
  sections = []
@@ -19,7 +19,10 @@ module RspecSprint
19
19
  sections << factory_prof_missing_hint unless result.factory_prof?
20
20
  sections << Diagnosis.from_files(
21
21
  rspec_json: result.rspec_json_path,
22
- factory_prof_json: result.factory_prof? ? result.factory_prof_path : nil
22
+ factory_prof_json: result.factory_prof? ? result.factory_prof_path : nil,
23
+ save_snapshot: !no_snapshot,
24
+ compare_last: compare_last,
25
+ save_baseline: save_baseline
23
26
  )
24
27
  sections.join("\n\n")
25
28
  end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "fileutils"
5
+ require "time"
6
+
7
+ module RspecSprint
8
+ # Persists normalized snapshots to .rspec-sprint/snapshots/ for future
9
+ # comparison (the seed for --compare-last / Test Performance Ledger).
10
+ # Failure-safe: disk errors warn and skip; doctor never crashes because of us.
11
+ class SnapshotStore
12
+ DEFAULT_DIR = ".rspec-sprint/snapshots"
13
+ BASELINE_PATH = ".rspec-sprint/baseline.json"
14
+ MAX_SNAPSHOTS = 3
15
+
16
+ def self.save(snapshot, dir: DEFAULT_DIR)
17
+ new(dir: dir).save(snapshot)
18
+ end
19
+
20
+ def self.load_last(dir: DEFAULT_DIR)
21
+ new(dir: dir).load_last
22
+ end
23
+
24
+ def self.save_baseline(snapshot, path: BASELINE_PATH)
25
+ new.save_baseline(snapshot, path: path)
26
+ end
27
+
28
+ def self.load_baseline(path: BASELINE_PATH)
29
+ new.load_baseline(path: path)
30
+ end
31
+
32
+ def initialize(dir: DEFAULT_DIR)
33
+ @dir = dir
34
+ end
35
+
36
+ def save(snapshot)
37
+ FileUtils.mkdir_p(@dir)
38
+ path = File.join(@dir, "#{timestamp}.json")
39
+ File.write(path, serialize(snapshot))
40
+ prune
41
+ rescue Errno::EACCES, Errno::ENOSPC => e
42
+ warn "rspec-sprint: snapshot not saved (#{e.class.name.split("::").last}: #{e.message})"
43
+ end
44
+
45
+ def list
46
+ Dir.glob(File.join(@dir, "*.json")).sort
47
+ end
48
+
49
+ # Returns the most recent snapshot as a symbol-keyed Hash, or nil if none.
50
+ def load_last
51
+ files = list
52
+ return nil if files.empty?
53
+
54
+ JSON.parse(File.read(files.last), symbolize_names: true)
55
+ rescue JSON::ParserError, Errno::ENOENT
56
+ nil
57
+ end
58
+
59
+ # Saves snapshot as the named baseline (single file, never pruned).
60
+ def save_baseline(snapshot, path: BASELINE_PATH)
61
+ FileUtils.mkdir_p(File.dirname(path))
62
+ File.write(path, serialize(snapshot))
63
+ rescue Errno::EACCES, Errno::ENOSPC => e
64
+ warn "rspec-sprint: baseline not saved (#{e.class.name.split("::").last}: #{e.message})"
65
+ end
66
+
67
+ # Returns the saved baseline as a symbol-keyed Hash, or nil if none.
68
+ def load_baseline(path: BASELINE_PATH)
69
+ JSON.parse(File.read(path), symbolize_names: true)
70
+ rescue JSON::ParserError, Errno::ENOENT
71
+ nil
72
+ end
73
+
74
+ private
75
+
76
+ def serialize(snapshot)
77
+ JSON.pretty_generate({
78
+ schema_version: 1,
79
+ created_at: Time.now.utc.iso8601,
80
+ suite_duration: snapshot.suite_duration,
81
+ factory_time: snapshot.factory_time,
82
+ example_count: snapshot.example_count,
83
+ failure_count: snapshot.failure_count,
84
+ factories: (snapshot.factories || []).map do |f|
85
+ { name: f.name, total_count: f.total_count,
86
+ top_level_count: f.top_level_count, total_time: f.total_time }
87
+ end,
88
+ examples: (snapshot.examples || []).map do |e|
89
+ { full_description: e.full_description,
90
+ file_path: sanitize_path(e.file_path),
91
+ run_time: e.run_time, status: e.status }
92
+ end
93
+ })
94
+ end
95
+
96
+ def sanitize_path(path)
97
+ path.to_s.sub(%r{\A\./}, "")
98
+ end
99
+
100
+ def prune
101
+ files = list
102
+ return if files.length <= MAX_SNAPSHOTS
103
+
104
+ files.first(files.length - MAX_SNAPSHOTS).each do |f|
105
+ File.delete(f)
106
+ rescue Errno::EACCES
107
+ nil
108
+ end
109
+ end
110
+
111
+ def timestamp
112
+ Time.now.strftime("%Y%m%d%H%M%S%L")
113
+ end
114
+ end
115
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RspecSprint
4
- VERSION = "0.1.0"
4
+ VERSION = "0.4.0"
5
5
  end
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspec-sprint
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - yasu551
8
+ autorequire:
8
9
  bindir: exe
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-06-18 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: test-prof
@@ -42,6 +43,7 @@ description: |
42
43
  JSON formatter), interprets the output against opinionated heuristics, and asserts
43
44
  the top few things to fix — diagnosis plus concrete prescription, not just numbers.
44
45
  It wraps existing profilers; it does not reimplement them.
46
+ email:
45
47
  executables:
46
48
  - rspec-sprint
47
49
  extensions: []
@@ -53,6 +55,7 @@ files:
53
55
  - lib/rspec_sprint.rb
54
56
  - lib/rspec_sprint/cli.rb
55
57
  - lib/rspec_sprint/collector.rb
58
+ - lib/rspec_sprint/comparator.rb
56
59
  - lib/rspec_sprint/diagnosis.rb
57
60
  - lib/rspec_sprint/doctor.rb
58
61
  - lib/rspec_sprint/finding.rb
@@ -60,6 +63,7 @@ files:
60
63
  - lib/rspec_sprint/normalizer.rb
61
64
  - lib/rspec_sprint/ranker.rb
62
65
  - lib/rspec_sprint/rules.rb
66
+ - lib/rspec_sprint/snapshot_store.rb
63
67
  - lib/rspec_sprint/version.rb
64
68
  homepage: https://github.com/yasu551/rspec-sprint
65
69
  licenses:
@@ -67,6 +71,7 @@ licenses:
67
71
  metadata:
68
72
  source_code_uri: https://github.com/yasu551/rspec-sprint
69
73
  changelog_uri: https://github.com/yasu551/rspec-sprint/blob/main/CHANGELOG.md
74
+ post_install_message:
70
75
  rdoc_options: []
71
76
  require_paths:
72
77
  - lib
@@ -81,7 +86,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
81
86
  - !ruby/object:Gem::Version
82
87
  version: '0'
83
88
  requirements: []
84
- rubygems_version: 4.0.10
89
+ rubygems_version: 3.5.22
90
+ signing_key:
85
91
  specification_version: 4
86
92
  summary: Diagnose RSpec/Rails test-suite slowness and assert the top repo-specific
87
93
  fixes.