rspec-abq 0.1.0
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 +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: []
|