fix 1.0.0.beta11 → 1.0.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/LICENSE.md +2 -2
- data/README.md +189 -155
- data/lib/fix/dsl.rb +149 -97
- data/lib/fix/environment.rb +25 -0
- data/lib/fix/level.rb +40 -0
- data/lib/fix/matcher.rb +50 -191
- data/lib/fix/node.rb +32 -0
- data/lib/fix/result.rb +36 -0
- data/lib/fix/spec.rb +167 -0
- data/lib/fix.rb +42 -12
- data/lib/kernel.rb +10 -31
- metadata +35 -19
- data/lib/fix/doc.rb +0 -24
- data/lib/fix/requirement.rb +0 -87
- data/lib/fix/run.rb +0 -55
- data/lib/fix/set.rb +0 -67
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 56afffa92641798956f66d8015ffaa394b229ca6a7a1af4f73aef233089b3d94
|
|
4
|
+
data.tar.gz: b6aa5f4f2cd7674d38e08ac78b3f53def1996760274c478676e1619dae5e809a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e50bb8c771b9bed55871df7d4115e7a0a50c3661ee09e86a82e6cc3def766c867f964e4f949c75d1a8191345918629ace15fad600d9906d6bf829ac9d31b3be5
|
|
7
|
+
data.tar.gz: 969531acec4e9f78a0df7641bfde3ca272bccc7cc038b2866e6ce406c9f6b3e339a73889a4ace65dd281d4663610a7559bcca289b1578c757e509e2e254f4ffd
|
data/LICENSE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
The MIT License (MIT)
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2014-
|
|
3
|
+
Copyright (c) 2014-2026 Cyril Kato
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
data/README.md
CHANGED
|
@@ -1,257 +1,291 @@
|
|
|
1
|
-
# Fix
|
|
1
|
+
# Fix
|
|
2
2
|
|
|
3
|
-
[](https://fixrb.dev/)
|
|
4
3
|
[](https://github.com/fixrb/fix/tags)
|
|
5
4
|
[](https://rubydoc.info/github/fixrb/fix/main)
|
|
6
5
|
[](https://github.com/fixrb/fix/raw/main/LICENSE.md)
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
Fix is a modern Ruby testing framework that emphasizes clear separation between specifications and examples. Unlike traditional testing frameworks, Fix focuses on creating pure specification documents that define expected behaviors without mixing in implementation details.
|
|
7
|
+
Fix writes a specification: what a subject MUST, SHOULD or MAY do, with the words of [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119), and nothing about how the subject is made. The specification is then tested against a subject, each context in a child process, and every expectation gets a result.
|
|
11
8
|
|
|
12
9
|
## Installation
|
|
13
10
|
|
|
14
|
-
Add to your Gemfile:
|
|
15
|
-
|
|
16
11
|
```ruby
|
|
17
|
-
|
|
12
|
+
# Gemfile
|
|
13
|
+
gem "fix"
|
|
18
14
|
```
|
|
19
15
|
|
|
20
|
-
|
|
16
|
+
Fix requires Ruby 3.3 or later on a system with `fork`, and depends on [Aw](https://github.com/fixrb/aw), [Defi](https://github.com/fixrb/defi), [Matchi](https://github.com/fixrb/matchi) and [Spectus](https://github.com/fixrb/spectus).
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
bundle install
|
|
24
|
-
```
|
|
18
|
+
## Usage
|
|
25
19
|
|
|
26
|
-
|
|
20
|
+
A file writes a specification, then tests a subject against it:
|
|
27
21
|
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
```ruby
|
|
23
|
+
# duck_fix.rb
|
|
24
|
+
require "fix"
|
|
31
25
|
|
|
32
|
-
|
|
26
|
+
class Duck
|
|
27
|
+
def walk = "Klop klop!"
|
|
28
|
+
def swim = "Swoosh..."
|
|
29
|
+
end
|
|
33
30
|
|
|
34
|
-
|
|
31
|
+
Fix :Duck do
|
|
32
|
+
on :walk do
|
|
33
|
+
it MUST eq "Klop klop!"
|
|
34
|
+
end
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
on :swim do
|
|
37
|
+
it MUST eq "Swoosh..."
|
|
38
|
+
end
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
on :speak do
|
|
41
|
+
it MAY eq "Quaaaaaack!"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
39
44
|
|
|
40
|
-
|
|
45
|
+
exit(Fix[:Duck].test { Duck.new })
|
|
46
|
+
```
|
|
41
47
|
|
|
42
|
-
|
|
48
|
+
```text
|
|
49
|
+
duck_fix.rb:11: Success: expected "Klop klop!" to eq "Klop klop!"
|
|
50
|
+
duck_fix.rb:15: Success: expected "Swoosh..." to eq "Swoosh..."
|
|
51
|
+
duck_fix.rb:19: Info: NoMethodError: undefined method 'speak' for an instance of Duck
|
|
52
|
+
```
|
|
43
53
|
|
|
44
|
-
|
|
54
|
+
`Fix :Duck do … end` builds the specification and registers it under its name; `Fix[:Duck]` finds it again. `test` builds a new subject with the block for every expectation, sends it the methods of the `on` around it, judges what comes out, writes one line per expectation, the backtrace of an error below it, and returns whether all of them pass (`true` when there is nothing to test). `exit` turns that into the status of the process: 0 when the specification is met, 1 otherwise.
|
|
45
55
|
|
|
46
|
-
|
|
56
|
+
Keep the parentheses: `exit Fix[:Duck].test do … end` gives the block to `exit`, and `test` raises `ArgumentError` for want of one. A file with several specifications tests them all before it exits, as `exit([Fix[:A].test { … }, Fix[:B].test { … }].all?)` does.
|
|
47
57
|
|
|
48
|
-
|
|
49
|
-
Fix do
|
|
50
|
-
let(:name) { "Bob" }
|
|
51
|
-
let(:age) { 42 }
|
|
58
|
+
## Writing a specification
|
|
52
59
|
|
|
53
|
-
|
|
54
|
-
end
|
|
55
|
-
```
|
|
60
|
+
The block of `Fix` is a document, read once when the file is loaded, with four words: `let`, `with`, `on` and `it`. Each `with`, each `on` opens a context, which inherits from the one around it.
|
|
56
61
|
|
|
57
|
-
###
|
|
62
|
+
### `on`
|
|
58
63
|
|
|
59
|
-
|
|
64
|
+
`on :name, *arguments, **keywords` sends a method to the subject, and the expectations inside judge what it returns. The `on` inside an `on` send their method to that value, and so on; with no `on` around, an expectation judges the subject itself.
|
|
60
65
|
|
|
61
66
|
```ruby
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
require "fix"
|
|
68
|
+
|
|
69
|
+
Fix :Answer do
|
|
70
|
+
it MUST be_an_instance_of Integer
|
|
71
|
+
|
|
72
|
+
on :+, 2 do
|
|
73
|
+
it MUST eq 44
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
on :to_s do
|
|
77
|
+
on :size do
|
|
78
|
+
it MUST eq 2
|
|
79
|
+
end
|
|
65
80
|
end
|
|
66
81
|
|
|
67
|
-
|
|
68
|
-
it
|
|
82
|
+
on :digits do
|
|
83
|
+
it MUST eq [2, 4]
|
|
69
84
|
end
|
|
70
85
|
end
|
|
86
|
+
|
|
87
|
+
exit(Fix[:Answer].test { 42 })
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```text
|
|
91
|
+
example.rb:4: Success: expected 42 to be an instance of Integer
|
|
92
|
+
example.rb:7: Success: expected 44 to eq 44
|
|
93
|
+
example.rb:12: Success: expected 2 to eq 2
|
|
94
|
+
example.rb:17: Success: expected [2, 4] to eq [2, 4]
|
|
71
95
|
```
|
|
72
96
|
|
|
73
|
-
|
|
97
|
+
A method that raises ends the chain there, and the expectation judges the exception: an error for most matchers, what `raise_exception` is for. There is no helper for a predicate: `on(:empty?) { it MUST be_true }` asks the question. The method is sent without a block, since the block of `on` is the context: a method that needs one is called in the subject block, or tested with `satisfy`. A `BasicObject` has no `public_send`, so an `on` sent to it is an error.
|
|
74
98
|
|
|
75
|
-
|
|
99
|
+
### `let` and `with`
|
|
100
|
+
|
|
101
|
+
A `let` is a parameter of the subject: a name and a block, for the block of `test` to call when it builds the subject. Its value is computed the first time it is called and kept for the rest of the expectation, and a `let` may call another. `with name: value` opens a context where the parameter has that value.
|
|
76
102
|
|
|
77
103
|
```ruby
|
|
78
|
-
|
|
79
|
-
on :upcase do
|
|
80
|
-
it MUST eq "HELLO"
|
|
81
|
-
end
|
|
104
|
+
require "fix"
|
|
82
105
|
|
|
83
|
-
|
|
84
|
-
|
|
106
|
+
Fix :Greeting do
|
|
107
|
+
let(:name) { "Bob" }
|
|
108
|
+
|
|
109
|
+
it MUST eq "Hello, Bob!"
|
|
110
|
+
|
|
111
|
+
with name: "Alice" do
|
|
112
|
+
it MUST eq "Hello, Alice!"
|
|
85
113
|
end
|
|
86
114
|
end
|
|
115
|
+
|
|
116
|
+
exit(Fix[:Greeting].test { "Hello, #{name}!" })
|
|
87
117
|
```
|
|
88
118
|
|
|
89
|
-
|
|
119
|
+
```text
|
|
120
|
+
example.rb:6: Success: expected "Hello, Bob!" to eq "Hello, Bob!"
|
|
121
|
+
example.rb:9: Success: expected "Hello, Alice!" to eq "Hello, Alice!"
|
|
122
|
+
```
|
|
90
123
|
|
|
91
|
-
|
|
124
|
+
The block of `test` runs on a fresh object for every expectation, where the lets of its context are methods: it sees them, and sees `Kernel` and the methods of the top level as any code does, but not the instance variables of the file. The value of a `with` is the object given, shared by the expectations of its context: `with items: []` gives the same array to each of them, where `let(:items) { [] }` gives a new one to each.
|
|
92
125
|
|
|
93
|
-
|
|
94
|
-
- **SHOULD/SHOULD_NOT**: Recommended practices with valid exceptions
|
|
95
|
-
- **MAY**: Optional features
|
|
126
|
+
A let is visible from the subject and from the other lets only. Named in an expectation, where it would be read before any subject exists, it raises `NameError` at load, with the remedy: `name is a let, visible in the subject block and in other lets only`. Its name cannot be a method of the object the subject is built on, public or private, nor a word of the language, so that nothing is redefined in silence: `let(:format)`, `let(:p)` or `let(:it)` raise `ArgumentError` at load, naming the owner (`format is already a method of Kernel`). A `with` has the same rule, and declares the lets it names: a name no `let` declared is a new parameter, not a mistake. A context may declare a name once, inherits the lets declared above it in the contexts around it, and may give a new value to one of them.
|
|
96
127
|
|
|
97
|
-
|
|
98
|
-
Fix do
|
|
99
|
-
it MUST be_valid # Required
|
|
100
|
-
it SHOULD be_optimized # Recommended
|
|
101
|
-
it MAY include_metadata # Optional
|
|
102
|
-
end
|
|
103
|
-
```
|
|
128
|
+
### `it`
|
|
104
129
|
|
|
105
|
-
|
|
130
|
+
`it` takes a requirement, not a block: a level, then a matcher, as in `it MUST eq 42`. The levels are `MUST`, `MUST_NOT`, `SHOULD`, `SHOULD_NOT` and `MAY`, the words of RFC 2119, which has no negative MAY. The matchers are those of Matchi:
|
|
106
131
|
|
|
107
|
-
|
|
132
|
+
| Word | Meets when the actual value… |
|
|
133
|
+
|------|------------------------------|
|
|
134
|
+
| `eq(expected)` | is `eql?` to the expected one |
|
|
135
|
+
| `be(expected)` | is the very same object (`equal?`) |
|
|
136
|
+
| `be_true`, `be_false`, `be_nil` | is `true`, `false`, `nil` |
|
|
137
|
+
| `be_within(delta).of(expected)` | is a number within the delta |
|
|
138
|
+
| `match(regexp)` | is a String or a Symbol that matches |
|
|
139
|
+
| `be_an_instance_of(klass)` | is an instance of that very class |
|
|
140
|
+
| `be_a_kind_of(mod)` | is an instance of that module or of a descendant |
|
|
141
|
+
| `raise_exception(klass, message = nil)` | is an exception of that class or of a subclass, with that message (a String, or a Regexp to match) when given |
|
|
142
|
+
| `change(object, :method)`, `.by(n)`, `.by_at_least(n)`, `.by_at_most(n)`, `.from(a).to(b)`, `.to(b)` | makes that method of that object change, so when refined |
|
|
143
|
+
| `satisfy { \|value\| … }` | makes the block answer `true`; an answer that is neither `true` nor `false` is an error |
|
|
144
|
+
| `fix(:Name)`, `fix { … }` | meets that specification, by name or written here |
|
|
145
|
+
|
|
146
|
+
Any other object with `match?` and `to_s` is a matcher too. The object of a `change` must exist when the file is loaded, since the matcher is built there: a let, which lives with the subject, cannot be observed. A specification is a matcher that tests its expectations against the actual value, so one specification may require another:
|
|
108
147
|
|
|
109
148
|
```ruby
|
|
110
|
-
# first_test.rb
|
|
111
149
|
require "fix"
|
|
112
150
|
|
|
113
|
-
Fix :
|
|
114
|
-
|
|
151
|
+
Fix :Positive do
|
|
152
|
+
on :positive? do
|
|
153
|
+
it MUST be_true
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
Fix :Score do
|
|
158
|
+
it MUST be_a_kind_of Numeric
|
|
159
|
+
it MUST fix(:Positive)
|
|
115
160
|
end
|
|
116
161
|
|
|
117
|
-
Fix[:
|
|
162
|
+
exit(Fix[:Score].test { 7 })
|
|
118
163
|
```
|
|
119
164
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
ruby first_test.rb
|
|
165
|
+
```text
|
|
166
|
+
example.rb:10: Success: expected 7 to be a kind of Numeric
|
|
167
|
+
example.rb:11: Success: expected 7 to fix Positive
|
|
124
168
|
```
|
|
125
169
|
|
|
126
|
-
|
|
170
|
+
A failed `fix` says that the specification is not met, not which of its expectations failed: test the specification on its own for that.
|
|
127
171
|
|
|
128
|
-
|
|
172
|
+
In Ruby 3.4 and later, a bare `it` in a block is the parameter of the block, while `it` with a requirement is the word of Fix, as before; on Ruby 3.3 the bare `it` is the word of Fix too. A `satisfy` meant for both names its parameter: `satisfy { |value| … }`.
|
|
129
173
|
|
|
130
|
-
|
|
174
|
+
The document runs on an instance of `Fix::Dsl`, and a gem may add a word to it that composes `on` and `it`: fix-its adds `its`. Such a word gives `it` the `location:` of its own caller, what `caller_locations(1, 1).first` gives, an object with `path` and `lineno`, so that the result points at the line of the specification.
|
|
131
175
|
|
|
132
|
-
|
|
176
|
+
### Loading
|
|
133
177
|
|
|
134
|
-
|
|
135
|
-
Fix :UserAccount do
|
|
136
|
-
# Define reusable properties
|
|
137
|
-
let(:admin) { User.new(role: "admin") }
|
|
138
|
-
let(:guest) { User.new(role: "guest") }
|
|
178
|
+
The specification is built when its block runs, before any subject exists, and nothing in it waits for a test: a name that is not a Symbol, a name already registered, a `let`, a `with` or an `on` without a block, a `with` without a value, an `it` with a block or without a requirement, a matcher without `match?` or whose `to_s` is not a `String`, raise then. A block that raises registers nothing. `Fix[:Name]` raises `KeyError` for a name it does not know, and `fix(:Name)` finds its specification then: write it first.
|
|
139
179
|
|
|
140
|
-
|
|
141
|
-
it MUST be_an_instance_of User
|
|
180
|
+
`Fix.spec(:Name) { … }` is the method `Fix` calls; without a name, both give a specification that is not registered. The registry is written by the thread that loads the file, read afterwards, and never emptied: a runner runs each file in a process of its own.
|
|
142
181
|
|
|
143
|
-
|
|
144
|
-
with role: "admin" do
|
|
145
|
-
it MUST be_admin
|
|
182
|
+
## Outcomes
|
|
146
183
|
|
|
147
|
-
|
|
148
|
-
it MUST be_true
|
|
149
|
-
end
|
|
150
|
-
end
|
|
184
|
+
Two questions tell the levels apart. Does the method exist? MUST and SHOULD require it, MAY does not. Does it do what is specified? MUST and MAY require it, SHOULD recommends it. Hence five outcomes:
|
|
151
185
|
|
|
152
|
-
|
|
153
|
-
|
|
186
|
+
| Level | Match is true | Match is false | Missing method | Other exception |
|
|
187
|
+
|--------|---------------|----------------|----------------|-----------------|
|
|
188
|
+
| MUST | success | failure | error | error |
|
|
189
|
+
| SHOULD | success | warning | error | error |
|
|
190
|
+
| MAY | success | failure | info | error |
|
|
154
191
|
|
|
155
|
-
|
|
156
|
-
it MUST be_false
|
|
157
|
-
end
|
|
158
|
-
end
|
|
192
|
+
A negation swaps the first two columns. A success, a warning and an info pass; a failure and an error do not. The missing method is the one named by the `on` of the context, when sending it raises `NoMethodError` for that method on that object, as the duck has no `speak`; Spectus adds the method a matcher calls on the actual value. A private method counts as missing, as does a method that exists and raises `NoMethodError` for itself; a method that raises `NoMethodError` on another object is an error.
|
|
159
193
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
it MUST eq "John Doe"
|
|
164
|
-
end
|
|
165
|
-
end
|
|
194
|
+
```ruby
|
|
195
|
+
# account_fix.rb
|
|
196
|
+
require "fix"
|
|
166
197
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
198
|
+
class Account
|
|
199
|
+
def initialize(balance)
|
|
200
|
+
@balance = balance
|
|
170
201
|
end
|
|
171
|
-
end
|
|
172
|
-
```
|
|
173
202
|
|
|
174
|
-
|
|
175
|
-
- Using `let` to define test fixtures
|
|
176
|
-
- Context-specific testing with `with`
|
|
177
|
-
- Method behavior testing with `on`
|
|
178
|
-
- Different requirement levels with `MUST`/`MUST_NOT`
|
|
179
|
-
- Testing state changes with the `change` matcher
|
|
180
|
-
- Nested contexts for complex scenarios
|
|
203
|
+
attr_reader :balance
|
|
181
204
|
|
|
182
|
-
|
|
205
|
+
def withdraw(amount)
|
|
206
|
+
raise ArgumentError, "insufficient funds" if amount > balance
|
|
183
207
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
Fix :Duck do
|
|
188
|
-
it SHOULD be_an_instance_of :Duck
|
|
208
|
+
@balance -= amount
|
|
209
|
+
end
|
|
210
|
+
end
|
|
189
211
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
it MUST
|
|
212
|
+
Fix :Account do
|
|
213
|
+
on :balance do
|
|
214
|
+
it MUST eq 100
|
|
215
|
+
it MUST eq 200
|
|
216
|
+
it SHOULD eq 200
|
|
193
217
|
end
|
|
194
218
|
|
|
195
|
-
on :
|
|
196
|
-
it
|
|
219
|
+
on :currency do
|
|
220
|
+
it MAY eq "EUR"
|
|
197
221
|
end
|
|
198
222
|
|
|
199
|
-
on :
|
|
200
|
-
it
|
|
223
|
+
on :withdraw, 500 do
|
|
224
|
+
it MUST eq(-400)
|
|
201
225
|
end
|
|
202
226
|
end
|
|
227
|
+
|
|
228
|
+
exit(Fix[:Account].test { Account.new(100) })
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
```text
|
|
232
|
+
account_fix.rb:20: Success: expected 100 to eq 100
|
|
233
|
+
account_fix.rb:21: Failure: expected 100 to eq 200
|
|
234
|
+
account_fix.rb:22: Warning: expected 100 to eq 200
|
|
235
|
+
account_fix.rb:26: Info: NoMethodError: undefined method 'currency' for an instance of Account
|
|
236
|
+
account_fix.rb:30: Error: ArgumentError: insufficient funds
|
|
237
|
+
account_fix.rb:12:in 'Account#withdraw'
|
|
238
|
+
account_fix.rb:34:in '<main>'
|
|
203
239
|
```
|
|
204
240
|
|
|
205
|
-
|
|
241
|
+
## Results
|
|
242
|
+
|
|
243
|
+
`test` writes on `$stdout`, for each expectation, one line: its location in the file, the outcome, and the summary of Spectus, `expected ACTUAL to DESCRIPTION`, with `not` for a negation, or `CLASS: MESSAGE`. A line break in either is written `\n`; a backslash stays as it is, the line being for reading, not for reading back. Below an error come the frames of its backtrace that are not in Fix, Aw, Defi, Matchi or Spectus, indented by two spaces. Nothing else is written: no letter, no colour, no count. What the subject writes on the standard output comes out as it is, before the lines of its context, so a text left without a line break joins the first of them.
|
|
244
|
+
|
|
245
|
+
That grammar, `LOCATION: OUTCOME: SUMMARY` then indented continuation lines, is what a runner reads: the location up to the first `:LINE: OUTCOME: ` of the line, the outcome one of the five words. What the subject writes is beyond the runner's reach; the status of the process is not.
|
|
246
|
+
|
|
247
|
+
`run` gives the same results as data, an `Array` of `Fix::Result` in the order of the contexts, a context before those inside it, and of the expectations within a context:
|
|
206
248
|
|
|
207
249
|
```ruby
|
|
208
|
-
|
|
209
|
-
def walks
|
|
210
|
-
"Klop klop!"
|
|
211
|
-
end
|
|
250
|
+
require "fix"
|
|
212
251
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
end
|
|
252
|
+
Fix :Size do
|
|
253
|
+
it MUST be_an_instance_of String
|
|
216
254
|
|
|
217
|
-
|
|
218
|
-
|
|
255
|
+
on :size do
|
|
256
|
+
it MUST eq 3
|
|
219
257
|
end
|
|
220
258
|
end
|
|
221
|
-
```
|
|
222
259
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
260
|
+
results = Fix[:Size].run { "abcd" }
|
|
261
|
+
results.map(&:outcome) # => [:success, :failure]
|
|
262
|
+
results.map(&:passed?) # => [true, false]
|
|
263
|
+
result = results.last
|
|
264
|
+
result.location # => "example.rb:7"
|
|
265
|
+
result.chain # => ".size"
|
|
266
|
+
result.summary # => "expected 4 to eq 3"
|
|
267
|
+
result.to_s # => "example.rb:7: Failure: expected 4 to eq 3"
|
|
268
|
+
result.result.class # => Spectus::Result
|
|
269
|
+
Fix[:Size].match? { "abc" } # => true
|
|
227
270
|
```
|
|
228
271
|
|
|
229
|
-
|
|
272
|
+
`location` is the file and the line of the `it`, relative to the current directory when under it; `chain` is the methods sent to the subject, as written after a dot with the arguments by their `inspect`, empty for the subject itself; `result` is the `Spectus::Result`, with its `level`, `negate`, `description`, `actual`, `error_class`, `error_message` and `backtrace`, unfiltered. `passed?`, `outcome` and `summary` are those of the result. A result is plain data, frozen.
|
|
230
273
|
|
|
231
|
-
|
|
274
|
+
`match?` tests a value, as a matcher does: the block is called once, where `match?` is called, and its value is the subject of every expectation, so the contexts cannot build it anew with their lets: a specification meant for `fix` is written without `with`.
|
|
232
275
|
|
|
233
|
-
|
|
234
|
-
- **Semantic Precision**: Express requirements with different levels of necessity
|
|
235
|
-
- **Fast Execution**: Get quick feedback on specification compliance
|
|
236
|
-
- **Pure Specifications**: Write specification documents that focus on behavior, not implementation
|
|
237
|
-
- **Rich Matcher Library**: Comprehensive set of matchers for different testing needs
|
|
238
|
-
- **Modern Ruby**: Takes advantage of modern Ruby features and practices
|
|
276
|
+
## Processes
|
|
239
277
|
|
|
240
|
-
|
|
278
|
+
Each context with expectations runs in a child process, forked from the one that tests: a subject that leaves something behind cannot touch the next context, and what it raises, an `exit` included, is an error of its expectation alone. The contexts run one after the other, a context before those inside it, and `test` writes the results of a context when its child returns them.
|
|
241
279
|
|
|
242
|
-
|
|
280
|
+
Nothing is rescued around a child, and Fix never writes a result that did not come from an expectation. An `Interrupt`, or a signal the child could trap, is raised again in the parent and stops the run. So does `Aw::ChildError`, for a child that died, was killed or left by `exit!`, or that a `break`, a `return` or a `throw` in the subject block left without a result: the results of the other expectations of its context are lost with it. The children a `fix` matcher forks are inside a matcher, so what they raise is an error of that expectation. On a Ruby without `fork`, a context with expectations raises `NotImplementedError`.
|
|
243
281
|
|
|
244
|
-
|
|
282
|
+
Each child starts as a copy of the parent, with its memory, its signal handlers and its open files, and ends with `exit!`, which runs no `at_exit` handler. Hence a `Random` seeded in the file gives every context the same sequence, while Ruby reseeds `Kernel#rand` in each child; what a subject writes to an IO other than the standard output and error is lost unless it flushes; and what SimpleCov measures in a child is lost, unless the result is stored before `exit!`, as the tests of Fix do, or the run uses `fork: false`. A subject that forks ends its child with `exit!`, as test code does: a child that runs on into Fix instead, by an `exit` or by nothing at all, is ended by Fix without a result. A `Timeout` or a signal in the parent ends the child at once, but not the children of that child, those a `fix` matcher forks or a subject's own, which run on until their work ends. A specification that requires itself at run time, through `satisfy` say, forks without end, and at the process limit a fork waits for a free slot rather than failing. The cost of a context is one fork of the testing process, a few milliseconds.
|
|
245
283
|
|
|
246
|
-
|
|
247
|
-
- [Bluesky](https://bsky.app/profile/fixrb.dev) - Latest updates and discussions
|
|
248
|
-
- [Documentation](https://www.rubydoc.info/gems/fix) - Comprehensive guides and API reference
|
|
249
|
-
- [Source Code](https://github.com/fixrb/fix) - Contribute and report issues
|
|
250
|
-
- [asciinema](https://asciinema.org/~fix) - Watch practical examples in action
|
|
284
|
+
`test(fork: false) { … }` and `run(fork: false) { … }` run the contexts in the process itself, for a debugger: an `exit!` of the subject then ends the process, what one context leaves behind reaches the next, and a `fix` matcher still forks.
|
|
251
285
|
|
|
252
286
|
## Versioning
|
|
253
287
|
|
|
254
|
-
|
|
288
|
+
Fix follows [Semantic Versioning 2.0](https://semver.org/).
|
|
255
289
|
|
|
256
290
|
## License
|
|
257
291
|
|