sus 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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