service_skeleton 1.0.1 → 2.0.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.
- checksums.yaml +4 -4
- data/.git-blame-ignore-revs +2 -0
- data/.github/workflows/ci.yml +50 -0
- data/.gitignore +0 -7
- data/.rubocop.yml +11 -1
- data/README.md +1 -53
- data/lib/service_skeleton/config.rb +20 -13
- data/lib/service_skeleton/generator.rb +4 -4
- data/lib/service_skeleton/runner.rb +3 -3
- data/lib/service_skeleton/ultravisor_children.rb +2 -1
- data/lib/service_skeleton/ultravisor_loggerstash.rb +9 -1
- data/service_skeleton.gemspec +4 -14
- data/ultravisor/.yardopts +1 -0
- data/ultravisor/Guardfile +9 -0
- data/ultravisor/README.md +404 -0
- data/ultravisor/lib/ultravisor.rb +216 -0
- data/ultravisor/lib/ultravisor/child.rb +485 -0
- data/ultravisor/lib/ultravisor/child/call.rb +21 -0
- data/ultravisor/lib/ultravisor/child/call_receiver.rb +14 -0
- data/ultravisor/lib/ultravisor/child/cast.rb +16 -0
- data/ultravisor/lib/ultravisor/child/cast_receiver.rb +11 -0
- data/ultravisor/lib/ultravisor/child/process_cast_call.rb +39 -0
- data/ultravisor/lib/ultravisor/error.rb +25 -0
- data/ultravisor/lib/ultravisor/logging_helpers.rb +32 -0
- data/ultravisor/spec/example_group_methods.rb +19 -0
- data/ultravisor/spec/example_methods.rb +8 -0
- data/ultravisor/spec/spec_helper.rb +56 -0
- data/ultravisor/spec/ultravisor/add_child_spec.rb +79 -0
- data/ultravisor/spec/ultravisor/child/call_spec.rb +121 -0
- data/ultravisor/spec/ultravisor/child/cast_spec.rb +111 -0
- data/ultravisor/spec/ultravisor/child/id_spec.rb +21 -0
- data/ultravisor/spec/ultravisor/child/new_spec.rb +152 -0
- data/ultravisor/spec/ultravisor/child/restart_delay_spec.rb +40 -0
- data/ultravisor/spec/ultravisor/child/restart_spec.rb +70 -0
- data/ultravisor/spec/ultravisor/child/run_spec.rb +95 -0
- data/ultravisor/spec/ultravisor/child/shutdown_spec.rb +124 -0
- data/ultravisor/spec/ultravisor/child/spawn_spec.rb +216 -0
- data/ultravisor/spec/ultravisor/child/unsafe_instance_spec.rb +55 -0
- data/ultravisor/spec/ultravisor/child/wait_spec.rb +32 -0
- data/ultravisor/spec/ultravisor/new_spec.rb +71 -0
- data/ultravisor/spec/ultravisor/remove_child_spec.rb +49 -0
- data/ultravisor/spec/ultravisor/run_spec.rb +334 -0
- data/ultravisor/spec/ultravisor/shutdown_spec.rb +106 -0
- metadata +48 -64
- data/.travis.yml +0 -11
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../../spec_helper"
|
4
|
+
|
5
|
+
require_relative "../../../lib/ultravisor/child"
|
6
|
+
require_relative "../../../lib/ultravisor/error"
|
7
|
+
|
8
|
+
describe Ultravisor::Child do
|
9
|
+
uses_logger
|
10
|
+
|
11
|
+
let(:base_args) { { id: :bob, klass: mock_class, method: :run } }
|
12
|
+
let(:child) { Ultravisor::Child.new(**args) }
|
13
|
+
let(:mock_class) { Class.new.tap { |k| k.class_eval { def run; end; def sigterm; end } } }
|
14
|
+
let(:mock_instance) { instance_double(mock_class) }
|
15
|
+
let(:term_queue) { instance_double(Queue) }
|
16
|
+
|
17
|
+
describe "#shutdown" do
|
18
|
+
let(:args) { base_args }
|
19
|
+
|
20
|
+
context "when the child isn't running" do
|
21
|
+
it "returns immediately" do
|
22
|
+
child.shutdown
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context "when the child is running" do
|
27
|
+
before(:each) do
|
28
|
+
orig_thread_new = Thread.method(:new)
|
29
|
+
allow(Thread).to receive(:new) do |&b|
|
30
|
+
orig_thread_new.call(&b).tap do |th|
|
31
|
+
@thread = th
|
32
|
+
allow(th).to receive(:kill).and_call_original
|
33
|
+
allow(th).to receive(:join).and_call_original
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
allow(mock_class).to receive(:new).and_return(mock_instance)
|
38
|
+
allow(mock_instance).to receive(:run)
|
39
|
+
|
40
|
+
allow(term_queue).to receive(:<<)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "kills the thread" do
|
44
|
+
child.spawn(term_queue).shutdown
|
45
|
+
|
46
|
+
expect(@thread).to have_received(:kill)
|
47
|
+
end
|
48
|
+
|
49
|
+
it "waits for the thread to be done" do
|
50
|
+
child.spawn(term_queue).shutdown
|
51
|
+
|
52
|
+
expect(@thread).to have_received(:join).with(1)
|
53
|
+
end
|
54
|
+
|
55
|
+
it "doesn't put anything on the queue" do
|
56
|
+
expect(term_queue).to_not receive(:<<)
|
57
|
+
|
58
|
+
child.spawn(term_queue).shutdown
|
59
|
+
end
|
60
|
+
|
61
|
+
context "when there's a shutdown spec" do
|
62
|
+
let(:args) { base_args.merge(shutdown: { method: :sigterm, timeout: 0.05 }) }
|
63
|
+
|
64
|
+
before(:each) do
|
65
|
+
allow(mock_instance).to receive(:sigterm)
|
66
|
+
end
|
67
|
+
|
68
|
+
it "calls the specified shutdown method" do
|
69
|
+
expect(mock_instance).to receive(:sigterm)
|
70
|
+
|
71
|
+
child.spawn(term_queue).shutdown
|
72
|
+
end
|
73
|
+
|
74
|
+
it "waits for up to the timeout period" do
|
75
|
+
child.spawn(term_queue).shutdown
|
76
|
+
|
77
|
+
expect(@thread).to have_received(:join).with(0.05)
|
78
|
+
end
|
79
|
+
|
80
|
+
context "the worker doesn't finish quickly enough" do
|
81
|
+
before(:each) do
|
82
|
+
allow(mock_instance).to receive(:run) { sleep 15 }
|
83
|
+
end
|
84
|
+
|
85
|
+
it "kills the thread" do
|
86
|
+
child.spawn(term_queue).shutdown
|
87
|
+
|
88
|
+
expect(@thread).to have_received(:kill)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
context "when the thread infinihangs" do
|
94
|
+
# No need for a big timeout, we know it's not going to succeed
|
95
|
+
let(:args) { base_args.merge(shutdown: { timeout: 0.000001 }) }
|
96
|
+
let(:m) { Mutex.new }
|
97
|
+
let(:cv) { ConditionVariable.new }
|
98
|
+
|
99
|
+
before(:each) do
|
100
|
+
allow(mock_instance).to receive(:run) do
|
101
|
+
Thread.handle_interrupt(Numeric => :never) do
|
102
|
+
m.synchronize do
|
103
|
+
@state = 1
|
104
|
+
cv.signal
|
105
|
+
cv.wait(m) until @state == 2
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
allow(logger).to receive(:error)
|
111
|
+
end
|
112
|
+
|
113
|
+
it "logs an error" do
|
114
|
+
expect(logger).to receive(:error)
|
115
|
+
|
116
|
+
child.spawn(term_queue)
|
117
|
+
m.synchronize { cv.wait(m) until @state == 1 }
|
118
|
+
child.shutdown
|
119
|
+
m.synchronize { @state = 2; cv.signal }
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,216 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../../spec_helper"
|
4
|
+
|
5
|
+
require_relative "../../../lib/ultravisor/child"
|
6
|
+
require_relative "../../../lib/ultravisor/error"
|
7
|
+
|
8
|
+
describe Ultravisor::Child do
|
9
|
+
let(:child) { Ultravisor::Child.new(**args) }
|
10
|
+
let(:mock_class) { Class.new.tap { |k| k.class_eval { def run; end } } }
|
11
|
+
let(:mock_instance) { double(mock_class) } # rubocop:disable RSpec/VerifiedDoubles
|
12
|
+
let(:term_queue) { instance_double(Queue) }
|
13
|
+
|
14
|
+
describe "#spawn" do
|
15
|
+
before(:each) do
|
16
|
+
allow(term_queue).to receive(:<<)
|
17
|
+
end
|
18
|
+
|
19
|
+
context "with minimal arguments" do
|
20
|
+
let(:args) { { id: :bob, klass: mock_class, method: :run } }
|
21
|
+
|
22
|
+
before(:each) do
|
23
|
+
allow(mock_class).to receive(:new).and_return(mock_instance)
|
24
|
+
allow(mock_instance).to receive(:run)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "instantiates the class" do
|
28
|
+
expect(mock_class).to receive(:new).with(no_args)
|
29
|
+
|
30
|
+
child.spawn(term_queue).wait
|
31
|
+
end
|
32
|
+
|
33
|
+
it "calls the run method on the class instance" do
|
34
|
+
expect(mock_instance).to receive(:run).with(no_args)
|
35
|
+
|
36
|
+
child.spawn(term_queue).wait
|
37
|
+
end
|
38
|
+
|
39
|
+
it "registers the thread it is running in" do
|
40
|
+
expect(mock_instance).to receive(:run) do
|
41
|
+
expect(child.instance_variable_get(:@thread)).to eq(Thread.current)
|
42
|
+
end
|
43
|
+
|
44
|
+
child.spawn(term_queue).wait
|
45
|
+
end
|
46
|
+
|
47
|
+
it "notes the start time" do
|
48
|
+
expect(mock_instance).to receive(:run) do
|
49
|
+
# Can only check @start_time while the child is running, as the
|
50
|
+
# variable gets nil'd after the run completes
|
51
|
+
expect(child.instance_variable_get(:@start_time).to_f).to be_within(0.01).of(Time.now.to_f)
|
52
|
+
end
|
53
|
+
|
54
|
+
child.spawn(term_queue).wait
|
55
|
+
end
|
56
|
+
|
57
|
+
it "notes the termination value" do
|
58
|
+
expect(mock_instance).to receive(:run).with(no_args).and_return(42)
|
59
|
+
|
60
|
+
child.spawn(term_queue)
|
61
|
+
|
62
|
+
expect(child.termination_value).to eq(42)
|
63
|
+
end
|
64
|
+
|
65
|
+
it "tells the ultravisor it terminated" do
|
66
|
+
expect(term_queue).to receive(:<<).with(child)
|
67
|
+
|
68
|
+
child.spawn(term_queue).wait
|
69
|
+
end
|
70
|
+
|
71
|
+
it "creates a new thread" do
|
72
|
+
expect(Thread).to receive(:new)
|
73
|
+
|
74
|
+
child.spawn(term_queue).wait
|
75
|
+
end
|
76
|
+
|
77
|
+
context "when the worker object's run method raises an exception" do
|
78
|
+
before(:each) do
|
79
|
+
allow(mock_instance).to receive(:run).and_raise(RuntimeError.new("FWACKOOM"))
|
80
|
+
end
|
81
|
+
|
82
|
+
it "makes a note of the exception" do
|
83
|
+
child.spawn(term_queue)
|
84
|
+
|
85
|
+
expect(child.termination_exception).to be_a(RuntimeError)
|
86
|
+
end
|
87
|
+
|
88
|
+
it "tells the ultravisor it terminated" do
|
89
|
+
expect(term_queue).to receive(:<<).with(child)
|
90
|
+
|
91
|
+
child.spawn(term_queue).wait
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
context "with a worker class that takes only non-keyword args" do
|
97
|
+
let(:args) { { id: :testy, klass: mock_class, args: ["foo", "bar"], method: :run } }
|
98
|
+
let(:mock_class) do
|
99
|
+
Class.new do
|
100
|
+
def initialize(foo, bar)
|
101
|
+
end
|
102
|
+
|
103
|
+
def run
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
it "creates the class instance with args" do
|
109
|
+
allow(mock_class).to receive(:new).and_call_original
|
110
|
+
|
111
|
+
child.spawn(term_queue).wait
|
112
|
+
expect(mock_class).to have_received(:new).with("foo", "bar")
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
context "with a worker class that takes non-keyword and optional args" do
|
117
|
+
let(:args) { { id: :testy, klass: mock_class, args: ["foo", "bar", baz: "wombat"], method: :run } }
|
118
|
+
let(:mock_class) do
|
119
|
+
Class.new do
|
120
|
+
def initialize(foo, bar, baz = {})
|
121
|
+
end
|
122
|
+
|
123
|
+
def run
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
it "creates the class instance with args" do
|
129
|
+
allow(mock_class).to receive(:new).and_call_original
|
130
|
+
|
131
|
+
child.spawn(term_queue).wait
|
132
|
+
expect(mock_class).to have_received(:new).with("foo", "bar", { baz: "wombat" })
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
context "with a worker class that takes mixed args" do
|
137
|
+
let(:args) { { id: :testy, klass: mock_class, args: ["foo", "bar", baz: "wombat"], method: :run } }
|
138
|
+
let(:mock_class) do
|
139
|
+
Class.new do
|
140
|
+
def initialize(foo, bar, baz:)
|
141
|
+
end
|
142
|
+
|
143
|
+
def run
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
it "creates the class instance with args" do
|
149
|
+
allow(mock_class).to receive(:new).and_call_original
|
150
|
+
|
151
|
+
child.spawn(term_queue).wait
|
152
|
+
expect(mock_class).to have_received(:new).with("foo", "bar", baz: "wombat")
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
context "with a worker class that takes keyword args" do
|
157
|
+
let(:args) { { id: :testy, klass: mock_class, args: [foo: "bar", baz: "wombat"], method: :run } }
|
158
|
+
let(:mock_class) do
|
159
|
+
Class.new do
|
160
|
+
def initialize(foo:, baz:)
|
161
|
+
end
|
162
|
+
|
163
|
+
def run
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
it "creates the class instance with args" do
|
169
|
+
allow(mock_class).to receive(:new).and_call_original
|
170
|
+
|
171
|
+
child.spawn(term_queue).wait
|
172
|
+
expect(mock_class).to have_received(:new).with(foo: "bar", baz: "wombat")
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
context "with a worker class that takes optional args" do
|
177
|
+
let(:args) { { id: :testy, klass: mock_class, args: [foo: "bar", baz: "wombat", fib: "wib", woop: "woob"], method: :run } }
|
178
|
+
let(:mock_class) do
|
179
|
+
Class.new do
|
180
|
+
def initialize(foo:, baz:, fib: "none", woop: nil)
|
181
|
+
end
|
182
|
+
|
183
|
+
def run
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
it "creates the class instance with args" do
|
189
|
+
allow(mock_class).to receive(:new).and_call_original
|
190
|
+
child.spawn(term_queue).wait
|
191
|
+
|
192
|
+
expect(mock_class).to have_received(:new).with(foo: "bar", baz: "wombat", fib: "wib", woop: "woob")
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
context "with a worker class that takes keyword args in form of a hash" do
|
197
|
+
let(:args) { { id: :testy, klass: mock_class, args: { foo: "bar", baz: "wombat" }, method: :run } }
|
198
|
+
let(:mock_class) do
|
199
|
+
Class.new do
|
200
|
+
def initialize(foo:, baz:)
|
201
|
+
end
|
202
|
+
|
203
|
+
def run
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
it "creates the class instance with args" do
|
209
|
+
allow(mock_class).to receive(:new).and_call_original
|
210
|
+
child.spawn(term_queue).wait
|
211
|
+
|
212
|
+
expect(mock_class).to have_received(:new).with(foo: "bar", baz: "wombat")
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../../spec_helper"
|
4
|
+
|
5
|
+
require_relative "../../../lib/ultravisor/child"
|
6
|
+
require_relative "../../../lib/ultravisor/error"
|
7
|
+
|
8
|
+
describe Ultravisor::Child do
|
9
|
+
let(:args) { { id: :bob, klass: Object, method: :to_s } }
|
10
|
+
let(:child) { Ultravisor::Child.new(**args) }
|
11
|
+
|
12
|
+
describe "#unsafe_instance" do
|
13
|
+
context "by default" do
|
14
|
+
it "explodes" do
|
15
|
+
expect { child.unsafe_instance }.to raise_error(Ultravisor::ThreadSafetyError)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
context "with access: :unsafe" do
|
20
|
+
let(:args) { { id: :bob, klass: Object, method: :to_s, access: :unsafe } }
|
21
|
+
|
22
|
+
context "when there's no instance object" do
|
23
|
+
it "waits for the instance object to appear" do
|
24
|
+
expect(child.instance_variable_get(:@spawn_cv)).to receive(:wait) do
|
25
|
+
child.instance_variable_set(:@instance, "gogogo")
|
26
|
+
end
|
27
|
+
|
28
|
+
child.unsafe_instance
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context "when there's an instance object" do
|
33
|
+
before(:each) do
|
34
|
+
child.instance_variable_set(:@instance, "bob")
|
35
|
+
end
|
36
|
+
|
37
|
+
it "returns the instance object" do
|
38
|
+
expect(child.unsafe_instance).to eq("bob")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context "when the child is running" do
|
44
|
+
it "only exits once the child has finished" do
|
45
|
+
child.instance_variable_set(:@thread, Thread.new {})
|
46
|
+
|
47
|
+
expect(child.instance_variable_get(:@spawn_cv)).to receive(:wait).with(child.instance_variable_get(:@spawn_m)) do
|
48
|
+
child.instance_variable_set(:@thread, nil)
|
49
|
+
end
|
50
|
+
|
51
|
+
child.wait
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../../spec_helper"
|
4
|
+
|
5
|
+
require_relative "../../../lib/ultravisor/child"
|
6
|
+
require_relative "../../../lib/ultravisor/error"
|
7
|
+
|
8
|
+
describe Ultravisor::Child do
|
9
|
+
let(:args) { { id: :bob, klass: mock_class, method: :run } }
|
10
|
+
let(:child) { Ultravisor::Child.new(**args) }
|
11
|
+
let(:mock_class) { Class.new.tap { |k| k.class_eval { def run; end } } }
|
12
|
+
|
13
|
+
describe "#wait" do
|
14
|
+
context "when the child isn't running" do
|
15
|
+
it "just returns straight away" do
|
16
|
+
child.wait
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context "when the child is running" do
|
21
|
+
it "only exits once the child has finished" do
|
22
|
+
child.instance_variable_set(:@thread, Thread.new {})
|
23
|
+
|
24
|
+
expect(child.instance_variable_get(:@spawn_cv)).to receive(:wait).with(child.instance_variable_get(:@spawn_m)) do
|
25
|
+
child.instance_variable_set(:@thread, nil)
|
26
|
+
end
|
27
|
+
|
28
|
+
child.wait
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative "../spec_helper"
|
3
|
+
|
4
|
+
require_relative "../../lib/ultravisor"
|
5
|
+
|
6
|
+
describe Ultravisor do
|
7
|
+
describe ".new" do
|
8
|
+
context "without arguments" do
|
9
|
+
it "does not explode" do
|
10
|
+
expect { Ultravisor.new }.to_not raise_error
|
11
|
+
end
|
12
|
+
|
13
|
+
it "gives us an Ultravisor instance" do
|
14
|
+
expect(Ultravisor.new).to be_a(Ultravisor)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context "with empty children" do
|
19
|
+
it "does not explode" do
|
20
|
+
expect { Ultravisor.new children: [] }.to_not raise_error
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context "with children that isn't an array" do
|
25
|
+
it "raises an error" do
|
26
|
+
[{}, "ohai!", nil, 42].each do |v|
|
27
|
+
expect { Ultravisor.new children: v }.to raise_error(ArgumentError)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context "with valid children" do
|
33
|
+
let(:ultravisor) { Ultravisor.new(children: [{ id: :testy, klass: Object, method: :to_s }]) }
|
34
|
+
|
35
|
+
it "registers the child by its ID" do
|
36
|
+
expect(ultravisor[:testy]).to be_a(Ultravisor::Child)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context "with two children with the same ID" do
|
41
|
+
it "explodes" do
|
42
|
+
expect do
|
43
|
+
Ultravisor.new(
|
44
|
+
children: [
|
45
|
+
{ id: :testy, klass: Object, method: :to_s },
|
46
|
+
{ id: :testy, klass: Class, method: :to_s },
|
47
|
+
]
|
48
|
+
)
|
49
|
+
end.to raise_error(Ultravisor::DuplicateChildError)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context "with a valid strategy" do
|
54
|
+
it "does not explode" do
|
55
|
+
expect { Ultravisor.new strategy: :all_for_one }.to_not raise_error
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
[
|
60
|
+
{ strategy: :bob },
|
61
|
+
{ strategy: "all_for_one" },
|
62
|
+
{ strategy: ["games"] },
|
63
|
+
].each do |s|
|
64
|
+
context "with invalid strategy #{s.inspect}" do
|
65
|
+
it "explodes" do
|
66
|
+
expect { Ultravisor.new **s }.to raise_error(ArgumentError)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|