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 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: []