33import io
44import os
55import pickle
6+ import struct
67import subprocess
78import sys
89import unittest
@@ -83,16 +84,27 @@ def have_python_version(py_version):
8384 return py_executable_map .get (py_version , None )
8485
8586
86- @support .requires_resource ('cpu' )
87+ def read_exact (f , n ):
88+ buf = b''
89+ while len (buf ) < n :
90+ chunk = f .read (n - len (buf ))
91+ if not chunk :
92+ raise EOFError
93+ buf += chunk
94+ return buf
95+
96+
8797class AbstractCompatTests (pickletester .AbstractPickleTests ):
8898 py_version = None
99+ worker = None
89100
90101 @classmethod
91102 def setUpClass (cls ):
92103 assert cls .py_version is not None , 'Needs a python version tuple'
93104 if not have_python_version (cls .py_version ):
94105 py_version_str = "." .join (map (str , cls .py_version ))
95106 raise unittest .SkipTest (f'Python { py_version_str } not available' )
107+ cls .addClassCleanup (cls .finish_worker )
96108 # Override the default pickle protocol to match what xpickle worker
97109 # will be running.
98110 highest_protocol = highest_proto_for_py_version (cls .py_version )
@@ -101,8 +113,32 @@ def setUpClass(cls):
101113 cls .enterClassContext (support .swap_attr (pickle , 'HIGHEST_PROTOCOL' ,
102114 highest_protocol ))
103115
104- @staticmethod
105- def send_to_worker (python , data ):
116+ @classmethod
117+ def start_worker (cls , python ):
118+ target = os .path .join (os .path .dirname (__file__ ), 'xpickle_worker.py' )
119+ worker = subprocess .Popen ([* python , target ],
120+ stdin = subprocess .PIPE ,
121+ stdout = subprocess .PIPE ,
122+ stderr = subprocess .PIPE ,
123+ # For windows bpo-17023.
124+ shell = is_windows )
125+ cls .worker = worker
126+ return worker
127+
128+ @classmethod
129+ def finish_worker (cls ):
130+ worker = cls .worker
131+ if worker is None :
132+ return
133+ cls .worker = None
134+ worker .stdin .close ()
135+ worker .stdout .close ()
136+ worker .stderr .close ()
137+ worker .terminate ()
138+ worker .wait ()
139+
140+ @classmethod
141+ def send_to_worker (cls , python , data ):
106142 """Bounce a pickled object through another version of Python.
107143 This will send data to a child process where it will
108144 be unpickled, then repickled and sent back to the parent process.
@@ -112,33 +148,33 @@ def send_to_worker(python, data):
112148 Returns:
113149 The pickled data received from the child process.
114150 """
115- target = os .path .join (os .path .dirname (__file__ ), 'xpickle_worker.py' )
116- worker = subprocess .Popen ([* python , target ],
117- stdin = subprocess .PIPE ,
118- stdout = subprocess .PIPE ,
119- stderr = subprocess .PIPE ,
120- # For windows bpo-17023.
121- shell = is_windows )
122- stdout , stderr = worker .communicate (data )
123- if worker .returncode == 0 :
124- return stdout
125- # if the worker fails, it will write the exception to stdout
151+ worker = cls .worker
152+ if worker is None :
153+ worker = cls .start_worker (python )
154+
126155 try :
127- exception = pickle .loads (stdout )
128- except (pickle .UnpicklingError , EOFError ):
156+ worker .stdin .write (struct .pack ('!i' , len (data )) + data )
157+ worker .stdin .flush ()
158+
159+ size , = struct .unpack ('!i' , read_exact (worker .stdout , 4 ))
160+ if size > 0 :
161+ return read_exact (worker .stdout , size )
162+ # if the worker fails, it will write the exception to stdout
163+ if size < 0 :
164+ stdout = read_exact (worker .stdout , - size )
165+ try :
166+ exception = pickle .loads (stdout )
167+ except (pickle .UnpicklingError , EOFError ):
168+ pass
169+ else :
170+ if isinstance (exception , Exception ):
171+ # To allow for tests which test for errors.
172+ raise exception
173+ _ , stderr = worker .communicate ()
129174 raise RuntimeError (stderr )
130- else :
131- if support .verbose > 1 :
132- print ()
133- print (f'{ data = } ' )
134- print (f'{ stdout = } ' )
135- print (f'{ stderr = } ' )
136- if isinstance (exception , Exception ):
137- # To allow for tests which test for errors.
138- raise exception
139- else :
140- raise RuntimeError (stderr )
141-
175+ except :
176+ cls .finish_worker ()
177+ raise
142178
143179 def dumps (self , arg , proto = 0 , ** kwargs ):
144180 # Skip tests that require buffer_callback arguments since
@@ -148,9 +184,8 @@ def dumps(self, arg, proto=0, **kwargs):
148184 self .skipTest ('Test does not support "buffer_callback" argument.' )
149185 f = io .BytesIO ()
150186 p = self .pickler (f , proto , ** kwargs )
151- p .dump ((proto , arg ))
152- f .seek (0 )
153- data = bytes (f .read ())
187+ p .dump (arg )
188+ data = struct .pack ('!i' , proto ) + f .getvalue ()
154189 python = py_executable_map [self .py_version ]
155190 return self .send_to_worker (python , data )
156191
0 commit comments