sus 0.6.2 → 0.8.1

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.
data/lib/sus/mock.rb ADDED
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright, 2022, by Samuel G. D. Williams. <http://www.codeotaku.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+
23
+ require_relative 'expect'
24
+
25
+ module Sus
26
+ class Mock
27
+ def initialize(target)
28
+ @target = target
29
+ @interceptor = Module.new
30
+
31
+ @target.singleton_class.prepend(@interceptor)
32
+ end
33
+
34
+ attr :target
35
+
36
+ def print(output)
37
+ output.write("mock ", :context, @target.inspect)
38
+ end
39
+
40
+ def clear
41
+ @interceptor.instance_methods.each do |method_name|
42
+ @interceptor.remove_method(method_name)
43
+ end
44
+ end
45
+
46
+ def replace(method, &hook)
47
+ execution_context = Thread.current
48
+
49
+ @interceptor.define_method(method) do |*arguments, **options, &block|
50
+ if execution_context == Thread.current
51
+ hook.call(*arguments, **options, &block)
52
+ else
53
+ super(*arguments, **options, &block)
54
+ end
55
+ end
56
+
57
+ return self
58
+ end
59
+
60
+ def before(method, &hook)
61
+ execution_context = Thread.current
62
+
63
+ @interceptor.define_method(method) do |*arguments, **options, &block|
64
+ hook.call(*arguments, **options, &block) if execution_context == Thread.current
65
+ super(*arguments, **options, &block)
66
+ end
67
+
68
+ return self
69
+ end
70
+
71
+ def after(method, &hook)
72
+ execution_context = Thread.current
73
+
74
+ @interceptor.define_method(method) do |*arguments, **options, &block|
75
+ result = super(*arguments, **options, &block)
76
+ hook.call(result, *arguments, **options, &block) if execution_context == Thread.current
77
+ return result
78
+ end
79
+
80
+ return self
81
+ end
82
+ end
83
+
84
+ module Mocks
85
+ def after
86
+ super
87
+
88
+ @mocks&.each_value(&:clear)
89
+ end
90
+
91
+ def mock(target)
92
+ validate_mock!(target)
93
+
94
+ mock = self.mocks[target]
95
+
96
+ if block_given?
97
+ yield mock
98
+ end
99
+
100
+ return mock
101
+ end
102
+
103
+ private
104
+
105
+ MockTargetError = Class.new(StandardError)
106
+
107
+ def validate_mock!(target)
108
+ if target.frozen?
109
+ raise MockTargetError, "Cannot mock frozen object #{target.inspect}!"
110
+ end
111
+ end
112
+
113
+ def mocks
114
+ @mocks ||= Hash.new{|h,k| h[k] = Mock.new(k)}.compare_by_identity
115
+ end
116
+ end
117
+
118
+ class Base
119
+ def mock(target, &block)
120
+ # Pull in the extra functionality:
121
+ self.singleton_class.prepend(Mocks)
122
+
123
+ # Redirect the method to the new functionality:
124
+ self.mock(target, &block)
125
+ end
126
+ end
127
+ end
@@ -27,34 +27,32 @@ module Sus
27
27
  # Styled output output.
28
28
  module Output
29
29
  class Buffered
30
- def initialize(output)
31
- @output = output
32
- @buffer = Array.new
30
+ def initialize
31
+ @output = Array.new
33
32
  end
34
33
 
35
34
  attr :output
36
- attr :buffer
37
35
 
38
- def append(output)
39
- @buffer.each do |operation|
40
- output.public_send(*operation)
41
- end
36
+ def each(&block)
37
+ @output.each(&block)
38
+ end
39
+
40
+ def append(buffer)
41
+ @output.concat(buffer.output)
42
42
  end
43
43
 
44
44
  def string
45
45
  io = StringIO.new
46
- append(Text.new(io))
46
+ Text.new(io).append(@output)
47
47
  return io.string
48
48
  end
49
49
 
50
50
  def indent
51
- @buffer << [:indent]
52
- @output.indent
51
+ @output << [:indent]
53
52
  end
54
53
 
55
54
  def outdent
56
- @buffer << [:outdent]
57
- @output.outdent
55
+ @output << [:outdent]
58
56
  end
59
57
 
60
58
  def indented
@@ -65,13 +63,11 @@ module Sus
65
63
  end
66
64
 
67
65
  def write(*arguments)
68
- @output.write(*arguments)
69
- @buffer << [:write, *arguments]
66
+ @output << [:write, *arguments]
70
67
  end
71
68
 
72
69
  def puts(*arguments)
73
- @output.puts(*arguments)
74
- @buffer << [:puts, *arguments]
70
+ @output << [:puts, *arguments]
75
71
  end
76
72
  end
77
73
  end
@@ -76,6 +76,7 @@ module Sus
76
76
  offset = @current_count - index
77
77
 
78
78
  @output.write("\e[#{offset}F\e[K")
79
+
79
80
  soft_wrap do
80
81
  line.print(@output)
81
82
  end
@@ -29,6 +29,9 @@ module Sus
29
29
  class Null
30
30
  def initialize
31
31
  end
32
+
33
+ def append(buffer)
34
+ end
32
35
 
33
36
  def indent
34
37
  end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+
23
+ require_relative 'bar'
24
+ require_relative 'status'
25
+ require_relative 'lines'
26
+
27
+ module Sus
28
+ module Output
29
+ class Progress
30
+ def self.now
31
+ ::Process.clock_gettime(Process::CLOCK_MONOTONIC)
32
+ end
33
+
34
+ def initialize(output, total = 0, minimum_output_duration: 1.0)
35
+ @output = output
36
+ @subject = subject
37
+
38
+ @start_time = Progress.now
39
+
40
+ if @output.interactive?
41
+ @bar = Bar.new
42
+ @lines = Lines.new(@output)
43
+ @lines[0] = @bar
44
+ end
45
+
46
+ @current = 0
47
+ @total = total
48
+ end
49
+
50
+ attr :subject
51
+ attr :current
52
+ attr :total
53
+
54
+ def duration
55
+ Progress.now - @start_time
56
+ end
57
+
58
+ def progress
59
+ @current.to_f / @total.to_f
60
+ end
61
+
62
+ def remaining
63
+ @total - @current
64
+ end
65
+
66
+ def average_duration
67
+ if @current > 0
68
+ duration / @current
69
+ end
70
+ end
71
+
72
+ def estimated_remaining_time
73
+ if average_duration = self.average_duration
74
+ average_duration * remaining
75
+ end
76
+ end
77
+
78
+ # Increase the amont of work done.
79
+ def increment(amount = 1)
80
+ @current += amount
81
+
82
+ @bar&.update(@current, @total, self.to_s)
83
+ @lines&.redraw(0)
84
+
85
+ return self
86
+ end
87
+
88
+ # Increase the total size of the progress.
89
+ def expand(amount = 1)
90
+ @total += amount
91
+
92
+ @bar&.update(@current, @total, self.to_s)
93
+ @lines&.redraw(0)
94
+
95
+ return self
96
+ end
97
+
98
+ def report(index, context, state)
99
+ @lines&.[]=(index+1, Status.new(state, context))
100
+
101
+ return self
102
+ end
103
+
104
+ def clear
105
+ @lines&.clear
106
+ end
107
+
108
+ def to_s
109
+ if estimated_remaining_time = self.estimated_remaining_time
110
+ "#{@current}/#{@total} completed in #{formatted_duration(self.duration)}, #{formatted_duration(estimated_remaining_time)} remaining"
111
+ else
112
+ "#{@current}/#{@total} completed"
113
+ end
114
+ end
115
+
116
+ private
117
+
118
+ def formatted_duration(duration)
119
+ seconds = duration.floor
120
+
121
+ if seconds < 60.0
122
+ return "#{seconds}s"
123
+ end
124
+
125
+ minutes = (duration / 60.0).floor
126
+ seconds = (seconds - (minutes * 60)).round
127
+
128
+ if minutes < 60.0
129
+ return "#{minutes}m#{seconds}s"
130
+ end
131
+
132
+ hours = (minutes / 60.0).floor
133
+ minutes = (minutes - (hours * 60)).round
134
+
135
+ if hours < 24.0
136
+ return "#{hours}h#{minutes}m"
137
+ end
138
+
139
+ days = (hours / 24.0).floor
140
+ hours = (hours - (days * 24)).round
141
+
142
+ return "#{days}d#{hours}h"
143
+ end
144
+ end
145
+ end
146
+ end
@@ -35,6 +35,14 @@ module Sus
35
35
  @styles[:indent] = @indent
36
36
  end
37
37
 
38
+ def append(buffer)
39
+ buffer.each do |operation|
40
+ self.public_send(*operation)
41
+ end
42
+ end
43
+
44
+ attr :io
45
+
38
46
  INDENTATION = "\t"
39
47
 
40
48
  def indent
data/lib/sus/output.rb CHANGED
@@ -4,6 +4,7 @@ require_relative 'output/text'
4
4
  require_relative 'output/xterm'
5
5
 
6
6
  require_relative 'output/null'
7
+ require_relative 'output/progress'
7
8
 
8
9
  module Sus
9
10
  module Output
@@ -35,7 +36,7 @@ module Sus
35
36
  end
36
37
 
37
38
  def self.buffered
38
- Buffered.new(Null.new)
39
+ Buffered.new
39
40
  end
40
41
  end
41
42
  end
@@ -12,17 +12,12 @@ module Sus
12
12
 
13
13
  # Didn't throw any exception, so the expectation failed:
14
14
  assertions.assert(false, self)
15
- rescue => exception
16
- # Did we throw the right kind of exception?
17
- if exception.is_a?(@exception_class)
18
- # Did it have the right message?
19
- if @message
20
- assertions.assert(@message === exception.message)
21
- else
22
- assertions.assert(true, self)
23
- end
15
+ rescue @exception_class => exception
16
+ # Did it have the right message?
17
+ if @message
18
+ assertions.assert(@message === exception.message)
24
19
  else
25
- raise
20
+ assertions.assert(true, self)
26
21
  end
27
22
  end
28
23
  end
@@ -0,0 +1,147 @@
1
+
2
+ require_relative 'respond_to'
3
+
4
+ module Sus
5
+ class Receive
6
+ CALL_ORIGINAL = Object.new
7
+
8
+ def initialize(base, method)
9
+ @base = base
10
+ @method = method
11
+
12
+ @times = Once.new
13
+ @arguments = nil
14
+ @options = nil
15
+ @block = nil
16
+ @returning = CALL_ORIGINAL
17
+ end
18
+
19
+ def print(output)
20
+ output.write("receive ", :variable, @method.to_s, :reset, " ")
21
+ end
22
+
23
+ def with_arguments(*arguments)
24
+ @arguments = WithArguments.new(arguments)
25
+ return self
26
+ end
27
+
28
+ def with_options(*options)
29
+ @options = WithOptions.new(options)
30
+ return self
31
+ end
32
+
33
+ def with_block
34
+ @block = WithBlock.new
35
+ return self
36
+ end
37
+
38
+ def and_return(*returning)
39
+ if returning.size == 1
40
+ @returning = returning.first
41
+ else
42
+ @returning = returning
43
+ end
44
+ return self
45
+ end
46
+
47
+ def validate(mock, assertions, arguments, options, block)
48
+ @arguments.call(assertions, arguments) if @arguments
49
+ @options.call(assertions, options) if @options
50
+ @block.call(assertions, block) if @block
51
+ end
52
+
53
+ def call(assertions, subject)
54
+ assertions.nested(self) do |assertions|
55
+ mock = @base.mock(subject)
56
+
57
+ called = 0
58
+
59
+ if call_original?
60
+ mock.before(@method) do |*arguments, **options, &block|
61
+ called += 1
62
+
63
+ validate(mock, assertions, arguments, options, block)
64
+ end
65
+ else
66
+ mock.replace(@method) do |*arguments, **options, &block|
67
+ called += 1
68
+
69
+ validate(mock, assertions, arguments, options, block)
70
+
71
+ next @returning
72
+ end
73
+ end
74
+
75
+ assertions.defer do
76
+ @times.call(assertions, called)
77
+ end
78
+ end
79
+ end
80
+
81
+ def call_original?
82
+ @returning == CALL_ORIGINAL
83
+ end
84
+
85
+ class WithArguments
86
+ def initialize(arguments)
87
+ @arguments = arguments
88
+ end
89
+
90
+ def print(output)
91
+ output.write("with arguments ", :variable, @arguments.inspect)
92
+ end
93
+
94
+ def call(assertions, subject)
95
+ assertions.nested(self) do |assertions|
96
+ Expect.new(assertions, subject).to(Be == @arguments)
97
+ end
98
+ end
99
+ end
100
+
101
+ class WithOptions
102
+ def initialize(options)
103
+ @options = options
104
+ end
105
+
106
+ def print(output)
107
+ output.write("with options ", :variable, @options.inspect)
108
+ end
109
+
110
+ def call(assertions, subject)
111
+ assertions.nested(self) do |assertions|
112
+ Expect.new(assertions, subject).to(Be.new(:include?, @options))
113
+ end
114
+ end
115
+ end
116
+
117
+ class WithBlock
118
+ def print(output)
119
+ output.write("with block")
120
+ end
121
+
122
+ def call(assertions, subject)
123
+ assertions.nested(self) do |assertions|
124
+ Expect.new(assertions, subject).not.to(Be == nil)
125
+ end
126
+ end
127
+ end
128
+
129
+ class Once
130
+ def print(output)
131
+ output.write("once")
132
+ end
133
+
134
+ def call(assertions, subject)
135
+ assertions.nested(self) do |assertions|
136
+ Expect.new(assertions, subject).to(Be == 1)
137
+ end
138
+ end
139
+ end
140
+ end
141
+
142
+ class Base
143
+ def receive(method)
144
+ Receive.new(self, method)
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,89 @@
1
+ module Sus
2
+ class RespondTo
3
+ class WithParameters
4
+ # @parameter [Array(Symbol)] List of method parameters in the expected order, must include at least all required parameters but can also list optional parameters.
5
+ def initialize(parameters)
6
+ @parameters = parameters
7
+ end
8
+
9
+ def call(assertions, subject)
10
+ parameters = @parameters.dup
11
+
12
+ assertions.nested(self) do |assertions|
13
+ expected_name = parameters.shift
14
+
15
+ subject.each do |type, name|
16
+ case type
17
+ when :req
18
+ assertions.assert(name == expected_name, "parameter #{expected_name} is required, but was #{name}")
19
+ when :opt
20
+ break if expected_name.nil?
21
+ assertions.assert(name == expected_name, "parameter #{expected_name} is specified, but was #{name}")
22
+ else
23
+ break
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ class WithOptions
31
+ def initialize(options)
32
+ @options = options
33
+ end
34
+
35
+ def print(output)
36
+ output.write("with options ", :variable, @options.inspect)
37
+ end
38
+
39
+ def call(assertions, subject)
40
+ options = {}
41
+ @options.each{|name| options[name] = nil}
42
+
43
+ subject.each do |type, name|
44
+ options[name] = type
45
+ end
46
+
47
+ assertions.nested(self) do |assertions|
48
+ options.each do |name, type|
49
+ assertions.assert(type != nil, "option #{name}: is required")
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ def initialize(method)
56
+ @method = method
57
+ @parameters = nil
58
+ @options = nil
59
+ end
60
+
61
+ def with_options(*options)
62
+ @options = WithOptions.new(options)
63
+ return self
64
+ end
65
+
66
+ def print(output)
67
+ output.write("respond to ", :variable, @method.to_s)
68
+ end
69
+
70
+ def call(assertions, subject)
71
+ assertions.nested(self) do |assertions|
72
+ condition = subject.respond_to?(@method)
73
+ assertions.assert(condition, self)
74
+
75
+ if condition and (@parameters or @options)
76
+ parameters = subject.method(@method).parameters
77
+ @parameters.call(assertions, parameters) if @parameters
78
+ @options.call(assertions, parameters) if @options
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ class Base
85
+ def respond_to(method)
86
+ RespondTo.new(method)
87
+ end
88
+ end
89
+ end
data/lib/sus/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sus
4
- VERSION = "0.6.2"
4
+ VERSION = "0.8.1"
5
5
  end
data/lib/sus.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'sus/version'
4
+ require_relative 'sus/config'
4
5
  require_relative 'sus/registry'
5
6
  require_relative 'sus/assertions'
6
7
 
@@ -8,6 +9,9 @@ require_relative 'sus/expect'
8
9
  require_relative 'sus/be'
9
10
  require_relative 'sus/be_within'
10
11
 
12
+ require_relative 'sus/mock'
13
+ require_relative 'sus/receive'
14
+
11
15
  require_relative 'sus/raise_exception'
12
16
  require_relative 'sus/have_duration'
13
17
 
data.tar.gz.sig ADDED
Binary file