sus 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: dccbd079d0154903705918ed6d572de0f4a4079d2f4d5fd352e8cef0a9694a05
4
+ data.tar.gz: 400eb2d789f2ff17209f289ad2f0e0d89486cd200e127e80f304fdb2e29c87f3
5
+ SHA512:
6
+ metadata.gz: f7c3cb03088ccd432b27101ab654032c713bbecff85b18e6bdadf7315361a3be306778bbda012ee04e69f325f989ec093809b13d070f20b5ae8818e7deccdb7a
7
+ data.tar.gz: b8519c677f9d0731bc5c5a7bbb0dd71ee302b9d5a68b115e1ec6feeb4baf6d841e76a0487b71a9ad0f78ae9a35e30370a88d26a937d1cb0c7d0a617ad77e123b
data/bin/sus ADDED
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/sus'
4
+
5
+ def prepare(paths, registry)
6
+ if paths&.any?
7
+ paths.each do |path|
8
+ registry.load(path)
9
+ end
10
+ else
11
+ Dir.glob("test/**/*.rb").each do |path|
12
+ registry.load(path)
13
+ end
14
+ end
15
+ end
16
+
17
+ filter = Sus::Filter.new
18
+ output = Sus::Output.default
19
+ assertions = Sus::Assertions.default(output: Sus::Output::Null.new)
20
+
21
+ prepare(ARGV, filter)
22
+
23
+ filter.call(assertions)
24
+
25
+ assertions.print(output)
26
+ output.puts
27
+
28
+ if assertions.failed.any?
29
+ output.puts
30
+
31
+ assertions.failed.each do |failure|
32
+ failure.output.append(output)
33
+ end
34
+ end
data/bin/sus-parallel ADDED
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/sus'
4
+ require_relative '../lib/sus/progress'
5
+
6
+ require 'etc'
7
+
8
+ def prepare(paths, registry)
9
+ if paths&.any?
10
+ paths.each do |path|
11
+ registry.load(path)
12
+ end
13
+ else
14
+ Dir.glob("test/**/*.rb").each do |path|
15
+ registry.load(path)
16
+ end
17
+ end
18
+ end
19
+
20
+ Result = Struct.new(:job, :assertions)
21
+
22
+ filter = Sus::Filter.new
23
+ output = Sus::Output.default
24
+
25
+ jobs = Thread::Queue.new
26
+ results = Thread::Queue.new
27
+ guard = Thread::Mutex.new
28
+ progress = Sus::Progress.new(output)
29
+ count = Etc.nprocessors
30
+
31
+ loader = Thread.new do
32
+ prepare(ARGV, filter)
33
+
34
+ filter.each do |child|
35
+ guard.synchronize{progress.expand}
36
+ jobs << child
37
+ end
38
+
39
+ jobs.close
40
+ end
41
+
42
+ aggregation = Thread.new do
43
+ top = Sus::Assertions.new(output: Sus::Output::Null.new)
44
+
45
+ while result = results.pop
46
+ guard.synchronize{progress.increment}
47
+
48
+ top.add(result.assertions)
49
+
50
+ guard.synchronize{progress.report(count, top, :busy)}
51
+ end
52
+
53
+ guard.synchronize{progress.clear}
54
+
55
+ top
56
+ end
57
+
58
+ workers = count.times.map do |index|
59
+ Thread.new do
60
+ while job = jobs.pop
61
+ guard.synchronize{progress.report(index, job, :busy)}
62
+
63
+ assertions = Sus::Assertions.new(output: Sus::Output::Null.new)
64
+ job.call(assertions)
65
+ results << Result.new(job, assertions)
66
+
67
+ guard.synchronize{progress.report(index, "idle", :free)}
68
+ end
69
+ end
70
+ end
71
+
72
+ loader.join
73
+
74
+ workers.each(&:join)
75
+ results.close
76
+
77
+ assertions = aggregation.value
78
+
79
+ assertions.print(output)
80
+ output.puts
81
+
82
+ if assertions.failed.any?
83
+ output.puts
84
+
85
+ assertions.failed.each do |failure|
86
+ failure.output.append(output)
87
+ end
88
+ end
@@ -0,0 +1,188 @@
1
+
2
+ require_relative 'output'
3
+
4
+ module Sus
5
+ class Assertions
6
+ def self.default(**options)
7
+ self.new(**options, verbose: true)
8
+ end
9
+
10
+ def initialize(target: nil, output: Output.default, inverted: false, verbose: false)
11
+ @target = target
12
+ @output = output
13
+ @inverted = inverted
14
+ @verbose = verbose
15
+
16
+ @passed = Array.new
17
+ @failed = Array.new
18
+ @count = 0
19
+ end
20
+
21
+ attr :target
22
+ attr :output
23
+ attr :level
24
+ attr :inverted
25
+ attr :verbose
26
+
27
+ # How many nested assertions passed.
28
+ attr :passed
29
+
30
+ # How many nested assertions failed.
31
+ attr :failed
32
+
33
+ # The total number of assertions performed:
34
+ attr :count
35
+
36
+ def inspect
37
+ "\#<#{self.class} #{@passed.size} passed #{@failed.size} failed>"
38
+ end
39
+
40
+ def total
41
+ @passed.size + @failed.size
42
+ end
43
+
44
+ def print(output, verbose: @verbose)
45
+ self
46
+
47
+ if verbose && @target
48
+ @target.print(output)
49
+ output.write(": ")
50
+ end
51
+
52
+ if @count.zero?
53
+ output.write("0 assertions")
54
+ else
55
+ if @passed.any?
56
+ output.write(:passed, @passed.size, " passed", :reset, " ")
57
+ end
58
+
59
+ if @failed.any?
60
+ output.write(:failed, @failed.size, " failed", :reset, " ")
61
+ end
62
+
63
+ output.write("out of ", self.total, " total (", @count, " assertions)")
64
+ end
65
+ end
66
+
67
+ def puts(*message)
68
+ @output.puts(:indent, *message)
69
+ end
70
+
71
+ def passed?
72
+ @failed.empty?
73
+ end
74
+
75
+ def failed?
76
+ @failed.any?
77
+ end
78
+
79
+ def assert(condition, message = nil)
80
+ @count += 1
81
+
82
+ if @inverted
83
+ condition = !condition
84
+ end
85
+
86
+ if condition
87
+ @passed << self
88
+
89
+ if @verbose
90
+ @output.puts(:indent, :passed, pass_prefix, message || "assertion")
91
+ end
92
+ else
93
+ @failed << self
94
+
95
+ @output.puts(:indent, :failed, fail_prefix, message || "assertion")
96
+ end
97
+ end
98
+
99
+ def fail(error)
100
+ @failed << self
101
+
102
+ @output.puts(:indent, :failed, fail_prefix, "Unhandled exception ", :value, error.class, ": ", error.message)
103
+ error.backtrace.each do |line|
104
+ @output.puts(:indent, line)
105
+ end
106
+ end
107
+
108
+ def nested(target, isolated: false, inverted: false, **options)
109
+ result = nil
110
+ output = @output
111
+
112
+ if inverted
113
+ inverted = !@inverted
114
+ else
115
+ inverted = @inverted
116
+ end
117
+
118
+ if isolated
119
+ output = Output::Buffered.new(output)
120
+ end
121
+
122
+ output.write(:indent)
123
+ target.print(output)
124
+ output.puts
125
+
126
+ assertions = self.class.new(target: target, output: output, inverted: inverted, **options)
127
+
128
+ begin
129
+ output.indented do
130
+ result = yield(assertions)
131
+ end
132
+ rescue StandardError => error
133
+ assertions.fail(error)
134
+ end
135
+
136
+ if assertions
137
+ if isolated
138
+ merge(assertions)
139
+ else
140
+ add(assertions)
141
+ end
142
+ end
143
+
144
+ return result
145
+ end
146
+
147
+ def merge(assertions)
148
+ @count += assertions.count
149
+
150
+ if assertions.passed?
151
+ @passed << assertions
152
+
153
+ if @verbose
154
+ @output.write(:indent, :passed, pass_prefix, :reset)
155
+ self.print(@output, verbose: false)
156
+ @output.puts
157
+ end
158
+ else
159
+ @failed << assertions
160
+
161
+ @output.write(:indent, :failed, fail_prefix, :reset)
162
+ self.print(@output, verbose: false)
163
+ @output.puts
164
+ end
165
+ end
166
+
167
+ def add(assertions)
168
+ @count += assertions.count
169
+ @passed.concat(assertions.passed)
170
+ @failed.concat(assertions.failed)
171
+
172
+ if @verbose
173
+ self.print(@output, verbose: false)
174
+ @output.puts
175
+ end
176
+ end
177
+
178
+ private
179
+
180
+ def pass_prefix
181
+ "✓ "
182
+ end
183
+
184
+ def fail_prefix
185
+ "✗ "
186
+ end
187
+ end
188
+ end
data/lib/sus/base.rb ADDED
@@ -0,0 +1,44 @@
1
+
2
+ require_relative 'context'
3
+
4
+ module Sus
5
+ class Base
6
+ def initialize(assertions)
7
+ @assertions = assertions
8
+ end
9
+
10
+ def before
11
+ end
12
+
13
+ def after
14
+ end
15
+
16
+ def around
17
+ self.before
18
+
19
+ return yield
20
+ ensure
21
+ self.after
22
+ end
23
+
24
+ def assert(...)
25
+ @assertions.assert(...)
26
+ end
27
+
28
+ def refute(...)
29
+ @assertions.refute(...)
30
+ end
31
+
32
+ def expect(subject)
33
+ Expect.new(subject)
34
+ end
35
+ end
36
+
37
+ def self.base(description = "base")
38
+ base = Class.new(Base)
39
+ base.extend(Context)
40
+ base.description = description
41
+
42
+ return base
43
+ end
44
+ end
data/lib/sus/be.rb ADDED
@@ -0,0 +1,62 @@
1
+
2
+ module Sus
3
+ class Be
4
+ def initialize(*arguments)
5
+ @arguments = arguments
6
+ end
7
+
8
+ def print(output)
9
+ output.write("be ", :be, *@arguments.join(" "))
10
+ end
11
+
12
+ def call(assertions, subject)
13
+ assertions.nested(self) do |assertions|
14
+ assertions.assert(subject.public_send(*@arguments), self)
15
+ end
16
+ end
17
+
18
+ class << self
19
+ def == value
20
+ Be.new(:==, value)
21
+ end
22
+
23
+ def != value
24
+ Be.new(:!=, value)
25
+ end
26
+
27
+ def > value
28
+ Be.new(:>, value)
29
+ end
30
+
31
+ def >= value
32
+ Be.new(:>=, value)
33
+ end
34
+
35
+ def < value
36
+ Be.new(:<, value)
37
+ end
38
+
39
+ def <= value
40
+ Be.new(:<=, value)
41
+ end
42
+
43
+ def =~ value
44
+ Be.new(:=~, value)
45
+ end
46
+
47
+ def === value
48
+ Be.new(:===, value)
49
+ end
50
+ end
51
+ end
52
+
53
+ class Base
54
+ def be(*arguments)
55
+ if arguments.any?
56
+ Be.new(*arguments)
57
+ else
58
+ Be
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,36 @@
1
+ require 'etc'
2
+ require 'samovar'
3
+
4
+ module Sus
5
+ module Command
6
+ class List < Samovar::Command
7
+ self.description = "List all available tests."
8
+
9
+ many :paths
10
+
11
+ def prepare(registry)
12
+ if paths&.any?
13
+ paths.each do |path|
14
+ registry.load(path)
15
+ end
16
+ else
17
+ Dir.glob("test/**/*.rb").each do |path|
18
+ registry.load(path)
19
+ end
20
+ end
21
+ end
22
+
23
+ def call
24
+ registry = Sus::Registry.new
25
+ output = Sus::Output.default
26
+
27
+ prepare(registry)
28
+
29
+ registry.each do |child|
30
+ child.print(output)
31
+ output.puts
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,104 @@
1
+ require 'etc'
2
+ require 'samovar'
3
+
4
+ module Sus
5
+ module Command
6
+ class Run < Samovar::Command
7
+ self.description = "Run one or more tests."
8
+
9
+ options do
10
+ option '-c/--count <n>', "The number of threads to use for running tests.", type: Integer, default: Etc.nprocessors
11
+ option '-r/--require <path>', ""
12
+ end
13
+
14
+ many :paths
15
+
16
+ def prepare(registry)
17
+ if paths&.any?
18
+ paths.each do |path|
19
+ registry.load(path)
20
+ end
21
+ else
22
+ Dir.glob("test/**/*.rb").each do |path|
23
+ registry.load(path)
24
+ end
25
+ end
26
+ end
27
+
28
+ Result = Struct.new(:job, :assertions)
29
+
30
+ def call
31
+ registry = Sus::Registry.new
32
+ jobs = Thread::Queue.new
33
+ results = Thread::Queue.new
34
+
35
+ output = Sus::Output.default
36
+ guard = Thread::Mutex.new
37
+ progress = Sus::Progress.new(output)
38
+ count = @options[:count]
39
+
40
+ loader = Thread.new do
41
+ prepare(registry)
42
+
43
+ registry.each do |child|
44
+ guard.synchronize{progress.expand}
45
+ jobs << child
46
+ end
47
+
48
+ jobs.close
49
+ end
50
+
51
+ aggregation = Thread.new do
52
+ assertions = Sus::Assertions.new(output: output.buffered)
53
+ first = true
54
+
55
+ while result = results.pop
56
+ guard.synchronize{progress.increment}
57
+
58
+ if result.assertions.failed?
59
+ if first
60
+ first = false
61
+ else
62
+ assertions.output.puts
63
+ end
64
+
65
+ result.assertions.output.append(assertions.output)
66
+ end
67
+
68
+ assertions.add(result.assertions)
69
+ guard.synchronize{progress.report(count, assertions, :busy)}
70
+ end
71
+
72
+ guard.synchronize{progress.clear}
73
+
74
+ assertions.output.puts unless first
75
+ assertions.output.append(output)
76
+
77
+ assertions.print(output)
78
+ output.puts
79
+ end
80
+
81
+ workers = count.times.map do |index|
82
+ Thread.new do
83
+ while job = jobs.pop
84
+ guard.synchronize{progress.report(index, job, :busy)}
85
+
86
+ assertions = Sus::Assertions.new(output: output.buffered)
87
+ job.call(assertions)
88
+ results << Result.new(job, assertions)
89
+
90
+ guard.synchronize{progress.report(index, "idle", :free)}
91
+ end
92
+ end
93
+ end
94
+
95
+ loader.join
96
+
97
+ workers.each(&:join)
98
+ results.close
99
+
100
+ aggregation.join
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,35 @@
1
+ require 'etc'
2
+ require 'samovar'
3
+
4
+ module Sus
5
+ module Command
6
+ class Sequential < Samovar::Command
7
+ self.description = "Run one or more tests."
8
+
9
+ many :paths
10
+
11
+ def prepare(registry)
12
+ if paths&.any?
13
+ paths.each do |path|
14
+ registry.load(path)
15
+ end
16
+ else
17
+ Dir.glob("test/**/*.rb").each do |path|
18
+ registry.load(path)
19
+ end
20
+ end
21
+ end
22
+
23
+ Result = Struct.new(:job, :assertions)
24
+
25
+ def call
26
+ registry = Sus::Registry.new
27
+ output = Sus::Output.default
28
+
29
+ prepare(registry)
30
+
31
+ registry.call
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,26 @@
1
+ require 'samovar'
2
+ require_relative 'run'
3
+ require_relative 'sequential'
4
+ require_relative 'list'
5
+
6
+ module Sus
7
+ module Command
8
+ class Top < Samovar::Command
9
+ self.description = "Test your code."
10
+
11
+ nested :command, {
12
+ 'run' => Run,
13
+ 'sequential' => Sequential,
14
+ 'list' => List,
15
+ }, default: 'run'
16
+
17
+ def call
18
+ if command = self.command
19
+ command.call
20
+ else
21
+ self.print_usage
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,63 @@
1
+
2
+ require_relative 'assertions'
3
+ require_relative 'identity'
4
+
5
+ module Sus
6
+ module Context
7
+ attr_accessor :identity
8
+ attr_accessor :description
9
+ attr_accessor :children
10
+
11
+ def self.extended(base)
12
+ base.children = Hash.new
13
+ end
14
+
15
+ def to_s
16
+ self.description || self.name
17
+ end
18
+
19
+ def inspect
20
+ if description = self.description
21
+ "\#<#{self.name || "Context"} #{self.description}>"
22
+ else
23
+ self.name
24
+ end
25
+ end
26
+
27
+ def add(child)
28
+ @children[child.identity] = child
29
+ end
30
+
31
+ def empty?
32
+ @children.nil? || @children.empty?
33
+ end
34
+
35
+ def leaf?
36
+ false
37
+ end
38
+
39
+ def print(output)
40
+ output.write("context ", :context, self.description)
41
+ end
42
+
43
+ def call(assertions)
44
+ return if self.empty?
45
+
46
+ assertions.nested(self) do |assertions|
47
+ self.children.each do |identity, child|
48
+ child.call(assertions)
49
+ end
50
+ end
51
+ end
52
+
53
+ def each(&block)
54
+ self.children.each do |identity, child|
55
+ if child.leaf?
56
+ yield child
57
+ else
58
+ child.each(&block)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end