topological_inventory-providers-common 1.0.2 → 1.0.7
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/.github/workflows/gem-push.yml +49 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +3 -0
- data/.rubocop_cc.yml +4 -0
- data/.rubocop_local.yml +2 -0
- data/.travis.yml +8 -0
- data/CHANGELOG.md +29 -1
- data/Gemfile +0 -3
- data/lib/topological_inventory/providers/common/logging.rb +8 -0
- data/lib/topological_inventory/providers/common/operations/endpoint_client.rb +3 -0
- data/lib/topological_inventory/providers/common/operations/source.rb +191 -0
- data/lib/topological_inventory/providers/common/operations/sources_api_client.rb +15 -6
- data/lib/topological_inventory/providers/common/save_inventory/saver.rb +13 -4
- data/lib/topological_inventory/providers/common/version.rb +1 -1
- data/spec/spec_helper.rb +22 -0
- data/spec/support/inventory_helper.rb +14 -0
- data/spec/support/shared/availability_check.rb +236 -0
- data/spec/topological_inventory/providers/common/collector_spec.rb +171 -0
- data/spec/topological_inventory/providers/common/collectors/inventory_collection_storage_spec.rb +44 -0
- data/spec/topological_inventory/providers/common/collectors/inventory_collection_wrapper_spec.rb +9 -0
- data/spec/topological_inventory/providers/common/collectors_pool_spec.rb +150 -0
- data/spec/topological_inventory/providers/common/logger_spec.rb +38 -0
- data/spec/topological_inventory/providers/common/operations/processor_spec.rb +102 -0
- data/spec/topological_inventory/providers/common/operations/source_spec.rb +5 -0
- data/spec/topological_inventory/providers/common/save_inventory/saver_spec.rb +65 -0
- data/spec/topological_inventory/providers/common_spec.rb +3 -0
- data/topological_inventory-providers-common.gemspec +7 -1
- metadata +104 -3
data/spec/topological_inventory/providers/common/collectors/inventory_collection_storage_spec.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
describe TopologicalInventory::Providers::Common::Collector::InventoryCollectionStorage do
|
2
|
+
before do
|
3
|
+
@storage = described_class.new
|
4
|
+
end
|
5
|
+
|
6
|
+
it "should add collection to data" do
|
7
|
+
@storage.add_collection(:vms)
|
8
|
+
|
9
|
+
expect(@storage.data[:vms]).not_to be_nil
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should access the same collection through brackets, method name and data" do
|
13
|
+
@storage.add_collection(:vms)
|
14
|
+
|
15
|
+
vm_name = "My VM"
|
16
|
+
|
17
|
+
@storage[:vms].build(:name => vm_name)
|
18
|
+
|
19
|
+
expect(@storage.vms.data[0].name).to eq(vm_name)
|
20
|
+
expect(@storage[:vms].data[0].name).to eq(vm_name)
|
21
|
+
expect(@storage.data[:vms].data[0].name).to eq(vm_name)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should create collection automatically when api object exists" do
|
25
|
+
expect("TopologicalInventoryIngressApiClient::Vm".safe_constantize).not_to be_nil
|
26
|
+
|
27
|
+
storage = described_class.new
|
28
|
+
expect(storage.vms).to be_kind_of(TopologicalInventory::Providers::Common::Collector::InventoryCollectionWrapper)
|
29
|
+
|
30
|
+
storage = described_class.new
|
31
|
+
expect(storage[:vms]).to be_kind_of(TopologicalInventory::Providers::Common::Collector::InventoryCollectionWrapper)
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should raise NameError when api object doesn't exist" do
|
35
|
+
expect("TopologicalInventoryIngressApiClient::SomethingNonexisting".safe_constantize).to be_nil
|
36
|
+
|
37
|
+
storage = described_class.new
|
38
|
+
|
39
|
+
expect { storage.add_collection(:something_nonexisting) }.to raise_error(NameError)
|
40
|
+
|
41
|
+
expect { storage.something_nonexisting.build(:name => "Vm") }.to raise_error(NameError)
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
data/spec/topological_inventory/providers/common/collectors/inventory_collection_wrapper_spec.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
describe TopologicalInventory::Providers::Common::Collector::InventoryCollectionWrapper do
|
2
|
+
it "builds only existing ingress api client's object" do
|
3
|
+
ic = described_class.new(:name => :some_undefined_class_in_api_models)
|
4
|
+
ic_existing = described_class.new(:name => :vm)
|
5
|
+
|
6
|
+
expect { ic.build({}) }.to raise_error(NameError)
|
7
|
+
expect(ic_existing.build({})).to be_kind_of(TopologicalInventoryIngressApiClient::Vm)
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require "tempfile"
|
2
|
+
require "yaml"
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
RSpec.describe TopologicalInventory::Providers::Common::CollectorsPool do
|
6
|
+
let(:source1) { {:source => '42b1893c-ebbd-44e9-89b1-5c29b5fe6e10', :schema => 'http', :host => 'cloud.redhat.com', :port => 80} }
|
7
|
+
let(:source2) { {:source => 'fe8bcaea-3670-42c7-bed9-71f6e0bceadd', :schema => 'https', :host => 'cloud.redhat.com', :port => 443} }
|
8
|
+
let(:source3) { {:source => '05838743-4285-404a-b4d6-294045c0d4be', :schema => 'xxx', :host => 'cloud.redhat.com', :port => 1234} }
|
9
|
+
let(:source4) { {:source => '5ed08a3c-3de4-4a90-8ce9-e0f724b2b2e6', :schema => 'xxx', :host => 'cloud.redhat.com', :port => 1234} }
|
10
|
+
let(:sources) { [source1, source2, source3] }
|
11
|
+
|
12
|
+
before do
|
13
|
+
clear_settings
|
14
|
+
end
|
15
|
+
|
16
|
+
subject { described_class.new(nil, nil, :thread_pool_size => 2) }
|
17
|
+
|
18
|
+
context "config reload" do
|
19
|
+
it "changes settings with different configs" do
|
20
|
+
settings = [{:sources => sources},
|
21
|
+
{:sources => [ source2, source4 ]}]
|
22
|
+
|
23
|
+
2.times do |i|
|
24
|
+
config = Tempfile.new(["config#{i}", '.yml'])
|
25
|
+
begin
|
26
|
+
config.write(settings[i].to_yaml)
|
27
|
+
config.rewind
|
28
|
+
|
29
|
+
name, path = path_and_filename(config)
|
30
|
+
subject.send(:config_name=, name.split('.')[0])
|
31
|
+
allow(subject).to receive(:path_to_config).and_return(path)
|
32
|
+
|
33
|
+
subject.send(:reload_config)
|
34
|
+
|
35
|
+
expect(::Settings.sources.to_a.collect(&:to_hash)).to eq(settings[i][:sources])
|
36
|
+
ensure
|
37
|
+
config.close
|
38
|
+
config.unlink
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context "secret reload" do
|
45
|
+
it "changes credentials with new secret" do
|
46
|
+
uuid = SecureRandom.uuid
|
47
|
+
|
48
|
+
secrets = [
|
49
|
+
{'updated_at' => Time.now.to_s, uuid => {'username' => 'admin1', 'password' => 'password1'}},
|
50
|
+
{'updated_at' => Time.now.to_s, uuid => {'username' => 'admin2', 'password' => 'password2'}},
|
51
|
+
]
|
52
|
+
|
53
|
+
2.times do |i|
|
54
|
+
secret = Tempfile.new(["credentials#{i}"])
|
55
|
+
begin
|
56
|
+
secret.write(secrets[i].to_json)
|
57
|
+
secret.rewind
|
58
|
+
|
59
|
+
name, path = path_and_filename(secret)
|
60
|
+
|
61
|
+
allow(subject).to receive(:path_to_secrets).and_return(path)
|
62
|
+
stub_const("#{described_class}::SECRET_FILENAME", name)
|
63
|
+
|
64
|
+
subject.send(:reload_secrets)
|
65
|
+
|
66
|
+
expect(subject.send(:secrets)).to eq(secrets[i])
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
context "add or remove collector" do
|
73
|
+
before do
|
74
|
+
::Config.load_and_set_settings('some-value-needed.txt')
|
75
|
+
@collector = double("collector")
|
76
|
+
allow(subject).to receive(:new_collector).and_return(@collector)
|
77
|
+
end
|
78
|
+
|
79
|
+
context "without secrets check" do
|
80
|
+
before do
|
81
|
+
allow(subject).to receive(:secrets_for_source).and_return({})
|
82
|
+
end
|
83
|
+
|
84
|
+
it "adds new collectors from settings" do
|
85
|
+
allow(@collector).to receive(:collect!).and_return(nil)
|
86
|
+
expect(@collector).to receive(:collect!).exactly(sources.size).times
|
87
|
+
|
88
|
+
sources.each do |source|
|
89
|
+
stub_settings_merge(:sources => ::Settings.sources.to_a + [source])
|
90
|
+
|
91
|
+
subject.send(:queue_collectors)
|
92
|
+
end
|
93
|
+
|
94
|
+
pool = subject.send(:thread_pool)
|
95
|
+
pool.shutdown
|
96
|
+
pool.wait_for_termination
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
context "with secrets check" do
|
101
|
+
let(:secrets) do
|
102
|
+
{ 'updated_at' => Time.now.to_s,
|
103
|
+
source1[:source] => { 'username' => 'admin1', 'password' => 'password1' },
|
104
|
+
source2[:source] => { 'username' => 'admin2', 'password' => 'password2' },
|
105
|
+
'unknown' => { 'username' => 'admin3', 'password' => 'password3' }
|
106
|
+
}
|
107
|
+
end
|
108
|
+
|
109
|
+
before do
|
110
|
+
allow(@collector).to receive(:collect!).and_return(nil)
|
111
|
+
end
|
112
|
+
|
113
|
+
it "creates only collectors found in both secret and config" do
|
114
|
+
# 4 sources in yaml config
|
115
|
+
stub_settings_merge(:sources => sources + [source4])
|
116
|
+
# 3 sources in secret
|
117
|
+
allow(subject).to receive(:secrets).and_return(secrets)
|
118
|
+
|
119
|
+
# for each source in yaml secret is searched (4x)
|
120
|
+
expect(subject).to receive(:secrets_for_source).and_call_original.exactly(4).times
|
121
|
+
# only 2 corresponding
|
122
|
+
expect(@collector).to receive(:collect!).exactly(2).times
|
123
|
+
|
124
|
+
subject.send(:queue_collectors)
|
125
|
+
|
126
|
+
pool = subject.send(:thread_pool)
|
127
|
+
pool.shutdown
|
128
|
+
pool.wait_for_termination
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def stub_settings_merge(hash)
|
134
|
+
if defined?(::Settings)
|
135
|
+
Settings.add_source!(hash)
|
136
|
+
Settings.reload!
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def clear_settings
|
141
|
+
::Settings.keys.dup.each { |k| ::Settings.delete_field(k) } if defined?(::Settings)
|
142
|
+
end
|
143
|
+
|
144
|
+
def path_and_filename(tempfile)
|
145
|
+
parts = tempfile.path.split('/')
|
146
|
+
name = parts[-1]
|
147
|
+
path = parts[0..-2].join('/')
|
148
|
+
[name, path]
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
RSpec.describe TopologicalInventory::Providers::Common::Logger do
|
2
|
+
let(:status) { :test }
|
3
|
+
let(:source) { '92844e11-17d5-4998-a33d-d886c3c7a80e' }
|
4
|
+
let(:entity_type) { 'test-entity' }
|
5
|
+
let(:refresh_state_uuid) { 'cd22ba1c-56f6-4fd4-a191-ec8eb8e993a8' }
|
6
|
+
let(:sweep_scope) { [entity_type] }
|
7
|
+
let(:total_parts) { 10 }
|
8
|
+
|
9
|
+
subject { described_class.new }
|
10
|
+
|
11
|
+
it 'receives collecting method' do
|
12
|
+
msg = "[#{status.to_s.upcase}] Collecting #{entity_type}"
|
13
|
+
msg += ", :total parts => #{total_parts}" if total_parts.present?
|
14
|
+
msg += ", :source_uid => #{source}, :refresh_state_uuid => #{refresh_state_uuid}"
|
15
|
+
expect(subject).to receive(:info).with(msg)
|
16
|
+
|
17
|
+
subject.collecting(status, source, entity_type, refresh_state_uuid, total_parts)
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'receives sweeping method' do
|
21
|
+
msg = "[#{status.to_s.upcase}] Sweeping inactive records, :sweep_scope => #{sweep_scope}, :source_uid => #{source}, :refresh_state_uuid => #{refresh_state_uuid}"
|
22
|
+
expect(subject).to receive(:info).with(msg)
|
23
|
+
|
24
|
+
subject.sweeping(status, source, sweep_scope, refresh_state_uuid)
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'receives collecting error method' do
|
28
|
+
begin
|
29
|
+
raise 'Test exception'
|
30
|
+
rescue => e
|
31
|
+
msg = "[ERROR] Collecting #{entity_type}, :source_uid => #{source}, :refresh_state_uuid => #{refresh_state_uuid}"
|
32
|
+
msg += ":message => #{e.message}\n#{e.backtrace.join("\n")}"
|
33
|
+
expect(subject).to receive(:error).with(msg)
|
34
|
+
|
35
|
+
subject.collecting_error(source, entity_type, refresh_state_uuid, e)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require "topological_inventory/providers/common/operations/processor"
|
2
|
+
|
3
|
+
RSpec.describe TopologicalInventory::Providers::Common::Operations::Processor do
|
4
|
+
let(:topology_api_client) { double }
|
5
|
+
let(:source_id) { 1 }
|
6
|
+
let(:source_ref) { 1000 }
|
7
|
+
let(:service_plan) { double("TopologicalInventoryApiClient::ServicePlan") }
|
8
|
+
let(:service_offering) { double("TopologicalInventoryApiClient::ServiceOffering") }
|
9
|
+
|
10
|
+
# Overriden in contexts
|
11
|
+
let(:payload) { {} }
|
12
|
+
|
13
|
+
before do
|
14
|
+
@processor = described_class.new(nil, nil, payload)
|
15
|
+
allow(@processor).to receive(:logger).and_return(double('null_object').as_null_object)
|
16
|
+
|
17
|
+
allow(service_plan).to receive(:service_offering_id).and_return(1)
|
18
|
+
allow(service_plan).to receive(:name).and_return(double)
|
19
|
+
|
20
|
+
allow(service_offering).to receive(:name).and_return(double)
|
21
|
+
allow(service_offering).to receive(:source_ref).and_return(source_ref)
|
22
|
+
allow(service_offering).to receive(:extra).and_return({:type => 'job_template'})
|
23
|
+
allow(service_offering).to receive(:source_id).and_return(source_id)
|
24
|
+
|
25
|
+
@endpoint_client = double
|
26
|
+
allow(@endpoint_client).to receive(:order_service)
|
27
|
+
|
28
|
+
allow(@processor).to receive(:endpoint_client).and_return(@endpoint_client)
|
29
|
+
allow(@processor).to receive(:topology_api_client).and_return(topology_api_client)
|
30
|
+
allow(topology_api_client).to receive(:update_task)
|
31
|
+
allow(topology_api_client).to receive(:show_service_plan).and_return(service_plan)
|
32
|
+
allow(topology_api_client).to receive(:show_service_offering).and_return(service_offering)
|
33
|
+
end
|
34
|
+
|
35
|
+
context "Order by ServicePlan" do
|
36
|
+
let(:payload) do
|
37
|
+
{
|
38
|
+
'request_context' => {"x-rh-identity" => 'abcd'},
|
39
|
+
'params' => {
|
40
|
+
'order_params' => {
|
41
|
+
'service_plan_id' => 1,
|
42
|
+
'service_parameters' => { :name => "Job 1",
|
43
|
+
:param1 => "Test Topology",
|
44
|
+
:param2 => 50 },
|
45
|
+
'provider_control_parameters' => {}
|
46
|
+
},
|
47
|
+
'service_plan_id' => 1,
|
48
|
+
'task_id' => 1 # in tp-inv api (Task)
|
49
|
+
}
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "#order_service" do
|
54
|
+
it "orders job" do
|
55
|
+
allow(@processor).to receive(:poll_order_complete_thread).and_return(double)
|
56
|
+
|
57
|
+
expect(@endpoint_client).to receive(:order_service).with(service_offering, service_plan, payload['params']['order_params'])
|
58
|
+
@processor.send(:order_service, payload['params'])
|
59
|
+
end
|
60
|
+
|
61
|
+
it "updates task on error" do
|
62
|
+
err_message = "Sample error"
|
63
|
+
|
64
|
+
allow(@processor).to receive(:poll_order_complete_thread).and_return(double)
|
65
|
+
allow(@processor).to receive(:update_task).and_return(double)
|
66
|
+
allow(@endpoint_client).to receive(:order_service).and_raise(err_message)
|
67
|
+
|
68
|
+
expect(@processor).to receive(:update_task).with(payload['params']['task_id'], :state => "completed", :status => "error", :context => { :error => err_message })
|
69
|
+
|
70
|
+
@processor.send(:order_service, payload['params'])
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
context "Order by ServiceOffering" do
|
76
|
+
let(:payload) do
|
77
|
+
{
|
78
|
+
'request_context' => {"x-rh-identity" => 'abcd'},
|
79
|
+
'params' => {
|
80
|
+
'order_params' => {
|
81
|
+
'service_offering_id' => 1,
|
82
|
+
'service_parameters' => { :name => "Job 1",
|
83
|
+
:param1 => "Test Topology",
|
84
|
+
:param2 => 50 },
|
85
|
+
'provider_control_parameters' => {}
|
86
|
+
},
|
87
|
+
'service_offering_id' => 1,
|
88
|
+
'task_id' => 1 # in tp-inv api (Task)
|
89
|
+
}
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
describe "#order_service" do
|
94
|
+
it "orders job" do
|
95
|
+
allow(@processor).to receive(:poll_order_complete_thread).and_return(double)
|
96
|
+
|
97
|
+
expect(@endpoint_client).to receive(:order_service).with(service_offering, nil, payload['params']['order_params'])
|
98
|
+
@processor.send(:order_service, payload['params'])
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require "topological_inventory/providers/common/save_inventory/saver"
|
2
|
+
|
3
|
+
RSpec.describe TopologicalInventory::Providers::Common::SaveInventory::Saver do
|
4
|
+
let(:client) { instance_double(TopologicalInventoryIngressApiClient::DefaultApi) }
|
5
|
+
let(:logger) { double }
|
6
|
+
let(:base_args) { {client: client, logger: logger} }
|
7
|
+
|
8
|
+
let(:small_json) { {:test => ["values"]} }
|
9
|
+
let(:big_json) { InventorySpecHelper.big_inventory(80_000, 1_000) }
|
10
|
+
|
11
|
+
describe "#save" do
|
12
|
+
subject { described_class.new(args).save(:inventory => inventory) }
|
13
|
+
|
14
|
+
context "when the data size is less than max_bytes" do
|
15
|
+
let(:args) { base_args }
|
16
|
+
let(:inventory) { small_json }
|
17
|
+
|
18
|
+
before do
|
19
|
+
allow(client).to receive(:save_inventory_with_http_info).with(small_json.to_json)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "returns that it saved one chunk" do
|
23
|
+
is_expected.to eq 1
|
24
|
+
end
|
25
|
+
|
26
|
+
it "does not split the payload into batches" do
|
27
|
+
expect(client).to receive(:save_inventory_with_http_info).with(small_json.to_json).once
|
28
|
+
subject
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context "when the data size is greater than specified max_bytes" do
|
33
|
+
let(:args) { base_args.merge!(:max_bytes => 19_512) }
|
34
|
+
let(:inventory) { big_json }
|
35
|
+
|
36
|
+
before do
|
37
|
+
allow(client).to receive(:save_inventory_with_http_info)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "returns that it saved five chunks" do
|
41
|
+
is_expected.to eq 5
|
42
|
+
end
|
43
|
+
|
44
|
+
it "splits the payload up into chunks" do
|
45
|
+
expect(client).to receive(:save_inventory_with_http_info).exactly(5).times
|
46
|
+
subject
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context "when the KAFKA_PAYLOAD_MAX_BYTES ENV var is set" do
|
51
|
+
let(:args) { base_args }
|
52
|
+
let(:inventory) { big_json }
|
53
|
+
|
54
|
+
before do
|
55
|
+
allow(ENV).to receive(:[]).with("KAFKA_PAYLOAD_MAX_BYTES").and_return("9_512")
|
56
|
+
allow(client).to receive(:save_inventory_with_http_info)
|
57
|
+
end
|
58
|
+
|
59
|
+
it "splits the payload into smaller chunks" do
|
60
|
+
expect(client).to receive(:save_inventory_with_http_info).exactly(10).times
|
61
|
+
is_expected.to eq 10
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
|
|
17
17
|
# Specify which files should be added to the gem when it is released.
|
18
18
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
19
19
|
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
20
|
-
`git ls-files -z`.split("\x0")
|
20
|
+
`git ls-files -z`.split("\x0")
|
21
21
|
end
|
22
22
|
spec.bindir = "exe"
|
23
23
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
@@ -27,9 +27,15 @@ Gem::Specification.new do |spec|
|
|
27
27
|
spec.add_runtime_dependency 'config', '~> 1.7', '>= 1.7.2'
|
28
28
|
spec.add_runtime_dependency 'json', '~> 2.3'
|
29
29
|
spec.add_runtime_dependency "manageiq-loggers", ">= 0.4.2"
|
30
|
+
spec.add_runtime_dependency "sources-api-client", "~> 3.0"
|
30
31
|
spec.add_runtime_dependency "topological_inventory-api-client", "~> 3.0", ">= 3.0.1"
|
32
|
+
spec.add_runtime_dependency "topological_inventory-ingress_api-client", "~> 1.0"
|
31
33
|
|
32
34
|
spec.add_development_dependency "bundler", "~> 2.0"
|
33
35
|
spec.add_development_dependency "rake", ">= 12.3.3"
|
34
36
|
spec.add_development_dependency "rspec", "~> 3.0"
|
37
|
+
spec.add_development_dependency 'rubocop', '~>0.69.0'
|
38
|
+
spec.add_development_dependency 'rubocop-performance', '~>1.3'
|
39
|
+
spec.add_development_dependency "simplecov", "~> 0.17.1"
|
40
|
+
spec.add_development_dependency 'webmock'
|
35
41
|
end
|