sus 0.34.0 → 0.35.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
- checksums.yaml.gz.sig +0 -0
- data/context/getting-started.md +352 -0
- data/context/index.yaml +9 -0
- data/context/mocking.md +100 -30
- data/context/{shared.md → shared-contexts.md} +29 -2
- data/lib/sus/assertions.rb +91 -18
- data/lib/sus/base.rb +13 -1
- data/lib/sus/be.rb +84 -0
- data/lib/sus/be_truthy.rb +16 -0
- data/lib/sus/be_within.rb +25 -0
- data/lib/sus/clock.rb +21 -0
- data/lib/sus/config.rb +58 -1
- data/lib/sus/context.rb +28 -5
- data/lib/sus/describe.rb +14 -0
- data/lib/sus/expect.rb +23 -0
- data/lib/sus/file.rb +38 -0
- data/lib/sus/filter.rb +21 -0
- data/lib/sus/fixtures/temporary_directory_context.rb +27 -0
- data/lib/sus/fixtures.rb +1 -0
- data/lib/sus/have/all.rb +8 -0
- data/lib/sus/have/any.rb +8 -0
- data/lib/sus/have.rb +42 -0
- data/lib/sus/have_duration.rb +16 -0
- data/lib/sus/identity.rb +44 -1
- data/lib/sus/integrations.rb +1 -0
- data/lib/sus/it.rb +33 -0
- data/lib/sus/it_behaves_like.rb +16 -0
- data/lib/sus/let.rb +3 -0
- data/lib/sus/mock.rb +39 -1
- data/lib/sus/output/backtrace.rb +31 -1
- data/lib/sus/output/bar.rb +17 -0
- data/lib/sus/output/buffered.rb +32 -1
- data/lib/sus/output/lines.rb +10 -0
- data/lib/sus/output/messages.rb +26 -3
- data/lib/sus/output/null.rb +16 -2
- data/lib/sus/output/progress.rb +29 -1
- data/lib/sus/output/status.rb +13 -0
- data/lib/sus/output/structured.rb +14 -1
- data/lib/sus/output/text.rb +33 -1
- data/lib/sus/output/xterm.rb +11 -1
- data/lib/sus/output.rb +9 -0
- data/lib/sus/raise_exception.rb +16 -0
- data/lib/sus/receive.rb +82 -0
- data/lib/sus/registry.rb +20 -1
- data/lib/sus/respond_to.rb +29 -2
- data/lib/sus/shared.rb +16 -0
- data/lib/sus/tree.rb +10 -0
- data/lib/sus/version.rb +2 -1
- data/lib/sus/with.rb +18 -0
- data/readme.md +8 -0
- data/releases.md +4 -0
- data.tar.gz.sig +0 -0
- metadata +3 -3
- metadata.gz.sig +0 -0
- data/context/usage.md +0 -380
data/lib/sus/be_within.rb
CHANGED
|
@@ -4,16 +4,25 @@
|
|
|
4
4
|
# Copyright, 2021-2024, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
module Sus
|
|
7
|
+
# Represents a predicate that checks if the subject is within a tolerance of a value.
|
|
7
8
|
class BeWithin
|
|
9
|
+
# Represents a bounded range check.
|
|
8
10
|
class Bounded
|
|
11
|
+
# Initialize a new bounded predicate.
|
|
12
|
+
# @parameter range [Range] The range to check against.
|
|
9
13
|
def initialize(range)
|
|
10
14
|
@range = range
|
|
11
15
|
end
|
|
12
16
|
|
|
17
|
+
# Print a representation of this predicate.
|
|
18
|
+
# @parameter output [Output] The output target.
|
|
13
19
|
def print(output)
|
|
14
20
|
output.write("be within ", :variable, @range, :reset)
|
|
15
21
|
end
|
|
16
22
|
|
|
23
|
+
# Evaluate this predicate against a subject.
|
|
24
|
+
# @parameter assertions [Assertions] The assertions instance to use.
|
|
25
|
+
# @parameter subject [Object] The subject to evaluate.
|
|
17
26
|
def call(assertions, subject)
|
|
18
27
|
assertions.nested(self) do |assertions|
|
|
19
28
|
assertions.assert(@range.include?(subject))
|
|
@@ -21,26 +30,39 @@ module Sus
|
|
|
21
30
|
end
|
|
22
31
|
end
|
|
23
32
|
|
|
33
|
+
# Initialize a new BeWithin predicate.
|
|
34
|
+
# @parameter tolerance [Numeric] The tolerance value.
|
|
24
35
|
def initialize(tolerance)
|
|
25
36
|
@tolerance = tolerance
|
|
26
37
|
end
|
|
27
38
|
|
|
39
|
+
# Create a bounded predicate that checks if the subject is within tolerance of a value.
|
|
40
|
+
# @parameter value [Numeric] The value to check against.
|
|
41
|
+
# @returns [Bounded] A new Bounded predicate.
|
|
28
42
|
def of(value)
|
|
29
43
|
tolerance = @tolerance.abs
|
|
30
44
|
|
|
31
45
|
return Bounded.new(Range.new(value - tolerance, value + tolerance))
|
|
32
46
|
end
|
|
33
47
|
|
|
48
|
+
# Create a bounded predicate that checks if the subject is within a percentage tolerance of a value.
|
|
49
|
+
# @parameter value [Numeric] The value to check against.
|
|
50
|
+
# @returns [Bounded] A new Bounded predicate.
|
|
34
51
|
def percent_of(value)
|
|
35
52
|
tolerance = Rational(@tolerance, 100)
|
|
36
53
|
|
|
37
54
|
return Bounded.new(Range.new(value - value * tolerance, value + value * tolerance))
|
|
38
55
|
end
|
|
39
56
|
|
|
57
|
+
# Print a representation of this predicate.
|
|
58
|
+
# @parameter output [Output] The output target.
|
|
40
59
|
def print(output)
|
|
41
60
|
output.write("be within ", :variable, @tolerance, :reset)
|
|
42
61
|
end
|
|
43
62
|
|
|
63
|
+
# Evaluate this predicate against a subject.
|
|
64
|
+
# @parameter assertions [Assertions] The assertions instance to use.
|
|
65
|
+
# @parameter subject [Object] The subject to evaluate.
|
|
44
66
|
def call(assertions, subject)
|
|
45
67
|
assertions.nested(self) do |assertions|
|
|
46
68
|
assertions.assert(subject < @tolerance, self)
|
|
@@ -49,6 +71,9 @@ module Sus
|
|
|
49
71
|
end
|
|
50
72
|
|
|
51
73
|
class Base
|
|
74
|
+
# Create a predicate that checks if the subject is within a tolerance or range.
|
|
75
|
+
# @parameter value [Numeric, Range] The tolerance value or range to check against.
|
|
76
|
+
# @returns [BeWithin, BeWithin::Bounded] A BeWithin predicate.
|
|
52
77
|
def be_within(value)
|
|
53
78
|
case value
|
|
54
79
|
when Range
|
data/lib/sus/clock.rb
CHANGED
|
@@ -4,17 +4,24 @@
|
|
|
4
4
|
# Copyright, 2022-2023, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
module Sus
|
|
7
|
+
# Represents a clock for measuring elapsed time during test execution.
|
|
7
8
|
class Clock
|
|
8
9
|
include Comparable
|
|
9
10
|
|
|
11
|
+
# Create a new clock and start it immediately.
|
|
12
|
+
# @returns [Clock] A new started clock.
|
|
10
13
|
def self.start!
|
|
11
14
|
self.new.tap(&:start!)
|
|
12
15
|
end
|
|
13
16
|
|
|
17
|
+
# Initialize a new clock.
|
|
18
|
+
# @parameter duration [Float] Initial duration in seconds.
|
|
14
19
|
def initialize(duration = 0.0)
|
|
15
20
|
@duration = duration
|
|
16
21
|
end
|
|
17
22
|
|
|
23
|
+
# Get the current elapsed duration.
|
|
24
|
+
# @returns [Float] The elapsed duration in seconds.
|
|
18
25
|
def duration
|
|
19
26
|
if @start_time
|
|
20
27
|
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
@@ -25,18 +32,27 @@ module Sus
|
|
|
25
32
|
return @duration
|
|
26
33
|
end
|
|
27
34
|
|
|
35
|
+
# Compare this clock's duration with another value.
|
|
36
|
+
# @parameter other [Numeric] The value to compare against.
|
|
37
|
+
# @returns [Integer] -1, 0, or 1 depending on comparison result.
|
|
28
38
|
def <=>(other)
|
|
29
39
|
duration <=> other.to_f
|
|
30
40
|
end
|
|
31
41
|
|
|
42
|
+
# Convert the duration to a float.
|
|
43
|
+
# @returns [Float] The duration in seconds.
|
|
32
44
|
def to_f
|
|
33
45
|
duration
|
|
34
46
|
end
|
|
35
47
|
|
|
48
|
+
# Get the duration in milliseconds.
|
|
49
|
+
# @returns [Float] The duration in milliseconds.
|
|
36
50
|
def ms
|
|
37
51
|
duration * 1000.0
|
|
38
52
|
end
|
|
39
53
|
|
|
54
|
+
# Get a human-readable string representation of the duration.
|
|
55
|
+
# @returns [String] A formatted duration string (e.g., "1.5ms", "2.3s").
|
|
40
56
|
def to_s
|
|
41
57
|
duration = self.duration
|
|
42
58
|
|
|
@@ -49,14 +65,19 @@ module Sus
|
|
|
49
65
|
end
|
|
50
66
|
end
|
|
51
67
|
|
|
68
|
+
# Reset the clock to a specific duration.
|
|
69
|
+
# @parameter duration [Float] The duration to reset to.
|
|
52
70
|
def reset!(duration = 0.0)
|
|
53
71
|
@duration = duration
|
|
54
72
|
end
|
|
55
73
|
|
|
74
|
+
# Start the clock.
|
|
56
75
|
def start!
|
|
57
76
|
@start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
58
77
|
end
|
|
59
78
|
|
|
79
|
+
# Stop the clock and return the final duration.
|
|
80
|
+
# @returns [Float] The final duration in seconds.
|
|
60
81
|
def stop!
|
|
61
82
|
if @start_time
|
|
62
83
|
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
data/lib/sus/config.rb
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2022-
|
|
4
|
+
# Copyright, 2022-2025, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require_relative "clock"
|
|
7
7
|
require_relative "registry"
|
|
8
8
|
|
|
9
9
|
module Sus
|
|
10
|
+
# Represents the configuration for running tests.
|
|
10
11
|
class Config
|
|
12
|
+
# The default path to the configuration file.
|
|
11
13
|
PATH = "config/sus.rb"
|
|
12
14
|
|
|
15
|
+
# Find the configuration file path for the given root directory.
|
|
16
|
+
# @parameter root [String] The root directory to search in.
|
|
17
|
+
# @returns [String | Nil] The path to the configuration file if it exists.
|
|
13
18
|
def self.path(root)
|
|
14
19
|
path = ::File.join(root, PATH)
|
|
15
20
|
|
|
@@ -18,6 +23,10 @@ module Sus
|
|
|
18
23
|
end
|
|
19
24
|
end
|
|
20
25
|
|
|
26
|
+
# Load configuration from the given root directory.
|
|
27
|
+
# @parameter root [String] The root directory to load configuration from.
|
|
28
|
+
# @parameter arguments [Array] Command line arguments to parse.
|
|
29
|
+
# @returns [Config] A new Config instance.
|
|
21
30
|
def self.load(root: Dir.pwd, arguments: ARGV)
|
|
22
31
|
derived = Class.new(self)
|
|
23
32
|
|
|
@@ -34,6 +43,10 @@ module Sus
|
|
|
34
43
|
return derived.new(root, arguments, **options)
|
|
35
44
|
end
|
|
36
45
|
|
|
46
|
+
# Initialize a new Config instance.
|
|
47
|
+
# @parameter root [String] The root directory for the project.
|
|
48
|
+
# @parameter paths [Array] Optional paths to specific test files.
|
|
49
|
+
# @parameter verbose [Boolean] Whether to output verbose information.
|
|
37
50
|
def initialize(root, paths, verbose: false)
|
|
38
51
|
@root = root
|
|
39
52
|
@paths = paths
|
|
@@ -44,6 +57,8 @@ module Sus
|
|
|
44
57
|
self.add_default_load_paths
|
|
45
58
|
end
|
|
46
59
|
|
|
60
|
+
# Add a directory to the load path.
|
|
61
|
+
# @parameter path [String] The path to add.
|
|
47
62
|
def add_load_path(path)
|
|
48
63
|
path = ::File.expand_path(path, @root)
|
|
49
64
|
|
|
@@ -52,36 +67,50 @@ module Sus
|
|
|
52
67
|
end
|
|
53
68
|
end
|
|
54
69
|
|
|
70
|
+
# Add default load paths (lib and fixtures).
|
|
55
71
|
def add_default_load_paths
|
|
56
72
|
add_load_path("lib")
|
|
57
73
|
add_load_path("fixtures")
|
|
58
74
|
end
|
|
59
75
|
|
|
76
|
+
# @attribute [String] The root directory for the project.
|
|
60
77
|
attr :root
|
|
78
|
+
|
|
79
|
+
# @attribute [Array] Optional paths to specific test files.
|
|
61
80
|
attr :paths
|
|
62
81
|
|
|
82
|
+
# @returns [Boolean] Whether verbose output is enabled.
|
|
63
83
|
def verbose?
|
|
64
84
|
@verbose
|
|
65
85
|
end
|
|
66
86
|
|
|
87
|
+
# @returns [Boolean] Whether only a partial set of tests is being run.
|
|
67
88
|
def partial?
|
|
68
89
|
@paths.any?
|
|
69
90
|
end
|
|
70
91
|
|
|
92
|
+
# @returns [Output] The output handler to use.
|
|
71
93
|
def output
|
|
72
94
|
@output ||= Sus::Output.default
|
|
73
95
|
end
|
|
74
96
|
|
|
97
|
+
# The default pattern for finding test files.
|
|
75
98
|
DEFAULT_TEST_PATTERN = "test/**/*.rb"
|
|
76
99
|
|
|
100
|
+
# @returns [Array(String)] Paths to all test files matching the default pattern.
|
|
77
101
|
def test_paths
|
|
78
102
|
return Dir.glob(DEFAULT_TEST_PATTERN, base: @root)
|
|
79
103
|
end
|
|
80
104
|
|
|
105
|
+
# Create a new registry instance.
|
|
106
|
+
# @returns [Registry] A new Registry instance.
|
|
81
107
|
def make_registry
|
|
82
108
|
Sus::Registry.new(root: @root)
|
|
83
109
|
end
|
|
84
110
|
|
|
111
|
+
# Load the test registry, optionally filtering by paths.
|
|
112
|
+
# @parameter paths [Array | Nil] Optional paths to filter tests by.
|
|
113
|
+
# @returns [Registry, Filter] The loaded registry, possibly wrapped in a Filter.
|
|
85
114
|
def load_registry(paths = @paths)
|
|
86
115
|
registry = make_registry
|
|
87
116
|
|
|
@@ -99,14 +128,19 @@ module Sus
|
|
|
99
128
|
return registry
|
|
100
129
|
end
|
|
101
130
|
|
|
131
|
+
# @returns [Registry] The test registry, loading it if necessary.
|
|
102
132
|
def registry
|
|
103
133
|
@registry ||= self.load_registry
|
|
104
134
|
end
|
|
105
135
|
|
|
136
|
+
# Prepare Ruby warnings for deprecated features.
|
|
106
137
|
def prepare_warnings!
|
|
107
138
|
Warning[:deprecated] = true
|
|
108
139
|
end
|
|
109
140
|
|
|
141
|
+
# Called before tests are run.
|
|
142
|
+
# @parameter assertions [Assertions] The assertions instance.
|
|
143
|
+
# @parameter output [Output] The output handler.
|
|
110
144
|
def before_tests(assertions, output: self.output)
|
|
111
145
|
@clock.reset!
|
|
112
146
|
@clock.start!
|
|
@@ -114,6 +148,9 @@ module Sus
|
|
|
114
148
|
prepare_warnings!
|
|
115
149
|
end
|
|
116
150
|
|
|
151
|
+
# Called after tests are run.
|
|
152
|
+
# @parameter assertions [Assertions] The assertions instance.
|
|
153
|
+
# @parameter output [Output] The output handler.
|
|
117
154
|
def after_tests(assertions, output: self.output)
|
|
118
155
|
@clock.stop!
|
|
119
156
|
|
|
@@ -122,6 +159,9 @@ module Sus
|
|
|
122
159
|
|
|
123
160
|
protected
|
|
124
161
|
|
|
162
|
+
# Print a summary of test results.
|
|
163
|
+
# @parameter output [Output] The output handler.
|
|
164
|
+
# @parameter assertions [Assertions] The assertions instance.
|
|
125
165
|
def print_summary(output, assertions)
|
|
126
166
|
assertions.print(output)
|
|
127
167
|
output.puts
|
|
@@ -136,6 +176,9 @@ module Sus
|
|
|
136
176
|
print_failed_assertions(output, assertions)
|
|
137
177
|
end
|
|
138
178
|
|
|
179
|
+
# Print finished statistics.
|
|
180
|
+
# @parameter output [Output] The output handler.
|
|
181
|
+
# @parameter assertions [Assertions] The assertions instance.
|
|
139
182
|
def print_finished_statistics(output, assertions)
|
|
140
183
|
duration = @clock.duration
|
|
141
184
|
rate = assertions.count / duration
|
|
@@ -143,6 +186,9 @@ module Sus
|
|
|
143
186
|
output.puts "🏁 Finished in ", @clock, "; #{rate.round(3)} assertions per second."
|
|
144
187
|
end
|
|
145
188
|
|
|
189
|
+
# Print feedback about the test suite.
|
|
190
|
+
# @parameter output [Output] The output handler.
|
|
191
|
+
# @parameter assertions [Assertions] The assertions instance.
|
|
146
192
|
def print_test_feedback(output, assertions)
|
|
147
193
|
duration = @clock.duration
|
|
148
194
|
rate = assertions.count / duration
|
|
@@ -188,6 +234,10 @@ module Sus
|
|
|
188
234
|
end
|
|
189
235
|
end
|
|
190
236
|
|
|
237
|
+
# Print information about slow tests.
|
|
238
|
+
# @parameter output [Output] The output handler.
|
|
239
|
+
# @parameter assertions [Assertions] The assertions instance.
|
|
240
|
+
# @parameter threshold [Float] The threshold in seconds for considering a test slow.
|
|
191
241
|
def print_slow_tests(output, assertions, threshold = 0.1)
|
|
192
242
|
slowest_tests = assertions.passed.select{|test| test.clock > threshold}.sort_by(&:clock).reverse!
|
|
193
243
|
|
|
@@ -202,6 +252,10 @@ module Sus
|
|
|
202
252
|
end
|
|
203
253
|
end
|
|
204
254
|
|
|
255
|
+
# Print a list of assertions.
|
|
256
|
+
# @parameter output [Output] The output handler.
|
|
257
|
+
# @parameter title [String] The title to print.
|
|
258
|
+
# @parameter assertions [Array] The assertions to print.
|
|
205
259
|
def print_assertions(output, title, assertions)
|
|
206
260
|
if assertions.any?
|
|
207
261
|
output.puts
|
|
@@ -213,6 +267,9 @@ module Sus
|
|
|
213
267
|
end
|
|
214
268
|
end
|
|
215
269
|
|
|
270
|
+
# Print failed and errored assertions.
|
|
271
|
+
# @parameter output [Output] The output handler.
|
|
272
|
+
# @parameter assertions [Assertions] The assertions instance.
|
|
216
273
|
def print_failed_assertions(output, assertions)
|
|
217
274
|
print_assertions(output, "🤔 Failed assertions:", assertions.failed)
|
|
218
275
|
print_assertions(output, "🔥 Errored assertions:", assertions.errored)
|
data/lib/sus/context.rb
CHANGED
|
@@ -7,24 +7,36 @@ require_relative "assertions"
|
|
|
7
7
|
require_relative "identity"
|
|
8
8
|
|
|
9
9
|
module Sus
|
|
10
|
+
# Represents a test context that can contain nested tests and other contexts.
|
|
10
11
|
module Context
|
|
12
|
+
# @attribute [Identity, nil] The identity of this context.
|
|
11
13
|
attr_accessor :identity
|
|
14
|
+
|
|
15
|
+
# @attribute [String, nil] The description of this context.
|
|
12
16
|
attr_accessor :description
|
|
17
|
+
|
|
18
|
+
# @attribute [Hash] The child contexts and tests.
|
|
13
19
|
attr_accessor :children
|
|
14
20
|
|
|
21
|
+
# Called when this module is extended.
|
|
22
|
+
# @parameter base [Class] The class being extended.
|
|
15
23
|
def self.extended(base)
|
|
16
24
|
base.children = Hash.new
|
|
17
25
|
end
|
|
18
26
|
|
|
19
27
|
unless respond_to?(:set_temporary_name)
|
|
28
|
+
# Set a temporary name for this context.
|
|
29
|
+
# @parameter name [String] The temporary name.
|
|
20
30
|
def set_temporary_name(name)
|
|
21
31
|
# No-op.
|
|
22
32
|
end
|
|
23
33
|
|
|
34
|
+
# @returns [String] A string representation of this context.
|
|
24
35
|
def to_s
|
|
25
36
|
(self.description || self.name).to_s
|
|
26
37
|
end
|
|
27
38
|
|
|
39
|
+
# @returns [String] An inspect representation of this context.
|
|
28
40
|
def inspect
|
|
29
41
|
if description = self.description
|
|
30
42
|
"\#<#{self.name || "Context"} #{self.description}>"
|
|
@@ -34,28 +46,37 @@ module Sus
|
|
|
34
46
|
end
|
|
35
47
|
end
|
|
36
48
|
|
|
49
|
+
# Add a child context or test to this context.
|
|
50
|
+
# @parameter child [Object] The child to add.
|
|
37
51
|
def add(child)
|
|
38
52
|
@children[child.identity] = child
|
|
39
53
|
end
|
|
40
54
|
|
|
55
|
+
# @returns [Boolean] Whether this context has no children.
|
|
41
56
|
def empty?
|
|
42
57
|
@children.nil? || @children.empty?
|
|
43
58
|
end
|
|
44
59
|
|
|
60
|
+
# @returns [Boolean] Always returns false, as contexts are not leaf nodes.
|
|
45
61
|
def leaf?
|
|
46
62
|
false
|
|
47
63
|
end
|
|
48
64
|
|
|
65
|
+
# Print a representation of this context.
|
|
66
|
+
# @parameter output [Output] The output target.
|
|
49
67
|
def print(output)
|
|
50
68
|
output.write("context ", :context, self.description, :reset)
|
|
51
69
|
end
|
|
52
70
|
|
|
71
|
+
# @returns [String] The full name of this context.
|
|
53
72
|
def full_name
|
|
54
73
|
output = Output::Buffered.new
|
|
55
74
|
print(output)
|
|
56
75
|
return output.string
|
|
57
76
|
end
|
|
58
77
|
|
|
78
|
+
# Execute all child contexts and tests.
|
|
79
|
+
# @parameter assertions [Assertions] The assertions instance to use.
|
|
59
80
|
def call(assertions)
|
|
60
81
|
return if self.empty?
|
|
61
82
|
|
|
@@ -66,6 +87,8 @@ module Sus
|
|
|
66
87
|
end
|
|
67
88
|
end
|
|
68
89
|
|
|
90
|
+
# Iterate over all leaf nodes (test cases) in this context.
|
|
91
|
+
# @yields {|test| ...} Each test case.
|
|
69
92
|
def each(&block)
|
|
70
93
|
self.children.each do |identity, child|
|
|
71
94
|
if child.leaf?
|
|
@@ -76,11 +99,11 @@ module Sus
|
|
|
76
99
|
end
|
|
77
100
|
end
|
|
78
101
|
|
|
79
|
-
# Include
|
|
102
|
+
# Include a before hook to the context class, that invokes the given block before running each test.
|
|
80
103
|
#
|
|
81
104
|
# Before hooks are usually invoked in the order they are defined, i.e. the first defined hook is invoked first.
|
|
82
105
|
#
|
|
83
|
-
# @
|
|
106
|
+
# @yields {...} The block to execute before each test.
|
|
84
107
|
def before(&hook)
|
|
85
108
|
wrapper = Module.new
|
|
86
109
|
|
|
@@ -93,11 +116,11 @@ module Sus
|
|
|
93
116
|
self.include(wrapper)
|
|
94
117
|
end
|
|
95
118
|
|
|
96
|
-
# Include an
|
|
119
|
+
# Include an after hook to the context class, that invokes the given block after running each test.
|
|
97
120
|
#
|
|
98
121
|
# After hooks are usually invoked in the reverse order they are defined, i.e. the last defined hook is invoked first.
|
|
99
122
|
#
|
|
100
|
-
# @
|
|
123
|
+
# @yields {|error| ...} The block to execute after each test. An `error` argument is passed if the test failed with an exception.
|
|
101
124
|
def after(&hook)
|
|
102
125
|
wrapper = Module.new
|
|
103
126
|
|
|
@@ -118,7 +141,7 @@ module Sus
|
|
|
118
141
|
#
|
|
119
142
|
# The top level `around` implementation invokes before and after hooks.
|
|
120
143
|
#
|
|
121
|
-
# @
|
|
144
|
+
# @yields {|&block| ...} The block to execute around each test.
|
|
122
145
|
def around(&block)
|
|
123
146
|
wrapper = Module.new
|
|
124
147
|
|
data/lib/sus/describe.rb
CHANGED
|
@@ -6,11 +6,19 @@
|
|
|
6
6
|
require_relative "context"
|
|
7
7
|
|
|
8
8
|
module Sus
|
|
9
|
+
# Represents a test group that describes a subject (class, module, or feature).
|
|
9
10
|
module Describe
|
|
10
11
|
extend Context
|
|
11
12
|
|
|
13
|
+
# @attribute [Object] The subject being described.
|
|
12
14
|
attr_accessor :subject
|
|
13
15
|
|
|
16
|
+
# Build a new describe block class.
|
|
17
|
+
# @parameter parent [Class] The parent context class.
|
|
18
|
+
# @parameter subject [Object] The subject to describe.
|
|
19
|
+
# @parameter unique [Boolean] Whether the identity should be unique.
|
|
20
|
+
# @yields {...} Optional block containing nested tests.
|
|
21
|
+
# @returns [Class] A new describe block class.
|
|
14
22
|
def self.build(parent, subject, unique: true, &block)
|
|
15
23
|
base = Class.new(parent)
|
|
16
24
|
base.singleton_class.prepend(Describe)
|
|
@@ -29,6 +37,8 @@ module Sus
|
|
|
29
37
|
return base
|
|
30
38
|
end
|
|
31
39
|
|
|
40
|
+
# Print a representation of this describe block.
|
|
41
|
+
# @parameter output [Output] The output target.
|
|
32
42
|
def print(output)
|
|
33
43
|
output.write(
|
|
34
44
|
"describe ", :describe, self.description, :reset,
|
|
@@ -38,6 +48,10 @@ module Sus
|
|
|
38
48
|
end
|
|
39
49
|
|
|
40
50
|
module Context
|
|
51
|
+
# Define a new test group describing a subject.
|
|
52
|
+
# @parameter subject [Object] The subject to describe (class, module, or feature).
|
|
53
|
+
# @parameter options [Hash] Additional options.
|
|
54
|
+
# @yields {...} Optional block containing nested tests.
|
|
41
55
|
def describe(subject, **options, &block)
|
|
42
56
|
add Describe.build(self, subject, **options, &block)
|
|
43
57
|
end
|
data/lib/sus/expect.rb
CHANGED
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
# Copyright, 2021-2024, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
module Sus
|
|
7
|
+
# Represents an expectation that can be used with predicates to make assertions.
|
|
7
8
|
class Expect
|
|
9
|
+
# Initialize a new Expect instance.
|
|
10
|
+
# @parameter assertions [Assertions] The assertions instance to use.
|
|
11
|
+
# @parameter subject [Object] The subject to make expectations about.
|
|
12
|
+
# @parameter inverted [Boolean] Whether the expectation is inverted (not).
|
|
13
|
+
# @parameter distinct [Boolean] Whether this expectation should be treated as distinct.
|
|
8
14
|
def initialize(assertions, subject, inverted: false, distinct: false)
|
|
9
15
|
@assertions = assertions
|
|
10
16
|
@subject = subject
|
|
@@ -16,15 +22,22 @@ module Sus
|
|
|
16
22
|
@distinct = true
|
|
17
23
|
end
|
|
18
24
|
|
|
25
|
+
# @attribute [Object] The subject being tested.
|
|
19
26
|
attr :subject
|
|
27
|
+
|
|
28
|
+
# @attribute [Boolean] Whether the expectation is inverted.
|
|
20
29
|
attr :inverted
|
|
21
30
|
|
|
31
|
+
# Invert this expectation (expect not).
|
|
32
|
+
# @returns [Expect] A new Expect instance with inverted expectation.
|
|
22
33
|
def not
|
|
23
34
|
self.dup.tap do |expect|
|
|
24
35
|
expect.instance_variable_set(:@inverted, !@inverted)
|
|
25
36
|
end
|
|
26
37
|
end
|
|
27
38
|
|
|
39
|
+
# Print a representation of this expectation.
|
|
40
|
+
# @parameter output [Output] The output target.
|
|
28
41
|
def print(output)
|
|
29
42
|
output.write("expect ", :variable, @inspect, :reset, " ")
|
|
30
43
|
|
|
@@ -35,6 +48,9 @@ module Sus
|
|
|
35
48
|
end
|
|
36
49
|
end
|
|
37
50
|
|
|
51
|
+
# Apply a predicate to this expectation.
|
|
52
|
+
# @parameter predicate [Object] The predicate to apply.
|
|
53
|
+
# @returns [Expect] Returns self for method chaining.
|
|
38
54
|
def to(predicate)
|
|
39
55
|
# This gets the identity scoped to the current call stack, which ensures that any failures are logged at this point in the code.
|
|
40
56
|
identity = @assertions.identity&.scoped
|
|
@@ -46,12 +62,19 @@ module Sus
|
|
|
46
62
|
return self
|
|
47
63
|
end
|
|
48
64
|
|
|
65
|
+
# Apply another predicate to this expectation (alias for {#to}).
|
|
66
|
+
# @parameter predicate [Object] The predicate to apply.
|
|
67
|
+
# @returns [Expect] Returns self for method chaining.
|
|
49
68
|
def and(predicate)
|
|
50
69
|
return to(predicate)
|
|
51
70
|
end
|
|
52
71
|
end
|
|
53
72
|
|
|
54
73
|
class Base
|
|
74
|
+
# Create an expectation about a subject or block.
|
|
75
|
+
# @parameter subject [Object, nil] The subject to make expectations about.
|
|
76
|
+
# @yields {...} Optional block to make expectations about.
|
|
77
|
+
# @returns [Expect] A new Expect instance.
|
|
55
78
|
def expect(subject = nil, &block)
|
|
56
79
|
if block_given?
|
|
57
80
|
Expect.new(@__assertions__, block, distinct: true)
|
data/lib/sus/file.rb
CHANGED
|
@@ -11,7 +11,10 @@ Sus::TOPLEVEL_CLASS_EVAL = ->(__klass__, __path__){__klass__.class_eval(::File.r
|
|
|
11
11
|
|
|
12
12
|
# This is a hack to allow us to get the line number of a syntax error.
|
|
13
13
|
unless SyntaxError.method_defined?(:lineno)
|
|
14
|
+
# Extension to SyntaxError to extract line numbers from error messages.
|
|
14
15
|
class SyntaxError
|
|
16
|
+
# Extract the line number from the error message.
|
|
17
|
+
# @returns [Integer, nil] The line number if found in the message.
|
|
15
18
|
def lineno
|
|
16
19
|
if message =~ /:(\d+):/
|
|
17
20
|
$1.to_i
|
|
@@ -21,17 +24,27 @@ unless SyntaxError.method_defined?(:lineno)
|
|
|
21
24
|
end
|
|
22
25
|
|
|
23
26
|
module Sus
|
|
27
|
+
# Represents a test file that can be loaded and executed.
|
|
24
28
|
module File
|
|
25
29
|
extend Context
|
|
26
30
|
|
|
31
|
+
# Load a test file.
|
|
32
|
+
# @parameter path [String] The path to the test file.
|
|
33
|
+
# @returns [Class] A test class representing the file.
|
|
27
34
|
def self.[] path
|
|
28
35
|
self.build(Sus.base, path)
|
|
29
36
|
end
|
|
30
37
|
|
|
38
|
+
# Called when this module is extended.
|
|
39
|
+
# @parameter base [Class] The class being extended.
|
|
31
40
|
def self.extended(base)
|
|
32
41
|
base.children = Hash.new
|
|
33
42
|
end
|
|
34
43
|
|
|
44
|
+
# Build a test class from a file path.
|
|
45
|
+
# @parameter parent [Class] The parent context class.
|
|
46
|
+
# @parameter path [String] The path to the test file.
|
|
47
|
+
# @returns [Class] A test class representing the file.
|
|
35
48
|
def self.build(parent, path)
|
|
36
49
|
base = Class.new(parent)
|
|
37
50
|
|
|
@@ -50,12 +63,20 @@ module Sus
|
|
|
50
63
|
return base
|
|
51
64
|
end
|
|
52
65
|
|
|
66
|
+
# Print a representation of this file context.
|
|
67
|
+
# @parameter output [Output] The output target.
|
|
53
68
|
def print(output)
|
|
54
69
|
output.write("file ", :path, self.identity, :reset)
|
|
55
70
|
end
|
|
56
71
|
end
|
|
57
72
|
|
|
73
|
+
# Represents an error that occurred while loading a test file.
|
|
58
74
|
class FileLoadError
|
|
75
|
+
# Build a new FileLoadError.
|
|
76
|
+
# @parameter parent [Object] The parent context.
|
|
77
|
+
# @parameter path [String] The path to the file that failed to load.
|
|
78
|
+
# @parameter error [Exception] The error that occurred.
|
|
79
|
+
# @returns [FileLoadError] A new FileLoadError instance.
|
|
59
80
|
def self.build(parent, path, error)
|
|
60
81
|
identity = Identity.file(parent.identity, path)
|
|
61
82
|
|
|
@@ -69,33 +90,48 @@ module Sus
|
|
|
69
90
|
self.new(identity, path, error)
|
|
70
91
|
end
|
|
71
92
|
|
|
93
|
+
# Initialize a new FileLoadError.
|
|
94
|
+
# @parameter identity [Identity] The identity where the error occurred.
|
|
95
|
+
# @parameter path [String] The path to the file.
|
|
96
|
+
# @parameter error [Exception] The error that occurred.
|
|
72
97
|
def initialize(identity, path, error)
|
|
73
98
|
@identity = identity
|
|
74
99
|
@path = path
|
|
75
100
|
@error = error
|
|
76
101
|
end
|
|
77
102
|
|
|
103
|
+
# @attribute [Identity] The identity where the error occurred.
|
|
78
104
|
attr :identity
|
|
105
|
+
|
|
106
|
+
# @attribute [Exception] The error that occurred.
|
|
79
107
|
attr :error
|
|
80
108
|
|
|
109
|
+
# @returns [Boolean] Always returns true, as errors are leaf nodes.
|
|
81
110
|
def leaf?
|
|
82
111
|
true
|
|
83
112
|
end
|
|
84
113
|
|
|
114
|
+
# An empty hash used for children.
|
|
85
115
|
EMPTY = Hash.new.freeze
|
|
86
116
|
|
|
117
|
+
# @returns [Hash] Always returns an empty hash.
|
|
87
118
|
def children
|
|
88
119
|
EMPTY
|
|
89
120
|
end
|
|
90
121
|
|
|
122
|
+
# @returns [String] The file path.
|
|
91
123
|
def description
|
|
92
124
|
@path
|
|
93
125
|
end
|
|
94
126
|
|
|
127
|
+
# Print a representation of this error.
|
|
128
|
+
# @parameter output [Output] The output target.
|
|
95
129
|
def print(output)
|
|
96
130
|
output.write("file ", :path, @identity)
|
|
97
131
|
end
|
|
98
132
|
|
|
133
|
+
# Execute this error, recording it in assertions.
|
|
134
|
+
# @parameter assertions [Assertions] The assertions instance.
|
|
99
135
|
def call(assertions)
|
|
100
136
|
assertions.nested(self, identity: @identity, isolated: true) do |assertions|
|
|
101
137
|
assertions.error!(@error)
|
|
@@ -106,6 +142,8 @@ module Sus
|
|
|
106
142
|
private_constant :FileLoadError
|
|
107
143
|
|
|
108
144
|
module Context
|
|
145
|
+
# Load a test file as a child context.
|
|
146
|
+
# @parameter path [String] The path to the test file.
|
|
109
147
|
def file(path)
|
|
110
148
|
add File.build(self, path)
|
|
111
149
|
end
|