Browse Source

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 <info@bnoordhuis.nl>
PR-URL: https://github.com/iojs/io.js/pull/172
Fix: iojs/io.js#139
archived-io.js-v0.12
Fedor Indutny 10 years ago
parent
commit
b7d247856e
  1. 1
      .gitignore
  2. 2
      Makefile
  3. 11
      test/common.js
  4. 27
      test/testpy/__init__.py
  5. 50
      tools/test.py
  6. 2
      vcbuild.bat

1
.gitignore

@ -9,6 +9,7 @@ tags
*.pyc *.pyc
doc/api.xml doc/api.xml
tmp/ tmp/
test/tmp*/
node node
node_g node_g
*.swp *.swp

2
Makefile

@ -90,7 +90,7 @@ distclean:
-rm -rf node_modules -rm -rf node_modules
test: all test: all
$(PYTHON) tools/test.py --mode=release message parallel sequential $(PYTHON) tools/test.py --mode=release message parallel sequential -J
$(MAKE) jslint $(MAKE) jslint
$(MAKE) cpplint $(MAKE) cpplint

11
test/common.js

@ -27,9 +27,18 @@ var os = require('os');
exports.testDir = path.dirname(__filename); exports.testDir = path.dirname(__filename);
exports.fixturesDir = path.join(exports.testDir, 'fixtures'); exports.fixturesDir = path.join(exports.testDir, 'fixtures');
exports.libDir = path.join(exports.testDir, '../lib'); exports.libDir = path.join(exports.testDir, '../lib');
exports.tmpDir = path.join(exports.testDir, 'tmp'); exports.tmpDirName = 'tmp';
exports.PORT = +process.env.NODE_COMMON_PORT || 12346; exports.PORT = +process.env.NODE_COMMON_PORT || 12346;
if (process.env.TEST_THREAD_ID) {
// Distribute ports in parallel tests
if (!process.env.NODE_COMMON_PORT)
exports.PORT += +process.env.TEST_THREAD_ID * 100;
exports.tmpDirName += '.' + process.env.TEST_THREAD_ID;
}
exports.tmpDir = path.join(exports.testDir, exports.tmpDirName);
exports.opensslCli = path.join(path.dirname(process.execPath), 'openssl-cli'); exports.opensslCli = path.join(path.dirname(process.execPath), 'openssl-cli');
if (process.platform === 'win32') { if (process.platform === 'win32') {
exports.PIPE = '\\\\.\\pipe\\libuv-test'; exports.PIPE = '\\\\.\\pipe\\libuv-test';

27
test/testpy/__init__.py

@ -49,30 +49,34 @@ class SimpleTestCase(test.TestCase):
self.mode = mode self.mode = mode
self.tmpdir = join(dirname(self.config.root), 'tmp') self.tmpdir = join(dirname(self.config.root), 'tmp')
self.additional_flags = additional self.additional_flags = additional
def GetTmpDir(self):
return "%s.%d" % (self.tmpdir, self.thread_id)
def AfterRun(self, result): def AfterRun(self, result):
# delete the whole tmp dir # delete the whole tmp dir
try: try:
rmtree(self.tmpdir) rmtree(self.GetTmpDir())
except: except:
pass pass
# make it again. # make it again.
try: try:
mkdir(self.tmpdir) mkdir(self.GetTmpDir())
except: except:
pass pass
def BeforeRun(self): def BeforeRun(self):
# delete the whole tmp dir # delete the whole tmp dir
try: try:
rmtree(self.tmpdir) rmtree(self.GetTmpDir())
except: except:
pass pass
# make it again. # make it again.
# intermittently fails on win32, so keep trying # intermittently fails on win32, so keep trying
while not os.path.exists(self.tmpdir): while not os.path.exists(self.GetTmpDir()):
try: try:
mkdir(self.tmpdir) mkdir(self.GetTmpDir())
except: except:
pass pass
@ -105,7 +109,6 @@ class SimpleTestCase(test.TestCase):
def GetSource(self): def GetSource(self):
return open(self.file).read() return open(self.file).read()
class SimpleTestConfiguration(test.TestConfiguration): class SimpleTestConfiguration(test.TestConfiguration):
def __init__(self, context, root, section, additional=[]): def __init__(self, context, root, section, additional=[]):
@ -136,6 +139,18 @@ class SimpleTestConfiguration(test.TestConfiguration):
if exists(status_file): if exists(status_file):
test.ReadConfigurationInto(status_file, sections, defs) test.ReadConfigurationInto(status_file, sections, defs)
class ParallelTestConfiguration(SimpleTestConfiguration):
def __init__(self, context, root, section, additional=[]):
super(ParallelTestConfiguration, self).__init__(context, root, section,
additional)
def ListTests(self, current_path, path, arch, mode):
result = super(ParallelTestConfiguration, self).ListTests(
current_path, path, arch, mode)
for test in result:
test.parallel = True
return result
class AddonTestConfiguration(SimpleTestConfiguration): class AddonTestConfiguration(SimpleTestConfiguration):
def __init__(self, context, root, section, additional=[]): def __init__(self, context, root, section, additional=[]):
super(AddonTestConfiguration, self).__init__(context, root, section) super(AddonTestConfiguration, self).__init__(context, root, section)

50
tools/test.py

@ -40,6 +40,7 @@ import tempfile
import time import time
import threading import threading
import utils import utils
import multiprocessing
from os.path import join, dirname, abspath, basename, isdir, exists from os.path import join, dirname, abspath, basename, isdir, exists
from datetime import datetime from datetime import datetime
@ -58,9 +59,13 @@ class ProgressIndicator(object):
def __init__(self, cases, flaky_tests_mode): def __init__(self, cases, flaky_tests_mode):
self.cases = cases self.cases = cases
self.flaky_tests_mode = flaky_tests_mode self.flaky_tests_mode = flaky_tests_mode
self.queue = Queue(len(cases)) self.parallel_queue = Queue(len(cases))
self.sequential_queue = Queue(len(cases))
for case in cases: for case in cases:
self.queue.put_nowait(case) if case.parallel:
self.parallel_queue.put_nowait(case)
else:
self.sequential_queue.put_nowait(case)
self.succeeded = 0 self.succeeded = 0
self.remaining = len(cases) self.remaining = len(cases)
self.total = len(cases) self.total = len(cases)
@ -87,11 +92,11 @@ class ProgressIndicator(object):
# That way -j1 avoids threading altogether which is a nice fallback # That way -j1 avoids threading altogether which is a nice fallback
# in case of threading problems. # in case of threading problems.
for i in xrange(tasks - 1): for i in xrange(tasks - 1):
thread = threading.Thread(target=self.RunSingle, args=[]) thread = threading.Thread(target=self.RunSingle, args=[True, i + 1])
threads.append(thread) threads.append(thread)
thread.start() thread.start()
try: try:
self.RunSingle() self.RunSingle(False, 0)
# Wait for the remaining threads # Wait for the remaining threads
for thread in threads: for thread in threads:
# Use a timeout so that signals (ctrl-c) will be processed. # Use a timeout so that signals (ctrl-c) will be processed.
@ -105,13 +110,19 @@ class ProgressIndicator(object):
self.Done() self.Done()
return not self.failed return not self.failed
def RunSingle(self): def RunSingle(self, parallel, thread_id):
while not self.terminate: while not self.terminate:
try: try:
test = self.queue.get_nowait() test = self.parallel_queue.get_nowait()
except Empty: except Empty:
return if parallel:
return
try:
test = self.sequential_queue.get_nowait()
except Empty:
return
case = test.case case = test.case
case.thread_id = thread_id
self.lock.acquire() self.lock.acquire()
self.AboutToRun(case) self.AboutToRun(case)
self.lock.release() self.lock.release()
@ -381,6 +392,8 @@ class TestCase(object):
self.duration = None self.duration = None
self.arch = arch self.arch = arch
self.mode = mode self.mode = mode
self.parallel = False
self.thread_id = 0
def IsNegative(self): def IsNegative(self):
return False return False
@ -399,11 +412,12 @@ class TestCase(object):
def GetSource(self): def GetSource(self):
return "(no source available)" return "(no source available)"
def RunCommand(self, command): def RunCommand(self, command, env):
full_command = self.context.processor(command) full_command = self.context.processor(command)
output = Execute(full_command, output = Execute(full_command,
self.context, self.context,
self.context.GetTimeout(self.mode)) self.context.GetTimeout(self.mode),
env)
self.Cleanup() self.Cleanup()
return TestOutput(self, return TestOutput(self,
full_command, full_command,
@ -420,7 +434,9 @@ class TestCase(object):
self.BeforeRun() self.BeforeRun()
try: try:
result = self.RunCommand(self.GetCommand()) result = self.RunCommand(self.GetCommand(), {
"TEST_THREAD_ID": "%d" % self.thread_id
})
finally: finally:
# Tests can leave the tty in non-blocking mode. If the test runner # Tests can leave the tty in non-blocking mode. If the test runner
# tries to print to stdout/stderr after that and the tty buffer is # tries to print to stdout/stderr after that and the tty buffer is
@ -559,15 +575,22 @@ def CheckedUnlink(name):
PrintError("os.unlink() " + str(e)) PrintError("os.unlink() " + str(e))
def Execute(args, context, timeout=None): def Execute(args, context, timeout=None, env={}):
(fd_out, outname) = tempfile.mkstemp() (fd_out, outname) = tempfile.mkstemp()
(fd_err, errname) = tempfile.mkstemp() (fd_err, errname) = tempfile.mkstemp()
# Extend environment
env_copy = os.environ.copy()
for key, value in env.iteritems():
env_copy[key] = value
(process, exit_code, timed_out) = RunProcess( (process, exit_code, timed_out) = RunProcess(
context, context,
timeout, timeout,
args = args, args = args,
stdout = fd_out, stdout = fd_out,
stderr = fd_err, stderr = fd_err,
env = env_copy
) )
os.close(fd_out) os.close(fd_out)
os.close(fd_err) os.close(fd_err)
@ -1068,6 +1091,7 @@ class ClassifiedTest(object):
def __init__(self, case, outcomes): def __init__(self, case, outcomes):
self.case = case self.case = case
self.outcomes = outcomes self.outcomes = outcomes
self.parallel = self.case.parallel
class Configuration(object): class Configuration(object):
@ -1224,6 +1248,8 @@ def BuildOptions():
default=False, action="store_true") default=False, action="store_true")
result.add_option("-j", help="The number of parallel tasks to run", result.add_option("-j", help="The number of parallel tasks to run",
default=1, type="int") default=1, type="int")
result.add_option("-J", help="Run tasks in parallel on all cores",
default=False, action="store_true")
result.add_option("--time", help="Print timing information after running", result.add_option("--time", help="Print timing information after running",
default=False, action="store_true") default=False, action="store_true")
result.add_option("--suppress-dialogs", help="Suppress Windows dialogs for crashing tests", result.add_option("--suppress-dialogs", help="Suppress Windows dialogs for crashing tests",
@ -1245,6 +1271,8 @@ def ProcessOptions(options):
VERBOSE = options.verbose VERBOSE = options.verbose
options.arch = options.arch.split(',') options.arch = options.arch.split(',')
options.mode = options.mode.split(',') options.mode = options.mode.split(',')
if options.J:
options.j = multiprocessing.cpu_count()
def CheckTestMode(name, option): def CheckTestMode(name, option):
if not option in ["run", "skip", "dontcare"]: if not option in ["run", "skip", "dontcare"]:
print "Unknown %s mode %s" % (name, option) print "Unknown %s mode %s" % (name, option)

2
vcbuild.bat

@ -163,7 +163,7 @@ if "%test%"=="" goto exit
if "%config%"=="Debug" set test_args=--mode=debug if "%config%"=="Debug" set test_args=--mode=debug
if "%config%"=="Release" set test_args=--mode=release if "%config%"=="Release" set test_args=--mode=release
if "%test%"=="test" set test_args=%test_args% sequential parallel message if "%test%"=="test" set test_args=%test_args% sequential parallel message -J
if "%test%"=="test-internet" set test_args=%test_args% internet if "%test%"=="test-internet" set test_args=%test_args% internet
if "%test%"=="test-pummel" set test_args=%test_args% pummel if "%test%"=="test-pummel" set test_args=%test_args% pummel
if "%test%"=="test-simple" set test_args=%test_args% sequential parallel if "%test%"=="test-simple" set test_args=%test_args% sequential parallel

Loading…
Cancel
Save