mcollective-client 2.5.3 → 2.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/mcollective.rb +1 -1
- data/lib/mcollective/application.rb +21 -6
- data/lib/mcollective/client.rb +7 -0
- data/lib/mcollective/config.rb +13 -1
- data/lib/mcollective/connector/base.rb +2 -0
- data/lib/mcollective/facts/base.rb +18 -5
- data/lib/mcollective/log.rb +7 -0
- data/lib/mcollective/logger/base.rb +12 -8
- data/lib/mcollective/logger/file_logger.rb +7 -0
- data/lib/mcollective/message.rb +1 -1
- data/lib/mcollective/optionparser.rb +4 -0
- data/lib/mcollective/registration/base.rb +24 -10
- data/lib/mcollective/rpc/agent.rb +7 -1
- data/lib/mcollective/rpc/client.rb +89 -35
- data/lib/mcollective/rpc/helpers.rb +8 -3
- data/lib/mcollective/rpc/result.rb +4 -0
- data/lib/mcollective/rpc/stats.rb +6 -2
- data/lib/mcollective/shell.rb +2 -0
- data/lib/mcollective/ssl.rb +5 -0
- data/lib/mcollective/util.rb +29 -1
- data/lib/mcollective/validator.rb +9 -4
- data/spec/spec_helper.rb +6 -0
- data/spec/unit/config_spec.rb +10 -0
- data/spec/unit/connector/base_spec.rb +28 -0
- data/spec/unit/facts/base_spec.rb +35 -0
- data/spec/unit/log_spec.rb +9 -0
- data/spec/unit/logger/base_spec.rb +12 -2
- data/spec/unit/logger/file_logger_spec.rb +82 -0
- data/spec/unit/plugins/mcollective/application/plugin_spec.rb +1 -0
- data/spec/unit/plugins/mcollective/connector/activemq_spec.rb +44 -17
- data/spec/unit/plugins/mcollective/connector/rabbitmq_spec.rb +20 -19
- data/spec/unit/plugins/mcollective/data/fact_data_spec.rb +92 -0
- data/spec/unit/registration/base_spec.rb +46 -0
- data/spec/unit/rpc/agent_spec.rb +37 -0
- data/spec/unit/rpc/client_spec.rb +68 -15
- data/spec/unit/rpc/result_spec.rb +21 -0
- data/spec/unit/runner_spec.rb +97 -19
- data/spec/unit/shell_spec.rb +5 -0
- data/spec/unit/ssl_spec.rb +5 -0
- data/spec/unit/util_spec.rb +163 -1
- metadata +215 -209
@@ -264,9 +264,14 @@ module MCollective
|
|
264
264
|
parser.on('--one', '-1', 'Send request to only one discovered nodes') do |v|
|
265
265
|
options[:mcollective_limit_targets] = 1
|
266
266
|
end
|
267
|
-
|
268
|
-
parser.on('--batch SIZE',
|
269
|
-
|
267
|
+
|
268
|
+
parser.on('--batch SIZE', 'Do requests in batches') do |v|
|
269
|
+
# validate batch string. Is it x% where x > 0 or is it an integer
|
270
|
+
if ((v =~ /^(\d+)%$/ && Integer($1) != 0) || v =~ /^(\d+)$/)
|
271
|
+
options[:batch_size] = v
|
272
|
+
else
|
273
|
+
raise(::OptionParser::InvalidArgument.new(v))
|
274
|
+
end
|
270
275
|
end
|
271
276
|
|
272
277
|
parser.on('--batch-sleep SECONDS', Float, 'Sleep time between batches') do |v|
|
@@ -242,8 +242,12 @@ module MCollective
|
|
242
242
|
result_text.puts Util.colorize(:red, "No response from:")
|
243
243
|
result_text.puts
|
244
244
|
|
245
|
-
@noresponsefrom
|
246
|
-
|
245
|
+
field_size = Util.field_size(@noresponsefrom, 30)
|
246
|
+
fields_num = Util.field_number(field_size)
|
247
|
+
format = " " + ( " %-#{field_size}s" * fields_num )
|
248
|
+
|
249
|
+
@noresponsefrom.sort.in_groups_of(fields_num) do |c|
|
250
|
+
result_text.puts format % c
|
247
251
|
end
|
248
252
|
|
249
253
|
result_text.puts
|
data/lib/mcollective/shell.rb
CHANGED
@@ -57,7 +57,9 @@ module MCollective
|
|
57
57
|
@environment = {}
|
58
58
|
else
|
59
59
|
@environment.merge!(val.dup)
|
60
|
+
@environment = @environment.delete_if { |k,v| v.nil? }
|
60
61
|
end
|
62
|
+
|
61
63
|
when "timeout"
|
62
64
|
raise "timeout should be a positive integer or the symbol :on_thread_exit symbol" unless val.eql?(:on_thread_exit) || ( val.is_a?(Fixnum) && val>0 )
|
63
65
|
@timeout = val
|
data/lib/mcollective/ssl.rb
CHANGED
@@ -193,6 +193,11 @@ module MCollective
|
|
193
193
|
end
|
194
194
|
|
195
195
|
def self.base64_decode(string)
|
196
|
+
# The Base 64 character set is A-Z a-z 0-9 + / =
|
197
|
+
# Also allow for whitespace, but raise if we get anything else
|
198
|
+
if string !~ /^[A-Za-z0-9+\/=\s]+$/
|
199
|
+
raise ArgumentError, 'invalid base64'
|
200
|
+
end
|
196
201
|
Base64.decode64(string)
|
197
202
|
end
|
198
203
|
|
data/lib/mcollective/util.rb
CHANGED
@@ -75,11 +75,21 @@ module MCollective
|
|
75
75
|
return false if fact.nil?
|
76
76
|
|
77
77
|
fact = fact.clone
|
78
|
+
case fact
|
79
|
+
when Array
|
80
|
+
return fact.any? { |element| test_fact_value(element, value, operator)}
|
81
|
+
when Hash
|
82
|
+
return fact.keys.any? { |element| test_fact_value(element, value, operator)}
|
83
|
+
else
|
84
|
+
return test_fact_value(fact, value, operator)
|
85
|
+
end
|
86
|
+
end
|
78
87
|
|
88
|
+
def self.test_fact_value(fact, value, operator)
|
79
89
|
if operator == '=~'
|
80
90
|
# to maintain backward compat we send the value
|
81
91
|
# as /.../ which is what 1.0.x needed. this strips
|
82
|
-
# off the /'s
|
92
|
+
# off the /'s which is what we need here
|
83
93
|
if value =~ /^\/(.+)\/$/
|
84
94
|
value = $1
|
85
95
|
end
|
@@ -104,6 +114,7 @@ module MCollective
|
|
104
114
|
|
105
115
|
false
|
106
116
|
end
|
117
|
+
private_class_method :test_fact_value
|
107
118
|
|
108
119
|
# Checks if the configured identity matches the one supplied
|
109
120
|
#
|
@@ -492,5 +503,22 @@ module MCollective
|
|
492
503
|
template_path = File.join("/etc/mcollective", template_file)
|
493
504
|
return template_path
|
494
505
|
end
|
506
|
+
|
507
|
+
# subscribe to the direct addressing queue
|
508
|
+
def self.subscribe_to_direct_addressing_queue
|
509
|
+
subscribe(make_subscriptions("mcollective", :directed))
|
510
|
+
end
|
511
|
+
|
512
|
+
# Get field size for printing
|
513
|
+
def self.field_size(elements, min_size=40)
|
514
|
+
max_length = elements.max_by { |e| e.length }.length
|
515
|
+
max_length > min_size ? max_length : min_size
|
516
|
+
end
|
517
|
+
|
518
|
+
# Calculate number of fields for printing
|
519
|
+
def self.field_number(field_size, max_size=90)
|
520
|
+
number = (max_size/field_size).to_i
|
521
|
+
(number == 0) ? 1 : number
|
522
|
+
end
|
495
523
|
end
|
496
524
|
end
|
@@ -1,12 +1,18 @@
|
|
1
1
|
module MCollective
|
2
2
|
module Validator
|
3
3
|
@last_load = nil
|
4
|
+
@@validator_mutex = Mutex.new
|
4
5
|
|
5
6
|
# Loads the validator plugins. Validators will only be loaded every 5 minutes
|
6
7
|
def self.load_validators
|
7
|
-
|
8
|
-
|
9
|
-
|
8
|
+
begin
|
9
|
+
@@validator_mutex.lock
|
10
|
+
if load_validators?
|
11
|
+
@last_load = Time.now.to_i
|
12
|
+
PluginManager.find_and_load("validator")
|
13
|
+
end
|
14
|
+
ensure
|
15
|
+
@@validator_mutex.unlock
|
10
16
|
end
|
11
17
|
end
|
12
18
|
|
@@ -44,7 +50,6 @@ module MCollective
|
|
44
50
|
|
45
51
|
def self.load_validators?
|
46
52
|
return true if @last_load.nil?
|
47
|
-
|
48
53
|
(@last_load - Time.now.to_i) > 300
|
49
54
|
end
|
50
55
|
|
data/spec/spec_helper.rb
CHANGED
@@ -28,3 +28,9 @@ RSpec.configure do |config|
|
|
28
28
|
MCollective::PluginManager.clear
|
29
29
|
end
|
30
30
|
end
|
31
|
+
|
32
|
+
# With the addition of the ddl requirement for connectors its becomes necessary
|
33
|
+
# to stub the inherited method. Because tests don't use a real config files libdirs
|
34
|
+
# aren't set and connectors have no way of finding their ddls so we stub it out
|
35
|
+
# in the general case and test for is specifically.
|
36
|
+
MCollective::Connector::Base.stubs(:inherited)
|
data/spec/unit/config_spec.rb
CHANGED
@@ -141,6 +141,16 @@ module MCollective
|
|
141
141
|
Config.instance.loadconfig("/nonexisting")
|
142
142
|
end
|
143
143
|
end
|
144
|
+
|
145
|
+
it 'should enable agents by default' do
|
146
|
+
File.expects(:readlines).with("/nonexisting").returns(["libdir=/nonexistinglib"])
|
147
|
+
File.expects(:exists?).with("/nonexisting").returns(true)
|
148
|
+
PluginManager.stubs(:loadclass)
|
149
|
+
PluginManager.stubs("<<")
|
150
|
+
|
151
|
+
Config.instance.loadconfig("/nonexisting")
|
152
|
+
Config.instance.activate_agents.should == true
|
153
|
+
end
|
144
154
|
end
|
145
155
|
|
146
156
|
describe "#read_plugin_config_dir" do
|
@@ -0,0 +1,28 @@
|
|
1
|
+
#!/usr/bin/env rspec
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
module MCollective
|
6
|
+
module Connector
|
7
|
+
describe "base" do
|
8
|
+
|
9
|
+
before :each do
|
10
|
+
Base.unstub(:inherited)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should fail if the ddl isn't valid" do
|
14
|
+
PluginManager.expects(:<<).never
|
15
|
+
|
16
|
+
expect {
|
17
|
+
class TestConnectorA<Connector::Base;end
|
18
|
+
}.to raise_error RuntimeError
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should load the ddl and add the connector to the PluginManager" do
|
22
|
+
DDL.stubs(:new)
|
23
|
+
class TestConnectorB<Connector::Base;end
|
24
|
+
PluginManager["connector_plugin"].class.should == MCollective::Connector::TestConnectorB
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -114,5 +114,40 @@ module MCollective::Facts
|
|
114
114
|
end
|
115
115
|
end
|
116
116
|
|
117
|
+
describe '#normalize_facts' do
|
118
|
+
it 'should make symbols that are keys be strings' do
|
119
|
+
Testfacts.new.send(:normalize_facts, {
|
120
|
+
:foo => "1",
|
121
|
+
"bar" => "2",
|
122
|
+
}).should == {
|
123
|
+
"foo" => "1",
|
124
|
+
"bar" => "2",
|
125
|
+
}
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'should make values that are not strings be strings' do
|
129
|
+
Testfacts.new.send(:normalize_facts, {
|
130
|
+
"foo" => 1,
|
131
|
+
"bar" => :baz,
|
132
|
+
}).should == {
|
133
|
+
"foo" => "1",
|
134
|
+
"bar" => "baz",
|
135
|
+
}
|
136
|
+
end
|
137
|
+
|
138
|
+
it 'should not flatten arrays or hashes' do
|
139
|
+
Testfacts.new.send(:normalize_facts, {
|
140
|
+
"foo" => [ "1", "quux", 2 ],
|
141
|
+
"bar" => {
|
142
|
+
:baz => "quux",
|
143
|
+
},
|
144
|
+
}).should == {
|
145
|
+
"foo" => [ "1", "quux", "2" ],
|
146
|
+
"bar" => {
|
147
|
+
"baz" => "quux",
|
148
|
+
},
|
149
|
+
}
|
150
|
+
end
|
151
|
+
end
|
117
152
|
end
|
118
153
|
end
|
data/spec/unit/log_spec.rb
CHANGED
@@ -53,6 +53,15 @@ module MCollective
|
|
53
53
|
end
|
54
54
|
end
|
55
55
|
|
56
|
+
describe '#reopen' do
|
57
|
+
it 'should delegate the the logger' do
|
58
|
+
@logger.expects(:reopen)
|
59
|
+
|
60
|
+
Log.configure(@logger)
|
61
|
+
Log.reopen
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
56
65
|
describe "#from" do
|
57
66
|
let(:execution_stack) do
|
58
67
|
if Util.windows?
|
@@ -42,7 +42,7 @@ module MCollective::Logger
|
|
42
42
|
logger = Base.new
|
43
43
|
|
44
44
|
expect {
|
45
|
-
logger.
|
45
|
+
logger.log(nil, nil, nil)
|
46
46
|
}.to raise_error("The logging class did not supply a log method")
|
47
47
|
end
|
48
48
|
end
|
@@ -52,11 +52,21 @@ module MCollective::Logger
|
|
52
52
|
logger = Base.new
|
53
53
|
|
54
54
|
expect {
|
55
|
-
logger.
|
55
|
+
logger.start
|
56
56
|
}.to raise_error("The logging class did not supply a start method")
|
57
57
|
end
|
58
58
|
end
|
59
59
|
|
60
|
+
describe '#reopen' do
|
61
|
+
it 'should do nothing' do
|
62
|
+
logger = Base.new
|
63
|
+
|
64
|
+
expect {
|
65
|
+
logger.reopen
|
66
|
+
}.to_not raise_error
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
60
70
|
describe "#map_level" do
|
61
71
|
it "should map levels correctly" do
|
62
72
|
logger = Base.new
|
@@ -0,0 +1,82 @@
|
|
1
|
+
#!/usr/bin/env rspec
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
module MCollective
|
6
|
+
require 'mcollective/logger/file_logger'
|
7
|
+
|
8
|
+
module Logger
|
9
|
+
describe File_logger do
|
10
|
+
let(:mock_logger) { mock('logger') }
|
11
|
+
|
12
|
+
before :each do
|
13
|
+
Config.instance.stubs(:loglevel).returns("error")
|
14
|
+
Config.instance.stubs(:logfile).returns("testfile")
|
15
|
+
Config.instance.stubs(:keeplogs).returns(false)
|
16
|
+
Config.instance.stubs(:max_log_size).returns(42)
|
17
|
+
::Logger.stubs(:new).returns(mock_logger)
|
18
|
+
mock_logger.stubs(:formatter=)
|
19
|
+
mock_logger.stubs(:level=)
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "#start" do
|
23
|
+
it "should set the level to be that specfied in the config" do
|
24
|
+
logger = File_logger.new
|
25
|
+
|
26
|
+
logger.expects(:set_level).with(:error)
|
27
|
+
logger.start
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe 'reopen' do
|
32
|
+
let(:logger) {
|
33
|
+
logger = File_logger.new
|
34
|
+
logger.instance_variable_set(:@logger, mock_logger)
|
35
|
+
logger
|
36
|
+
}
|
37
|
+
|
38
|
+
before :each do
|
39
|
+
mock_logger.stubs(:level)
|
40
|
+
mock_logger.stubs(:close)
|
41
|
+
logger.stubs(:start)
|
42
|
+
mock_logger.stubs(:level=)
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'should close the current handle' do
|
46
|
+
mock_logger.expects(:close)
|
47
|
+
logger.reopen
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'should open a new handle' do
|
51
|
+
logger.expects(:start)
|
52
|
+
logger.reopen
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should preserve the level' do
|
56
|
+
mock_logger.expects(:level).returns(12252)
|
57
|
+
mock_logger.expects(:level=).with(12252)
|
58
|
+
logger.reopen
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe '#set_logging_level' do
|
63
|
+
it 'should set the level' do
|
64
|
+
logger = File_logger.new
|
65
|
+
logger.instance_variable_set(:@logger, mock_logger)
|
66
|
+
mock_logger.expects(:level=).with(::Logger::ERROR)
|
67
|
+
logger.set_level("error")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe "#log" do
|
72
|
+
it "should delegate to logger" do
|
73
|
+
logger = File_logger.new
|
74
|
+
logger.instance_variable_set(:@logger, mock_logger)
|
75
|
+
|
76
|
+
mock_logger.expects(:add).with(::Logger::INFO)
|
77
|
+
logger.log(:info, "rspec", "message")
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -22,6 +22,7 @@ module MCollective
|
|
22
22
|
PluginManager.stubs(:find).with(:data, "ddl").returns([""])
|
23
23
|
PluginManager.stubs(:find).with(:discovery, "ddl").returns([""])
|
24
24
|
PluginManager.stubs(:find).with(:validator, "ddl").returns([""])
|
25
|
+
PluginManager.stubs(:find).with(:connector, "ddl").returns([""])
|
25
26
|
@app.stubs(:load_plugin_ddl).with('rspec', :agent).returns(ddl)
|
26
27
|
ddl.expects(:help).with("rspec-helptemplate.erb").returns("agent_template")
|
27
28
|
@app.expects(:puts).with("agent_template")
|
@@ -47,9 +47,9 @@ module MCollective
|
|
47
47
|
|
48
48
|
let(:subscription) do
|
49
49
|
sub = mock
|
50
|
-
sub.stubs(
|
51
|
-
sub.stubs(
|
52
|
-
sub.stubs(
|
50
|
+
sub.stubs(:<<).returns(true)
|
51
|
+
sub.stubs(:include?).returns(false)
|
52
|
+
sub.stubs(:delete).returns(false)
|
53
53
|
sub
|
54
54
|
end
|
55
55
|
|
@@ -452,46 +452,46 @@ module MCollective
|
|
452
452
|
end
|
453
453
|
|
454
454
|
it "should use the make_target correctly" do
|
455
|
-
connector.expects(
|
455
|
+
connector.expects(:make_target).with("test", :broadcast, "mcollective").returns({:name => "test", :headers => {}})
|
456
456
|
connector.subscribe("test", :broadcast, "mcollective")
|
457
457
|
end
|
458
458
|
|
459
459
|
it "should check for existing subscriptions" do
|
460
|
-
connector.expects(
|
461
|
-
subscription.expects(
|
460
|
+
connector.expects(:make_target).with("test", :broadcast, "mcollective").returns({:name => "test", :headers => {}, :id => "rspec"})
|
461
|
+
subscription.expects(:include?).with("rspec").returns(false)
|
462
462
|
connection.expects(:subscribe).never
|
463
463
|
|
464
464
|
connector.subscribe("test", :broadcast, "mcollective")
|
465
465
|
end
|
466
466
|
|
467
467
|
it "should subscribe to the middleware" do
|
468
|
-
connector.expects(
|
468
|
+
connector.expects(:make_target).with("test", :broadcast, "mcollective").returns({:name => "test", :headers => {}, :id => "rspec"})
|
469
469
|
connection.expects(:subscribe).with("test", {}, "rspec")
|
470
470
|
connector.subscribe("test", :broadcast, "mcollective")
|
471
471
|
end
|
472
472
|
|
473
473
|
it "should add to the list of subscriptions" do
|
474
|
-
connector.expects(
|
475
|
-
subscription.expects(
|
474
|
+
connector.expects(:make_target).with("test", :broadcast, "mcollective").returns({:name => "test", :headers => {}, :id => "rspec"})
|
475
|
+
subscription.expects(:<<).with("rspec")
|
476
476
|
connector.subscribe("test", :broadcast, "mcollective")
|
477
477
|
end
|
478
478
|
end
|
479
479
|
|
480
480
|
describe "#unsubscribe" do
|
481
481
|
it "should use make_target correctly" do
|
482
|
-
connector.expects(
|
482
|
+
connector.expects(:make_target).with("test", :broadcast, "mcollective").returns({:name => "test", :headers => {}})
|
483
483
|
connector.unsubscribe("test", :broadcast, "mcollective")
|
484
484
|
end
|
485
485
|
|
486
486
|
it "should unsubscribe from the target" do
|
487
|
-
connector.expects(
|
487
|
+
connector.expects(:make_target).with("test", :broadcast, "mcollective").returns({:name => "test", :headers => {}, :id => "rspec"})
|
488
488
|
connection.expects(:unsubscribe).with("test", {}, "rspec").once
|
489
489
|
|
490
490
|
connector.unsubscribe("test", :broadcast, "mcollective")
|
491
491
|
end
|
492
492
|
|
493
493
|
it "should delete the source from subscriptions" do
|
494
|
-
connector.expects(
|
494
|
+
connector.expects(:make_target).with("test", :broadcast, "mcollective").returns({:name => "test", :headers => {}, :id => "rspec"})
|
495
495
|
subscription.expects(:delete).with("rspec").once
|
496
496
|
|
497
497
|
connector.unsubscribe("test", :broadcast, "mcollective")
|
@@ -633,11 +633,38 @@ module MCollective
|
|
633
633
|
|
634
634
|
describe "#make_target" do
|
635
635
|
it "should create correct targets" do
|
636
|
-
|
637
|
-
connector.make_target("test", :
|
638
|
-
|
639
|
-
|
640
|
-
|
636
|
+
Client.stubs(:request_sequence).returns(42)
|
637
|
+
connector.make_target("test", :reply, "mcollective").should == {
|
638
|
+
:name => "/queue/mcollective.reply.rspec_#{$$}.42",
|
639
|
+
:headers => {},
|
640
|
+
:id => "/queue/mcollective.reply.rspec_#{$$}.42",
|
641
|
+
}
|
642
|
+
|
643
|
+
connector.make_target("test", :broadcast, "mcollective").should == {
|
644
|
+
:name => "/topic/mcollective.test.agent",
|
645
|
+
:headers => {},
|
646
|
+
:id => "/topic/mcollective.test.agent",
|
647
|
+
}
|
648
|
+
|
649
|
+
connector.make_target("test", :request, "mcollective").should == {
|
650
|
+
:name => "/topic/mcollective.test.agent",
|
651
|
+
:headers => {},
|
652
|
+
:id => "/topic/mcollective.test.agent",
|
653
|
+
}
|
654
|
+
|
655
|
+
connector.make_target("test", :direct_request, "mcollective").should == {
|
656
|
+
:name => "/queue/mcollective.nodes",
|
657
|
+
:headers => {},
|
658
|
+
:id => "/queue/mcollective.nodes",
|
659
|
+
}
|
660
|
+
|
661
|
+
connector.make_target("test", :directed, "mcollective").should == {
|
662
|
+
:name => "/queue/mcollective.nodes",
|
663
|
+
:headers => {
|
664
|
+
"selector" => "mc_identity = 'rspec'",
|
665
|
+
},
|
666
|
+
:id => "mcollective_directed_to_identity",
|
667
|
+
}
|
641
668
|
end
|
642
669
|
|
643
670
|
it "should raise an error for unknown collectives" do
|