minitest-fork_executor 1.0.2 → 1.0.3

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: e166bd6d5d09dc03b3888c59beb8a30ba259c7cef826f0bcfa373375b232151c
4
+ data.tar.gz: caadad6c3971b6acafbe50fc0e1a3f76f7062d8c03bfe27ba11a578ecd91920e
5
5
  SHA512:
6
- metadata.gz: 8ff4e0922b3282c87b4c39de27aac33173e56d9cc0d4362591050a33cc4e6483a6b0071a23862f93a60c8e530a95d4830dd6a486fa7b87dc31df65cbca204b3b
7
- data.tar.gz: 63eafec89873a5828f170f321bffaef38bb1a64e051345536c7073c6b725b978aecdf1de4016ce6fef78822e3f4c759f89c00e539c543b4d7e957c268107c4cf
6
+ metadata.gz: 5775181f930796a6b81c36cf265a0fe562f216e21fa6ae16c42b0dc1318c5ceacd078d580071d8d8a6f8c511d8f78b5fe7af52258930bea6787bb015a172bffc
7
+ data.tar.gz: 9a683822a4dd87af11d56088854e2cc10caf537388644f5f0ddabe8612b5ea92196b0b6f277a64758914fd0bc6712e33c3f90511d42d29bf707b40bc55c8dc69
@@ -1,13 +1,17 @@
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.
10
+ # #start is called by Minitest when initializing the executor. This is where
11
+ # we override some Minitest internals to implement fork-based execution.
9
12
  def start
10
- # Store the reference to the original run_one_method singleton method.
13
+ # Store the reference to the original run_one_method method in order to
14
+ # use it to actually run the test case.
11
15
  original_run_one_method = Minitest.method(:run_one_method)
12
16
 
13
17
  # Remove the original singleton method from Minitest in order to avoid
@@ -18,31 +22,66 @@ module Minitest
18
22
 
19
23
  # Define a new version of run_one_method that forks, calls the original
20
24
  # run_one_method in the child process, and sends results back to the
21
- # parent.
25
+ # parent. klass and method_name are the two parameters accepted by the
26
+ # original run_one_method - they're the test class (e.g. UserTest) and
27
+ # the test method name (e.g. :test_email_must_be_unique).
22
28
  Minitest.define_singleton_method(:run_one_method) do |klass, method_name|
29
+ # Set up a binary pipe for transporting test results from the child
30
+ # to the parent process.
23
31
  read_io, write_io = IO.pipe
24
32
  read_io.binmode
25
33
  write_io.binmode
26
34
 
27
- if fork
28
- # Parent: load the result sent from the child
35
+ if Process.fork
36
+ # The parent process responsible for collecting results.
29
37
 
38
+ # The parent process doesn't write anything.
30
39
  write_io.close
40
+
41
+ # Load the result object passed by the child process.
31
42
  result = Marshal.load(read_io)
43
+
44
+ # Unwrap all failures from FailureTransport so that they can be
45
+ # safely presented to the user.
46
+ result.failures.map!(&:failure)
47
+
48
+ # We're done reading results from the child so it's safe to close the
49
+ # IO object now.
32
50
  read_io.close
33
51
 
52
+ # Wait for the child process to finish before returning the result.
34
53
  Process.wait
35
54
  else
36
- # Child: just run normally, dump the result, and exit the process to
37
- # avoid double-reporting.
55
+ # The child process responsible for running the test case.
56
+
57
+ # Run the test case method via the original .run_one_method.
38
58
  result = original_run_one_method.call(klass, method_name)
39
59
 
60
+ # Wrap failures in FailureTransport to avoid issue when marshalling.
61
+ # Some failures correspond to exceptions referencing unmarshallable
62
+ # objects. For example, a PostgreSQL exception may reference
63
+ # PG::Connection that cannot be marshalled. In those case, we replace
64
+ # the original error with UnmarshallableError retaining as much
65
+ # detail as possible.
66
+ result.failures.map! { |failure| FailureTransport.new(failure) }
67
+
68
+ # The child process doesn't read anything.
40
69
  read_io.close
70
+
71
+ # Dump the result object to the write IO object so that it can be
72
+ # read by the parent process.
41
73
  Marshal.dump(result, write_io)
74
+
75
+ # We're done sending results to the parent so it's safe to close the
76
+ # IO object now.
42
77
  write_io.close
78
+
79
+ # Exit the child process as its job is now done.
43
80
  exit
44
81
  end
45
82
 
83
+ # This value is returned ONLY in the parent process, not in the child
84
+ # process.
46
85
  result
47
86
  end
48
87
  end
@@ -51,5 +90,73 @@ module Minitest
51
90
  # Nothing to do here but required by Minitest. In a future version, we may
52
91
  # reinstate the original Minitest.run_one_method here.
53
92
  end
93
+
94
+ # A Minitest Failure transport class enabling passing non-marshallable
95
+ # objects (e.g. IO or sockets) via Marshal. The basic idea is replacing
96
+ # Minitest failures referencing unmarshallable objects with
97
+ # UnmarshallableError retaining as much detail as possible.
98
+ class FailureTransport
99
+ attr_reader :failure
100
+
101
+ def initialize(failure)
102
+ @failure = failure
103
+ end
104
+
105
+ def marshal_dump
106
+ Marshal.dump(failure)
107
+ rescue TypeError
108
+ # CAREFUL! WE'RE MODIFYING FAILURE IN PLACE UNDER THE ASSUMPTION THAT
109
+ # IT LIVES IN A MEMORY SPACE OF A SHORT-LIVED PROCESS, NAMELY THE CHILD
110
+ # PROCESS RESPONSIBLE FOR RUNNING A SINGLE TEST. IF THIS ASSUMPTION IS
111
+ # VIOLATED THEN AN ALTERNATIVE APPROACH (E.G. DUPLICATING THE FAILURE)
112
+ # MIGHT BE NECESSARY.
113
+
114
+ if failure.respond_to?(:exception) && failure.respond_to?(:exception=)
115
+ failure.exception = UnmarshallableError.new(failure.exception)
116
+ elsif failure.respond_to?(:error) && failure.respond_to?(:error=)
117
+ failure.error = UnmarshallableError.new(failure.error)
118
+ else
119
+ raise(<<ERROR)
120
+ Minitest failures should respond respond to exception/exception= (versions prior
121
+ to 5.14.0) or error/error= (version 5.14.0 and newer). The received failure does
122
+ responds to neither. Are you using an newer Minitest version?
123
+ ERROR
124
+ end
125
+
126
+ Marshal.dump(failure)
127
+ end
128
+
129
+ def marshal_load(dump)
130
+ @failure = Marshal.load(dump)
131
+ end
132
+ end
133
+
134
+ # An always marshallable exception class that can be derived from another
135
+ # (potentially non-marshallable exception). It's actually not intended to be
136
+ # raised but merely instantiated when passing Minitest failures from the
137
+ # runner to the reporter.
138
+ class UnmarshallableError < RuntimeError
139
+ def initialize(exc)
140
+ super(<<MESSAGE)
141
+ An unmarshallable error has occured. Below is its best-effort representation.
142
+ In order to receive the error itself, please disable Minitest::ForkExecutor.
143
+
144
+ Error class:
145
+ #{exc.class.name}
146
+
147
+ Error message:
148
+ #{exc.message}
149
+
150
+ Attributes:
151
+ #{exc.instance_variables.map do |name|
152
+ " #{name} = #{exc.instance_variable_get(name).inspect}"
153
+ end.join("\n")}
154
+
155
+ END OF ERROR MESSAGE (ORIGINAL BACKTRACE MAY FOLLOW)
156
+ MESSAGE
157
+
158
+ set_backtrace(exc.backtrace)
159
+ end
160
+ end
54
161
  end
55
162
  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.0.3
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
@@ -46,12 +46,13 @@ extensions: []
46
46
  extra_rdoc_files: []
47
47
  files:
48
48
  - lib/minitest/fork_executor.rb
49
- - test/minitest/fork_executor_test.rb
49
+ - test/acceptance/unmarshallable_test.rb
50
+ - test/unit/minitest/fork_executor_test.rb
50
51
  homepage: https://github.com/gregnavis/minitest-fork_executor
51
52
  licenses:
52
53
  - MIT
53
54
  metadata: {}
54
- post_install_message:
55
+ post_install_message:
55
56
  rdoc_options: []
56
57
  require_paths:
57
58
  - lib
@@ -66,9 +67,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
66
67
  - !ruby/object:Gem::Version
67
68
  version: '0'
68
69
  requirements: []
69
- rubygems_version: 3.1.4
70
- signing_key:
70
+ rubygems_version: 3.2.22
71
+ signing_key:
71
72
  specification_version: 4
72
73
  summary: Near-perfect process-level test case isolation.
73
74
  test_files:
74
- - test/minitest/fork_executor_test.rb
75
+ - test/acceptance/unmarshallable_test.rb
76
+ - test/unit/minitest/fork_executor_test.rb