scrap4rb 0.1.0 → 0.2.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/CHANGELOG.md +20 -1
- data/README.md +13 -4
- data/docs/compare-verdict.md +55 -0
- data/docs/setup-depth.md +72 -0
- data/lib/scrap4rb/cli.rb +31 -6
- data/lib/scrap4rb/example.rb +29 -16
- data/lib/scrap4rb/judgment.rb +2 -1
- data/lib/scrap4rb/policy.rb +1 -0
- data/lib/scrap4rb/syntax.rb +15 -6
- data/lib/scrap4rb/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 96286449e9afa50fcf63563ab4c24e5ea34422d4b78aef3c8ba5c5fecd0dad0a
|
|
4
|
+
data.tar.gz: 75e1b075bdc33a520a45c1cf95b5e20f484d6893969e5b138f1e6514e0fda703
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 76526de107521d564ae46d855eb70920b67f1029a9cf13d8f1831ba8ad2db2173a9a550926ec8e01ebb849f1559d153f8d095157e76dd632005a0e68619ca062
|
|
7
|
+
data.tar.gz: 0c6caaaf36b1d12058035482a7f8af88fabd089da3b8f3c54ccd08992bcadb1c80baa2fdf27204dfe866ddd378ac09a5368a623771a79057fdeeebc2a40a5daa
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,24 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.0] - 2026-10-15
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- The setup depth counts 1 for each `let`, `let!`, `subject` and instance variable in a setup block, plus 1 for a setup block with other statements. Before, it counted 1 for each statement in a setup block, and 0 for `let` and `subject`. See `docs/setup-depth.md`.
|
|
13
|
+
- temp-resource-work also counts file reads and writes, directories and processes: `File`, `IO` and `Dir` calls such as `read`, `open` and `mkdir`, `IO.popen`, `Open3` and `exec`. `File.join` and `file_fixture(...).read` do not count.
|
|
14
|
+
- A `begin` with several `rescue` clauses is one branch, as a `try` is in SCRAP. Before, each clause counted 1.
|
|
15
|
+
- The `--compare` verdict is `worse` for hidden helper lines only when more tests have them than the number of new tests. Before, a new test that called a helper made the verdict `worse`. See `docs/compare-verdict.md`.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- A `why` line for each file, with the ratios and counts behind the score, as in the SCRAP report.
|
|
19
|
+
- `where` lines for a file with more than one describe block or class: the three blocks with the most pressure and the worst test of each. The JSON report has them as `top_blocks`.
|
|
20
|
+
- `--compare` shows `example_count_delta`.
|
|
21
|
+
- `loop do` is a branch, and `for x in` is a table branch. A `for` loop over a literal table is table-driven.
|
|
22
|
+
- The setup depth follows `setup :method_name` to the body of the method.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- A bare `super` in `def setup` no longer counts as a setup statement.
|
|
26
|
+
|
|
9
27
|
## [0.1.0] - 2026-10-02
|
|
10
28
|
|
|
11
29
|
### Added
|
|
@@ -19,5 +37,6 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
19
37
|
`describe`/`it` blocks. A `define_method` test inside a loop over a table
|
|
20
38
|
(`CASES.each`) is one table-driven test that spans the loop.
|
|
21
39
|
|
|
22
|
-
[Unreleased]: https://github.com/roberthopman/scrap4rb/compare/v0.
|
|
40
|
+
[Unreleased]: https://github.com/roberthopman/scrap4rb/compare/v0.2.0...HEAD
|
|
41
|
+
[0.2.0]: https://github.com/roberthopman/scrap4rb/compare/v0.1.0...v0.2.0
|
|
23
42
|
[0.1.0]: https://github.com/roberthopman/scrap4rb/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -55,6 +55,7 @@ The comparison gives each changed file a verdict: `improved`, `worse`, `mixed` o
|
|
|
55
55
|
```
|
|
56
56
|
test/models/smelly_test.rb
|
|
57
57
|
score 48.46 HIGH mode LOCAL ai AUTO_REFACTOR tests=6 avg=12.78 max=18.26
|
|
58
|
+
why low-assert=0.83 branching=0.17 mocking=0.17 helper-hidden=1 harmful-dup=11 extraction=0.0 matrix=1 subject-rep=0
|
|
58
59
|
HIGH Strengthen assertions in weak tests before doing structural cleanup.
|
|
59
60
|
LOW Be skeptical of helper extraction that only hides setup; ...
|
|
60
61
|
18.3 L59 it uses a long local helper low-assertion-density,helper-hidden-complexity
|
|
@@ -70,7 +71,12 @@ score Refactor pressure of the file
|
|
|
70
71
|
level STABLE, LOW, MEDIUM, HIGH or CRITICAL
|
|
71
72
|
mode STABLE: leave it. LOCAL: fix tests in place. SPLIT: split the file first
|
|
72
73
|
ai LEAVE_ALONE, AUTO_TABLE_DRIVE, AUTO_REFACTOR, MANUAL_SPLIT or REVIEW_FIRST
|
|
74
|
+
why The ratios and counts behind the score: tests with 1 assertion or fewer, with
|
|
75
|
+
branches, with stubs and with hidden helper lines; duplication; extraction
|
|
76
|
+
pressure; table-drive candidates; subject repetition
|
|
73
77
|
HIGH/... Recommendations, ranked by confidence, at most four
|
|
78
|
+
where The three describe blocks or classes with the most pressure, with the worst
|
|
79
|
+
test of each. Only for a file with more than one block
|
|
74
80
|
tests The SCRAP score of each test, worst first, with its smells
|
|
75
81
|
EXTRACT A group of tests where a shared helper pays for itself
|
|
76
82
|
```
|
|
@@ -95,7 +101,7 @@ multiple-phases assert, act, assert again 5
|
|
|
95
101
|
high-mocking more than 3 stubs or mocks 4
|
|
96
102
|
large-example more than 20 lines 4
|
|
97
103
|
helper-hidden-complexity more than 8 lines hidden in local helpers 4
|
|
98
|
-
temp-resource-work
|
|
104
|
+
temp-resource-work files, directories, threads or processes 3
|
|
99
105
|
literal-heavy-setup a string over 5 lines, a hash or array over 10 3
|
|
100
106
|
```
|
|
101
107
|
|
|
@@ -114,13 +120,15 @@ SCRAP (speclj) scrap4rb (Minitest)
|
|
|
114
120
|
-------------- -------------------
|
|
115
121
|
describe, context a test class, or a minitest/spec describe block
|
|
116
122
|
it test "..." do, def test_*, it "..." do, define_method(:"test_...")
|
|
117
|
-
before, with setup blocks and def setup,
|
|
123
|
+
before, with setup blocks, setup :method and def setup, let and subject,
|
|
124
|
+
inherited by every test in the class (see docs/setup-depth.md)
|
|
118
125
|
let, binding travel_to, freeze_time, within, using_session,
|
|
119
126
|
perform_enqueued_jobs, with_*, and stub with a block
|
|
120
127
|
with-redefs stub, stub_request, any_instance, expects, stubs, Minitest::Mock.new
|
|
121
128
|
should* assert*, refute*, must_*, wont_*
|
|
122
|
-
doseq, for, every? each, each_with_index, map, times, all?, ...
|
|
123
|
-
if, when, cond, and if, unless, case, while, until, &&, ||, rescue
|
|
129
|
+
doseq, for, every? for, each, each_with_index, map, times, all?, ...
|
|
130
|
+
if, when, cond, and if, unless, case, while, until, &&, ||, rescue modifier
|
|
131
|
+
loop, try loop, begin/rescue (one per begin)
|
|
124
132
|
```
|
|
125
133
|
|
|
126
134
|
The policy numbers (weights, thresholds, levels) are SCRAP's, unchanged.
|
|
@@ -130,6 +138,7 @@ The policy numbers (weights, thresholds, levels) are SCRAP's, unchanged.
|
|
|
130
138
|
- A test whose whole body is one context block, such as `travel_to(...) do ... end`, is unwrapped before the phase count. In Ruby that block is test context, not arrange code.
|
|
131
139
|
- A call to an assertion helper defined in `test/test_helper.rb`, `test/application_system_test_case.rb`, `test/test_helpers/` or `test/support/` counts as one assertion. SCRAP only reads the spec file itself. Without this rule, every test that uses a shared assertion helper would be "no-assertions".
|
|
132
140
|
- A loop over a literal hash (`{ 5 => 0, 6 => 10 }.each`) counts as table-driven, the same as a loop over an array of arrays.
|
|
141
|
+
- `--compare` does not call new tests that use a helper a helper regression. See `docs/compare-verdict.md`.
|
|
133
142
|
|
|
134
143
|
## Notes for Rails suites
|
|
135
144
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# The compare verdict
|
|
2
|
+
|
|
3
|
+
This document explains one deviation from SCRAP in `--compare`: when hidden helper lines make the verdict `worse`.
|
|
4
|
+
|
|
5
|
+
## The rule in SCRAP
|
|
6
|
+
|
|
7
|
+
SCRAP compares the summary of each file with the baseline, and gives a verdict in this order:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Verdict When
|
|
11
|
+
------- ----
|
|
12
|
+
improved file score -5 or more, no rise in extraction pressure or in max SCRAP
|
|
13
|
+
worse helper regression: more tests with hidden helper lines, no fall in
|
|
14
|
+
extraction pressure, and no more table-drive candidates
|
|
15
|
+
worse any rise in extraction pressure or max SCRAP, or file score +5 or more
|
|
16
|
+
unchanged file score delta 0
|
|
17
|
+
mixed all other cases
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The helper regression rule looks for a refactor that moves setup into a local helper. The setup is then hidden, but the duplication does not go down. The rule uses `helper_hidden_delta`: the change in the number of tests that call a local helper.
|
|
21
|
+
|
|
22
|
+
The source is `comparison.clj` in [unclebob/scrap](https://github.com/unclebob/scrap).
|
|
23
|
+
|
|
24
|
+
## The problem
|
|
25
|
+
|
|
26
|
+
`helper_hidden_delta` is a count, not a ratio. A new test that calls an existing helper adds 1. Thus a change that adds tests which call a helper gets the verdict `worse`, also when all scores improve. scrap4rb's own test suite shows this:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
Change file score max SCRAP helper_hidden tests SCRAP verdict
|
|
30
|
+
------ ---------- --------- ------------- ----- -------------
|
|
31
|
+
4 tests for setup depth -0.75 0.0 +2 +4 worse
|
|
32
|
+
6 tests for branches and files -0.76 0.0 +6 +6 worse
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The advice that follows, "revert it, or simplify the helpers", is wrong for these changes. SCRAP has the same behavior.
|
|
36
|
+
|
|
37
|
+
## The rule in scrap4rb
|
|
38
|
+
|
|
39
|
+
scrap4rb compares `helper_hidden_delta` with the number of new tests:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
helper regression = helper_hidden_delta > max(example_count_delta, 0)
|
|
43
|
+
and extraction pressure did not fall
|
|
44
|
+
and table-drive candidates did not rise
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- A new test that calls a helper is not a regression.
|
|
48
|
+
- An existing test that starts to call a helper is a regression, as in SCRAP.
|
|
49
|
+
- When tests are removed, the limit is 0, so removed tests cannot hide a regression.
|
|
50
|
+
|
|
51
|
+
`--compare` shows `example_count_delta`, so the reason for a verdict is visible.
|
|
52
|
+
|
|
53
|
+
## The limit of the rule
|
|
54
|
+
|
|
55
|
+
The rule counts tests, not names. Suppose a change adds 2 tests without helpers and moves 2 existing tests to a helper. Then `helper_hidden_delta` is 2, `example_count_delta` is 2, and the rule does not see the regression. To see it, scrap4rb would have to match the tests in the baseline by name. The baseline does not store the tests, so scrap4rb does not do this.
|
data/docs/setup-depth.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Setup depth
|
|
2
|
+
|
|
3
|
+
This document explains how scrap4rb counts the setup depth of a test, and why the count differs from a literal port of SCRAP.
|
|
4
|
+
|
|
5
|
+
## Setup depth in SCRAP
|
|
6
|
+
|
|
7
|
+
SCRAP gives each test (`it`) a setup depth in two steps:
|
|
8
|
+
|
|
9
|
+
1. The start value is the number of setup forms in the enclosing `describe` and `context` blocks: `before`, `before-all`, `with`, `around`, `with-stubs`, `let`, `binding` and `with-redefs`. Each form counts 1, however many lines it has.
|
|
10
|
+
2. Inside the test, each nested `let`, `binding` or `with-redefs` adds 1. SCRAP keeps the deepest level, not the sum.
|
|
11
|
+
|
|
12
|
+
The setup depth has three effects:
|
|
13
|
+
|
|
14
|
+
- It goes into the complexity score: `saturating(1 + branches + setup_depth + helper_calls)`. The curve rises quickly at the start: 1 gives 1.0, 3 gives 8.26 and 5 gives 13.32.
|
|
15
|
+
- An API-contract test gets 2 levels free.
|
|
16
|
+
- A test with a setup depth above 2 cannot be a coverage-matrix candidate. Thus a file of such tests does not get the "make it table-driven" advice.
|
|
17
|
+
|
|
18
|
+
The source is `example_collect.clj`, `example_node.clj` and `example_metrics.clj` in [unclebob/scrap](https://github.com/unclebob/scrap).
|
|
19
|
+
|
|
20
|
+
## Why a literal port does not work
|
|
21
|
+
|
|
22
|
+
speclj has two kinds of setup:
|
|
23
|
+
|
|
24
|
+
- `with` is a named, lazy binding. The Minitest equivalent is `let`, or an instance variable in `setup`.
|
|
25
|
+
- `before` is a block of actions. It counts 1, however many lines it has.
|
|
26
|
+
|
|
27
|
+
A Rails `setup` block mixes the two kinds:
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
setup do
|
|
31
|
+
@user = users(:alice)
|
|
32
|
+
@order = orders(:open)
|
|
33
|
+
sign_in @user
|
|
34
|
+
visit order_path(@order)
|
|
35
|
+
end
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
In speclj, this is two `with` forms and one `before`, so the setup depth is 3.
|
|
39
|
+
|
|
40
|
+
## The rule
|
|
41
|
+
|
|
42
|
+
scrap4rb counts the setup depth of a test class or a `describe` block as follows:
|
|
43
|
+
|
|
44
|
+
- Each `let`, `let!` and `subject` counts 1.
|
|
45
|
+
- In each `setup` block, `before` block and `def setup`, each instance variable assignment counts 1. For `setup :build_order`, scrap4rb reads the body of the method `build_order`.
|
|
46
|
+
- A setup block that also has other statements counts 1 more. A `super` call is not a statement for this rule.
|
|
47
|
+
|
|
48
|
+
A nested `describe` block adds its own setup depth to the setup depth of its parent. Inside a test, a context block (`travel_to`, `with_*`, `stub` with a block) adds 1, as a nested `with-redefs` does in SCRAP.
|
|
49
|
+
|
|
50
|
+
The three possible rules give different results for the setup block above:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
Rule Depth API test Large test Matrix candidate
|
|
54
|
+
---- ----- -------- ---------- ----------------
|
|
55
|
+
A: 1 per setup block (+1 per let) 1 1.0 4.95 yes
|
|
56
|
+
B: 1 per statement (in 0.1.0) 4 8.26 13.32 no
|
|
57
|
+
C: 1 per @ivar or let, +1 if actions 3 4.95 11.0 no
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- Rule A is the literal mapping of `before`. A 20-line setup block costs 1, but SCRAP has the same weakness with a large `before`.
|
|
61
|
+
- Rule B counts too many. Each action line costs as much as a binding. With a 10-line setup block, the complexity score of a large test is 21.03 instead of 4.95. `let` costs nothing, so a move of the same setup from `setup` into `let` improves the score without a real change.
|
|
62
|
+
- Rule C gives the same number as the equivalent speclj code. scrap4rb uses rule C.
|
|
63
|
+
|
|
64
|
+
## Locals in a test
|
|
65
|
+
|
|
66
|
+
In Clojure, a test keeps its locals in a `let`. That `let` adds 1 to the setup depth. SCRAP also gives the `let` form the setup phase, so the assertions inside it do not count as phases. Thus SCRAP never reports multiple-phases for this test:
|
|
67
|
+
|
|
68
|
+
```clojure
|
|
69
|
+
(it "x" (let [x (atom 1)] (should= 1 @x) (reset! x 2) (should= 2 @x)))
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The same test in Ruby has flat locals. It gets a setup depth of 0 and multiple-phases (5 points). scrap4rb does not copy the Clojure behavior, because Ruby has no `let` scope for the locals of a test. The result is that a Ruby score and a speclj score of the same test can differ.
|
data/lib/scrap4rb/cli.rb
CHANGED
|
@@ -41,16 +41,26 @@ module Scrap4rb
|
|
|
41
41
|
opts
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
def analyze_file(path, shared_asserts)
|
|
45
|
-
|
|
44
|
+
def analyze_file(path, shared_asserts) = analyze_source(path, File.read(path), shared_asserts)
|
|
45
|
+
|
|
46
|
+
def analyze_source(path, src, shared_asserts)
|
|
46
47
|
result = Prism.parse(src)
|
|
47
48
|
exs = Example.from_tree(result.value, shared_assert_helpers: shared_asserts)
|
|
48
49
|
summary = Summary.summarize(exs)
|
|
49
|
-
blocks = exs.group_by { _1[:describe_path] }.reject { |p, _| p.empty? }.map { |p, b| { path: p, summary: Summary.summarize(b) } }
|
|
50
|
+
blocks = exs.group_by { _1[:describe_path] }.reject { |p, _| p.empty? }.map { |p, b| { path: p, summary: Summary.summarize(b), examples: b } }
|
|
50
51
|
mode = Judgment.remediation_mode(summary, blocks)
|
|
51
52
|
{ path:, content_hash: Digest::SHA256.hexdigest(src)[0, 16], errors: Structure.errors(result), examples: exs, summary:,
|
|
52
53
|
file_score: Judgment.pressure_score(summary), level: Judgment.level(summary), mode:,
|
|
53
|
-
ai: Judgment.actionability(summary, mode), actions: Judgment.actions(summary, mode) }
|
|
54
|
+
ai: Judgment.actionability(summary, mode), actions: Judgment.actions(summary, mode), top_blocks: top_blocks(blocks) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def top_blocks(blocks)
|
|
58
|
+
blocks.map do |b|
|
|
59
|
+
worst = b[:examples].max_by { _1[:scrap] }
|
|
60
|
+
{ path: b[:path], score: Judgment.pressure_score(b[:summary]), level: Judgment.level(b[:summary]),
|
|
61
|
+
avg_scrap: b[:summary][:avg_scrap], harmful_duplication_score: b[:summary][:harmful_duplication_score],
|
|
62
|
+
worst: worst.slice(:name, :line, :scrap) }
|
|
63
|
+
end.sort_by { -_1[:score] }.first(Policy::REPORT[:top_block_count])
|
|
54
64
|
end
|
|
55
65
|
|
|
56
66
|
def print_report(r, verbose:)
|
|
@@ -58,9 +68,11 @@ module Scrap4rb
|
|
|
58
68
|
puts "\n#{r[:path]}"
|
|
59
69
|
r[:errors].each { puts " #{_1}" }
|
|
60
70
|
puts " score #{r[:file_score]} #{r[:level]} mode #{r[:mode]} ai #{r[:ai]} tests=#{s[:example_count]} avg=#{s[:avg_scrap]} max=#{s[:max_scrap]}"
|
|
71
|
+
puts why_line(s)
|
|
61
72
|
r[:actions].each { |c, t| puts " #{LABEL[c].ljust(6)} #{t}" }
|
|
73
|
+
r[:top_blocks].each { puts where_line(_1) } if r[:top_blocks].size > 1
|
|
62
74
|
worst = r[:examples].sort_by { -_1[:scrap] }
|
|
63
|
-
worst = worst.first(
|
|
75
|
+
worst = worst.first(Policy::REPORT[:top_example_count]) unless verbose
|
|
64
76
|
worst.each { puts example_line(_1, verbose) }
|
|
65
77
|
s[:recommended_extractions].each do |x|
|
|
66
78
|
puts " EXTRACT L#{x[:line_start]}-#{x[:line_end]} net=#{x[:net_benefit]} (I=#{x[:instances]} shared=#{x[:shared_forms]} variable=#{x[:variable_points]}): " +
|
|
@@ -68,6 +80,18 @@ module Scrap4rb
|
|
|
68
80
|
end
|
|
69
81
|
end
|
|
70
82
|
|
|
83
|
+
def why_line(s)
|
|
84
|
+
r = Judgment.ratios(s)
|
|
85
|
+
format(" why low-assert=%.2f branching=%.2f mocking=%.2f helper-hidden=%d harmful-dup=%d extraction=%.1f matrix=%d subject-rep=%d",
|
|
86
|
+
r[:low], r[:branching], r[:mocking], s[:helper_hidden_example_count], s[:harmful_duplication_score],
|
|
87
|
+
s[:extraction_pressure_score], s[:coverage_matrix_candidates], s[:subject_repetition_score])
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def where_line(b)
|
|
91
|
+
format(" where %s %s %.1f avg=%.1f dup=%d worst: %s (%.1f)", b[:path].join(" / "), b[:level], b[:score],
|
|
92
|
+
b[:avg_scrap], b[:harmful_duplication_score], b[:worst][:name], b[:worst][:scrap])
|
|
93
|
+
end
|
|
94
|
+
|
|
71
95
|
def example_line(e, verbose)
|
|
72
96
|
extra = if verbose
|
|
73
97
|
" [a=#{e[:assertions]} b=#{e[:branches]} d=#{e[:setup_depth]} stub=#{e[:with_redefs]} h=#{e[:helper_calls]} " \
|
|
@@ -92,7 +116,8 @@ module Scrap4rb
|
|
|
92
116
|
max_scrap_delta: (a[:max_scrap] - b[:max_scrap]).round(2),
|
|
93
117
|
extraction_pressure_delta: (a[:effective_duplication_score] - b[:effective_duplication_score]).round(2),
|
|
94
118
|
case_matrix_delta: a[:case_matrix_repetition] - b[:case_matrix_repetition],
|
|
95
|
-
helper_hidden_delta: a[:helper_hidden_example_count] - b[:helper_hidden_example_count]
|
|
119
|
+
helper_hidden_delta: a[:helper_hidden_example_count] - b[:helper_hidden_example_count],
|
|
120
|
+
example_count_delta: a[:example_count] - b[:example_count] }
|
|
96
121
|
[r[:path], cmp.merge(verdict: Judgment.verdict(cmp))]
|
|
97
122
|
end
|
|
98
123
|
end
|
data/lib/scrap4rb/example.rb
CHANGED
|
@@ -61,8 +61,8 @@ module Scrap4rb
|
|
|
61
61
|
expanded = helper && !stack.include?(node.name) ? expand(ctx, node.name, stack | [node.name], depth) : nil
|
|
62
62
|
local = {
|
|
63
63
|
assertions: assertion?(node) || shared_assert ? 1 : 0,
|
|
64
|
-
branches:
|
|
65
|
-
table_branches:
|
|
64
|
+
branches: branch?(node) ? 1 : 0,
|
|
65
|
+
table_branches: table_branch?(node) ? 1 : 0,
|
|
66
66
|
with_redefs: stub_call?(node) ? 1 : 0,
|
|
67
67
|
helper_calls: helper ? 1 : 0,
|
|
68
68
|
temp_resources: temp_resource?(node) ? 1 : 0,
|
|
@@ -121,8 +121,8 @@ module Scrap4rb
|
|
|
121
121
|
].select(&:first).map { { label: _2, penalty: _3 } }
|
|
122
122
|
end
|
|
123
123
|
|
|
124
|
-
def score(name:, node:, body:, ctx:, inherited_setup:, describe_path:, table_driven: false)
|
|
125
|
-
m = combine(*body.map { analyze(_1, ctx,
|
|
124
|
+
def score(name:, node:, body:, ctx:, inherited_setup:, inherited_depth:, describe_path:, table_driven: false)
|
|
125
|
+
m = combine(*body.map { analyze(_1, ctx, inherited_depth, Set.new) }, { table_driven: })
|
|
126
126
|
raw_lines = lines_of(node)
|
|
127
127
|
line_count = raw_lines + m[:helper_hidden_lines]
|
|
128
128
|
forms = top_forms(body)
|
|
@@ -159,29 +159,42 @@ module Scrap4rb
|
|
|
159
159
|
}
|
|
160
160
|
end
|
|
161
161
|
|
|
162
|
-
def
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
162
|
+
def setup_body(node, helpers)
|
|
163
|
+
if bare_call?(node) && SETUP_CALLS.include?(node.name) && node.block.is_a?(Prism::BlockNode) then block_body(node)
|
|
164
|
+
elsif bare_call?(node) && SETUP_CALLS.include?(node.name) && node.block.nil? && node.arguments
|
|
165
|
+
node.arguments.arguments.grep(Prism::SymbolNode).flat_map { helpers.fetch(_1.unescaped.to_sym, []) }
|
|
166
|
+
elsif node.is_a?(Prism::DefNode) && node.name == :setup
|
|
167
|
+
statements(node.body).reject { _1.is_a?(Prism::ForwardingSuperNode) || _1.is_a?(Prism::SuperNode) }
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def setup_forms(scope_body, helpers) = scope_body.flat_map { setup_body(_1, helpers) || [] }
|
|
172
|
+
|
|
173
|
+
def setup_depth(scope_body, helpers)
|
|
174
|
+
scope_body.sum do |n|
|
|
175
|
+
next 1 if bare_call?(n) && LET_CALLS.include?(n.name) && n.block
|
|
176
|
+
bindings, actions = (setup_body(n, helpers) || []).partition { _1.is_a?(Prism::InstanceVariableWriteNode) }
|
|
177
|
+
bindings.size + (actions.empty? ? 0 : 1)
|
|
168
178
|
end
|
|
169
179
|
end
|
|
170
180
|
|
|
171
181
|
def test_body(node) = node.is_a?(Prism::DefNode) ? statements(node.body) : block_body(node)
|
|
172
182
|
|
|
173
|
-
def collect(forms, ctx, path = [], inherited = [], out = [])
|
|
174
|
-
setup = inherited + setup_forms(forms)
|
|
183
|
+
def collect(forms, ctx, path = [], inherited = [], inherited_depth = 0, out = [])
|
|
184
|
+
setup = inherited + setup_forms(forms, ctx.helpers)
|
|
185
|
+
depth = inherited_depth + setup_depth(forms, ctx.helpers)
|
|
175
186
|
forms.each do |f|
|
|
176
187
|
if (name, body = scope_children(f))
|
|
177
|
-
collect(body, ctx, path + [name], setup, out)
|
|
188
|
+
collect(body, ctx, path + [name], setup, depth, out)
|
|
178
189
|
elsif test_node?(f)
|
|
179
|
-
out << score(name: test_name(f), node: f, body: test_body(f), ctx:, inherited_setup: setup,
|
|
190
|
+
out << score(name: test_name(f), node: f, body: test_body(f), ctx:, inherited_setup: setup,
|
|
191
|
+
inherited_depth: depth, describe_path: path)
|
|
180
192
|
elsif (inner = dynamic_test_in_loop(f))
|
|
181
193
|
out << score(name: dynamic_test_name(inner), node: f, body: block_body(inner), ctx:, inherited_setup: setup,
|
|
182
|
-
describe_path: path, table_driven: true)
|
|
194
|
+
inherited_depth: depth, describe_path: path, table_driven: true)
|
|
183
195
|
elsif dynamic_test?(f)
|
|
184
|
-
out << score(name: dynamic_test_name(f), node: f, body: block_body(f), ctx:, inherited_setup: setup,
|
|
196
|
+
out << score(name: dynamic_test_name(f), node: f, body: block_body(f), ctx:, inherited_setup: setup,
|
|
197
|
+
inherited_depth: depth, describe_path: path)
|
|
185
198
|
end
|
|
186
199
|
end
|
|
187
200
|
out
|
data/lib/scrap4rb/judgment.rb
CHANGED
|
@@ -101,7 +101,8 @@ module Scrap4rb
|
|
|
101
101
|
|
|
102
102
|
def verdict(c)
|
|
103
103
|
if c[:file_score_delta] <= -5 && c[:extraction_pressure_delta] <= 0 && c[:max_scrap_delta] <= 0 then "improved"
|
|
104
|
-
elsif c[:helper_hidden_delta].
|
|
104
|
+
elsif c[:helper_hidden_delta] > [c[:example_count_delta], 0].max && c[:extraction_pressure_delta] >= 0 && c[:case_matrix_delta] <= 0
|
|
105
|
+
"worse"
|
|
105
106
|
elsif c[:extraction_pressure_delta].positive? || c[:max_scrap_delta].positive? || c[:file_score_delta] >= 5 then "worse"
|
|
106
107
|
elsif c[:file_score_delta].zero? then "unchanged"
|
|
107
108
|
else "mixed"
|
data/lib/scrap4rb/policy.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
module Scrap4rb
|
|
2
2
|
module Policy
|
|
3
|
+
REPORT = { top_example_count: 5, top_block_count: 3 }.freeze
|
|
3
4
|
DUPLICATION = { threshold: 0.5, matrix_max_scrap: 18, matrix_max_lines: 12, matrix_max_assertions: 1,
|
|
4
5
|
matrix_max_branches: 0, matrix_max_setup_depth: 2, matrix_max_with_redefs: 0,
|
|
5
6
|
matrix_max_temp_resources: 0, matrix_max_helper_hidden_lines: 0, matrix_max_subject_symbols: 2 }.freeze
|
data/lib/scrap4rb/syntax.rb
CHANGED
|
@@ -8,10 +8,12 @@ module Scrap4rb
|
|
|
8
8
|
STUB_CALLS = %i[stub stub_request any_instance expects stubs stub_const].to_set
|
|
9
9
|
TABLE_CALLS = %i[each each_with_index each_pair each_with_object each_slice map flat_map times all? any? none?].to_set
|
|
10
10
|
BRANCH_NODES = [Prism::IfNode, Prism::UnlessNode, Prism::CaseNode, Prism::CaseMatchNode, Prism::WhileNode,
|
|
11
|
-
Prism::UntilNode, Prism::AndNode, Prism::OrNode, Prism::
|
|
11
|
+
Prism::UntilNode, Prism::AndNode, Prism::OrNode, Prism::RescueModifierNode].freeze
|
|
12
|
+
FILE_CALLS = %i[read readlines binread write binwrite open foreach popen mkdir].to_set
|
|
12
13
|
TEST_CALLS = %i[test it specify].to_set
|
|
13
14
|
BLOCK_CALLS = %i[describe context].to_set
|
|
14
15
|
SETUP_CALLS = %i[setup before].to_set
|
|
16
|
+
LET_CALLS = %i[let let! subject].to_set
|
|
15
17
|
|
|
16
18
|
extend self
|
|
17
19
|
|
|
@@ -44,6 +46,13 @@ module Scrap4rb
|
|
|
44
46
|
|
|
45
47
|
def assertion?(node) = call?(node) && ASSERTION.match?(node.name)
|
|
46
48
|
|
|
49
|
+
def branch?(node)
|
|
50
|
+
BRANCH_NODES.any? { node.is_a?(_1) } || (node.is_a?(Prism::BeginNode) && !node.rescue_clause.nil?) ||
|
|
51
|
+
(bare_call?(node) && node.name == :loop && !node.block.nil?)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def table_branch?(node) = node.is_a?(Prism::ForNode) || (call?(node) && TABLE_CALLS.include?(node.name) && !node.block.nil?)
|
|
55
|
+
|
|
47
56
|
def table_literal?(node)
|
|
48
57
|
case node
|
|
49
58
|
when Prism::ArrayNode then node.elements.size >= 2 && node.elements.all? { _1.is_a?(Prism::ArrayNode) || _1.is_a?(Prism::HashNode) }
|
|
@@ -53,6 +62,7 @@ module Scrap4rb
|
|
|
53
62
|
end
|
|
54
63
|
|
|
55
64
|
def table_driven_form?(node)
|
|
65
|
+
return table_literal?(node.collection) if node.is_a?(Prism::ForNode)
|
|
56
66
|
return false unless call?(node)
|
|
57
67
|
(TABLE_CALLS.include?(node.name) && node.block && table_literal?(node.receiver)) ||
|
|
58
68
|
(assertion?(node) && (node.arguments&.arguments || []).any? { table_literal?(_1) })
|
|
@@ -62,10 +72,9 @@ module Scrap4rb
|
|
|
62
72
|
return true if node.is_a?(Prism::XStringNode) || node.is_a?(Prism::InterpolatedXStringNode)
|
|
63
73
|
return false unless call?(node)
|
|
64
74
|
recv = node.receiver&.slice
|
|
65
|
-
%i[mktmpdir tmpdir system spawn fork].include?(node.name) ||
|
|
66
|
-
|
|
67
|
-
(
|
|
68
|
-
(recv == "FileUtils")
|
|
75
|
+
%i[mktmpdir tmpdir system spawn fork exec].include?(node.name) ||
|
|
76
|
+
%w[Tempfile Thread FileUtils Open3].include?(recv) ||
|
|
77
|
+
(%w[File IO Dir].include?(recv) && FILE_CALLS.include?(node.name))
|
|
69
78
|
end
|
|
70
79
|
|
|
71
80
|
def large_literal?(node)
|
|
@@ -85,7 +94,7 @@ module Scrap4rb
|
|
|
85
94
|
end
|
|
86
95
|
|
|
87
96
|
def control_call?(node)
|
|
88
|
-
assertion?(node) || context_head?(node) || stub_call?(node) || TABLE_CALLS.include?(node.name) ||
|
|
97
|
+
assertion?(node) || context_head?(node) || stub_call?(node) || TABLE_CALLS.include?(node.name) || node.name == :loop ||
|
|
89
98
|
TEST_CALLS.include?(node.name) || SETUP_CALLS.include?(node.name) || !node.name.match?(/\A[a-z_]/i)
|
|
90
99
|
end
|
|
91
100
|
|
data/lib/scrap4rb/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: scrap4rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Robert Hopman
|
|
@@ -71,6 +71,8 @@ files:
|
|
|
71
71
|
- CHANGELOG.md
|
|
72
72
|
- LICENSE
|
|
73
73
|
- README.md
|
|
74
|
+
- docs/compare-verdict.md
|
|
75
|
+
- docs/setup-depth.md
|
|
74
76
|
- exe/scrap4rb
|
|
75
77
|
- lib/scrap4rb.rb
|
|
76
78
|
- lib/scrap4rb/cli.rb
|