minitest-fork_executor 1.0.2 → 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
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