scrap4rb 0.2.0 → 0.2.2
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 +14 -1
- data/README.md +15 -8
- data/lib/scrap4rb/example.rb +4 -2
- data/lib/scrap4rb/syntax.rb +28 -0
- data/lib/scrap4rb/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6b7d1598905c975218cc1e268749cb4ab21ef2277fc28414536dcb6e8c0132fd
|
|
4
|
+
data.tar.gz: ca6df9174a9d75f3a3cdbef37213a2b9f596b15b6661f402c1705d674c085f7e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1f858b26273076b66cfc9d2f1abde656f81a18c0cd0129a075ea9f5f33b180b0e90f4c9032562829a1417b4c19d39965982f45ecef3a947cced2e1af31631a04
|
|
7
|
+
data.tar.gz: d01334fa91516614bbb58a909650bb99f2acb3100741daef2bd4b66ed8bdad2f630bd60f633adb0f30e431c73ecd68750e82d2d11ef6d9e32cf9c5921c8a71fe
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,17 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.2] - 2026-11-01
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- The `symmetric-fixture` smell, penalty 3. It fires when one arrange form gives the same number or string twice, such as `Zone.new(area_m2: 50, plint_m: 50)`. A test like that cannot tell a swap of the two fields apart. It is not a SCRAP smell. See Deviations from SCRAP in the README.
|
|
13
|
+
|
|
14
|
+
## [0.2.1] - 2026-10-16
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- The README tells you to run scrap4rb from the project root when you run it from a checkout. From the scrap4rb folder, scrap4rb does not find the shared assertion helpers of the project, and a test that uses one reads as `no-assertions`.
|
|
18
|
+
- The README lists all structure errors, limits the 35% low-assertion rule to files with more than two tests, and lists three more deviations from SCRAP.
|
|
19
|
+
|
|
9
20
|
## [0.2.0] - 2026-10-15
|
|
10
21
|
|
|
11
22
|
### Changed
|
|
@@ -37,6 +48,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
37
48
|
`describe`/`it` blocks. A `define_method` test inside a loop over a table
|
|
38
49
|
(`CASES.each`) is one table-driven test that spans the loop.
|
|
39
50
|
|
|
40
|
-
[Unreleased]: https://github.com/roberthopman/scrap4rb/compare/v0.2.
|
|
51
|
+
[Unreleased]: https://github.com/roberthopman/scrap4rb/compare/v0.2.2...HEAD
|
|
52
|
+
[0.2.2]: https://github.com/roberthopman/scrap4rb/compare/v0.2.1...v0.2.2
|
|
53
|
+
[0.2.1]: https://github.com/roberthopman/scrap4rb/compare/v0.2.0...v0.2.1
|
|
41
54
|
[0.2.0]: https://github.com/roberthopman/scrap4rb/compare/v0.1.0...v0.2.0
|
|
42
55
|
[0.1.0]: https://github.com/roberthopman/scrap4rb/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -22,10 +22,11 @@ Or add it to a Gemfile:
|
|
|
22
22
|
gem "scrap4rb", group: :development
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
From a checkout you can run it without installing:
|
|
25
|
+
From a checkout you can run it without installing. Run it from the project root, because scrap4rb finds the shared assertion helpers from the current directory:
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
|
|
28
|
+
cd path/to/project
|
|
29
|
+
ruby -I path/to/scrap4rb/lib path/to/scrap4rb/exe/scrap4rb
|
|
29
30
|
```
|
|
30
31
|
|
|
31
32
|
## Usage
|
|
@@ -52,6 +53,8 @@ The comparison gives each changed file a verdict: `improved`, `worse`, `mixed` o
|
|
|
52
53
|
|
|
53
54
|
## Output
|
|
54
55
|
|
|
56
|
+
An example, with some lines left out and some lines shortened:
|
|
57
|
+
|
|
55
58
|
```
|
|
56
59
|
test/models/smelly_test.rb
|
|
57
60
|
score 48.46 HIGH mode LOCAL ai AUTO_REFACTOR tests=6 avg=12.78 max=18.26
|
|
@@ -85,8 +88,8 @@ EXTRACT A group of tests where a shared helper pays for itself
|
|
|
85
88
|
|
|
86
89
|
Structure errors:
|
|
87
90
|
|
|
88
|
-
- a `test` inside a
|
|
89
|
-
- a `setup`, `teardown`, `describe`, `context` or `class` inside a test
|
|
91
|
+
- a `test`, `it` or `specify` inside a test
|
|
92
|
+
- a `setup`, `teardown`, `before`, `after`, `describe`, `context`, `def setup` or `class` inside a test
|
|
90
93
|
- a second `def test_x` with the same name in one class (the first one never runs)
|
|
91
94
|
- parse errors
|
|
92
95
|
|
|
@@ -103,6 +106,7 @@ large-example more than 20 lines 4
|
|
|
103
106
|
helper-hidden-complexity more than 8 lines hidden in local helpers 4
|
|
104
107
|
temp-resource-work files, directories, threads or processes 3
|
|
105
108
|
literal-heavy-setup a string over 5 lines, a hash or array over 10 3
|
|
109
|
+
symmetric-fixture the same value twice in one arrange fixture 3
|
|
106
110
|
```
|
|
107
111
|
|
|
108
112
|
A short test with one or two assertions and few subjects is an "API contract" test. SCRAP excuses it from the low-assertion-density and large-example smells.
|
|
@@ -136,16 +140,19 @@ The policy numbers (weights, thresholds, levels) are SCRAP's, unchanged.
|
|
|
136
140
|
## Deviations from SCRAP
|
|
137
141
|
|
|
138
142
|
- 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.
|
|
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".
|
|
143
|
+
- 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. scrap4rb finds these files from the current directory. SCRAP only reads the spec file itself. Without this rule, every test that uses a shared assertion helper would be "no-assertions".
|
|
140
144
|
- 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
145
|
- `--compare` does not call new tests that use a helper a helper regression. See `docs/compare-verdict.md`.
|
|
146
|
+
- `symmetric-fixture` is not a SCRAP smell. It fires when one arrange form gives the same number or string twice, as in `Zone.new(area_m2: 50, plint_m: 50)`. A test like that cannot tell a swap of the two fields apart. The values `0`, `1` and `""` do not count, because a test about zero cannot pick another value. Assertions and shared `setup` blocks are not read: a repeated value in an expected result is the result, and one repeated value in a shared fixture would mark every test in the class.
|
|
147
|
+
- The `where` lines show only for a file with more than one describe block or class. SCRAP shows them for each file.
|
|
148
|
+
- A `describe` inside a `describe` is not a structure error, because minitest/spec nests them. A second test with the same name is a structure error. SCRAP has no such check.
|
|
142
149
|
|
|
143
150
|
## Notes for Rails suites
|
|
144
151
|
|
|
145
152
|
Two SCRAP rules can disagree with common Rails practice. Decide for your own suite.
|
|
146
153
|
|
|
147
154
|
- **multiple-phases**: a system test that asserts after each click (to prove that the page changed before the next step) has several phases, and SCRAP gives it 5 points.
|
|
148
|
-
- **Low-assertion files**: a file is not STABLE when more than 35% of its tests have one assertion or fewer. A suite with "one behavior, one assertion" tests will have many LOCAL files for this reason only.
|
|
155
|
+
- **Low-assertion files**: a file with more than two tests is not STABLE when more than 35% of its tests have one assertion or fewer. A suite with "one behavior, one assertion" tests will have many LOCAL files for this reason only.
|
|
149
156
|
|
|
150
157
|
## Development
|
|
151
158
|
|
|
@@ -159,9 +166,9 @@ The golden tests run the executable against the fixture suite in `test/fixtures/
|
|
|
159
166
|
|
|
160
167
|
## Credits
|
|
161
168
|
|
|
162
|
-
SCRAP is by Robert C. Martin (Uncle Bob): [github.com/unclebob/scrap](https://github.com/unclebob/scrap). The algorithm, the metrics, the smells and the policy numbers in scrap4rb come from SCRAP. scrap4rb is an independent Ruby implementation. It contains no SCRAP source code.
|
|
169
|
+
SCRAP is by Robert C. Martin (Uncle Bob): [github.com/unclebob/scrap](https://github.com/unclebob/scrap). The algorithm, the metrics, the smells and the policy numbers in scrap4rb come from SCRAP, except the `symmetric-fixture` smell, which is listed under Deviations from SCRAP. scrap4rb is an independent Ruby implementation. It contains no SCRAP source code.
|
|
163
170
|
|
|
164
|
-
scrap4rb
|
|
171
|
+
scrap4rb follows SCRAP at commit [`f793b28`](https://github.com/unclebob/scrap/tree/f793b28906d3c60e30b128e1ec08df6269a72b13) (2026-03-17). When SCRAP changes its rules or numbers, scrap4rb can differ until it follows again.
|
|
165
172
|
|
|
166
173
|
Related tools by the same author: [crap4clj](https://github.com/unclebob/crap4clj), [crap4java](https://github.com/unclebob/crap4java), [crap4go](https://github.com/unclebob/crap4go) and [clj-mutate](https://github.com/unclebob/clj-mutate).
|
|
167
174
|
|
data/lib/scrap4rb/example.rb
CHANGED
|
@@ -108,7 +108,7 @@ module Scrap4rb
|
|
|
108
108
|
m[:large_literals] <= 0 && m[:subject_symbols].size <= 4 && phases <= 1 && line_count <= 18
|
|
109
109
|
end
|
|
110
110
|
|
|
111
|
-
def smells(m, line_count, phases, table_driven, api_contract)
|
|
111
|
+
def smells(m, line_count, phases, table_driven, api_contract, symmetric_fixture = false)
|
|
112
112
|
[
|
|
113
113
|
[m[:assertions].zero?, "no-assertions", 10],
|
|
114
114
|
[m[:assertions] == 1 && line_count > 10 && !table_driven && !api_contract, "low-assertion-density", 6],
|
|
@@ -117,6 +117,7 @@ module Scrap4rb
|
|
|
117
117
|
[line_count > 20 && !api_contract, "large-example", 4],
|
|
118
118
|
[m[:temp_resources].positive?, "temp-resource-work", 3],
|
|
119
119
|
[m[:large_literals].positive?, "literal-heavy-setup", 3],
|
|
120
|
+
[symmetric_fixture, "symmetric-fixture", 3],
|
|
120
121
|
[m[:helper_hidden_lines] > 8, "helper-hidden-complexity", 4]
|
|
121
122
|
].select(&:first).map { { label: _2, penalty: _3 } }
|
|
122
123
|
end
|
|
@@ -131,12 +132,13 @@ module Scrap4rb
|
|
|
131
132
|
setup_depth = api ? [0, m[:max_setup_depth] - 2].max : m[:max_setup_depth]
|
|
132
133
|
branch_penalty = m[:table_driven] ? m[:table_branches] : m[:branches] + m[:table_branches]
|
|
133
134
|
branch_penalty = [0, branch_penalty - 2].max if api
|
|
134
|
-
found = smells(m, line_count, phases, m[:table_driven], api)
|
|
135
135
|
complexity_score = saturating(1 + branch_penalty + setup_depth + m[:helper_calls])
|
|
136
136
|
|
|
137
137
|
setup = inherited_setup + forms.select { context_head?(_1) }
|
|
138
138
|
asserts = forms.select { phase(_1, ctx) == :assert }
|
|
139
139
|
arrange = forms.reject { phase(_1, ctx) == :assert }
|
|
140
|
+
symmetric = arrange.any? { |f| [f, *descendants(f)].any? { symmetric_fixture?(_1) } }
|
|
141
|
+
found = smells(m, line_count, phases, m[:table_driven], api, symmetric)
|
|
140
142
|
setup_signatures = form_signatures(setup)
|
|
141
143
|
literal_signatures = body.flat_map { large_literal_signatures(_1) }
|
|
142
144
|
{
|
data/lib/scrap4rb/syntax.rb
CHANGED
|
@@ -14,6 +14,7 @@ module Scrap4rb
|
|
|
14
14
|
BLOCK_CALLS = %i[describe context].to_set
|
|
15
15
|
SETUP_CALLS = %i[setup before].to_set
|
|
16
16
|
LET_CALLS = %i[let let! subject].to_set
|
|
17
|
+
DEGENERATE_VALUES = [0, 1, "0", "1", ""].freeze
|
|
17
18
|
|
|
18
19
|
extend self
|
|
19
20
|
|
|
@@ -93,6 +94,33 @@ module Scrap4rb
|
|
|
93
94
|
end
|
|
94
95
|
end
|
|
95
96
|
|
|
97
|
+
def fixture_value(node)
|
|
98
|
+
value = case node
|
|
99
|
+
when Prism::IntegerNode, Prism::FloatNode then node.value
|
|
100
|
+
when Prism::StringNode then node.unescaped
|
|
101
|
+
end
|
|
102
|
+
value unless DEGENERATE_VALUES.include?(value)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def argument_groups(call)
|
|
106
|
+
hashes, plain = (call.arguments&.arguments || []).partition { _1.is_a?(Prism::HashNode) || _1.is_a?(Prism::KeywordHashNode) }
|
|
107
|
+
[plain] + hashes.map { _1.elements.grep(Prism::AssocNode).map(&:value) }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def repeated_fixture_values?(nodes)
|
|
111
|
+
values = nodes.filter_map { fixture_value(_1) }
|
|
112
|
+
values.size >= 2 && values.uniq.size < values.size
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def symmetric_fixture?(node)
|
|
116
|
+
groups = case node
|
|
117
|
+
when Prism::ArrayNode then [node.elements]
|
|
118
|
+
when Prism::CallNode then argument_groups(node)
|
|
119
|
+
else []
|
|
120
|
+
end
|
|
121
|
+
groups.any? { repeated_fixture_values?(_1) }
|
|
122
|
+
end
|
|
123
|
+
|
|
96
124
|
def control_call?(node)
|
|
97
125
|
assertion?(node) || context_head?(node) || stub_call?(node) || TABLE_CALLS.include?(node.name) || node.name == :loop ||
|
|
98
126
|
TEST_CALLS.include?(node.name) || SETUP_CALLS.include?(node.name) || !node.name.match?(/\A[a-z_]/i)
|
data/lib/scrap4rb/version.rb
CHANGED