rspec-abq 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +45 -0
- data/lib/rspec/abq/extensions.rb +131 -0
- data/lib/rspec/abq/manifest.rb +52 -0
- data/lib/rspec/abq/ordering.rb +34 -0
- data/lib/rspec/abq/reporter.rb +85 -0
- data/lib/rspec/abq/test_case.rb +73 -0
- data/lib/rspec/abq/version.rb +6 -0
- data/lib/rspec/abq.rb +202 -0
- metadata +73 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f99eb724ca058a63aa026b82c6d9bfda4a5aaf98df3d7fc0485025db98e46b43
|
4
|
+
data.tar.gz: bcfa57ca1f21298e63f4feed8e3d845a1304c9db9915360146931eb0c521105f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a95ccc0549bc1d46f79c25fbe7de2f7b3f88cd74474039333d8b72b31858bf4c903169e4b7bbb55cfb277b4844bcc7073a56eb87bc332bac82d41261de31ef6a
|
7
|
+
data.tar.gz: 9b497c0bd97ee1760a18077d88bfa3da9c0340393648d01a1a53d8ec4f91a95303303b5a329b933bd0212a246a83477f7912e1359aca9b5f5aac365ec9dc1cf3
|
data/README.md
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# Rspec::Abq
|
2
|
+
|
3
|
+
This gem helps you use rspec with abq.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
group :test do
|
11
|
+
gem 'rspec-core'
|
12
|
+
...
|
13
|
+
gem 'rspec-abq'
|
14
|
+
end
|
15
|
+
```
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
$ bundle
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
Use the included binary with abq:
|
24
|
+
|
25
|
+
```
|
26
|
+
abq test -- bundle exec rspec
|
27
|
+
```
|
28
|
+
|
29
|
+
## Development
|
30
|
+
|
31
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
32
|
+
|
33
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
34
|
+
|
35
|
+
### Releasing the gem
|
36
|
+
|
37
|
+
use the release script, `./release_gem.rb`
|
38
|
+
|
39
|
+
## Contributing
|
40
|
+
|
41
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/rwx-research/rspec-abq. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
42
|
+
|
43
|
+
## Code of Conduct
|
44
|
+
|
45
|
+
Everyone interacting in the Rspec::Abq project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/rwx-research/rspec-abq/blob/master/CODE_OF_CONDUCT.md).
|
@@ -0,0 +1,131 @@
|
|
1
|
+
module RSpec
|
2
|
+
module Abq
|
3
|
+
# A few extensions to RSpec::Core classes to hook in abq
|
4
|
+
module Extensions
|
5
|
+
# adds our functionality to RSpec::Core::Configuration
|
6
|
+
# @!visibility private
|
7
|
+
def self.setup!
|
8
|
+
RSpec::Core::ExampleGroup.extend(ExampleGroup)
|
9
|
+
RSpec::Core::Runner.prepend(Runner)
|
10
|
+
end
|
11
|
+
|
12
|
+
# ExampleGroups are nodes in a tree with
|
13
|
+
# - a (potentially empty) list of Examples (the value of the node)
|
14
|
+
# - AND a (potentialy empty) list of children ExampleGroups (... the ... children ... are the children of the
|
15
|
+
# node 😅)
|
16
|
+
# ExampleGroups are defined by `context` and `describe` in the RSpec DSL
|
17
|
+
# Examples are defined dby `it` RSpec DSL
|
18
|
+
module ExampleGroup
|
19
|
+
# This method
|
20
|
+
# - iterates over the current ExampleGroup's Examples to find the Example that is the same as
|
21
|
+
# Abq.target_test_case
|
22
|
+
# - runs the example
|
23
|
+
# - and fetches example that is now the `Abq.target_test_case`a
|
24
|
+
#
|
25
|
+
# the next target_test_case is either
|
26
|
+
# - later in this ExampleGroup's examples
|
27
|
+
# - so we continue iterating until we get there
|
28
|
+
# - or in another ExampleGroup
|
29
|
+
# - so we bail from this iteration and let the caller (run_with_abq) iterate to the right ExampleGroup
|
30
|
+
def run_examples_with_abq
|
31
|
+
all_examples_succeeded = true
|
32
|
+
ordering_strategy.order(filtered_examples).each do |considered_example|
|
33
|
+
next unless Abq.target_test_case.is_example?(considered_example)
|
34
|
+
next if RSpec.world.wants_to_quit
|
35
|
+
|
36
|
+
instance = new(considered_example.inspect_output)
|
37
|
+
set_ivars(instance, before_context_ivars)
|
38
|
+
|
39
|
+
all_examples_succeeded &&= Abq.send_test_result_and_advance { |abq_reporter| considered_example.run(instance, abq_reporter) }
|
40
|
+
|
41
|
+
break unless Abq.target_test_case.directly_in_group?(self)
|
42
|
+
end
|
43
|
+
all_examples_succeeded
|
44
|
+
end
|
45
|
+
|
46
|
+
# same as .run but using abq
|
47
|
+
def run_with_abq(reporter)
|
48
|
+
# The next test isn't in this group or any child; we can skip
|
49
|
+
# over this group entirely.
|
50
|
+
return true unless Abq.target_test_case.in_group?(self)
|
51
|
+
|
52
|
+
reporter.example_group_started(self)
|
53
|
+
|
54
|
+
should_run_context_hooks = descendant_filtered_examples.any?
|
55
|
+
begin
|
56
|
+
RSpec.current_scope = :before_context_hook
|
57
|
+
run_before_context_hooks(new("before(:context) hook")) if should_run_context_hooks
|
58
|
+
|
59
|
+
# If the next example to run is on the surface of this group, scan all
|
60
|
+
# the examples; otherwise, we just need to check the children groups.
|
61
|
+
result_for_this_group =
|
62
|
+
if Abq.target_test_case.directly_in_group?(self)
|
63
|
+
run_examples_with_abq
|
64
|
+
else
|
65
|
+
true
|
66
|
+
end
|
67
|
+
|
68
|
+
results_for_descendants = ordering_strategy.order(children).map { |child| child.run_with_abq(reporter) }.all?
|
69
|
+
result_for_this_group && results_for_descendants
|
70
|
+
rescue RSpec::Core::Pending::SkipDeclaredInExample => ex
|
71
|
+
for_filtered_examples(reporter) { |example| example.skip_with_exception(reporter, ex) }
|
72
|
+
true
|
73
|
+
rescue RSpec::Support::AllExceptionsExceptOnesWeMustNotRescue => ex
|
74
|
+
# If an exception reaches here, that means we must fail the entire
|
75
|
+
# group (otherwise we would have handled the exception locally at an
|
76
|
+
# example). Since we know of the examples in the same order as they'll
|
77
|
+
# be sent to us from ABQ, we now loop over all the examples, and mark
|
78
|
+
# every one that we must run in this group as a failure.
|
79
|
+
for_filtered_examples(reporter) do |example|
|
80
|
+
next unless Abq.target_test_case.is_example?(example)
|
81
|
+
|
82
|
+
Abq.send_test_result_and_advance { |abq_reporter| example.fail_with_exception(abq_reporter, ex) }
|
83
|
+
end
|
84
|
+
|
85
|
+
RSpec.world.wants_to_quit = true if reporter.fail_fast_limit_met?
|
86
|
+
false
|
87
|
+
ensure
|
88
|
+
RSpec.current_scope = :after_context_hook
|
89
|
+
run_after_context_hooks(new("after(:context) hook")) if should_run_context_hooks
|
90
|
+
reporter.example_group_finished(self)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Runner is class responsbile for execution in RSpec
|
96
|
+
module Runner
|
97
|
+
# Runs the provided example groups.
|
98
|
+
#
|
99
|
+
# @param example_groups [Array<RSpec::Core::ExampleGroup>] groups to run
|
100
|
+
# @return [Fixnum] exit status code. 0 if all specs passed,
|
101
|
+
# or the configured failure exit code (1 by default) if specs
|
102
|
+
# failed.
|
103
|
+
def run_specs(example_groups)
|
104
|
+
should_quit = RSpec::Abq.setup_after_specs_loaded!
|
105
|
+
return 0 if should_quit
|
106
|
+
|
107
|
+
examples_count = @world.example_count(example_groups)
|
108
|
+
examples_passed = @configuration.reporter.report(examples_count) do |reporter|
|
109
|
+
@configuration.with_suite_hooks do
|
110
|
+
if examples_count == 0 && @configuration.fail_if_no_examples
|
111
|
+
return @configuration.failure_exit_code
|
112
|
+
end
|
113
|
+
|
114
|
+
example_groups.map { |g| g.run_with_abq(reporter) }.all?
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
exit_code(examples_passed)
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def persist_example_statuses
|
124
|
+
if RSpec.configuration.example_status_persistence_file_path
|
125
|
+
warn "persisting example status disabled by abq"
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require "set"
|
2
|
+
module RSpec
|
3
|
+
module Abq
|
4
|
+
# A module for abstracting ABQ Manifest
|
5
|
+
module Manifest
|
6
|
+
# writes manifest to abq socket
|
7
|
+
def self.write_manifest(ordered_groups, random_seed, registry)
|
8
|
+
Abq.protocol_write(generate(ordered_groups, random_seed, registry))
|
9
|
+
end
|
10
|
+
|
11
|
+
# Generates an ABQ Manifest
|
12
|
+
# @param ordered_groups [Array<RSpec::Core::ExampleGroup>] ordered groups to assemble into a manifest
|
13
|
+
def self.generate(ordered_groups, random_seed, registry)
|
14
|
+
{
|
15
|
+
manifest: {
|
16
|
+
init_meta: RSpec::Abq::Ordering.to_meta(random_seed, registry),
|
17
|
+
members: ordered_groups.map { |group| to_manifest_group(group) }.compact
|
18
|
+
}
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
# @!visibility private
|
23
|
+
# @param group [RSpec::Core::ExampleGroup]
|
24
|
+
private_class_method def self.to_manifest_group(group)
|
25
|
+
# NB: It's important to write examples first and then children groups,
|
26
|
+
# because that's how the runner will execute them.
|
27
|
+
members =
|
28
|
+
group.ordering_strategy.order(group.filtered_examples).map { |example|
|
29
|
+
tags, metadata = Abq.extract_metadata_and_tags(example.metadata)
|
30
|
+
{
|
31
|
+
type: "test",
|
32
|
+
id: example.id,
|
33
|
+
tags: tags,
|
34
|
+
meta: metadata
|
35
|
+
}
|
36
|
+
}
|
37
|
+
.concat(
|
38
|
+
group.ordering_strategy.order(group.children).map { |child_group| to_manifest_group(child_group) }.compact
|
39
|
+
)
|
40
|
+
return nil if members.empty?
|
41
|
+
tags, metadata = Abq.extract_metadata_and_tags(group.metadata)
|
42
|
+
{
|
43
|
+
type: "group",
|
44
|
+
name: group.id,
|
45
|
+
tags: tags,
|
46
|
+
meta: metadata,
|
47
|
+
members: members
|
48
|
+
}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module RSpec
|
2
|
+
module Abq
|
3
|
+
# This module is responsible for recording ordering for the manifest
|
4
|
+
# and reading the ordering from `init_meta` to set up the current processes settings
|
5
|
+
module Ordering
|
6
|
+
# notably: we don't support custom orderings
|
7
|
+
SUPPORTED_ORDERINGS = [:defined, :recently_modified, :random]
|
8
|
+
|
9
|
+
# Raised when we experience an ordering that doesn't exist in SUPPORTED_ORDERINGS
|
10
|
+
UnsupportedOrderingError = Class.new(StandardError)
|
11
|
+
|
12
|
+
# takes a seed and a registry and produces a hash for the manifest
|
13
|
+
def self.to_meta(seed, registry)
|
14
|
+
global_ordering = registry.fetch(:global)
|
15
|
+
ordering_name = SUPPORTED_ORDERINGS.find { |name| registry.fetch(name) == global_ordering }
|
16
|
+
fail(UnsupportedOrderingError, "can't order based on unknown ordering: `#{global_ordering.class}`") unless ordering_name
|
17
|
+
{
|
18
|
+
ordering: ordering_name,
|
19
|
+
seed: seed
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
# takes the meta (prodced in .to_meta) and applies the settings to the current process
|
24
|
+
def self.setup!(init_meta, configuration)
|
25
|
+
configuration.seed = init_meta["seed"]
|
26
|
+
registry = configuration.ordering_registry
|
27
|
+
ordering_from_manifest = registry.fetch(init_meta["ordering"].to_sym) do
|
28
|
+
fail(UnsupportedOrderingError, "can't order based on unknown ordering: `#{init_meta["ordering"]}`")
|
29
|
+
end
|
30
|
+
registry.register(:global, ordering_from_manifest)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module RSpec
|
2
|
+
module Abq
|
3
|
+
# Realistically we should instead extend [RSpec::Core::Reporter], but
|
4
|
+
# there's some indirection there I don't yet understand, so instead just
|
5
|
+
# build it up from scratch for now.
|
6
|
+
class Reporter
|
7
|
+
attr_reader :status
|
8
|
+
attr_reader :id
|
9
|
+
attr_reader :display_name
|
10
|
+
attr_reader :tags
|
11
|
+
attr_reader :meta
|
12
|
+
|
13
|
+
# @param example [RSpec::Core::Example]
|
14
|
+
def example_started(example)
|
15
|
+
@id = example.id
|
16
|
+
@example = example
|
17
|
+
@display_name = example.metadata[:full_description]
|
18
|
+
@tags, @meta = Abq.extract_metadata_and_tags(example.metadata)
|
19
|
+
end
|
20
|
+
|
21
|
+
# @param example [RSpec::Core::Example]
|
22
|
+
def example_finished(example)
|
23
|
+
@execution_result = example.metadata[:execution_result]
|
24
|
+
end
|
25
|
+
|
26
|
+
# @param example [RSpec::Core::Example]
|
27
|
+
def example_failed(example)
|
28
|
+
@status =
|
29
|
+
if example.execution_result.exception.is_a? RSpec::Expectations::ExpectationNotMetError
|
30
|
+
:failure
|
31
|
+
else
|
32
|
+
:error
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# @param _example [RSpec::Core::Example]
|
37
|
+
def example_passed(_example)
|
38
|
+
@status = :success
|
39
|
+
end
|
40
|
+
|
41
|
+
# @param example [RSpec::Core::Example]
|
42
|
+
def example_pending(example)
|
43
|
+
@status =
|
44
|
+
if example.execution_result.example_skipped?
|
45
|
+
:skipped
|
46
|
+
else
|
47
|
+
:pending
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# @return [String, nil]
|
52
|
+
def output
|
53
|
+
if @execution_result.exception
|
54
|
+
presenter = RSpec::Core::Formatters::ExceptionPresenter.new @execution_result.exception, @example
|
55
|
+
return presenter.fully_formatted 1
|
56
|
+
end
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return Int
|
61
|
+
def runtime_ms
|
62
|
+
@execution_result.run_time * 1000
|
63
|
+
end
|
64
|
+
|
65
|
+
# does nothing, just here to fulfill reporter api
|
66
|
+
def example_group_finished(_)
|
67
|
+
end
|
68
|
+
|
69
|
+
# creates a hash that fits the abq worker result protocol
|
70
|
+
def abq_result
|
71
|
+
{
|
72
|
+
test_result: {
|
73
|
+
status: status,
|
74
|
+
id: id,
|
75
|
+
display_name: display_name,
|
76
|
+
output: output,
|
77
|
+
runtime: runtime_ms,
|
78
|
+
tags: tags,
|
79
|
+
meta: meta
|
80
|
+
}
|
81
|
+
}
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module RSpec
|
2
|
+
module Abq
|
3
|
+
# ABQ's representation of a test case
|
4
|
+
class TestCase
|
5
|
+
def initialize(id, tags, meta)
|
6
|
+
@id = id
|
7
|
+
@tags = tags
|
8
|
+
@meta = meta
|
9
|
+
@rerun_file_path, scoped_id = RSpec::Core::Example.parse_id @id
|
10
|
+
@scope = self.class.parse_scope(scoped_id || "")
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :id
|
14
|
+
attr_reader :tags
|
15
|
+
attr_reader :meta
|
16
|
+
attr_reader :rerun_file_path
|
17
|
+
attr_reader :scoped_id
|
18
|
+
|
19
|
+
# Parses a scope n:m:q:r into [n, m, q, r]
|
20
|
+
# Invariant of RSpec is that a scope n:m:q:r is contained in a scope n:m:q
|
21
|
+
def self.parse_scope(scope)
|
22
|
+
scope.split(":")
|
23
|
+
end
|
24
|
+
|
25
|
+
# `scope_contains outer inner` is true iff the inner scope is deeper
|
26
|
+
# than the outer scope.
|
27
|
+
#
|
28
|
+
# @param outer [Array<String>] parsed scope
|
29
|
+
# @param inner [Array<String>] parsed scope
|
30
|
+
def self.scope_contains(outer, inner)
|
31
|
+
inner.take(outer.length) == outer
|
32
|
+
end
|
33
|
+
|
34
|
+
# `scope_leftover outer inner` returns the partial scopes of `inner`
|
35
|
+
# that are deeper than `outer`.
|
36
|
+
#
|
37
|
+
# @param outer [Array<String>] parsed scope
|
38
|
+
# @param inner [Array<String>] parsed scope
|
39
|
+
def self.scope_leftover(outer, inner)
|
40
|
+
inner[outer.length..-1] || []
|
41
|
+
end
|
42
|
+
|
43
|
+
# @param group [RSpec::Core::ExampleGroup]
|
44
|
+
def in_group?(group)
|
45
|
+
return false if group.metadata[:rerun_file_path] != @rerun_file_path
|
46
|
+
|
47
|
+
group_scope = self.class.parse_scope(group.metadata[:scoped_id])
|
48
|
+
self.class.scope_contains(group_scope, @scope)
|
49
|
+
end
|
50
|
+
|
51
|
+
# @param group [RSpec::Core::ExampleGroup]
|
52
|
+
def directly_in_group?(group)
|
53
|
+
return false unless in_group?(group)
|
54
|
+
|
55
|
+
group_scope = self.class.parse_scope(group.metadata[:scoped_id])
|
56
|
+
additional_scoping = self.class.scope_leftover(group_scope, @scope)
|
57
|
+
raise "#{@id} not inside #{group_scope}, but we thought it was" if additional_scoping.empty?
|
58
|
+
additional_scoping.length == 1
|
59
|
+
end
|
60
|
+
|
61
|
+
# @param example [RSpec::Core::Example]
|
62
|
+
def is_example?(example)
|
63
|
+
example.metadata[:rerun_file_path] == @rerun_file_path && self.class.parse_scope(example.metadata[:scoped_id]) == @scope
|
64
|
+
end
|
65
|
+
|
66
|
+
# Faux test case to mark end of all tests. Will never match any group or
|
67
|
+
# test ID, since the scoped_id is empty.
|
68
|
+
def self.end_marker
|
69
|
+
@end_marker ||= TestCase.new("[]", [], {})
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/rspec/abq.rb
ADDED
@@ -0,0 +1,202 @@
|
|
1
|
+
require "set"
|
2
|
+
require "rspec/core"
|
3
|
+
require "socket"
|
4
|
+
require "json"
|
5
|
+
require_relative "abq/extensions"
|
6
|
+
require_relative "abq/manifest"
|
7
|
+
require_relative "abq/ordering"
|
8
|
+
require_relative "abq/reporter"
|
9
|
+
require_relative "abq/test_case"
|
10
|
+
require_relative "abq/version"
|
11
|
+
|
12
|
+
# We nest our patch into RSpec's module -- why not?
|
13
|
+
module RSpec
|
14
|
+
# An abq adapter for RSpec!
|
15
|
+
module Abq
|
16
|
+
# the socket used to communicate to the abq worker
|
17
|
+
# looks like "ip.address.3.4:port" e.g. "0.0.0.0:1234"
|
18
|
+
# @!visibility private
|
19
|
+
ABQ_SOCKET = "ABQ_SOCKET"
|
20
|
+
|
21
|
+
# the abq worker will set this environmental variable if it needs this process to generate a manifest
|
22
|
+
# @!visibility private
|
23
|
+
ABQ_GENERATE_MANIFEST = "ABQ_GENERATE_MANIFEST"
|
24
|
+
|
25
|
+
# this is set by the outer-most rspec runner to ensure nested rspecs aren't ABQ aware.
|
26
|
+
# if we ever want nested ABQ rspec, we'll need to change this.
|
27
|
+
# this env var is unrelated to the abq worker!
|
28
|
+
# @!visibility private
|
29
|
+
ABQ_RSPEC_PID = "ABQ_RSPEC_PID"
|
30
|
+
|
31
|
+
# The [ABQ protocol version message](https://www.notion.so/rwx/ABQ-Worker-Native-Test-Runner-IPC-Interface-0959f5a9144741d798ac122566a3d887#8587ee4fd01e41ec880dcbe212562172).
|
32
|
+
# Must be sent to ABQ_SOCKET on startup.
|
33
|
+
# @!visibility private
|
34
|
+
PROTOCOL_VERSION_MESSAGE = {
|
35
|
+
type: "abq_protocol_version",
|
36
|
+
major: 0,
|
37
|
+
minor: 1
|
38
|
+
}
|
39
|
+
|
40
|
+
# The [ABQ initialization success
|
41
|
+
# message](https://www.notion.so/rwx/ABQ-Worker-Native-Test-Runner-IPC-Interface-0959f5a9144741d798ac122566a3d887#538582a3049f4934a5cb563d815c1247)
|
42
|
+
# Must be sent after receiving the ABQ initialization message.
|
43
|
+
# @!visibility private
|
44
|
+
INIT_SUCCESS_MESSAGE = {}
|
45
|
+
|
46
|
+
# Whether this rspec process is running in ABQ mode.
|
47
|
+
# @return [Boolean]
|
48
|
+
def self.enabled?(env = ENV)
|
49
|
+
env.key?(ABQ_SOCKET) && # this is the basic check for rspec being called from an abq worker
|
50
|
+
(!env.key?(ABQ_RSPEC_PID) || env[ABQ_RSPEC_PID] == Process.pid.to_s) # and this check ensures that any _nested_ processes do not communicate with the worker.
|
51
|
+
end
|
52
|
+
|
53
|
+
# Disables tests so we can compare runtime of rspec core vs parallelized version. Additionally, disables tests
|
54
|
+
# if forced via ABQ_DISABLE_TESTS env var.
|
55
|
+
# @return [Boolean]
|
56
|
+
def self.disable_tests_when_run_by_abq?
|
57
|
+
enabled? ||
|
58
|
+
ENV.key?("ABQ_DISABLE_TESTS")
|
59
|
+
end
|
60
|
+
|
61
|
+
# This is the main entry point for abq-rspec, and it's called when the gem is loaded
|
62
|
+
# @!visibility private
|
63
|
+
# @return [void]
|
64
|
+
def self.setup!
|
65
|
+
return unless enabled?
|
66
|
+
Extensions.setup!
|
67
|
+
end
|
68
|
+
|
69
|
+
# @!visibility private
|
70
|
+
# @return [Boolean]
|
71
|
+
def self.setup_after_specs_loaded!
|
72
|
+
ENV[ABQ_RSPEC_PID] = Process.pid.to_s
|
73
|
+
# ABQ doesn't support writing example status to disk yet.
|
74
|
+
# in its simple implementation, status persistance write the status of all tests which ends up hanging with under
|
75
|
+
# abq because we haven't run most of the tests in this worker. (maybe it's running the tests?). In any case:
|
76
|
+
# it's disabled.
|
77
|
+
RSpec.configuration.example_status_persistence_file_path = nil
|
78
|
+
|
79
|
+
# before abq can start workers, it asks for a manifest
|
80
|
+
if !!ENV[ABQ_GENERATE_MANIFEST] # the abq worker will set this env var if it needs a manifest
|
81
|
+
RSpec::Abq::Manifest.write_manifest(RSpec.world.ordered_example_groups, RSpec.configuration.seed, RSpec.configuration.ordering_registry)
|
82
|
+
# ... Maybe it's fine to just exit(0)
|
83
|
+
RSpec.world.wants_to_quit = true # ask rspec to exit
|
84
|
+
RSpec.configuration.error_exit_code = 0 # exit without error
|
85
|
+
RSpec.world.non_example_failure = true # exit has nothing to do with tests
|
86
|
+
return true
|
87
|
+
end
|
88
|
+
|
89
|
+
# after the manfiest has been sent to the worker, the rspec process will quit and the workers will each start a
|
90
|
+
# new rspec process
|
91
|
+
|
92
|
+
# enabling colors allows us to pass through nicer error messages
|
93
|
+
RSpec.configuration.color_mode = :on
|
94
|
+
|
95
|
+
# the first message is the init_meta block of the manifest. This is used to share runtime configuration
|
96
|
+
# information amongst worker processes. In RSpec, it is used to ensure that random ordering between workers
|
97
|
+
# shares the same seed, so can be deterministic.
|
98
|
+
message = protocol_read
|
99
|
+
init_message = message["init_meta"]
|
100
|
+
if init_message
|
101
|
+
protocol_write(INIT_SUCCESS_MESSAGE)
|
102
|
+
# todo: get rid of this unless init_message.empty? as soon as the bug is fixed in abq
|
103
|
+
Ordering.setup!(init_message, RSpec.configuration) unless init_message.empty?
|
104
|
+
fetch_next_example
|
105
|
+
else
|
106
|
+
# to support the old protocol, we don't depend on the initialization method, however we don't support random
|
107
|
+
# ordering via config, only via a shared command line seed. `abq test -- rspec --seed 4` will pass the
|
108
|
+
# deterministic seed to all workers.
|
109
|
+
fetch_next_example(message)
|
110
|
+
end
|
111
|
+
nil
|
112
|
+
end
|
113
|
+
|
114
|
+
# Creates the socket to communicate with the worker and sends the worker the protocol
|
115
|
+
# @!visibility private
|
116
|
+
def self.socket
|
117
|
+
@socket ||= TCPSocket.new(*ENV[ABQ_SOCKET].split(":")).tap do |socket|
|
118
|
+
protocol_write(PROTOCOL_VERSION_MESSAGE, socket)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# These are the metadata keys that rspec uses internally on examples and groups
|
123
|
+
# When we want to report custom tags that rspec-users write, we need to remove these from the example metadata
|
124
|
+
# @!visibility private
|
125
|
+
RESERVED_METADATA_KEYS = Set.new(RSpec::Core::Metadata::RESERVED_KEYS + [:if, :unless])
|
126
|
+
|
127
|
+
# Takes group or example metadata and returns a two-element array:
|
128
|
+
# a tag is any piece of metadata that has a value of true
|
129
|
+
# @return [Array<Array<Symbol>, Hash<Symbol, Object>>] tags and metadata
|
130
|
+
# @!visibility private
|
131
|
+
def self.extract_metadata_and_tags(metadata)
|
132
|
+
# we use `.dup.reject! because `.reject` raises a warning (because it doesn't dup procs)`
|
133
|
+
user_metadata = metadata.dup.reject! { |k, _v| RESERVED_METADATA_KEYS.include?(k) }
|
134
|
+
tags_array, metadata_array = user_metadata.partition { |_k, v| v == true }
|
135
|
+
[tags_array.map(&:first), metadata_array.to_h]
|
136
|
+
end
|
137
|
+
|
138
|
+
class << self
|
139
|
+
# the target_test_case is the test case the abq worker wants results for
|
140
|
+
# @!visibility private
|
141
|
+
attr_reader :target_test_case
|
142
|
+
end
|
143
|
+
|
144
|
+
# pulls next example from the abq worker and sets it to #target_test_case
|
145
|
+
# @!visibility private
|
146
|
+
def self.fetch_next_example(message = protocol_read)
|
147
|
+
@target_test_case =
|
148
|
+
if message == :abq_done
|
149
|
+
TestCase.end_marker
|
150
|
+
else
|
151
|
+
TestCase.new(*message["test_case"].values_at("id", "tags", "meta"))
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Communication between abq sockets follows the following protocol:
|
156
|
+
# - The first 4 bytes an unsigned 32-bit integer (big-endian) representing
|
157
|
+
# the size of the rest of the message.
|
158
|
+
# - The rest of the message is a JSON-encoded payload.
|
159
|
+
class AbqConnBroken < StandardError
|
160
|
+
end
|
161
|
+
|
162
|
+
# Writes a message to an Abq socket using the 4-byte header protocol.
|
163
|
+
#
|
164
|
+
# @param socket [TCPSocket]
|
165
|
+
# @param msg
|
166
|
+
def self.protocol_write(msg, socket = Abq.socket)
|
167
|
+
json_msg = JSON.dump msg
|
168
|
+
begin
|
169
|
+
socket.write [json_msg.bytesize].pack("N")
|
170
|
+
socket.write json_msg
|
171
|
+
rescue
|
172
|
+
raise AbqConnBroken
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# Writes a message to an Abq socket using the 4-byte header protocol.
|
177
|
+
#
|
178
|
+
# @param socket [TCPSocket]
|
179
|
+
# @return msg
|
180
|
+
def self.protocol_read(socket = Abq.socket)
|
181
|
+
len_bytes = socket.read 4
|
182
|
+
return :abq_done if len_bytes.nil?
|
183
|
+
len = len_bytes.unpack1("N")
|
184
|
+
json_msg = socket.read len
|
185
|
+
return :abq_done if json_msg.nil?
|
186
|
+
JSON.parse json_msg
|
187
|
+
end
|
188
|
+
|
189
|
+
# sends test results to ABQ and advances by one
|
190
|
+
# @!visibility private
|
191
|
+
def self.send_test_result_and_advance(&block)
|
192
|
+
reporter = Reporter.new
|
193
|
+
test_succeeded = block.call(reporter)
|
194
|
+
protocol_write(reporter.abq_result)
|
195
|
+
fetch_next_example
|
196
|
+
# return whether the test succeeded or not
|
197
|
+
test_succeeded
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
RSpec::Abq.setup!
|
metadata
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rspec-abq
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ayaz Hafiz
|
8
|
+
- Michael Glass
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 1980-01-01 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec-core
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: 3.11.0
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: 3.11.0
|
28
|
+
description: RSpec::Abq is an rspec plugin that replaces its ordering with one that
|
29
|
+
is controlled by abq. It allows for parallelization of rspec on a single machine
|
30
|
+
or across multiple workers.
|
31
|
+
email:
|
32
|
+
- ayaz@rwx.com
|
33
|
+
- me@rwx.com
|
34
|
+
executables: []
|
35
|
+
extensions: []
|
36
|
+
extra_rdoc_files: []
|
37
|
+
files:
|
38
|
+
- README.md
|
39
|
+
- lib/rspec/abq.rb
|
40
|
+
- lib/rspec/abq/extensions.rb
|
41
|
+
- lib/rspec/abq/manifest.rb
|
42
|
+
- lib/rspec/abq/ordering.rb
|
43
|
+
- lib/rspec/abq/reporter.rb
|
44
|
+
- lib/rspec/abq/test_case.rb
|
45
|
+
- lib/rspec/abq/version.rb
|
46
|
+
homepage: https://github.com/rwx-research/rspec-abq
|
47
|
+
licenses: []
|
48
|
+
metadata:
|
49
|
+
homepage_uri: https://github.com/rwx-research/rspec-abq
|
50
|
+
bug_tracker_uri: https://github.com/rwx-research/rspec-abq/issues
|
51
|
+
changelog_uri: https://github.com/rwx-research/rspec-abq/releases
|
52
|
+
documentation_uri: https://rwx-research.github.io/rspec-abq/
|
53
|
+
source_code_uri: https://github.com/rwx-research/rspec-abq
|
54
|
+
post_install_message:
|
55
|
+
rdoc_options: []
|
56
|
+
require_paths:
|
57
|
+
- lib
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
requirements: []
|
69
|
+
rubygems_version: 3.2.26
|
70
|
+
signing_key:
|
71
|
+
specification_version: 4
|
72
|
+
summary: RSpec::Abq allows for parallel rspec runs using abq
|
73
|
+
test_files: []
|