minitest-fork_executor 1.0.2 → 1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e731cf4446b5cd86c62960733c6b246ddb448b0ec3de336dfffce8a4dd3ec42a
4
- data.tar.gz: f576f6b03abe886997f9e8aa7769f78fd6d8e0ac83aeeabff7e01a9e6ff9e1f0
3
+ metadata.gz: 6bd1e593335a149a422ff23630519000bd5f84e04bb8855682bd98511b07c6d1
4
+ data.tar.gz: 35f0d39d2498bd157e547ba852680a82fe3b5566616a7cdd28fa81b947cb160b
5
5
  SHA512:
6
- metadata.gz: 8ff4e0922b3282c87b4c39de27aac33173e56d9cc0d4362591050a33cc4e6483a6b0071a23862f93a60c8e530a95d4830dd6a486fa7b87dc31df65cbca204b3b
7
- data.tar.gz: 63eafec89873a5828f170f321bffaef38bb1a64e051345536c7073c6b725b978aecdf1de4016ce6fef78822e3f4c759f89c00e539c543b4d7e957c268107c4cf
6
+ metadata.gz: fc6a7ebe2f72cd6214469d3755325839289b0c65e05fa32544981bd2badbf865822ca62c07eeb731e39ce3f9267971b1426d092d37a68e6ec993083e4757b7b6
7
+ data.tar.gz: de20c9433516f9bfd89275e9d08802aa0ae50c2ffe5de60676dc148898012f000532328a67126a77600243452f297ea74c656bdbf5a9cba7ccb22568ceca1fcb
@@ -1,49 +1,112 @@
1
1
  module Minitest
2
+ # Minitest runs individual test cases via Minitest.run_one_method. When
3
+ # ForkExecutor is started, we need to override it and implement the fork
4
+ # algorithm. See the detailed comments below to understand how it's done.
5
+ #
6
+ # Please keep in mind we support Ruby 1.9 and hence can't use conveniences
7
+ # offered by more modern Rubies.
8
+
2
9
  class ForkExecutor
3
- # Minitest runs individual test cases via Minitest.run_one_method. When
4
- # ForkExecutor is started, we need to override it and implement the fork
5
- # algorithm. See the detailed comments below to understand how it's done.
6
- #
7
- # Please keep in mind we support Ruby 1.9 and hence can't use conveniences
8
- # offered by more modern Rubies.
9
- def start
10
- # Store the reference to the original run_one_method singleton method.
11
- original_run_one_method = Minitest.method(:run_one_method)
10
+ # Run a fork-based test case. The block passed to the method should actually
11
+ # run the test using the original code from Minitest.
12
+ def self.run
13
+ # Set up a binary pipe for transporting test results from the child
14
+ # to the parent process.
15
+ read_io, write_io = IO.pipe
16
+ read_io.binmode
17
+ write_io.binmode
18
+
19
+ if Process.fork
20
+ # The parent process responsible for collecting results.
21
+
22
+ # The parent process doesn't write anything.
23
+ write_io.close
24
+
25
+ # Load the result object passed by the child process.
26
+ result = Marshal.load(read_io)
27
+
28
+ # Unwrap all failures from FailureTransport so that they can be
29
+ # safely presented to the user.
30
+ result.failures.map!(&:failure)
31
+
32
+ # We're done reading results from the child so it's safe to close the
33
+ # IO object now.
34
+ read_io.close
35
+
36
+ # Wait for the child process to finish before returning the result.
37
+ Process.wait
38
+ else
39
+ # The child process responsible for running the test case.
40
+
41
+ # Run the test case method via the original .run_one_method.
42
+ result = yield
43
+
44
+ # Wrap failures in FailureTransport to avoid issue when marshalling.
45
+ # Some failures correspond to exceptions referencing unmarshallable
46
+ # objects. For example, a PostgreSQL exception may reference
47
+ # PG::Connection that cannot be marshalled. In those case, we replace
48
+ # the original error with UnmarshallableError retaining as much
49
+ # detail as possible.
50
+ result.failures.map! { |failure| FailureTransport.new(failure) }
12
51
 
13
- # Remove the original singleton method from Minitest in order to avoid
14
- # method redefinition warnings when patching it in the next step.
15
- class << Minitest
16
- remove_method(:run_one_method)
52
+ # The child process doesn't read anything.
53
+ read_io.close
54
+
55
+ # Dump the result object to the write IO object so that it can be
56
+ # read by the parent process.
57
+ Marshal.dump(result, write_io)
58
+
59
+ # We're done sending results to the parent so it's safe to close the
60
+ # IO object now.
61
+ write_io.close
62
+
63
+ # Exit the child process as its job is now done.
64
+ exit
17
65
  end
18
66
 
19
- # Define a new version of run_one_method that forks, calls the original
20
- # run_one_method in the child process, and sends results back to the
21
- # parent.
22
- Minitest.define_singleton_method(:run_one_method) do |klass, method_name|
23
- read_io, write_io = IO.pipe
24
- read_io.binmode
25
- write_io.binmode
67
+ # This value is returned ONLY in the parent process, not in the child
68
+ # process.
69
+ result
70
+ end
71
+
72
+ # #start is called by Minitest when initializing the executor. This is where
73
+ # we override some Minitest internals to implement fork-based execution.
74
+ def start
75
+ if Minitest.respond_to?(:run_one_method)
76
+ # Minitest 5
26
77
 
27
- if fork
28
- # Parent: load the result sent from the child
78
+ # Store the reference to the original run_one_method method in order to
79
+ # use it to actually run the test case.
80
+ original_run_one_method = Minitest.method(:run_one_method)
29
81
 
30
- write_io.close
31
- result = Marshal.load(read_io)
32
- read_io.close
82
+ # Remove the original singleton method from Minitest in order to avoid
83
+ # method redefinition warnings when patching it in the next step.
84
+ class << Minitest
85
+ remove_method(:run_one_method)
86
+ end
33
87
 
34
- Process.wait
35
- else
36
- # Child: just run normally, dump the result, and exit the process to
37
- # avoid double-reporting.
38
- result = original_run_one_method.call(klass, method_name)
39
-
40
- read_io.close
41
- Marshal.dump(result, write_io)
42
- write_io.close
43
- exit
88
+ # Define a new version of run_one_method that forks, calls the original
89
+ # run_one_method in the child process, and sends results back to the
90
+ # parent. klass and method_name are the two parameters accepted by the
91
+ # original run_one_method - they're the test class (e.g. UserTest) and
92
+ # the test method name (e.g. :test_email_must_be_unique).
93
+ Minitest.define_singleton_method(:run_one_method) do |klass, method_name|
94
+ ForkExecutor.run do
95
+ original_run_one_method.call(klass, method_name)
96
+ end
44
97
  end
98
+ else
99
+ # Minitest 6
45
100
 
46
- result
101
+ Runnable.runnables.each do |runnable_class|
102
+ original_run_method = runnable_class.instance_method(:run)
103
+ runnable_class.define_method(:run) do
104
+ bound_original_run_method = original_run_method.bind(self)
105
+ ForkExecutor.run do
106
+ bound_original_run_method.call
107
+ end
108
+ end
109
+ end
47
110
  end
48
111
  end
49
112
 
@@ -51,5 +114,73 @@ module Minitest
51
114
  # Nothing to do here but required by Minitest. In a future version, we may
52
115
  # reinstate the original Minitest.run_one_method here.
53
116
  end
117
+
118
+ # A Minitest Failure transport class enabling passing non-marshallable
119
+ # objects (e.g. IO or sockets) via Marshal. The basic idea is replacing
120
+ # Minitest failures referencing unmarshallable objects with
121
+ # UnmarshallableError retaining as much detail as possible.
122
+ class FailureTransport
123
+ attr_reader :failure
124
+
125
+ def initialize(failure)
126
+ @failure = failure
127
+ end
128
+
129
+ def marshal_dump
130
+ Marshal.dump(failure)
131
+ rescue TypeError
132
+ # CAREFUL! WE'RE MODIFYING FAILURE IN PLACE UNDER THE ASSUMPTION THAT
133
+ # IT LIVES IN A MEMORY SPACE OF A SHORT-LIVED PROCESS, NAMELY THE CHILD
134
+ # PROCESS RESPONSIBLE FOR RUNNING A SINGLE TEST. IF THIS ASSUMPTION IS
135
+ # VIOLATED THEN AN ALTERNATIVE APPROACH (E.G. DUPLICATING THE FAILURE)
136
+ # MIGHT BE NECESSARY.
137
+
138
+ if failure.respond_to?(:exception) && failure.respond_to?(:exception=)
139
+ failure.exception = UnmarshallableError.new(failure.exception)
140
+ elsif failure.respond_to?(:error) && failure.respond_to?(:error=)
141
+ failure.error = UnmarshallableError.new(failure.error)
142
+ else
143
+ raise(<<ERROR)
144
+ Minitest failures should respond respond to exception/exception= (versions prior
145
+ to 5.14.0) or error/error= (version 5.14.0 and newer). The received failure does
146
+ responds to neither. Are you using an newer Minitest version?
147
+ ERROR
148
+ end
149
+
150
+ Marshal.dump(failure)
151
+ end
152
+
153
+ def marshal_load(dump)
154
+ @failure = Marshal.load(dump)
155
+ end
156
+ end
157
+
158
+ # An always marshallable exception class that can be derived from another
159
+ # (potentially non-marshallable exception). It's actually not intended to be
160
+ # raised but merely instantiated when passing Minitest failures from the
161
+ # runner to the reporter.
162
+ class UnmarshallableError < RuntimeError
163
+ def initialize(exc)
164
+ super(<<MESSAGE)
165
+ An unmarshallable error has occured. Below is its best-effort representation.
166
+ In order to receive the error itself, please disable Minitest::ForkExecutor.
167
+
168
+ Error class:
169
+ #{exc.class.name}
170
+
171
+ Error message:
172
+ #{exc.message}
173
+
174
+ Attributes:
175
+ #{exc.instance_variables.map do |name|
176
+ " #{name} = #{exc.instance_variable_get(name).inspect}"
177
+ end.join("\n")}
178
+
179
+ END OF ERROR MESSAGE (ORIGINAL BACKTRACE MAY FOLLOW)
180
+ MESSAGE
181
+
182
+ set_backtrace(exc.backtrace)
183
+ end
184
+ end
54
185
  end
55
186
  end
@@ -0,0 +1,34 @@
1
+ require 'tempfile'
2
+
3
+ require 'minitest/autorun'
4
+ require 'minitest/fork_executor'
5
+
6
+ Minitest.parallel_executor = Minitest::ForkExecutor.new
7
+
8
+ class UnmarshallableException < RuntimeError
9
+ def initialize(io)
10
+ super("Unmarshallable test exception")
11
+ @io = io
12
+ end
13
+ end
14
+
15
+ class UnmarshallableTest < Minitest::Test
16
+ def test_unmarshallable_wrapping
17
+ # We'd like to see a backtrace in the output hence several raise_in_* calls.
18
+ raise_in_2
19
+ end
20
+
21
+ private
22
+
23
+ def raise_in_2
24
+ raise_in_1
25
+ end
26
+
27
+ def raise_in_1
28
+ raise_in_0
29
+ end
30
+
31
+ def raise_in_0
32
+ raise UnmarshallableException.new($stdout)
33
+ end
34
+ end
metadata CHANGED
@@ -1,11 +1,11 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: minitest-fork_executor
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Greg Navis
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
  date: 2019-01-03 00:00:00.000000000 Z
@@ -28,16 +28,30 @@ dependencies:
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - '='
31
+ - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 12.2.1
33
+ version: 13.0.0
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - '='
38
+ - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 12.2.1
40
+ version: 13.0.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: ostruct
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
41
55
  description: Run each test_* method in a separate process thus eliminating test case
42
56
  interference.
43
57
  email: contact@gregnavis.com
@@ -46,12 +60,13 @@ extensions: []
46
60
  extra_rdoc_files: []
47
61
  files:
48
62
  - lib/minitest/fork_executor.rb
49
- - test/minitest/fork_executor_test.rb
63
+ - test/acceptance/unmarshallable_test.rb
64
+ - test/unit/minitest/fork_executor_test.rb
50
65
  homepage: https://github.com/gregnavis/minitest-fork_executor
51
66
  licenses:
52
67
  - MIT
53
68
  metadata: {}
54
- post_install_message:
69
+ post_install_message:
55
70
  rdoc_options: []
56
71
  require_paths:
57
72
  - lib
@@ -66,9 +81,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
66
81
  - !ruby/object:Gem::Version
67
82
  version: '0'
68
83
  requirements: []
69
- rubygems_version: 3.1.4
70
- signing_key:
84
+ rubygems_version: 3.4.19
85
+ signing_key:
71
86
  specification_version: 4
72
87
  summary: Near-perfect process-level test case isolation.
73
88
  test_files:
74
- - test/minitest/fork_executor_test.rb
89
+ - test/acceptance/unmarshallable_test.rb
90
+ - test/unit/minitest/fork_executor_test.rb