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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/context/getting-started.md +352 -0
  4. data/context/index.yaml +9 -0
  5. data/context/mocking.md +100 -30
  6. data/context/{shared.md → shared-contexts.md} +29 -2
  7. data/lib/sus/assertions.rb +91 -18
  8. data/lib/sus/base.rb +13 -1
  9. data/lib/sus/be.rb +84 -0
  10. data/lib/sus/be_truthy.rb +16 -0
  11. data/lib/sus/be_within.rb +25 -0
  12. data/lib/sus/clock.rb +21 -0
  13. data/lib/sus/config.rb +58 -1
  14. data/lib/sus/context.rb +28 -5
  15. data/lib/sus/describe.rb +14 -0
  16. data/lib/sus/expect.rb +23 -0
  17. data/lib/sus/file.rb +38 -0
  18. data/lib/sus/filter.rb +21 -0
  19. data/lib/sus/fixtures/temporary_directory_context.rb +27 -0
  20. data/lib/sus/fixtures.rb +1 -0
  21. data/lib/sus/have/all.rb +8 -0
  22. data/lib/sus/have/any.rb +8 -0
  23. data/lib/sus/have.rb +42 -0
  24. data/lib/sus/have_duration.rb +16 -0
  25. data/lib/sus/identity.rb +44 -1
  26. data/lib/sus/integrations.rb +1 -0
  27. data/lib/sus/it.rb +33 -0
  28. data/lib/sus/it_behaves_like.rb +16 -0
  29. data/lib/sus/let.rb +3 -0
  30. data/lib/sus/mock.rb +39 -1
  31. data/lib/sus/output/backtrace.rb +31 -1
  32. data/lib/sus/output/bar.rb +17 -0
  33. data/lib/sus/output/buffered.rb +32 -1
  34. data/lib/sus/output/lines.rb +10 -0
  35. data/lib/sus/output/messages.rb +26 -3
  36. data/lib/sus/output/null.rb +16 -2
  37. data/lib/sus/output/progress.rb +29 -1
  38. data/lib/sus/output/status.rb +13 -0
  39. data/lib/sus/output/structured.rb +14 -1
  40. data/lib/sus/output/text.rb +33 -1
  41. data/lib/sus/output/xterm.rb +11 -1
  42. data/lib/sus/output.rb +9 -0
  43. data/lib/sus/raise_exception.rb +16 -0
  44. data/lib/sus/receive.rb +82 -0
  45. data/lib/sus/registry.rb +20 -1
  46. data/lib/sus/respond_to.rb +29 -2
  47. data/lib/sus/shared.rb +16 -0
  48. data/lib/sus/tree.rb +10 -0
  49. data/lib/sus/version.rb +2 -1
  50. data/lib/sus/with.rb +18 -0
  51. data/readme.md +8 -0
  52. data/releases.md +4 -0
  53. data.tar.gz.sig +0 -0
  54. metadata +3 -3
  55. metadata.gz.sig +0 -0
  56. 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-2024, by Samuel Williams.
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 an around method to the context class, that invokes the given block before running the test.
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
- # @parameter hook [Proc] The block to execute before each test.
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 around method to the context class, that invokes the given block after running the test.
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
- # @parameter hook [Proc] The block to execute after each test. An `error` argument is passed if the test failed with an exception.
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
- # @paremeter block [Proc] The block to execute around each test.
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