rspec-abq 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,6 @@
1
+ module RSpec
2
+ module Abq
3
+ # current version!
4
+ VERSION = "0.1.0"
5
+ end
6
+ 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: []