1414from ... import config
1515from ... import core
1616from ... import utils
17- from ...utils import registry
1817
1918
20- class _SingleJSTestCase (interface .TestCase ):
19+ class JSTestCase (interface .TestCase ):
2120 """
2221 A jstest to execute.
2322 """
2423
25- REGISTERED_NAME = registry .LEAVE_UNREGISTERED
24+ REGISTERED_NAME = "js_test"
25+
26+ # A wrapper for the thread class that lets us propagate exceptions.
27+ class ExceptionThread (threading .Thread ):
28+ def __init__ (self , my_target , my_args ):
29+ threading .Thread .__init__ (self , target = my_target , args = my_args )
30+ self .err = None
31+
32+ def run (self ):
33+ try :
34+ threading .Thread .run (self )
35+ except :
36+ self .err = sys .exc_info ()[1 ]
37+ else :
38+ self .err = None
39+
40+ def _get_exception (self ):
41+ return self .err
42+
43+ DEFAULT_CLIENT_NUM = 1
2644
2745 def __init__ (self ,
2846 logger ,
2947 js_filename ,
3048 shell_executable = None ,
31- shell_options = None ):
32- """
33- Initializes the _SingleJSTestCase with the JS file to run.
34- """
49+ shell_options = None ,
50+ test_kind = "JSTest" ):
51+ """Initializes the JSTestCase with the JS file to run."""
3552
36- interface .TestCase .__init__ (self , logger , "JSTest" , js_filename )
53+ interface .TestCase .__init__ (self , logger , test_kind , js_filename )
3754
3855 # Command line options override the YAML configuration.
3956 self .shell_executable = utils .default_if_none (config .MONGO_EXECUTABLE , shell_executable )
4057
4158 self .js_filename = js_filename
4259 self .shell_options = utils .default_if_none (shell_options , {}).copy ()
60+ self .num_clients = JSTestCase .DEFAULT_CLIENT_NUM
4361
44- def configure (self , fixture , * args , ** kwargs ):
62+ def configure (self , fixture , num_clients = DEFAULT_CLIENT_NUM , * args , ** kwargs ):
4563 interface .TestCase .configure (self , fixture , * args , ** kwargs )
4664
47- def configure_shell (self ):
48- """
49- Sets up the global variables for the shell, and data/ directory for the mongod.
50-
51- configure_shell() only needs to be called once per test. Therefore if creating multiple
52- _SingleJSTestCase instances to be run in parallel, only call configure_shell() on one of
53- them.
54- """
5565 global_vars = self .shell_options .get ("global_vars" , {}).copy ()
5666 data_dir = self ._get_data_dir (global_vars )
5767
@@ -76,14 +86,16 @@ def configure_shell(self):
7686 global_vars ["MongoRunner.mongoShellPath" ] = self .shell_executable
7787
7888 test_data = global_vars .get ("TestData" , {}).copy ()
79- test_data ["minPort" ] = core .network .PortAllocator .min_test_port (self . fixture .job_num )
80- test_data ["maxPort" ] = core .network .PortAllocator .max_test_port (self . fixture .job_num )
89+ test_data ["minPort" ] = core .network .PortAllocator .min_test_port (fixture .job_num )
90+ test_data ["maxPort" ] = core .network .PortAllocator .max_test_port (fixture .job_num )
8191
8292 global_vars ["TestData" ] = test_data
8393 self .shell_options ["global_vars" ] = global_vars
8494
8595 shutil .rmtree (data_dir , ignore_errors = True )
8696
97+ self .num_clients = num_clients
98+
8799 try :
88100 os .makedirs (data_dir )
89101 except os .error :
@@ -118,78 +130,34 @@ def _get_data_dir(self, global_vars):
118130 config .MONGO_RUNNER_SUBDIR )
119131
120132 def run_test (self ):
133+ threads = []
121134 try :
122- shell = self ._make_process ()
123- self ._execute (shell )
135+ # Don't thread if there is only one client.
136+ if self .num_clients == 1 :
137+ shell = self ._make_process (self .logger )
138+ self ._execute (shell )
139+ else :
140+ # If there are multiple clients, make a new thread for each client.
141+ for i in xrange (self .num_clients ):
142+ t = self .ExceptionThread (my_target = self ._run_test_in_thread , my_args = [i ])
143+ t .start ()
144+ threads .append (t )
124145 except self .failureException :
125146 raise
126147 except :
127148 self .logger .exception ("Encountered an error running jstest %s." , self .basename ())
128149 raise
129-
130- def _make_process (self ):
131- return core .programs .mongo_shell_program (
132- self .logger ,
133- executable = self .shell_executable ,
134- filename = self .js_filename ,
135- connection_string = self .fixture .get_driver_connection_url (),
136- ** self .shell_options )
137-
138-
139- class JSTestCase (interface .TestCase ):
140- """
141- A wrapper for several copies of a SingleJSTest to execute.
142- """
143-
144- REGISTERED_NAME = "js_test"
145-
146- class ThreadWithException (threading .Thread ):
147- """
148- A wrapper for the thread class that lets us propagate exceptions.
149- """
150-
151- def __init__ (self , * args , ** kwargs ):
152- threading .Thread .__init__ (self , * args , ** kwargs )
153- self .exc_info = None
154-
155- def run (self ):
156- try :
157- threading .Thread .run (self )
158- except :
159- self .exc_info = sys .exc_info ()
160-
161- def __init__ (self ,
162- logger ,
163- js_filename ,
164- shell_executable = None ,
165- shell_options = None ,
166- test_kind = "JSTest" ):
167- """
168- Initializes the JSTestCase with the JS file to run.
169- """
170-
171- interface .TestCase .__init__ (self , logger , test_kind , js_filename )
172-
173- self .num_clients = config .NUM_CLIENTS_PER_FIXTURE
174- self .test_case_template = _SingleJSTestCase (logger , js_filename , shell_executable ,
175- shell_options )
176-
177- def configure (self , fixture , * args , ** kwargs ):
178- interface .TestCase .configure (self , fixture , * args , ** kwargs )
179- self .test_case_template .configure (fixture , * args , ** kwargs )
180- self .test_case_template .configure_shell ()
181-
182- def _make_process (self ):
183- # This function should only be called by interface.py's as_command().
184- return self .test_case_template ._make_process ()
185-
186- def _get_shell_options_for_thread (self , thread_id ):
187- """
188- Get shell_options with an initialized TestData object for given thread.
189- """
190-
191- # We give each _SingleJSTestCase its own copy of the shell_options.
192- shell_options = self .test_case_template .shell_options .copy ()
150+ finally :
151+ for t in threads :
152+ t .join ()
153+ for t in threads :
154+ if t ._get_exception () is not None :
155+ raise t ._get_exception ()
156+
157+ def _make_process (self , logger = None , thread_id = 0 ):
158+ # Since _make_process() is called by each thread, we make a shallow copy of the mongo shell
159+ # options to avoid modifying the shared options for the JSTestCase.
160+ shell_options = self .shell_options .copy ()
193161 global_vars = shell_options ["global_vars" ].copy ()
194162 test_data = global_vars ["TestData" ].copy ()
195163
@@ -204,70 +172,20 @@ def _get_shell_options_for_thread(self, thread_id):
204172 global_vars ["TestData" ] = test_data
205173 shell_options ["global_vars" ] = global_vars
206174
207- return shell_options
175+ # If logger is none, it means that it's not running in a thread and thus logger should be
176+ # set to self.logger.
177+ logger = utils .default_if_none (logger , self .logger )
208178
209- def _create_test_case_for_thread (self , logger , thread_id ):
210- """
211- Create and configure a _SingleJSTestCase to be run in a separate thread.
212- """
213-
214- shell_options = self ._get_shell_options_for_thread (thread_id )
215- test_case = _SingleJSTestCase (logger ,
216- self .test_case_template .js_filename ,
217- self .test_case_template .shell_executable ,
218- shell_options )
219-
220- test_case .configure (self .fixture )
221- return test_case
222-
223- def _run_single_copy (self ):
224- test_case = self ._create_test_case_for_thread (self .logger , thread_id = 0 )
225- try :
226- test_case .run_test ()
227- # If there was an exception, it will be logged in test_case's run_test function.
228- finally :
229- self .return_code = test_case .return_code
230-
231- def _run_multiple_copies (self ):
232- threads = []
233- test_cases = []
234- try :
235- # If there are multiple clients, make a new thread for each client.
236- for thread_id in xrange (self .num_clients ):
237- logger = self .logger .new_test_thread_logger (self .test_kind , str (thread_id ))
238- test_case = self ._create_test_case_for_thread (logger , thread_id )
239- test_cases .append (test_case )
240-
241- thread = self .ThreadWithException (target = test_case .run_test )
242- threads .append (thread )
243- thread .start ()
244- except :
245- self .logger .exception ("Encountered an error starting threads for jstest %s." ,
246- self .basename ())
247- raise
248- finally :
249- for thread in threads :
250- thread .join ()
251-
252- # Go through each test's return code and store the first nonzero one if it exists.
253- return_code = 0
254- for test_case in test_cases :
255- if test_case .return_code != 0 :
256- return_code = test_case .return_code
257- break
258- self .return_code = return_code
259-
260- for (thread_id , thread ) in enumerate (threads ):
261- if thread .exc_info is not None :
262- if not isinstance (thread .exc_info [1 ], self .failureException ):
263- self .logger .error (
264- "Encountered an error inside thread %d running jstest %s." ,
265- thread_id , self .basename (),
266- exc_info = thread .exc_info )
267- raise thread .exc_info
268-
269- def run_test (self ):
270- if self .num_clients == 1 :
271- self ._run_single_copy ()
272- else :
273- self ._run_multiple_copies ()
179+ return core .programs .mongo_shell_program (
180+ logger ,
181+ executable = self .shell_executable ,
182+ filename = self .js_filename ,
183+ connection_string = self .fixture .get_driver_connection_url (),
184+ ** shell_options )
185+
186+ def _run_test_in_thread (self , thread_id ):
187+ # Make a logger for each thread. When this method gets called self.logger has been
188+ # overridden with a TestLogger instance by the TestReport in the startTest() method.
189+ logger = self .logger .new_test_thread_logger (self .test_kind , str (thread_id ))
190+ shell = self ._make_process (logger , thread_id )
191+ self ._execute (shell )
0 commit comments