Skip to content

Commit b7d2478

Browse files
committed
test: run tests in parallel, common improvements
* Allow running tests in mixed parallel/sequential modes * Add -J flag for running tests on all available CPUs * Support TEST_THREAD_ID in test/common.js and use it for tmpDir and PORT * make: use -J flag Reviewed-By: Ben Noordhuis <[email protected]> PR-URL: #172 Fix: #139
1 parent 0e19476 commit b7d2478

File tree

6 files changed

+73
-20
lines changed

6 files changed

+73
-20
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ tags
99
*.pyc
1010
doc/api.xml
1111
tmp/
12+
test/tmp*/
1213
node
1314
node_g
1415
*.swp

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ distclean:
9090
-rm -rf node_modules
9191

9292
test: all
93-
$(PYTHON) tools/test.py --mode=release message parallel sequential
93+
$(PYTHON) tools/test.py --mode=release message parallel sequential -J
9494
$(MAKE) jslint
9595
$(MAKE) cpplint
9696

test/common.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,18 @@ var os = require('os');
2727
exports.testDir = path.dirname(__filename);
2828
exports.fixturesDir = path.join(exports.testDir, 'fixtures');
2929
exports.libDir = path.join(exports.testDir, '../lib');
30-
exports.tmpDir = path.join(exports.testDir, 'tmp');
30+
exports.tmpDirName = 'tmp';
3131
exports.PORT = +process.env.NODE_COMMON_PORT || 12346;
3232

33+
if (process.env.TEST_THREAD_ID) {
34+
// Distribute ports in parallel tests
35+
if (!process.env.NODE_COMMON_PORT)
36+
exports.PORT += +process.env.TEST_THREAD_ID * 100;
37+
38+
exports.tmpDirName += '.' + process.env.TEST_THREAD_ID;
39+
}
40+
exports.tmpDir = path.join(exports.testDir, exports.tmpDirName);
41+
3342
exports.opensslCli = path.join(path.dirname(process.execPath), 'openssl-cli');
3443
if (process.platform === 'win32') {
3544
exports.PIPE = '\\\\.\\pipe\\libuv-test';

test/testpy/__init__.py

+21-6
Original file line numberDiff line numberDiff line change
@@ -49,30 +49,34 @@ def __init__(self, path, file, arch, mode, context, config, additional=[]):
4949
self.mode = mode
5050
self.tmpdir = join(dirname(self.config.root), 'tmp')
5151
self.additional_flags = additional
52+
53+
def GetTmpDir(self):
54+
return "%s.%d" % (self.tmpdir, self.thread_id)
55+
5256

5357
def AfterRun(self, result):
5458
# delete the whole tmp dir
5559
try:
56-
rmtree(self.tmpdir)
60+
rmtree(self.GetTmpDir())
5761
except:
5862
pass
5963
# make it again.
6064
try:
61-
mkdir(self.tmpdir)
65+
mkdir(self.GetTmpDir())
6266
except:
6367
pass
6468

6569
def BeforeRun(self):
6670
# delete the whole tmp dir
6771
try:
68-
rmtree(self.tmpdir)
72+
rmtree(self.GetTmpDir())
6973
except:
7074
pass
7175
# make it again.
7276
# intermittently fails on win32, so keep trying
73-
while not os.path.exists(self.tmpdir):
77+
while not os.path.exists(self.GetTmpDir()):
7478
try:
75-
mkdir(self.tmpdir)
79+
mkdir(self.GetTmpDir())
7680
except:
7781
pass
7882

@@ -105,7 +109,6 @@ def GetCommand(self):
105109
def GetSource(self):
106110
return open(self.file).read()
107111

108-
109112
class SimpleTestConfiguration(test.TestConfiguration):
110113

111114
def __init__(self, context, root, section, additional=[]):
@@ -136,6 +139,18 @@ def GetTestStatus(self, sections, defs):
136139
if exists(status_file):
137140
test.ReadConfigurationInto(status_file, sections, defs)
138141

142+
class ParallelTestConfiguration(SimpleTestConfiguration):
143+
def __init__(self, context, root, section, additional=[]):
144+
super(ParallelTestConfiguration, self).__init__(context, root, section,
145+
additional)
146+
147+
def ListTests(self, current_path, path, arch, mode):
148+
result = super(ParallelTestConfiguration, self).ListTests(
149+
current_path, path, arch, mode)
150+
for test in result:
151+
test.parallel = True
152+
return result
153+
139154
class AddonTestConfiguration(SimpleTestConfiguration):
140155
def __init__(self, context, root, section, additional=[]):
141156
super(AddonTestConfiguration, self).__init__(context, root, section)

tools/test.py

+39-11
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import time
4141
import threading
4242
import utils
43+
import multiprocessing
4344

4445
from os.path import join, dirname, abspath, basename, isdir, exists
4546
from datetime import datetime
@@ -58,9 +59,13 @@ class ProgressIndicator(object):
5859
def __init__(self, cases, flaky_tests_mode):
5960
self.cases = cases
6061
self.flaky_tests_mode = flaky_tests_mode
61-
self.queue = Queue(len(cases))
62+
self.parallel_queue = Queue(len(cases))
63+
self.sequential_queue = Queue(len(cases))
6264
for case in cases:
63-
self.queue.put_nowait(case)
65+
if case.parallel:
66+
self.parallel_queue.put_nowait(case)
67+
else:
68+
self.sequential_queue.put_nowait(case)
6469
self.succeeded = 0
6570
self.remaining = len(cases)
6671
self.total = len(cases)
@@ -87,11 +92,11 @@ def Run(self, tasks):
8792
# That way -j1 avoids threading altogether which is a nice fallback
8893
# in case of threading problems.
8994
for i in xrange(tasks - 1):
90-
thread = threading.Thread(target=self.RunSingle, args=[])
95+
thread = threading.Thread(target=self.RunSingle, args=[True, i + 1])
9196
threads.append(thread)
9297
thread.start()
9398
try:
94-
self.RunSingle()
99+
self.RunSingle(False, 0)
95100
# Wait for the remaining threads
96101
for thread in threads:
97102
# Use a timeout so that signals (ctrl-c) will be processed.
@@ -105,13 +110,19 @@ def Run(self, tasks):
105110
self.Done()
106111
return not self.failed
107112

108-
def RunSingle(self):
113+
def RunSingle(self, parallel, thread_id):
109114
while not self.terminate:
110115
try:
111-
test = self.queue.get_nowait()
116+
test = self.parallel_queue.get_nowait()
112117
except Empty:
113-
return
118+
if parallel:
119+
return
120+
try:
121+
test = self.sequential_queue.get_nowait()
122+
except Empty:
123+
return
114124
case = test.case
125+
case.thread_id = thread_id
115126
self.lock.acquire()
116127
self.AboutToRun(case)
117128
self.lock.release()
@@ -381,6 +392,8 @@ def __init__(self, context, path, arch, mode):
381392
self.duration = None
382393
self.arch = arch
383394
self.mode = mode
395+
self.parallel = False
396+
self.thread_id = 0
384397

385398
def IsNegative(self):
386399
return False
@@ -399,11 +412,12 @@ def IsFailureOutput(self, output):
399412
def GetSource(self):
400413
return "(no source available)"
401414

402-
def RunCommand(self, command):
415+
def RunCommand(self, command, env):
403416
full_command = self.context.processor(command)
404417
output = Execute(full_command,
405418
self.context,
406-
self.context.GetTimeout(self.mode))
419+
self.context.GetTimeout(self.mode),
420+
env)
407421
self.Cleanup()
408422
return TestOutput(self,
409423
full_command,
@@ -420,7 +434,9 @@ def Run(self):
420434
self.BeforeRun()
421435

422436
try:
423-
result = self.RunCommand(self.GetCommand())
437+
result = self.RunCommand(self.GetCommand(), {
438+
"TEST_THREAD_ID": "%d" % self.thread_id
439+
})
424440
finally:
425441
# Tests can leave the tty in non-blocking mode. If the test runner
426442
# tries to print to stdout/stderr after that and the tty buffer is
@@ -559,15 +575,22 @@ def CheckedUnlink(name):
559575
PrintError("os.unlink() " + str(e))
560576

561577

562-
def Execute(args, context, timeout=None):
578+
def Execute(args, context, timeout=None, env={}):
563579
(fd_out, outname) = tempfile.mkstemp()
564580
(fd_err, errname) = tempfile.mkstemp()
581+
582+
# Extend environment
583+
env_copy = os.environ.copy()
584+
for key, value in env.iteritems():
585+
env_copy[key] = value
586+
565587
(process, exit_code, timed_out) = RunProcess(
566588
context,
567589
timeout,
568590
args = args,
569591
stdout = fd_out,
570592
stderr = fd_err,
593+
env = env_copy
571594
)
572595
os.close(fd_out)
573596
os.close(fd_err)
@@ -1068,6 +1091,7 @@ class ClassifiedTest(object):
10681091
def __init__(self, case, outcomes):
10691092
self.case = case
10701093
self.outcomes = outcomes
1094+
self.parallel = self.case.parallel
10711095

10721096

10731097
class Configuration(object):
@@ -1224,6 +1248,8 @@ def BuildOptions():
12241248
default=False, action="store_true")
12251249
result.add_option("-j", help="The number of parallel tasks to run",
12261250
default=1, type="int")
1251+
result.add_option("-J", help="Run tasks in parallel on all cores",
1252+
default=False, action="store_true")
12271253
result.add_option("--time", help="Print timing information after running",
12281254
default=False, action="store_true")
12291255
result.add_option("--suppress-dialogs", help="Suppress Windows dialogs for crashing tests",
@@ -1245,6 +1271,8 @@ def ProcessOptions(options):
12451271
VERBOSE = options.verbose
12461272
options.arch = options.arch.split(',')
12471273
options.mode = options.mode.split(',')
1274+
if options.J:
1275+
options.j = multiprocessing.cpu_count()
12481276
def CheckTestMode(name, option):
12491277
if not option in ["run", "skip", "dontcare"]:
12501278
print "Unknown %s mode %s" % (name, option)

vcbuild.bat

+1-1
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ if "%test%"=="" goto exit
163163
if "%config%"=="Debug" set test_args=--mode=debug
164164
if "%config%"=="Release" set test_args=--mode=release
165165

166-
if "%test%"=="test" set test_args=%test_args% sequential parallel message
166+
if "%test%"=="test" set test_args=%test_args% sequential parallel message -J
167167
if "%test%"=="test-internet" set test_args=%test_args% internet
168168
if "%test%"=="test-pummel" set test_args=%test_args% pummel
169169
if "%test%"=="test-simple" set test_args=%test_args% sequential parallel

0 commit comments

Comments
 (0)