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/mock.rb CHANGED
@@ -1,12 +1,15 @@
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 "expect"
7
7
 
8
8
  module Sus
9
+ # Represents a mock object that can intercept and replace method calls on a target object.
9
10
  class Mock
11
+ # Initialize a new mock for the given target.
12
+ # @parameter target [Object] The object to mock.
10
13
  def initialize(target)
11
14
  @target = target
12
15
  @interceptor = Module.new
@@ -14,18 +17,26 @@ module Sus
14
17
  @target.singleton_class.prepend(@interceptor)
15
18
  end
16
19
 
20
+ # @attribute [Object] The target object being mocked.
17
21
  attr :target
18
22
 
23
+ # Print a representation of this mock.
24
+ # @parameter output [Output] The output target.
19
25
  def print(output)
20
26
  output.write("mock ", :context, @target.inspect)
21
27
  end
22
28
 
29
+ # Clear all mocked methods from the target.
23
30
  def clear
24
31
  @interceptor.instance_methods.each do |method_name|
25
32
  @interceptor.remove_method(method_name)
26
33
  end
27
34
  end
28
35
 
36
+ # Replace a method implementation.
37
+ # @parameter method [Symbol] The method name to replace.
38
+ # @yields {|*arguments, **options, &block| ...} The replacement implementation.
39
+ # @returns [Mock] Returns self for method chaining.
29
40
  def replace(method, &hook)
30
41
  execution_context = Thread.current
31
42
 
@@ -40,6 +51,10 @@ module Sus
40
51
  return self
41
52
  end
42
53
 
54
+ # Add a hook that runs before a method is called.
55
+ # @parameter method [Symbol] The method name to hook.
56
+ # @yields {|*arguments, **options, &block| ...} The hook to execute before the method.
57
+ # @returns [Mock] Returns self for method chaining.
43
58
  def before(method, &hook)
44
59
  execution_context = Thread.current
45
60
 
@@ -51,6 +66,10 @@ module Sus
51
66
  return self
52
67
  end
53
68
 
69
+ # Add a hook that runs after a method is called.
70
+ # @parameter method [Symbol] The method name to hook.
71
+ # @yields {|result, *arguments, **options, &block| ...} The hook to execute after the method, receiving the result as the first argument.
72
+ # @returns [Mock] Returns self for method chaining.
54
73
  def after(method, &hook)
55
74
  execution_context = Thread.current
56
75
 
@@ -64,6 +83,8 @@ module Sus
64
83
  end
65
84
 
66
85
  # Wrap a method, yielding the original method as the first argument, so you can call it from within the hook.
86
+ # @parameter method [Symbol] The method name to wrap.
87
+ # @yields {|original, *arguments, **options, &block| ...} The wrapper implementation, receiving the original method as the first argument.
67
88
  def wrap(method, &hook)
68
89
  execution_context = Thread.current
69
90
 
@@ -81,13 +102,20 @@ module Sus
81
102
  end
82
103
  end
83
104
 
105
+ # Provides mock management functionality for test cases.
84
106
  module Mocks
107
+ # Clean up all mocks after the test completes.
108
+ # @parameter error [Exception | Nil] The error that occurred, if any.
85
109
  def after(error = nil)
86
110
  super
87
111
 
88
112
  @mocks&.each_value(&:clear)
89
113
  end
90
114
 
115
+ # Create or access a mock for the given target.
116
+ # @parameter target [Object] The object to mock.
117
+ # @yields {|mock| ...} Optional block to configure the mock.
118
+ # @returns [Mock] The mock instance for the target.
91
119
  def mock(target)
92
120
  validate_mock!(target)
93
121
 
@@ -102,20 +130,30 @@ module Sus
102
130
 
103
131
  private
104
132
 
133
+ # Error raised when attempting to mock a frozen object.
105
134
  MockTargetError = Class.new(StandardError)
106
135
 
136
+ # Validate that the target can be mocked.
137
+ # @parameter target [Object] The object to validate.
138
+ # @raises [MockTargetError] If the target is frozen.
107
139
  def validate_mock!(target)
108
140
  if target.frozen?
109
141
  raise MockTargetError, "Cannot mock frozen object #{target.inspect}!"
110
142
  end
111
143
  end
112
144
 
145
+ # Get the mocks hash, creating it if necessary.
146
+ # @returns [Hash] A hash mapping targets to their mock instances.
113
147
  def mocks
114
148
  @mocks ||= Hash.new{|h,k| h[k] = Mock.new(k)}.compare_by_identity
115
149
  end
116
150
  end
117
151
 
118
152
  class Base
153
+ # Create or access a mock for the given target.
154
+ # @parameter target [Object] The object to mock.
155
+ # @yields {|mock| ...} Optional block to configure the mock.
156
+ # @returns [Mock] The mock instance for the target.
119
157
  def mock(target, &block)
120
158
  # Pull in the extra functionality:
121
159
  self.singleton_class.prepend(Mocks)
@@ -5,21 +5,32 @@
5
5
 
6
6
  module Sus
7
7
  module Output
8
- # Print out a backtrace relevant to the given test identity if provided.
8
+ # Represents a backtrace for displaying error locations.
9
9
  class Backtrace
10
+ # Create a backtrace from the first caller location.
11
+ # @parameter identity [Identity, nil] Optional identity to filter by path.
12
+ # @returns [Backtrace] A new Backtrace instance.
10
13
  def self.first(identity = nil)
11
14
  # This implementation could be a little more efficient.
12
15
  self.new(caller_locations(1), identity&.path, 1)
13
16
  end
14
17
 
18
+ # Create a backtrace from an exception.
19
+ # @parameter exception [Exception] The exception to extract the backtrace from.
20
+ # @parameter identity [Identity, nil] Optional identity to filter by path.
21
+ # @returns [Backtrace] A new Backtrace instance.
15
22
  def self.for(exception, identity = nil)
16
23
  # I've disabled the root filter here, because partial backtraces are not very useful.
17
24
  # We might want to do something to improve presentation of the backtrace based on the root instead.
18
25
  self.new(extract_stack(exception), identity&.path)
19
26
  end
20
27
 
28
+ # Represents a location in a backtrace.
21
29
  Location = Struct.new(:path, :lineno, :label)
22
30
 
31
+ # Extract the stack trace from an exception.
32
+ # @parameter exception [Exception] The exception to extract from.
33
+ # @returns [Array] An array of location objects.
23
34
  def self.extract_stack(exception)
24
35
  if stack = exception.backtrace_locations
25
36
  return stack
@@ -32,16 +43,29 @@ module Sus
32
43
  end
33
44
  end
34
45
 
46
+ # Initialize a new Backtrace.
47
+ # @parameter stack [Array] The stack trace locations.
48
+ # @parameter root [String, nil] Optional root path to filter by.
49
+ # @parameter limit [Integer, nil] Optional limit on the number of frames.
35
50
  def initialize(stack, root = nil, limit = nil)
36
51
  @stack = stack
37
52
  @root = root
38
53
  @limit = limit
39
54
  end
40
55
 
56
+ # @attribute [Array] The stack trace locations.
41
57
  attr :stack
58
+
59
+ # @attribute [String, nil] The root path to filter by.
42
60
  attr :root
61
+
62
+ # @attribute [Integer, nil] The limit on the number of frames.
43
63
  attr :limit
44
64
 
65
+ # Filter the backtrace by root path and limit.
66
+ # @parameter root [String, nil] Optional root path to filter by.
67
+ # @parameter limit [Integer, nil] Optional limit on the number of frames.
68
+ # @returns [Array, Enumerator] The filtered stack trace.
45
69
  def filter(root: @root, limit: @limit)
46
70
  if root
47
71
  if limit
@@ -60,6 +84,8 @@ module Sus
60
84
  end
61
85
  end
62
86
 
87
+ # Print the backtrace to the output.
88
+ # @parameter output [Output] The output handler.
63
89
  def print(output)
64
90
  if @limit == 1
65
91
  filter.each do |frame|
@@ -74,6 +100,10 @@ module Sus
74
100
  end
75
101
  end
76
102
 
103
+ # Select items up to and matching a condition.
104
+ # @parameter things [Enumerable] The items to filter.
105
+ # @yields {|thing| ...} The condition to match.
106
+ # @returns [Array] The filtered items.
77
107
  private def up_to_and_matching(things, &block)
78
108
  preface = true
79
109
  things.select do |thing|
@@ -5,7 +5,9 @@
5
5
 
6
6
  module Sus
7
7
  module Output
8
+ # Represents a progress bar for displaying test execution progress.
8
9
  class Bar
10
+ # Unicode block characters for drawing the progress bar.
9
11
  BLOCK = [
10
12
  " ",
11
13
  "▏",
@@ -18,6 +20,10 @@ module Sus
18
20
  "█",
19
21
  ]
20
22
 
23
+ # Initialize a new progress bar.
24
+ # @parameter current [Integer] The current progress value.
25
+ # @parameter total [Integer] The total value.
26
+ # @parameter message [String, nil] Optional message to display.
21
27
  def initialize(current = 0, total = 0, message = nil)
22
28
  @maximum_message_width = 0
23
29
 
@@ -26,19 +32,30 @@ module Sus
26
32
  @message = message
27
33
  end
28
34
 
35
+ # Update the progress bar values.
36
+ # @parameter current [Integer] The current progress value.
37
+ # @parameter total [Integer] The total value.
38
+ # @parameter message [String, nil] Optional message to display.
29
39
  def update(current, total, message)
30
40
  @current = current
31
41
  @total = total
32
42
  @message = message
33
43
  end
34
44
 
45
+ # Register progress bar styling with an output handler.
46
+ # @parameter output [Output] The output handler to register with.
35
47
  def self.register(output)
36
48
  output[:progress_bar] ||= output.style(:blue, :white)
37
49
  end
38
50
 
51
+ # The minimum width for the progress bar.
39
52
  MINIMUM_WIDTH = 8
53
+
54
+ # The suffix to append to messages.
40
55
  MESSAGE_SUFFIX = ": "
41
56
 
57
+ # Print the progress bar to the output.
58
+ # @parameter output [Output] The output handler.
42
59
  def print(output)
43
60
  width = output.width
44
61
 
@@ -7,17 +7,23 @@ require "io/console"
7
7
  require "stringio"
8
8
 
9
9
  module Sus
10
- # Styled output output.
11
10
  module Output
11
+ # Represents a buffered output handler that stores output operations for later replay.
12
12
  class Buffered
13
+ # Initialize a new Buffered output handler.
14
+ # @parameter tee [Output, nil] Optional output handler to tee output to.
13
15
  def initialize(tee = nil)
14
16
  @chunks = Array.new
15
17
  @tee = tee
16
18
  end
17
19
 
20
+ # @attribute [Array] The stored output chunks.
18
21
  attr :chunks
22
+
23
+ # @attribute [Output, nil] The output handler to tee to.
19
24
  attr :tee
20
25
 
26
+ # @returns [String] A string representation of this buffered output.
21
27
  def inspect
22
28
  if @tee
23
29
  "\#<#{self.class.name} #{@chunks.size} chunks -> #{@tee.class}>"
@@ -26,39 +32,52 @@ module Sus
26
32
  end
27
33
  end
28
34
 
35
+ # Create a nested buffered output handler.
36
+ # @returns [Buffered] A new Buffered instance that tees to this one.
29
37
  def buffered
30
38
  self.class.new(self)
31
39
  end
32
40
 
41
+ # Iterate over stored chunks.
42
+ # @yields {|chunk| ...} Each stored chunk.
33
43
  def each(&block)
34
44
  @chunks.each(&block)
35
45
  end
36
46
 
47
+ # Append chunks from another buffer.
48
+ # @parameter buffer [Buffered] The buffer to append from.
37
49
  def append(buffer)
38
50
  @chunks.concat(buffer.chunks)
39
51
  @tee&.append(buffer)
40
52
  end
41
53
 
54
+ # @returns [String] The buffered output as a string.
42
55
  def string
43
56
  io = StringIO.new
44
57
  Text.new(io).append(@chunks)
45
58
  return io.string
46
59
  end
47
60
 
61
+ # The indent operation marker.
48
62
  INDENT = [:indent].freeze
49
63
 
64
+ # Increase indentation level.
50
65
  def indent
51
66
  @chunks << INDENT
52
67
  @tee&.indent
53
68
  end
54
69
 
70
+ # The outdent operation marker.
55
71
  OUTDENT = [:outdent].freeze
56
72
 
73
+ # Decrease indentation level.
57
74
  def outdent
58
75
  @chunks << OUTDENT
59
76
  @tee&.outdent
60
77
  end
61
78
 
79
+ # Execute a block with increased indentation.
80
+ # @yields {...} The block to execute.
62
81
  def indented
63
82
  self.indent
64
83
  yield
@@ -66,31 +85,43 @@ module Sus
66
85
  self.outdent
67
86
  end
68
87
 
88
+ # Write output.
89
+ # @parameter arguments [Array] The arguments to write.
69
90
  def write(*arguments)
70
91
  @chunks << [:write, *arguments]
71
92
  @tee&.write(*arguments)
72
93
  end
73
94
 
95
+ # Write output followed by a newline.
96
+ # @parameter arguments [Array] The arguments to write.
74
97
  def puts(*arguments)
75
98
  @chunks << [:puts, *arguments]
76
99
  @tee&.puts(*arguments)
77
100
  end
78
101
 
102
+ # Record an assertion.
103
+ # @parameter arguments [Array] The assertion arguments.
79
104
  def assert(*arguments)
80
105
  @chunks << [:assert, *arguments]
81
106
  @tee&.assert(*arguments)
82
107
  end
83
108
 
109
+ # Record a skip.
110
+ # @parameter arguments [Array] The skip arguments.
84
111
  def skip(*arguments)
85
112
  @chunks << [:skip, *arguments]
86
113
  @tee&.skip(*arguments)
87
114
  end
88
115
 
116
+ # Record an error.
117
+ # @parameter arguments [Array] The error arguments.
89
118
  def error(*arguments)
90
119
  @chunks << [:error, *arguments]
91
120
  @tee&.error(*arguments)
92
121
  end
93
122
 
123
+ # Record an informational message.
124
+ # @parameter arguments [Array] The message arguments.
94
125
  def inform(*arguments)
95
126
  @chunks << [:inform, *arguments]
96
127
  @tee&.inform(*arguments)
@@ -7,7 +7,10 @@ require "io/console"
7
7
 
8
8
  module Sus
9
9
  module Output
10
+ # Represents a line buffer for managing multiple lines of output on a terminal.
10
11
  class Lines
12
+ # Initialize a new Lines buffer.
13
+ # @parameter output [Output] The output handler to write to.
11
14
  def initialize(output)
12
15
  @output = output
13
16
  @lines = []
@@ -15,21 +18,28 @@ module Sus
15
18
  @current_count = 0
16
19
  end
17
20
 
21
+ # @returns [Integer] The height of the terminal.
18
22
  def height
19
23
  @output.size.first
20
24
  end
21
25
 
26
+ # Set a line at the given index.
27
+ # @parameter index [Integer] The line index.
28
+ # @parameter line [Object] The line content (should respond to #print).
22
29
  def []= index, line
23
30
  @lines[index] = line
24
31
 
25
32
  redraw(index)
26
33
  end
27
34
 
35
+ # Clear all lines.
28
36
  def clear
29
37
  @lines.clear
30
38
  write
31
39
  end
32
40
 
41
+ # Redraw a specific line or all lines.
42
+ # @parameter index [Integer] The line index to redraw.
33
43
  def redraw(index)
34
44
  if index < @current_count
35
45
  update(index, @lines[index])
@@ -1,15 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2023-2024, by Samuel Williams.
4
+ # Copyright, 2023-2025, by Samuel Williams.
5
5
 
6
6
  module Sus
7
- # Styled output output.
8
7
  module Output
8
+ # Provides message formatting methods for output handlers.
9
9
  module Messages
10
+ # The prefix for passed assertions.
10
11
  PASSED_PREFIX = [:passed, "✓ "].freeze
12
+
13
+ # The prefix for failed assertions.
11
14
  FAILED_PREFIX = [:failed, "✗ "].freeze
12
15
 
16
+ # Get the prefix for a passed assertion based on orientation.
17
+ # @parameter orientation [Boolean] The orientation of the assertions.
18
+ # @returns [Array] The prefix array.
13
19
  def pass_prefix(orientation)
14
20
  if orientation
15
21
  PASSED_PREFIX
@@ -18,6 +24,9 @@ module Sus
18
24
  end
19
25
  end
20
26
 
27
+ # Get the prefix for a failed assertion based on orientation.
28
+ # @parameter orientation [Boolean] The orientation of the assertions.
29
+ # @returns [Array] The prefix array.
21
30
  def fail_prefix(orientation)
22
31
  if orientation
23
32
  FAILED_PREFIX
@@ -26,6 +35,7 @@ module Sus
26
35
  end
27
36
  end
28
37
 
38
+ # Print an assertion result.
29
39
  # If the orientation is true, and the test passed, then it is a successful outcome.
30
40
  # If the orientation is false, and the test failed, then it is a successful outcome.
31
41
  # Otherwise, it is a failed outcome.
@@ -33,7 +43,7 @@ module Sus
33
43
  # @parameter condition [Boolean] The result of the test.
34
44
  # @parameter orientation [Boolean] The orientation of the assertions.
35
45
  # @parameter message [String] The message to display.
36
- # @parameter backtrace [Array] The backtrace to display.
46
+ # @parameter backtrace [Backtrace] The backtrace to display.
37
47
  def assert(condition, orientation, message, backtrace)
38
48
  if condition
39
49
  self.puts(:indent, *pass_prefix(orientation), message, backtrace)
@@ -42,18 +52,27 @@ module Sus
42
52
  end
43
53
  end
44
54
 
55
+ # @returns [String] The prefix for skipped tests.
45
56
  def skip_prefix
46
57
  "⏸ "
47
58
  end
48
59
 
60
+ # Print a skip message.
61
+ # @parameter reason [String] The reason for skipping.
62
+ # @parameter identity [Identity, nil] The identity where the skip occurred.
49
63
  def skip(reason, identity)
50
64
  self.puts(:indent, :skipped, skip_prefix, reason)
51
65
  end
52
66
 
67
+ # @returns [Array] The prefix for error messages.
53
68
  def error_prefix
54
69
  [:errored, "⚠ "]
55
70
  end
56
71
 
72
+ # Print an error message.
73
+ # @parameter error [Exception] The error to display.
74
+ # @parameter identity [Identity, nil] The identity where the error occurred.
75
+ # @parameter prefix [Array] Optional prefix to use.
57
76
  def error(error, identity, prefix = error_prefix)
58
77
  lines = error.message.split(/\r?\n/)
59
78
 
@@ -70,10 +89,14 @@ module Sus
70
89
  end
71
90
  end
72
91
 
92
+ # @returns [String] The prefix for informational messages.
73
93
  def inform_prefix
74
94
  "ℹ "
75
95
  end
76
96
 
97
+ # Print an informational message.
98
+ # @parameter message [String] The message to display.
99
+ # @parameter identity [Identity, nil] The identity where the message was generated.
77
100
  def inform(message, identity)
78
101
  self.puts(:indent, :inform, inform_prefix, message)
79
102
  end
@@ -1,42 +1,56 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2021-2024, by Samuel Williams.
4
+ # Copyright, 2021-2025, by Samuel Williams.
5
5
 
6
6
  require_relative "messages"
7
7
 
8
8
  module Sus
9
- # Styled output output.
10
9
  module Output
10
+ # Represents a null output handler that discards all output.
11
11
  class Null
12
12
  include Messages
13
13
 
14
+ # Initialize a new Null output handler.
14
15
  def initialize
15
16
  end
16
17
 
18
+ # Create a buffered output handler.
19
+ # @returns [Buffered] A new Buffered instance.
17
20
  def buffered
18
21
  Buffered.new(nil)
19
22
  end
20
23
 
24
+ # @attribute [Hash, nil] Optional options (unused).
21
25
  attr :options
22
26
 
27
+ # Append chunks from a buffer (no-op).
28
+ # @parameter buffer [Buffered] The buffer to append from.
23
29
  def append(buffer)
24
30
  end
25
31
 
32
+ # Increase indentation (no-op).
26
33
  def indent
27
34
  end
28
35
 
36
+ # Decrease indentation (no-op).
29
37
  def outdent
30
38
  end
31
39
 
40
+ # Execute a block with indentation (no-op, just yields).
41
+ # @yields {...} The block to execute.
32
42
  def indented
33
43
  yield
34
44
  end
35
45
 
46
+ # Write output (no-op).
47
+ # @parameter arguments [Array] The arguments to write.
36
48
  def write(*arguments)
37
49
  # Do nothing.
38
50
  end
39
51
 
52
+ # Write output followed by a newline (no-op).
53
+ # @parameter arguments [Array] The arguments to write.
40
54
  def puts(*arguments)
41
55
  # Do nothing.
42
56
  end
@@ -9,11 +9,18 @@ require_relative "lines"
9
9
 
10
10
  module Sus
11
11
  module Output
12
+ # Represents a progress tracker for test execution.
12
13
  class Progress
14
+ # Get the current monotonic time.
15
+ # @returns [Float] The current time in seconds.
13
16
  def self.now
14
17
  ::Process.clock_gettime(Process::CLOCK_MONOTONIC)
15
18
  end
16
19
 
20
+ # Initialize a new Progress tracker.
21
+ # @parameter output [Output] The output handler.
22
+ # @parameter total [Integer] The total number of items to track.
23
+ # @parameter minimum_output_duration [Float] Minimum duration before showing output (unused).
17
24
  def initialize(output, total = 0, minimum_output_duration: 1.0)
18
25
  @output = output
19
26
  @subject = subject
@@ -30,35 +37,47 @@ module Sus
30
37
  @total = total
31
38
  end
32
39
 
40
+ # @attribute [Object, nil] The subject being tracked.
33
41
  attr :subject
42
+
43
+ # @attribute [Integer] The current progress value.
34
44
  attr :current
45
+
46
+ # @attribute [Integer] The total value.
35
47
  attr :total
36
48
 
49
+ # @returns [Float] The elapsed duration in seconds.
37
50
  def duration
38
51
  Progress.now - @start_time
39
52
  end
40
53
 
54
+ # @returns [Float] The progress as a fraction (0.0 to 1.0).
41
55
  def progress
42
56
  @current.to_f / @total.to_f
43
57
  end
44
58
 
59
+ # @returns [Integer] The remaining items to process.
45
60
  def remaining
46
61
  @total - @current
47
62
  end
48
63
 
64
+ # @returns [Float, nil] The average duration per item, or nil if no items completed.
49
65
  def average_duration
50
66
  if @current > 0
51
67
  duration / @current
52
68
  end
53
69
  end
54
70
 
71
+ # @returns [Float, nil] The estimated remaining time, or nil if cannot be calculated.
55
72
  def estimated_remaining_time
56
73
  if average_duration = self.average_duration
57
74
  average_duration * remaining
58
75
  end
59
76
  end
60
77
 
61
- # Increase the amont of work done.
78
+ # Increase the amount of work done.
79
+ # @parameter amount [Integer] The amount to increment by.
80
+ # @returns [Progress] Returns self for method chaining.
62
81
  def increment(amount = 1)
63
82
  @current += amount
64
83
 
@@ -69,6 +88,8 @@ module Sus
69
88
  end
70
89
 
71
90
  # Increase the total size of the progress.
91
+ # @parameter amount [Integer] The amount to expand by.
92
+ # @returns [Progress] Returns self for method chaining.
72
93
  def expand(amount = 1)
73
94
  @total += amount
74
95
 
@@ -78,16 +99,23 @@ module Sus
78
99
  return self
79
100
  end
80
101
 
102
+ # Report the status of a specific item.
103
+ # @parameter index [Integer] The index of the item.
104
+ # @parameter context [Object] The context to display.
105
+ # @parameter state [Symbol] The state (:free or :busy).
106
+ # @returns [Progress] Returns self for method chaining.
81
107
  def report(index, context, state)
82
108
  @lines&.[]=(index+1, Status.new(state, context))
83
109
 
84
110
  return self
85
111
  end
86
112
 
113
+ # Clear the progress display.
87
114
  def clear
88
115
  @lines&.clear
89
116
  end
90
117
 
118
+ # @returns [String] A string representation of the progress.
91
119
  def to_s
92
120
  if estimated_remaining_time = self.estimated_remaining_time
93
121
  "#{@current}/#{@total} completed in #{formatted_duration(self.duration)}, #{formatted_duration(estimated_remaining_time)} remaining"