Skip to content

Commit 7bcdd55

Browse files
committed
first_stage_test: Add more tests test_stdin_non_blocking and test_stdin_blocking
While at it, refactor the code, convert the inline comments to docstrings. Those docstrings are later reused as the test description by the test runner. Signed-off-by: Marc Hartmayer <[email protected]>
1 parent 9c708aa commit 7bcdd55

File tree

1 file changed

+207
-61
lines changed

1 file changed

+207
-61
lines changed

tests/first_stage_test.py

Lines changed: 207 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import fcntl
2+
import os
3+
import select
14
import subprocess
25
import sys
36

@@ -17,27 +20,36 @@ class CommandLineTest(testlib.RouterMixin, testlib.TestCase):
1720
# * 2.7 starting 3.x
1821
# * 3.x starting 2.7
1922

20-
def test_valid_syntax(self):
23+
def setUp(self):
24+
super(CommandLineTest, self).setUp()
2125
options = mitogen.parent.Options(max_message_size=123)
2226
conn = mitogen.parent.Connection(options, self.router)
2327
conn.context = mitogen.core.Context(None, 123)
24-
args = conn.get_boot_command()
28+
self.args = conn.get_boot_command()
29+
self.preamble = conn.get_preamble()
30+
self.conn = conn
31+
32+
def test_valid_syntax(self):
33+
"""Test valid syntax
2534
26-
# The boot command should write an ECO marker to stdout, read the
27-
# preamble from stdin, then execute it.
35+
The boot command should write an ECO marker to stdout, read the
36+
preamble from stdin, then execute it.
2837
29-
# This test attaches /dev/zero to stdin to create a specific failure
30-
# 1. Fork child reads <compressed preamble size> bytes of NUL (`b'\0'`)
31-
# 2. Fork child crashes (trying to decompress the junk data)
32-
# 3. Fork child's file descriptors (write pipes) are closed by the OS
33-
# 4. Fork parent does `dup(<read pipe>, <stdin>)` and `exec(<python>)`
34-
# 5. Python reads `b''` (i.e. EOF) from stdin (a closed pipe)
35-
# 6. Python runs `''` (a valid script) and exits with success
38+
This test attaches /dev/zero to stdin to create a specific failure
39+
40+
1. Fork child reads <compressed preamble size> bytes of NUL (`b'\0'`)
41+
2. Fork child crashes (trying to decompress the junk data)
42+
3. Fork child's file descriptors (write pipes) are closed by the OS
43+
4. Fork parent does `dup(<read pipe>, <stdin>)` and `exec(<python>)`
44+
5. Python reads `b''` (i.e. EOF) from stdin (a closed pipe)
45+
6. Python runs `''` (a valid script) and exits with success
46+
47+
"""
3648

3749
fp = open("/dev/zero", "r")
3850
try:
3951
proc = subprocess.Popen(
40-
args,
52+
self.args,
4153
stdin=fp,
4254
stdout=subprocess.PIPE,
4355
stderr=subprocess.PIPE,
@@ -55,32 +67,29 @@ def test_valid_syntax(self):
5567
fp.close()
5668

5769
def test_stage(self):
58-
options = mitogen.parent.Options(max_message_size=123)
59-
conn = mitogen.parent.Connection(options, self.router)
60-
conn.context = mitogen.core.Context(None, 123)
61-
args = conn.get_boot_command()
62-
preamble = conn.get_preamble()
70+
"""Test that first stage works
71+
72+
The boot command should read the preamble from STDIN, write all ECO
73+
markers to STDOUT, and then execute the preamble.
6374
64-
# The boot command should write all ECO markers to stdout, read the
65-
# preamble from stdin, then execute it.
75+
This test writes the complete preamble to STDIN.
6676
67-
# This test writes the preamble to STDIN and closes it then to create an
68-
# EOF situation.
69-
# 1. Fork child tries to read from STDIN, but stops as EOF is received.
70-
# 2. Fork child writes all ECO markers to stdout
71-
# TBD
77+
1. Fork child reads from STDIN
78+
2. Fork child writes all ECO markers to stdout as expected.
79+
80+
"""
7281

7382
proc = subprocess.Popen(
74-
args=args,
83+
args=self.args,
7584
stdout=subprocess.PIPE,
7685
stderr=subprocess.PIPE,
7786
stdin=subprocess.PIPE,
7887
)
7988
try:
8089
try:
81-
stdout, stderr = proc.communicate(input=preamble, timeout=10)
90+
stdout, stderr = proc.communicate(input=self.preamble, timeout=10)
8291
except TypeError:
83-
stdout, stderr = proc.communicate(input=preamble)
92+
stdout, stderr = proc.communicate(input=self.preamble)
8493
except:
8594
proc.kill()
8695
self.fail("First stage did not finish")
@@ -101,34 +110,172 @@ def test_stage(self):
101110
stderr,
102111
)
103112

104-
def test_eof_too_early(self):
105-
options = mitogen.parent.Options(max_message_size=123)
106-
conn = mitogen.parent.Connection(options, self.router)
107-
conn.context = mitogen.core.Context(None, 123)
108-
args = conn.get_boot_command()
109-
preamble = conn.get_preamble()
113+
def test_stdin_non_blocking(self):
114+
"""Test that first stage works with non-blocking STDIN
115+
116+
The boot command should read the preamble from STDIN, write all ECO
117+
markers to STDOUT, and then execute the preamble.
118+
119+
This test writes the complete preamble to non-blocking STDIN.
110120
111-
# The boot command should write an ECO marker to stdout, read the
112-
# preamble from stdin, then execute it.
121+
1. Fork child reads from non-blocking STDIN
122+
2. Fork child writes all ECO markers to stdout as expected.
113123
114-
# This test writes some data to STDIN and closes it then to create an
115-
# EOF situation.
116-
# 1. Fork child tries to read from STDIN, but stops as EOF is received.
117-
# 2. Fork child crashes (trying to decompress the junk data)
118-
# 3. Fork child's file descriptors (write pipes) are closed by the OS
119-
# 4. Fork parent does `dup(<read pipe>, <stdin>)` and `exec(<python>)`
120-
# 5. Python reads `b''` (i.e. EOF) from stdin (a closed pipe)
121-
# 6. Python runs `''` (a valid script) and exits with success
124+
"""
125+
126+
PIPE_SIZE = 4096
127+
# Make sure that not all data can be written in one write operation.
128+
CHUNK_SIZE = 2 * PIPE_SIZE
129+
r, w = mitogen.core.pipe(blocking=False)
130+
with w:
131+
try:
132+
fcntl.fcntl(w.fileno(), fcntl.F_SETPIPE_SZ, PIPE_SIZE)
133+
except AttributeError:
134+
pass
135+
else:
136+
self.assertEqual(fcntl.fcntl(w.fileno(), fcntl.F_GETPIPE_SZ), PIPE_SIZE)
137+
138+
proc = subprocess.Popen(
139+
args=self.args,
140+
stdout=subprocess.PIPE,
141+
stderr=subprocess.PIPE,
142+
stdin=r,
143+
close_fds=True,
144+
)
145+
# Close the read fd in the parent
146+
r.close()
147+
148+
view = mitogen.core.BufferType(self.preamble, 0)
149+
offset = 0
150+
while offset < len(view):
151+
end = min(offset + CHUNK_SIZE, len(view))
152+
_, w_fds, _ = select.select([], [w.fileno()], [], 10)
153+
self.assertIn(w.fileno(), w_fds)
154+
written = os.write(w.fileno(), view[offset:end])
155+
self.assertGreater(written, 0)
156+
offset += written
157+
158+
try:
159+
try:
160+
returncode = proc.wait(timeout=10)
161+
except TypeError:
162+
returncode = proc.wait()
163+
except:
164+
proc.kill()
165+
self.fail("First stage did not finish")
166+
else:
167+
try:
168+
# proc was killed by SIGTERM?!? TODO Where does this come from?
169+
self.assertEqual(-15, proc.returncode)
170+
self.assertEqual(
171+
proc.stdout.read(),
172+
mitogen.parent.BootstrapProtocol.EC0_MARKER
173+
+ b("\n")
174+
+ mitogen.parent.BootstrapProtocol.EC1_MARKER
175+
+ b("\n")
176+
+ mitogen.parent.BootstrapProtocol.EC2_MARKER
177+
+ b("\n"),
178+
)
179+
self.assertEqual(
180+
b(""),
181+
proc.stderr.read(),
182+
)
183+
finally:
184+
proc.stdout.close()
185+
proc.stderr.close()
186+
187+
def test_stdin_blocking(self):
188+
"""Test that first stage works with blocking STDIN
189+
190+
The boot command should read the preamble from STDIN, write all ECO
191+
markers to STDOUT, and then execute the preamble.
192+
193+
This test writes the complete preamble to blocking STDIN.
194+
195+
1. Fork child reads from blocking STDIN
196+
2. Fork child writes all ECO markers to stdout as expected.
197+
198+
"""
199+
PIPE_SIZE = 4096
200+
# Make sure that not all data can be written in one write operation.
201+
CHUNK_SIZE = 2 * PIPE_SIZE
202+
r, w = mitogen.core.pipe(blocking=True)
203+
with w:
204+
try:
205+
fcntl.fcntl(w.fileno(), fcntl.F_SETPIPE_SZ, PIPE_SIZE)
206+
except AttributeError:
207+
pass
208+
else:
209+
self.assertEqual(fcntl.fcntl(w.fileno(), fcntl.F_GETPIPE_SZ), PIPE_SIZE)
210+
proc = subprocess.Popen(
211+
args=self.args,
212+
stdout=subprocess.PIPE,
213+
stderr=subprocess.PIPE,
214+
stdin=r,
215+
close_fds=True,
216+
)
217+
# Close the read fd in the parent
218+
r.close()
219+
220+
view = mitogen.core.BufferType(self.preamble, 0)
221+
offset = 0
222+
while offset < len(view):
223+
end = min(offset + CHUNK_SIZE, len(view))
224+
written = os.write(w.fileno(), view[offset:end])
225+
self.assertGreater(written, 0)
226+
offset += written
227+
228+
try:
229+
try:
230+
returncode = proc.wait(timeout=10)
231+
except TypeError:
232+
returncode = proc.wait()
233+
except:
234+
proc.kill()
235+
self.fail("First stage did not finish")
236+
else:
237+
try:
238+
# proc was killed by SIGTERM?!? TODO Where does this come from?
239+
self.assertEqual(-15, proc.returncode)
240+
self.assertEqual(
241+
proc.stdout.read(),
242+
mitogen.parent.BootstrapProtocol.EC0_MARKER
243+
+ b("\n")
244+
+ mitogen.parent.BootstrapProtocol.EC1_MARKER
245+
+ b("\n")
246+
+ mitogen.parent.BootstrapProtocol.EC2_MARKER
247+
+ b("\n"),
248+
)
249+
self.assertEqual(
250+
b(""),
251+
proc.stderr.read(),
252+
)
253+
finally:
254+
proc.stdout.close()
255+
proc.stderr.close()
256+
257+
def test_eof_too_early(self):
258+
"""The boot command should write an ECO marker to stdout, read the
259+
preamble from stdin, then execute it.
260+
261+
This test writes some data to STDIN and closes it then to create an
262+
EOF situation.
263+
1. Fork child tries to read from STDIN, but stops as EOF is received.
264+
2. Fork child crashes (trying to decompress the junk data)
265+
3. Fork child's file descriptors (write pipes) are closed by the OS
266+
4. Fork parent does `dup(<read pipe>, <stdin>)` and `exec(<python>)`
267+
5. Python reads `b''` (i.e. EOF) from stdin (a closed pipe)
268+
6. Python runs `''` (a valid script) and exits with success"""
122269

123270
proc = subprocess.Popen(
124-
args=args,
271+
args=self.args,
125272
stdout=subprocess.PIPE,
126273
stderr=subprocess.PIPE,
127274
stdin=subprocess.PIPE,
128275
)
129276

130277
# Do not send all of the data from the preamble
131-
proc.stdin.write(preamble[:-128])
278+
proc.stdin.write(self.preamble[:-128])
132279
proc.stdin.flush()
133280
proc.stdin.close()
134281
try:
@@ -155,23 +302,22 @@ def test_eof_too_early(self):
155302
proc.stderr.close()
156303

157304
def test_timeout_error(self):
158-
options = mitogen.parent.Options(max_message_size=123)
159-
conn = mitogen.parent.Connection(options, self.router)
160-
conn.context = mitogen.core.Context(None, 123)
305+
"""
306+
The boot command should write an ECO marker to stdout, read the
307+
preamble from stdin, then execute it.
308+
309+
This test attaches closes stdin to create a specific failure
310+
1. Fork child tries to read from STDIN, but fails as it is closed
311+
2. Fork child raises TimeoutError
312+
3. Fork child's file descriptors (write pipes) are closed by the OS
313+
4. Fork parent does `dup(<read pipe>, <stdin>)` and `exec(<python>)`
314+
5. Python reads `b''` (i.e. EOF) from stdin (a closed pipe)
315+
6. Python runs `''` (a valid script) and exits with success
316+
"""
317+
161318
# We do not want to wait the default of 10s, change it to 0.3s
162-
conn._first_stage_timeout = 0.3
163-
args = conn.get_boot_command()
164-
165-
# The boot command should write an ECO marker to stdout, read the
166-
# preamble from stdin, then execute it.
167-
168-
# This test attaches closes stdin to create a specific failure
169-
# 1. Fork child tries to read from STDIN, but fails as it is closed
170-
# 2. Fork child raises TimeoutError
171-
# 3. Fork child's file descriptors (write pipes) are closed by the OS
172-
# 4. Fork parent does `dup(<read pipe>, <stdin>)` and `exec(<python>)`
173-
# 5. Python reads `b''` (i.e. EOF) from stdin (a closed pipe)
174-
# 6. Python runs `''` (a valid script) and exits with success
319+
self.conn._first_stage_timeout = 0.3
320+
args = self.conn.get_boot_command()
175321

176322
proc = subprocess.Popen(
177323
args=args,

0 commit comments

Comments
 (0)