rspec-sprint 0.1.0 → 0.3.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 +4 -4
- data/lib/rspec_sprint/cli.rb +6 -1
- data/lib/rspec_sprint/comparator.rb +43 -0
- data/lib/rspec_sprint/diagnosis.rb +12 -2
- data/lib/rspec_sprint/doctor.rb +4 -2
- data/lib/rspec_sprint/snapshot_store.rb +91 -0
- data/lib/rspec_sprint/version.rb +1 -1
- metadata +9 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 806c50acbafad7460f0a755f0bf56d6f38c7c28c9fad2726810d45bcd19c23fc
|
|
4
|
+
data.tar.gz: 9958794c6f2f4d456246feeb2acae5576dc12a0565f697e0791e246b1fdfe83d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 458773150de314011f601648d4d50b48bdce862e6381ea21d19393ded0b9a3ac23a3691d1333cfc670f7ae00016091d61b5013032a9ab56f3290e57fe0810e1e
|
|
7
|
+
data.tar.gz: 9969a1b44ed4a96cfc81262cabb41a6a86256007086211d0eb78b52263d572f929b0a7b8ff8c42764b33d56020492d22fffe766582a14f4720c491d6234175a3
|
data/lib/rspec_sprint/cli.rb
CHANGED
|
@@ -21,9 +21,14 @@ 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])
|
|
27
32
|
end
|
|
28
33
|
|
|
29
34
|
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,17 @@ 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)
|
|
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
|
-
|
|
21
|
+
SnapshotStore.save(snapshot) if save_snapshot
|
|
22
|
+
|
|
23
|
+
report = Formatter.format(Ranker.call(snapshot))
|
|
24
|
+
return report unless compare_last
|
|
25
|
+
|
|
26
|
+
[report, Comparator.format_delta(previous, snapshot)].join("\n\n")
|
|
17
27
|
end
|
|
18
28
|
end
|
|
19
29
|
end
|
data/lib/rspec_sprint/doctor.rb
CHANGED
|
@@ -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)
|
|
15
15
|
return diagnosis_impossible(result) unless result.rspec_json?
|
|
16
16
|
|
|
17
17
|
sections = []
|
|
@@ -19,7 +19,9 @@ 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
|
|
23
25
|
)
|
|
24
26
|
sections.join("\n\n")
|
|
25
27
|
end
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
MAX_SNAPSHOTS = 3
|
|
14
|
+
|
|
15
|
+
def self.save(snapshot, dir: DEFAULT_DIR)
|
|
16
|
+
new(dir: dir).save(snapshot)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.load_last(dir: DEFAULT_DIR)
|
|
20
|
+
new(dir: dir).load_last
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(dir: DEFAULT_DIR)
|
|
24
|
+
@dir = dir
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def save(snapshot)
|
|
28
|
+
FileUtils.mkdir_p(@dir)
|
|
29
|
+
path = File.join(@dir, "#{timestamp}.json")
|
|
30
|
+
File.write(path, serialize(snapshot))
|
|
31
|
+
prune
|
|
32
|
+
rescue Errno::EACCES, Errno::ENOSPC => e
|
|
33
|
+
warn "rspec-sprint: snapshot not saved (#{e.class.name.split("::").last}: #{e.message})"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def list
|
|
37
|
+
Dir.glob(File.join(@dir, "*.json")).sort
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Returns the most recent snapshot as a symbol-keyed Hash, or nil if none.
|
|
41
|
+
def load_last
|
|
42
|
+
files = list
|
|
43
|
+
return nil if files.empty?
|
|
44
|
+
|
|
45
|
+
JSON.parse(File.read(files.last), symbolize_names: true)
|
|
46
|
+
rescue JSON::ParserError, Errno::ENOENT
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def serialize(snapshot)
|
|
53
|
+
JSON.pretty_generate({
|
|
54
|
+
schema_version: 1,
|
|
55
|
+
created_at: Time.now.utc.iso8601,
|
|
56
|
+
suite_duration: snapshot.suite_duration,
|
|
57
|
+
factory_time: snapshot.factory_time,
|
|
58
|
+
example_count: snapshot.example_count,
|
|
59
|
+
failure_count: snapshot.failure_count,
|
|
60
|
+
factories: (snapshot.factories || []).map do |f|
|
|
61
|
+
{ name: f.name, total_count: f.total_count,
|
|
62
|
+
top_level_count: f.top_level_count, total_time: f.total_time }
|
|
63
|
+
end,
|
|
64
|
+
examples: (snapshot.examples || []).map do |e|
|
|
65
|
+
{ full_description: e.full_description,
|
|
66
|
+
file_path: sanitize_path(e.file_path),
|
|
67
|
+
run_time: e.run_time, status: e.status }
|
|
68
|
+
end
|
|
69
|
+
})
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def sanitize_path(path)
|
|
73
|
+
path.to_s.sub(%r{\A\./}, "")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def prune
|
|
77
|
+
files = list
|
|
78
|
+
return if files.length <= MAX_SNAPSHOTS
|
|
79
|
+
|
|
80
|
+
files.first(files.length - MAX_SNAPSHOTS).each do |f|
|
|
81
|
+
File.delete(f)
|
|
82
|
+
rescue Errno::EACCES
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def timestamp
|
|
88
|
+
Time.now.strftime("%Y%m%d%H%M%S%L")
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
data/lib/rspec_sprint/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- yasu551
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: exe
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
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:
|
|
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.
|