1+ import fcntl
2+ import os
3+ import select
14import subprocess
25import 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