sus 0.7.0 → 0.9.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.
data/lib/sus/expect.rb CHANGED
@@ -1,11 +1,14 @@
1
1
 
2
2
  module Sus
3
3
  class Expect
4
- def initialize(assertions, subject)
4
+ def initialize(assertions, subject, inverted: false)
5
5
  @assertions = assertions
6
6
  @subject = subject
7
- @inverted = false
7
+ @inverted = inverted
8
8
  end
9
+
10
+ attr :subject
11
+ attr :inverted
9
12
 
10
13
  def not
11
14
  self.dup.tap do |expect|
@@ -17,9 +20,9 @@ module Sus
17
20
  output.write("expect ", :variable, @subject.inspect, :reset, " ")
18
21
 
19
22
  if @inverted
20
- output.write(:failed, "to not", :reset)
23
+ output.write("to not", :reset)
21
24
  else
22
- output.write(:passed, "to", :reset)
25
+ output.write("to", :reset)
23
26
  end
24
27
  end
25
28
 
@@ -30,14 +33,18 @@ module Sus
30
33
 
31
34
  return self
32
35
  end
36
+
37
+ def and(predicate)
38
+ return to(predicate)
39
+ end
33
40
  end
34
41
 
35
42
  class Base
36
- def expect(subject = nil, &block)
43
+ def expect(subject = nil, **options, &block)
37
44
  if block_given?
38
- Expect.new(@assertions, block)
45
+ Expect.new(@__assertions__, block, **options)
39
46
  else
40
- Expect.new(@assertions, subject)
47
+ Expect.new(@__assertions__, subject, **options)
41
48
  end
42
49
  end
43
50
  end
data/lib/sus/file.rb CHANGED
@@ -7,9 +7,7 @@ Sus::TOPLEVEL_CLASS_EVAL = ->(klass, path){klass.class_eval(::File.read(path), p
7
7
  module Sus
8
8
  module File
9
9
  extend Context
10
-
11
- attr_accessor :path
12
-
10
+
13
11
  def self.extended(base)
14
12
  base.children = Hash.new
15
13
  end
@@ -24,6 +22,10 @@ module Sus
24
22
 
25
23
  return base
26
24
  end
25
+
26
+ def print(output)
27
+ output.write("file ", :path, self.identity)
28
+ end
27
29
  end
28
30
 
29
31
  module Context
data/lib/sus/filter.rb CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  module Sus
3
2
  class Filter
4
3
  class Index
@@ -30,7 +29,7 @@ module Sus
30
29
  end
31
30
  end
32
31
 
33
- def initialize(registry: Registry.new)
32
+ def initialize(registry = Registry.new)
34
33
  @registry = registry
35
34
  @index = nil
36
35
  @keys = Array.new
data/lib/sus/it.rb CHANGED
@@ -22,11 +22,11 @@ module Sus
22
22
 
23
23
  def print(output)
24
24
  self.superclass.print(output)
25
- output.write(" it ", :it, self.description, :reset, " ", self.identity.to_s)
25
+ output.write(" it ", :it, self.description, :reset, " ", :identity, self.identity.to_s, :reset)
26
26
  end
27
27
 
28
28
  def call(assertions)
29
- assertions.nested(self, isolated: true) do |assertions|
29
+ assertions.nested(self, identity: self.identity, isolated: true, measure: true) do |assertions|
30
30
  instance = self.new(assertions)
31
31
 
32
32
  instance.around do
data/lib/sus/let.rb CHANGED
@@ -4,13 +4,13 @@ require_relative 'context'
4
4
  module Sus
5
5
  module Context
6
6
  def let(name, &block)
7
- ivar = :"@#{name}"
7
+ instance_variable = :"@#{name}"
8
8
 
9
9
  self.define_method(name) do
10
- if value = self.instance_variable_get(ivar)
11
- return value
10
+ if self.instance_variable_defined?(instance_variable)
11
+ return self.instance_variable_get(instance_variable)
12
12
  else
13
- self.instance_variable_set(ivar, self.instance_exec(&block))
13
+ self.instance_variable_set(instance_variable, self.instance_exec(&block))
14
14
  end
15
15
  end
16
16
  end
data/lib/sus/loader.rb ADDED
@@ -0,0 +1,11 @@
1
+ module Sus
2
+ module Loader
3
+ def require_library(path)
4
+ require(::File.join(self.require_root, "lib", path))
5
+ end
6
+
7
+ def require_fixture(path)
8
+ require(::File.join(self.require_root, "fixtures", path))
9
+ end
10
+ end
11
+ end
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
@@ -0,0 +1,73 @@
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
+ module Sus
24
+ module Output
25
+ # Print out a backtrace relevant to the given test identity if provided.
26
+ class Backtrace
27
+ def self.first(identity = nil)
28
+ # This implementation could be a little more efficient.
29
+ self.new(caller_locations(1), identity&.path, 1)
30
+ end
31
+
32
+ def self.for(exception, identity = nil)
33
+ self.new(exception.backtrace_locations, identity&.path)
34
+ end
35
+
36
+ def initialize(stack, root = nil, limit = nil)
37
+ @stack = stack
38
+ @root = root
39
+ @limit = limit
40
+ end
41
+
42
+ def filter(root = @root)
43
+ if @root
44
+ stack = @stack.select do |frame|
45
+ frame.path.start_with?(@root)
46
+ end
47
+ else
48
+ stack = @stack
49
+ end
50
+
51
+ if @limit
52
+ stack = stack.take(@limit)
53
+ end
54
+
55
+ return stack
56
+ end
57
+
58
+ def print(output)
59
+ if @limit == 1
60
+ filter.each do |frame|
61
+ output.write " ", :path, frame.path, :line, ":", frame.lineno
62
+ end
63
+ else
64
+ output.indented do
65
+ filter.each do |frame|
66
+ output.puts :indent, :path, frame.path, :line, ":", frame.lineno, :reset, " ", frame.label
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -27,34 +27,55 @@ 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(tee = nil)
31
+ @chunks = Array.new
32
+ @tee = tee
33
33
  end
34
34
 
35
- attr :output
36
- attr :buffer
35
+ attr :chunks
36
+ attr :tee
37
37
 
38
- def append(output)
39
- @buffer.each do |operation|
40
- output.public_send(*operation)
38
+ def inspect
39
+ if @tee
40
+ "\#<#{self.class.name} #{@chunks.size} chunks -> #{@tee.class}>"
41
+ else
42
+ "\#<#{self.class.name} #{@chunks.size} chunks>"
41
43
  end
42
44
  end
43
45
 
46
+ def buffered
47
+ self.class.new(self)
48
+ end
49
+
50
+ attr :output
51
+
52
+ def each(&block)
53
+ @chunks.each(&block)
54
+ end
55
+
56
+ def append(buffer)
57
+ @chunks.concat(buffer.output)
58
+ @tee&.append(buffer)
59
+ end
60
+
44
61
  def string
45
62
  io = StringIO.new
46
- append(Text.new(io))
63
+ Text.new(io).append(@chunks)
47
64
  return io.string
48
65
  end
49
66
 
67
+ INDENT = [:indent].freeze
68
+
50
69
  def indent
51
- @buffer << [:indent]
52
- @output.indent
70
+ @chunks << INDENT
71
+ @tee&.indent
53
72
  end
54
73
 
74
+ OUTDENT = [:outdent].freeze
75
+
55
76
  def outdent
56
- @buffer << [:outdent]
57
- @output.outdent
77
+ @chunks << OUTDENT
78
+ @tee&.outdent
58
79
  end
59
80
 
60
81
  def indented
@@ -65,13 +86,13 @@ module Sus
65
86
  end
66
87
 
67
88
  def write(*arguments)
68
- @output.write(*arguments)
69
- @buffer << [:write, *arguments]
89
+ @chunks << [:write, *arguments]
90
+ @tee&.write(*arguments)
70
91
  end
71
92
 
72
93
  def puts(*arguments)
73
- @output.puts(*arguments)
74
- @buffer << [:puts, *arguments]
94
+ @chunks << [:puts, *arguments]
95
+ @tee&.puts(*arguments)
75
96
  end
76
97
  end
77
98
  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,15 @@ module Sus
29
29
  class Null
30
30
  def initialize
31
31
  end
32
+
33
+ def buffered
34
+ Buffered.new(nil)
35
+ end
36
+
37
+ attr :options
38
+
39
+ def append(buffer)
40
+ end
32
41
 
33
42
  def indent
34
43
  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
@@ -29,12 +29,23 @@ module Sus
29
29
  class Text
30
30
  def initialize(io)
31
31
  @io = io
32
+
32
33
  @styles = {reset: self.reset}
33
34
 
34
35
  @indent = String.new
35
36
  @styles[:indent] = @indent
36
37
  end
37
38
 
39
+ def buffered
40
+ Buffered.new(self)
41
+ end
42
+
43
+ def append(buffer)
44
+ buffer.each do |operation|
45
+ self.public_send(*operation)
46
+ end
47
+ end
48
+
38
49
  attr :io
39
50
 
40
51
  INDENTATION = "\t"
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
@@ -22,20 +23,25 @@ module Sus
22
23
 
23
24
  output[:context] = output.style(nil, nil, :bold)
24
25
 
25
- output[:describe] = output.style(:cyan, nil, :bold)
26
+ output[:describe] = output.style(:cyan)
26
27
  output[:it] = output.style(:cyan)
27
28
  output[:with] = output.style(:cyan)
28
29
 
29
30
  output[:variable] = output.style(:blue, nil, :bold)
30
31
 
31
- output[:passed] = output.style(:green, nil, :bold)
32
- output[:failed] = output.style(:red, nil, :bold)
32
+ output[:path] = output.style(:yellow)
33
+ output[:line] = output.style(:yellow)
34
+ output[:identity] = output.style(:yellow)
35
+
36
+ output[:passed] = output.style(:green)
37
+ output[:failed] = output.style(:red)
38
+ output[:error] = output.style(:red)
33
39
 
34
40
  return output
35
41
  end
36
42
 
37
43
  def self.buffered
38
- Buffered.new(Null.new)
44
+ Buffered.new
39
45
  end
40
46
  end
41
47
  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