paraspec 0.0.2 → 0.0.3
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/README.md +53 -2
- data/bin/paraspec +2 -2
- data/lib/paraspec.rb +1 -0
- data/lib/paraspec/errors.rb +13 -0
- data/lib/paraspec/http_client.rb +1 -1
- data/lib/paraspec/ipc.rb +19 -4
- data/lib/paraspec/master.rb +14 -7
- data/lib/paraspec/msgpack_client.rb +1 -1
- data/lib/paraspec/msgpack_server.rb +38 -21
- data/lib/paraspec/rspec_facade.rb +21 -1
- data/lib/paraspec/supervisor.rb +17 -8
- data/lib/paraspec/version.rb +1 -1
- data/lib/paraspec/worker.rb +1 -1
- data/lib/paraspec/worker_formatter.rb +2 -2
- data/lib/paraspec/worker_runner.rb +24 -8
- metadata +13 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d6a3db1f3be0ea3c3b137d2d22ed55d29e402a94f72123c2c5d04eb5c3560569
|
4
|
+
data.tar.gz: 25ea9c3f19e0802ca3db168c0090d1283f5db337477fd42dbcb993f9ccbf2c0a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6f71a077b52ac683f766e15104204fb27e25c276d08d203f27d3eae7171f5d52ddf4003951f1a8185042d9c382065c2e5d3824719462efb8eb7897063cb978db
|
7
|
+
data.tar.gz: 8cf4dece33abe87139dc218ad1848f30d4ed02b2e60df98005aa7f6c2253b96f42c9eb1b16cfb782ca903328f275c6823a83c737277df1c946956c6055928bdb
|
data/README.md
CHANGED
@@ -18,7 +18,8 @@ test by test basis, avoiding interleaving output of different tests together.
|
|
18
18
|
This output capture can be performed for output generated by C extensions
|
19
19
|
as well as plain Ruby code.
|
20
20
|
4. Test results are seamlessly integrated by the master, such that
|
21
|
-
a parallel run produces a single progress bar with
|
21
|
+
a parallel run produces a single progress bar with
|
22
|
+
[Fuubar](https://github.com/thekompanee/fuubar) across all workers.
|
22
23
|
5. Test grouping: paraspec can[**] group the tests by file, top level example
|
23
24
|
group, bottom level example group and individually by examples.
|
24
25
|
Larger groupings reduce overhead but limit concurrency.
|
@@ -33,6 +34,27 @@ is not currently on the roadmap.
|
|
33
34
|
|
34
35
|
[**] Currently only grouping by bottom level example group is implemented.
|
35
36
|
|
37
|
+
## Performance
|
38
|
+
|
39
|
+
How much of a difference does paraspec make? The answer, as one might
|
40
|
+
expect, varies greatly with the test suite being run as well as available
|
41
|
+
hardware. Here are some examples:
|
42
|
+
|
43
|
+
| Example | Hardware | Sequential | Paraspec (c=2) | Paraspec (c=4) |
|
44
|
+
|---------|------------|----------------|----------------|----------|
|
45
|
+
| [MongoDB Ruby Driver](https://docs.mongodb.com/ruby-driver/current/) test suite | Travis CI | 16 minutes | 13 minutes | 10-11 minutes |
|
46
|
+
| [MongoDB Ruby Driver](https://docs.mongodb.com/ruby-driver/current/) test suite | 14-core workstation | 15 minutes | | 4 minutes |
|
47
|
+
|
48
|
+
[Exampe Travis build](https://travis-ci.org/p-mongo/mongo-ruby-driver-paraspec/builds/411986888)
|
49
|
+
|
50
|
+
Even on Travis, which is likely limited to a single core, using 4x concurrency
|
51
|
+
reduces the runtime by 5 minutes. On a developer workstation which doesn't
|
52
|
+
download binaries on every test run the speedup is closer to linear.
|
53
|
+
Waiting 4 minutes instead of 15 for a complete test suite means the engineers
|
54
|
+
can actually run the complete test suite as part of their normal workflow,
|
55
|
+
instead of sending the code to a CI platform and context switching to
|
56
|
+
a different project.
|
57
|
+
|
36
58
|
## Usage
|
37
59
|
|
38
60
|
Add paraspec to your Gemfile:
|
@@ -88,7 +110,8 @@ failing tests.
|
|
88
110
|
|
89
111
|
### Debugging
|
90
112
|
|
91
|
-
Paraspec offers several debugging aids.
|
113
|
+
Paraspec offers several debugging aids. For interactive debugging use the
|
114
|
+
terminal option:
|
92
115
|
|
93
116
|
paraspec -T
|
94
117
|
|
@@ -108,6 +131,34 @@ The debugging output is turned on with `-d`/`--debug` option:
|
|
108
131
|
paraspec -d ipc # IPC requests and responses
|
109
132
|
paraspec -d perf # timing & performance information
|
110
133
|
|
134
|
+
### Executing Tests Together
|
135
|
+
|
136
|
+
It is possible to specify that a group of tests should be executed in the
|
137
|
+
same worker rather than distributed. This is useful when the setup for
|
138
|
+
the tests is expensive and therefore is done once with multiple tests
|
139
|
+
defined on the result.
|
140
|
+
|
141
|
+
The grouping is defined by specifying `{group: true}` paraspec option on
|
142
|
+
a `describe` or a `context` block as follows:
|
143
|
+
|
144
|
+
describe 'Run these together', paraspec: {group: true} do
|
145
|
+
before(:all) do
|
146
|
+
# expensive setup
|
147
|
+
end
|
148
|
+
|
149
|
+
it 'does something' do
|
150
|
+
# ...
|
151
|
+
end
|
152
|
+
|
153
|
+
it 'does something else' do
|
154
|
+
# ...
|
155
|
+
end
|
156
|
+
|
157
|
+
after(:all) do
|
158
|
+
# teardown
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
111
162
|
## Caveats
|
112
163
|
|
113
164
|
The master and workers need to all define the same example groups and
|
data/bin/paraspec
CHANGED
@@ -16,7 +16,7 @@ OptionParser.new do |opts|
|
|
16
16
|
options[:concurrency] = v.to_i
|
17
17
|
end
|
18
18
|
|
19
|
-
opts.on('-d', '--debug=SUBSYSTEM', 'Output debugging information for SUBSYSTEM') do |v|
|
19
|
+
opts.on('-d', '--debug=SUBSYSTEM', 'Output debugging information for SUBSYSTEM (state/ipc/perf)') do |v|
|
20
20
|
options[:"debug_#{v}"] = true
|
21
21
|
options[:debug] = true
|
22
22
|
end
|
@@ -44,7 +44,7 @@ end
|
|
44
44
|
RSpec.configuration.files_or_directories_to_run = files
|
45
45
|
=end
|
46
46
|
|
47
|
-
%w(ipc
|
47
|
+
%w(state ipc perf).each do |subsystem|
|
48
48
|
if options.delete(:"debug_#{subsystem}")
|
49
49
|
Paraspec.logger.send("log_#{subsystem}=", true)
|
50
50
|
end
|
data/lib/paraspec.rb
CHANGED
@@ -0,0 +1,13 @@
|
|
1
|
+
module Paraspec
|
2
|
+
# Base class for all Paraspec errors
|
3
|
+
class Error < StandardError; end
|
4
|
+
|
5
|
+
# Different set of examples between master and a worker
|
6
|
+
class InconsistentTestSuite < Error; end
|
7
|
+
|
8
|
+
# A worker process exited with non-zero status
|
9
|
+
class WorkerFailed < StandardError; end
|
10
|
+
|
11
|
+
# Master process exited with non-zero status
|
12
|
+
class MasterFailed < StandardError; end
|
13
|
+
end
|
data/lib/paraspec/http_client.rb
CHANGED
@@ -6,7 +6,7 @@ module Paraspec
|
|
6
6
|
def initialize(options={})
|
7
7
|
@terminal = options[:terminal]
|
8
8
|
|
9
|
-
@client = Faraday.new(url: "http://localhost:#{Paraspec::
|
9
|
+
@client = Faraday.new(url: "http://localhost:#{Paraspec::Ipc.master_app_port}") do |client|
|
10
10
|
client.adapter :net_http
|
11
11
|
if @terminal
|
12
12
|
client.options.timeout = 100000
|
data/lib/paraspec/ipc.rb
CHANGED
@@ -1,11 +1,26 @@
|
|
1
1
|
require 'hashie'
|
2
|
+
require 'socket'
|
2
3
|
|
3
4
|
module Paraspec
|
4
|
-
#SUPERVISOR_DRB_URI = "druby://localhost:6030"
|
5
|
-
MASTER_DRB_URI = "druby://localhost:6031"
|
6
|
-
MASTER_APP_PORT = 6031
|
7
|
-
|
8
5
|
class IpcHash < Hash
|
9
6
|
include Hashie::Extensions::IndifferentAccess
|
10
7
|
end
|
8
|
+
|
9
|
+
module Ipc
|
10
|
+
class << self
|
11
|
+
def pick_master_app_port
|
12
|
+
port = 10000 + rand(40000)
|
13
|
+
begin
|
14
|
+
server = TCPServer.new('127.0.0.1', port)
|
15
|
+
server.close
|
16
|
+
return @master_app_port = port
|
17
|
+
rescue Errno::EADDRINUSE
|
18
|
+
port = 10000 + rand(40000)
|
19
|
+
retry
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :master_app_port
|
24
|
+
end
|
25
|
+
end
|
11
26
|
end
|
data/lib/paraspec/master.rb
CHANGED
@@ -58,7 +58,7 @@ module Paraspec
|
|
58
58
|
end
|
59
59
|
@queue = []
|
60
60
|
if @non_example_exception_count == 0
|
61
|
-
@queue += RSpecFacade.
|
61
|
+
@queue += RSpecFacade.queueable_example_groups
|
62
62
|
puts "#{@queue.length} example groups queued"
|
63
63
|
else
|
64
64
|
puts "#{@non_example_exception_count} errors outside of examples, aborting"
|
@@ -104,13 +104,20 @@ module Paraspec
|
|
104
104
|
example_group = @queue.shift
|
105
105
|
return nil if example_group.nil?
|
106
106
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
107
|
+
if example_group.metadata[:paraspec] && example_group.metadata[:paraspec][:group]
|
108
|
+
# unsplittable example group
|
109
|
+
# pass to worker as is and have the worker prune examples
|
110
|
+
m = example_group.metadata
|
111
|
+
else
|
112
|
+
# TODO I am still not 100% on what should be filtered and pruned where,
|
113
|
+
# but we shouldn't be returning a specification here unless
|
114
|
+
# there are tests in it that a worker will run
|
115
|
+
pruned_examples = RSpec.configuration.filter_manager.prune(example_group.examples)
|
116
|
+
next if pruned_examples.empty?
|
117
|
+
|
118
|
+
m = example_group.metadata
|
119
|
+
end
|
112
120
|
|
113
|
-
m = example_group.metadata
|
114
121
|
return {
|
115
122
|
file_path: m[:file_path],
|
116
123
|
scoped_id: m[:scoped_id],
|
@@ -39,7 +39,7 @@ module Paraspec
|
|
39
39
|
private def connect
|
40
40
|
start_time = Time.now
|
41
41
|
begin
|
42
|
-
@socket = TCPSocket.new('127.0.0.1',
|
42
|
+
@socket = TCPSocket.new('127.0.0.1', Paraspec::Ipc.master_app_port)
|
43
43
|
rescue Errno::ECONNREFUSED
|
44
44
|
if !@terminal && Time.now - start_time > DrbHelpers::WAIT_TIME
|
45
45
|
raise
|
@@ -8,10 +8,11 @@ module Paraspec
|
|
8
8
|
|
9
9
|
def initialize(master)
|
10
10
|
@master = master
|
11
|
+
@lock = Mutex.new
|
11
12
|
end
|
12
13
|
|
13
14
|
def run
|
14
|
-
@socket = ::TCPServer.new('127.0.0.1',
|
15
|
+
@socket = ::TCPServer.new('127.0.0.1', Paraspec::Ipc.master_app_port)
|
15
16
|
begin
|
16
17
|
while true
|
17
18
|
s = @socket.accept_nonblock
|
@@ -29,30 +30,46 @@ module Paraspec
|
|
29
30
|
Thread.new do
|
30
31
|
u = unpacker(s)
|
31
32
|
u.each do |obj|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
args = []
|
41
|
-
end
|
33
|
+
# Apparently this block may be run concurrently, which we are not
|
34
|
+
# equipped for.
|
35
|
+
@lock.synchronize do
|
36
|
+
process_request(s, obj)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
42
41
|
|
43
|
-
|
44
|
-
|
42
|
+
def process_request(s, obj)
|
43
|
+
result = nil
|
44
|
+
time = Benchmark.realtime do
|
45
|
+
action = obj['action'].gsub('-', '_')
|
46
|
+
payload = obj['payload']
|
47
|
+
if payload
|
48
|
+
payload = IpcHash.new.merge(payload)
|
49
|
+
args = [payload]
|
50
|
+
else
|
51
|
+
args = []
|
52
|
+
end
|
45
53
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
+
Paraspec.logger.debug_ipc("SrvReq:#{obj['id']} #{obj}")
|
55
|
+
result = @master.send(action, *args)
|
56
|
+
|
57
|
+
pk = packer(s)
|
58
|
+
resp = {result: result}
|
59
|
+
Paraspec.logger.debug_ipc("SrvRes:#{obj['id']} #{resp}")
|
60
|
+
if payload && payload['_noret']
|
61
|
+
# Requester does not want a response.
|
62
|
+
# We return a hash here because deserializer expects a hash.
|
63
|
+
# Always returning `resp` is problematic because it can
|
64
|
+
# be non-serializable.
|
65
|
+
pk.write({})
|
66
|
+
else
|
67
|
+
pk.write(resp)
|
54
68
|
end
|
69
|
+
pk.flush
|
70
|
+
s.flush
|
55
71
|
end
|
72
|
+
Paraspec.logger.debug_perf("SrvReq:#{obj['id']} #{obj['action']}: #{result} #{'%.3f msec' % (time*1000)}")
|
56
73
|
end
|
57
74
|
end
|
58
75
|
end
|
@@ -7,7 +7,27 @@ module Paraspec
|
|
7
7
|
|
8
8
|
class << self
|
9
9
|
extend Forwardable
|
10
|
-
def_delegators :instance, :all_example_groups, :all_examples
|
10
|
+
def_delegators :instance, :queueable_example_groups, :all_example_groups, :all_examples
|
11
|
+
end
|
12
|
+
|
13
|
+
def queueable_example_groups
|
14
|
+
@queueable_example_groups ||= begin
|
15
|
+
groups = [] + RSpec.world.example_groups
|
16
|
+
all_groups = []
|
17
|
+
until groups.empty?
|
18
|
+
new_groups = []
|
19
|
+
groups.each do |group|
|
20
|
+
all_groups << group
|
21
|
+
if group.metadata[:paraspec] && group.metadata[:paraspec][:group]
|
22
|
+
# unsplittable group
|
23
|
+
else
|
24
|
+
new_groups += group.children
|
25
|
+
end
|
26
|
+
end
|
27
|
+
groups = new_groups
|
28
|
+
end
|
29
|
+
all_groups
|
30
|
+
end
|
11
31
|
end
|
12
32
|
|
13
33
|
def all_example_groups
|
data/lib/paraspec/supervisor.rb
CHANGED
@@ -21,6 +21,14 @@ module Paraspec
|
|
21
21
|
Process.setpgrp
|
22
22
|
end
|
23
23
|
|
24
|
+
at_exit do
|
25
|
+
# It seems that when the tests are run in Travis, killing
|
26
|
+
# paraspec vaporizes the standard output... flush the
|
27
|
+
# standard output streams here to work around.
|
28
|
+
STDERR.flush
|
29
|
+
STDOUT.flush
|
30
|
+
end
|
31
|
+
|
24
32
|
supervisor_pid = $$
|
25
33
|
at_exit do
|
26
34
|
# We fork, therefore this handler will be run in master and
|
@@ -41,9 +49,11 @@ module Paraspec
|
|
41
49
|
end
|
42
50
|
end
|
43
51
|
|
52
|
+
Paraspec::Ipc.pick_master_app_port
|
53
|
+
|
44
54
|
rd, wr = IO.pipe
|
45
55
|
if @master_pid = fork
|
46
|
-
# parent
|
56
|
+
# parent - supervisor
|
47
57
|
wr.close
|
48
58
|
@master_pipe = rd
|
49
59
|
run_supervisor
|
@@ -93,7 +103,7 @@ module Paraspec
|
|
93
103
|
Paraspec.logger.debug_state("Waiting for workers")
|
94
104
|
@worker_pids.each_with_index do |pid, i|
|
95
105
|
Paraspec.logger.debug_state("Waiting for worker #{i+1} at #{pid}")
|
96
|
-
wait_for_process(pid)
|
106
|
+
wait_for_process(pid, "Worker #{i+1}", WorkerFailed)
|
97
107
|
end
|
98
108
|
status = 0
|
99
109
|
else
|
@@ -108,15 +118,14 @@ module Paraspec
|
|
108
118
|
end
|
109
119
|
Paraspec.logger.debug_state("Asking master to stop")
|
110
120
|
master_client.request('stop')
|
111
|
-
wait_for_process(@master_pid)
|
121
|
+
wait_for_process(@master_pid, 'Master', MasterFailed)
|
112
122
|
exit status
|
113
123
|
end
|
114
124
|
|
115
|
-
def wait_for_process(pid)
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
# already dead
|
125
|
+
def wait_for_process(pid, process_name, exception_class)
|
126
|
+
pid, status = Process.wait2(pid)
|
127
|
+
if status.exitstatus != 0
|
128
|
+
raise exception_class, "#{process_name} exited with status #{status.exitstatus}"
|
120
129
|
end
|
121
130
|
end
|
122
131
|
|
data/lib/paraspec/version.rb
CHANGED
data/lib/paraspec/worker.rb
CHANGED
@@ -46,7 +46,7 @@ module Paraspec
|
|
46
46
|
# being run.
|
47
47
|
puts "Worker #{@number} has #{RSpecFacade.all_examples.count} examples, master has #{master_example_count}"
|
48
48
|
#byebug
|
49
|
-
raise "Worker #{@number} has #{RSpecFacade.all_examples.count} examples, master has #{master_example_count}"
|
49
|
+
raise InconsistentTestSuite, "Worker #{@number} has #{RSpecFacade.all_examples.count} examples, master has #{master_example_count}"
|
50
50
|
end
|
51
51
|
|
52
52
|
while true
|
@@ -54,7 +54,7 @@ module Paraspec
|
|
54
54
|
file_path: notification.example.metadata[:file_path],
|
55
55
|
scoped_id: notification.example.metadata[:scoped_id],
|
56
56
|
}
|
57
|
-
@master_client.request('notify-example-started', spec: spec)
|
57
|
+
@master_client.request('notify-example-started', spec: spec, _noret: true)
|
58
58
|
end
|
59
59
|
|
60
60
|
def example_passed(notification)
|
@@ -68,7 +68,7 @@ module Paraspec
|
|
68
68
|
}
|
69
69
|
execution_result = notification.example.execution_result
|
70
70
|
@master_client.request('example-passed',
|
71
|
-
spec: spec, result: execution_result)
|
71
|
+
spec: spec, result: execution_result, _noret: true)
|
72
72
|
end
|
73
73
|
|
74
74
|
def example_failed(notification)
|
@@ -29,14 +29,30 @@ module Paraspec
|
|
29
29
|
#byebug
|
30
30
|
raise "No example group for #{spec.inspect}"
|
31
31
|
end
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
32
|
+
if group.metadata[:paraspec] && group.metadata[:paraspec][:group]
|
33
|
+
# unsplittable group
|
34
|
+
# get all examples in child groups
|
35
|
+
examples = []
|
36
|
+
group_queue = [group]
|
37
|
+
until group_queue.empty?
|
38
|
+
next_group_queue = []
|
39
|
+
group_queue.each do |group|
|
40
|
+
next_group_queue += group.children
|
41
|
+
examples += group.examples
|
42
|
+
end
|
43
|
+
group_queue = next_group_queue
|
44
|
+
end
|
45
|
+
else
|
46
|
+
# leaf group
|
47
|
+
examples = group.examples
|
48
|
+
#Paraspec.logger.debug_state("Spec #{spec}: #{examples.length} examples")
|
49
|
+
return if examples.empty?
|
50
|
+
ids = examples.map { |e| e.metadata[:scoped_id] }
|
51
|
+
RSpec.configuration.send(:instance_variable_set, '@filter_manager', RSpec::Core::FilterManager.new)
|
52
|
+
RSpec.configuration.filter_manager.add_ids(spec[:file_path], ids)
|
53
|
+
RSpec.world.filter_examples
|
54
|
+
examples = RSpec.configuration.filter_manager.prune(examples)
|
55
|
+
end
|
40
56
|
return if examples.empty?
|
41
57
|
# It is important to run the entire world here because if
|
42
58
|
# a particular example group is run, before/after :all hooks
|
metadata
CHANGED
@@ -1,29 +1,35 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: paraspec
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Oleg Pudeyev
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2019-08-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec-core
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: 3.7.1
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '4.0'
|
20
23
|
type: :runtime
|
21
24
|
prerelease: false
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
23
26
|
requirements:
|
24
|
-
- - "
|
27
|
+
- - ">="
|
25
28
|
- !ruby/object:Gem::Version
|
26
29
|
version: 3.7.1
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '4.0'
|
27
33
|
- !ruby/object:Gem::Dependency
|
28
34
|
name: childprocess
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -78,6 +84,7 @@ files:
|
|
78
84
|
- bin/paraspec
|
79
85
|
- lib/paraspec.rb
|
80
86
|
- lib/paraspec/drb_helpers.rb
|
87
|
+
- lib/paraspec/errors.rb
|
81
88
|
- lib/paraspec/http_client.rb
|
82
89
|
- lib/paraspec/http_server.rb
|
83
90
|
- lib/paraspec/ipc.rb
|
@@ -115,9 +122,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
115
122
|
- !ruby/object:Gem::Version
|
116
123
|
version: '0'
|
117
124
|
requirements: []
|
118
|
-
|
119
|
-
rubygems_version: 2.7.3
|
125
|
+
rubygems_version: 3.0.3
|
120
126
|
signing_key:
|
121
127
|
specification_version: 4
|
122
|
-
summary: paraspec-0.0.
|
128
|
+
summary: paraspec-0.0.3
|
123
129
|
test_files: []
|