Commit cfff52c1 authored by Rafael Monnerat's avatar Rafael Monnerat

Move generic testsuite code to erp5.util.testsuite

This code is generic enough to be used by others libraries.

The code on ERP5TypeTestSuite is preserved for backward compatibility for
now, until it got updated.
parent 53e63ce1
import re, sys, threading, os, subprocess
import traceback
import errno
from pprint import pprint
_format_command_search = re.compile("[[\\s $({?*\\`#~';<>&|]").search
_format_command_escape = lambda s: "'%s'" % r"'\''".join(s.split("'"))
def format_command(*args, **kw):
cmdline = []
for k, v in sorted(kw.items()):
if _format_command_search(v):
v = _format_command_escape(v)
cmdline.append('%s=%s' % (k, v))
for v in args:
if _format_command_search(v):
v = _format_command_escape(v)
cmdline.append(v)
return ' '.join(cmdline)
def subprocess_capture(p, quiet=False):
def readerthread(input, output, buffer):
while True:
data = input.readline()
if not data:
break
output(data)
buffer.append(data)
if p.stdout:
stdout = []
output = quiet and (lambda data: None) or sys.stdout.write
stdout_thread = threading.Thread(target=readerthread,
args=(p.stdout, output, stdout))
stdout_thread.setDaemon(True)
stdout_thread.start()
if p.stderr:
stderr = []
stderr_thread = threading.Thread(target=readerthread,
args=(p.stderr, sys.stderr.write, stderr))
stderr_thread.setDaemon(True)
stderr_thread.start()
if p.stdout:
stdout_thread.join()
if p.stderr:
stderr_thread.join()
p.wait()
return (p.stdout and ''.join(stdout),
p.stderr and ''.join(stderr))
class SubprocessError(EnvironmentError):
def __init__(self, status_dict):
self.status_dict = status_dict
def __getattr__(self, name):
return self.status_dict[name]
def __str__(self):
return 'Error %i' % self.status_code
class Persistent(object):
"""Very simple persistent data storage for optimization purpose
This tool should become a standalone daemon communicating only with an ERP5
instance. But for the moment, it only execute 1 test suite and exists,
and test suite classes may want some information from previous runs.
"""
def __init__(self, filename):
self._filename = filename
def __getattr__(self, attr):
if attr == '_db':
try:
db = file(self._filename, 'r+')
except IOError, e:
if e.errno != errno.ENOENT:
raise
db = file(self._filename, 'w+')
else:
try:
self.__dict__.update(eval(db.read()))
except StandardError:
pass
self._db = db
return db
self._db
return super(Persistent, self).__getattribute__(attr)
def sync(self):
self._db.seek(0)
db = dict(x for x in self.__dict__.iteritems() if x[0][:1] != '_')
pprint.pprint(db, self._db)
self._db.truncate()
class TestSuite(object):
"""
Subclasses may redefine the following properties:
mysql_db_count (integer, >=1)
Maximum number of SQL databases connection strings needed by any tests ran
in this suite. Tests will get mysql_db_count - 1 connection strings in
extra_sql_connection_string_list environment variable.
"""
RUN_RE = re.compile(
r'Ran (?P<all_tests>\d+) tests? in (?P<seconds>\d+\.\d+)s',
re.DOTALL)
STATUS_RE = re.compile(r"""
(OK|FAILED)\s+\(
(failures=(?P<failures>\d+),?\s*)?
(errors=(?P<errors>\d+),?\s*)?
(skipped=(?P<skips>\d+),?\s*)?
(expected\s+failures=(?P<expected_failures>\d+),?\s*)?
(unexpected\s+successes=(?P<unexpected_successes>\d+),?\s*)?
\)
""", re.DOTALL | re.VERBOSE)
mysql_db_count = 1
allow_restart = False
realtime_output = True
stdin = file(os.devnull)
def __init__(self, max_instance_count, **kw):
self.__dict__.update(kw)
self._path_list = ['tests']
pool = threading.Semaphore(max_instance_count)
self.acquire = pool.acquire
self.release = pool.release
self._instance = threading.local()
self._pool = max_instance_count == 1 and [None] or \
range(1, max_instance_count + 1)
self._ready = set()
self.running = {}
if max_instance_count != 1:
self.realtime_output = False
elif os.isatty(1):
self.realtime_output = True
self.persistent = Persistent('run_test_suite-%s.tmp'
% self.__class__.__name__)
instance = property(lambda self: self._instance.id)
def start(self, test, on_stop=None):
assert test not in self.running
self.running[test] = instance = self._pool.pop(0)
def run():
try:
self._instance.id = instance
if instance not in self._ready:
self._ready.add(instance)
self.setup()
status_dict = self.run(test)
if on_stop is not None:
on_stop(status_dict)
self._pool.append(self.running.pop(test))
finally:
self.release()
thread = threading.Thread(target=run)
thread.setDaemon(True)
thread.start()
def update(self):
self.checkout() # by default, update everything
def setup(self):
pass
def run(self, test):
raise NotImplementedError
def getTestList(self):
raise NotImplementedError
def spawn(self, *args, **kw):
quiet = kw.pop('quiet', False)
env = kw and dict(os.environ, **kw) or None
command = format_command(*args, **kw)
print '\n$ ' + command
sys.stdout.flush()
try:
p = subprocess.Popen(args, stdin=self.stdin, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, env=env)
except Exception:
# Catch any exception here, to warn user instead of beeing silent,
# by generating fake error result
result = dict(status_code=-1,
command=command,
stderr=traceback.format_exc(),
stdout='')
raise SubprocessError(result)
if self.realtime_output:
stdout, stderr = subprocess_capture(p, quiet)
else:
stdout, stderr = p.communicate()
if not quiet:
sys.stdout.write(stdout)
sys.stderr.write(stderr)
result = dict(status_code=p.returncode, command=command,
stdout=stdout, stderr=stderr)
if p.returncode:
raise SubprocessError(result)
return result
import re, imp, sys, threading, os, shlex, subprocess, shutil, glob, random import re, imp, sys, os, shlex, shutil, glob, random
import traceback try:
from erp5.util.testsuite import TestSuite, SubprocessError
# The content of this file might be partially moved to an egg except ImportError:
# in order to allows parallel tests without the code of ERP5 # erp5.util.testsuite should be updated
print "Please update erp5.util to version >= 0.4.3"
_format_command_search = re.compile("[[\\s $({?*\\`#~';<>&|]").search
_format_command_escape = lambda s: "'%s'" % r"'\''".join(s.split("'")) import threading, subprocess
def format_command(*args, **kw): import traceback
cmdline = [] import errno
for k, v in sorted(kw.items()): from pprint import pprint
if _format_command_search(v):
v = _format_command_escape(v) _format_command_search = re.compile("[[\\s $({?*\\`#~';<>&|]").search
cmdline.append('%s=%s' % (k, v)) _format_command_escape = lambda s: "'%s'" % r"'\''".join(s.split("'"))
for v in args: def format_command(*args, **kw):
if _format_command_search(v): cmdline = []
v = _format_command_escape(v) for k, v in sorted(kw.items()):
cmdline.append(v) if _format_command_search(v):
return ' '.join(cmdline) v = _format_command_escape(v)
cmdline.append('%s=%s' % (k, v))
def subprocess_capture(p, quiet=False): for v in args:
def readerthread(input, output, buffer): if _format_command_search(v):
while True: v = _format_command_escape(v)
data = input.readline() cmdline.append(v)
if not data: return ' '.join(cmdline)
break
output(data) def subprocess_capture(p, quiet=False):
buffer.append(data) def readerthread(input, output, buffer):
if p.stdout: while True:
stdout = [] data = input.readline()
output = quiet and (lambda data: None) or sys.stdout.write if not data:
stdout_thread = threading.Thread(target=readerthread, break
args=(p.stdout, output, stdout)) output(data)
stdout_thread.setDaemon(True) buffer.append(data)
stdout_thread.start() if p.stdout:
if p.stderr: stdout = []
stderr = [] output = quiet and (lambda data: None) or sys.stdout.write
stderr_thread = threading.Thread(target=readerthread, stdout_thread = threading.Thread(target=readerthread,
args=(p.stderr, sys.stderr.write, stderr)) args=(p.stdout, output, stdout))
stderr_thread.setDaemon(True) stdout_thread.setDaemon(True)
stderr_thread.start() stdout_thread.start()
if p.stdout: if p.stderr:
stdout_thread.join() stderr = []
if p.stderr: stderr_thread = threading.Thread(target=readerthread,
stderr_thread.join() args=(p.stderr, sys.stderr.write, stderr))
p.wait() stderr_thread.setDaemon(True)
return (p.stdout and ''.join(stdout), stderr_thread.start()
p.stderr and ''.join(stderr)) if p.stdout:
stdout_thread.join()
class Persistent(object): if p.stderr:
"""Very simple persistent data storage for optimization purpose stderr_thread.join()
p.wait()
This tool should become a standalone daemon communicating only with an ERP5 return (p.stdout and ''.join(stdout),
instance. But for the moment, it only execute 1 test suite and exists, p.stderr and ''.join(stderr))
and test suite classes may want some information from previous runs.
""" class SubprocessError(EnvironmentError):
def __init__(self, status_dict):
self.status_dict = status_dict
def __getattr__(self, name):
return self.status_dict[name]
def __str__(self):
return 'Error %i' % self.status_code
class Persistent(object):
"""Very simple persistent data storage for optimization purpose
This tool should become a standalone daemon communicating only with an ERP5
instance. But for the moment, it only execute 1 test suite and exists,
and test suite classes may want some information from previous runs.
"""
def __init__(self, filename): def __init__(self, filename):
self._filename = filename self._filename = filename
def __getattr__(self, attr): def __getattr__(self, attr):
if attr == '_db': if attr == '_db':
try:
db = file(self._filename, 'r+')
except IOError, e:
if e.errno != errno.ENOENT:
raise
db = file(self._filename, 'w+')
else:
try: try:
self.__dict__.update(eval(db.read())) db = file(self._filename, 'r+')
except StandardError: except IOError, e:
pass if e.errno != errno.ENOENT:
self._db = db raise
return db db = file(self._filename, 'w+')
self._db else:
return super(Persistent, self).__getattribute__(attr) try:
self.__dict__.update(eval(db.read()))
def sync(self): except StandardError:
self._db.seek(0) pass
db = dict(x for x in self.__dict__.iteritems() if x[0][:1] != '_') self._db = db
pprint.pprint(db, self._db) return db
self._db.truncate() self._db
return super(Persistent, self).__getattribute__(attr)
class TestSuite(object):
""" def sync(self):
Subclasses may redefine the following properties: self._db.seek(0)
mysql_db_count (integer, >=1) db = dict(x for x in self.__dict__.iteritems() if x[0][:1] != '_')
Maximum number of SQL databases connection strings needed by any tests ran pprint.pprint(db, self._db)
in this suite. Tests will get mysql_db_count - 1 connection strings in self._db.truncate()
extra_sql_connection_string_list environment variable.
""" class TestSuite(object):
"""
Subclasses may redefine the following properties:
mysql_db_count (integer, >=1)
Maximum number of SQL databases connection strings needed by any tests ran
in this suite. Tests will get mysql_db_count - 1 connection strings in
extra_sql_connection_string_list environment variable.
"""
mysql_db_count = 1 RUN_RE = re.compile(
allow_restart = False r'Ran (?P<all_tests>\d+) tests? in (?P<seconds>\d+\.\d+)s',
realtime_output = True re.DOTALL)
stdin = file(os.devnull)
STATUS_RE = re.compile(r"""
def __init__(self, max_instance_count, **kw): (OK|FAILED)\s+\(
self.__dict__.update(kw) (failures=(?P<failures>\d+),?\s*)?
self._path_list = ['tests'] (errors=(?P<errors>\d+),?\s*)?
pool = threading.Semaphore(max_instance_count) (skipped=(?P<skips>\d+),?\s*)?
self.acquire = pool.acquire (expected\s+failures=(?P<expected_failures>\d+),?\s*)?
self.release = pool.release (unexpected\s+successes=(?P<unexpected_successes>\d+),?\s*)?
self._instance = threading.local() \)
self._pool = max_instance_count == 1 and [None] or \ """, re.DOTALL | re.VERBOSE)
range(1, max_instance_count + 1)
self._ready = set()
self.running = {} mysql_db_count = 1
if max_instance_count != 1: allow_restart = False
self.realtime_output = False realtime_output = True
elif os.isatty(1): stdin = file(os.devnull)
self.realtime_output = True
self.persistent = Persistent('run_test_suite-%s.tmp' def __init__(self, max_instance_count, **kw):
% self.__class__.__name__) self.__dict__.update(kw)
self._path_list = ['tests']
instance = property(lambda self: self._instance.id) pool = threading.Semaphore(max_instance_count)
self.acquire = pool.acquire
def start(self, test, on_stop=None): self.release = pool.release
assert test not in self.running self._instance = threading.local()
self.running[test] = instance = self._pool.pop(0) self._pool = max_instance_count == 1 and [None] or \
def run(): range(1, max_instance_count + 1)
self._ready = set()
self.running = {}
if max_instance_count != 1:
self.realtime_output = False
elif os.isatty(1):
self.realtime_output = True
self.persistent = Persistent('run_test_suite-%s.tmp'
% self.__class__.__name__)
instance = property(lambda self: self._instance.id)
def start(self, test, on_stop=None):
assert test not in self.running
self.running[test] = instance = self._pool.pop(0)
def run():
try:
self._instance.id = instance
if instance not in self._ready:
self._ready.add(instance)
self.setup()
status_dict = self.run(test)
if on_stop is not None:
on_stop(status_dict)
self._pool.append(self.running.pop(test))
finally:
self.release()
thread = threading.Thread(target=run)
thread.setDaemon(True)
thread.start()
def update(self):
self.checkout() # by default, update everything
def setup(self):
pass
def run(self, test):
raise NotImplementedError
def getTestList(self):
raise NotImplementedError
def spawn(self, *args, **kw):
quiet = kw.pop('quiet', False)
env = kw and dict(os.environ, **kw) or None
command = format_command(*args, **kw)
print '\n$ ' + command
sys.stdout.flush()
try: try:
self._instance.id = instance p = subprocess.Popen(args, stdin=self.stdin, stdout=subprocess.PIPE,
if instance not in self._ready: stderr=subprocess.PIPE, env=env)
self._ready.add(instance) except Exception:
self.setup() # Catch any exception here, to warn user instead of beeing silent,
status_dict = self.run(test) # by generating fake error result
if on_stop is not None: result = dict(status_code=-1,
on_stop(status_dict) command=command,
self._pool.append(self.running.pop(test)) stderr=traceback.format_exc(),
finally: stdout='')
self.release() raise SubprocessError(result)
thread = threading.Thread(target=run) if self.realtime_output:
thread.setDaemon(True) stdout, stderr = subprocess_capture(p, quiet)
thread.start() else:
stdout, stderr = p.communicate()
def update(self): if not quiet:
self.checkout() # by default, update everything sys.stdout.write(stdout)
sys.stderr.write(stderr)
def setup(self): result = dict(status_code=p.returncode, command=command,
pass stdout=stdout, stderr=stderr)
if p.returncode:
raise SubprocessError(result)
return result
def run(self, test):
raise NotImplementedError
def getTestList(self):
raise NotImplementedError
def spawn(self, *args, **kw):
quiet = kw.pop('quiet', False)
env = kw and dict(os.environ, **kw) or None
command = format_command(*args, **kw)
print '\n$ ' + command
sys.stdout.flush()
try:
p = subprocess.Popen(args, stdin=self.stdin, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, env=env)
except Exception:
# Catch any exception here, to warn user instead of beeing silent,
# by generating fake error result
result = dict(status_code=-1,
command=command,
stderr=traceback.format_exc(),
stdout='')
raise SubprocessError(result)
if self.realtime_output:
stdout, stderr = subprocess_capture(p, quiet)
else:
stdout, stderr = p.communicate()
if not quiet:
sys.stdout.write(stdout)
sys.stderr.write(stderr)
result = dict(status_code=p.returncode, command=command,
stdout=stdout, stderr=stderr)
if p.returncode:
raise SubprocessError(result)
return result
class ERP5TypeTestSuite(TestSuite): class ERP5TypeTestSuite(TestSuite):
RUN_RE = re.compile(
r'Ran (?P<all_tests>\d+) tests? in (?P<seconds>\d+\.\d+)s',
re.DOTALL)
STATUS_RE = re.compile(r"""
(OK|FAILED)\s+\(
(failures=(?P<failures>\d+),?\s*)?
(errors=(?P<errors>\d+),?\s*)?
(skipped=(?P<skips>\d+),?\s*)?
(expected\s+failures=(?P<expected_failures>\d+),?\s*)?
(unexpected\s+successes=(?P<unexpected_successes>\d+),?\s*)?
\)
""", re.DOTALL | re.VERBOSE)
FTEST_PASS_FAIL_RE = re.compile( FTEST_PASS_FAIL_RE = re.compile(
".*Functional Tests (?P<total>\d+) Tests, (?P<failures>\d+) " + \ ".*Functional Tests (?P<total>\d+) Tests, (?P<failures>\d+) " + \
"Failures(\,\ (?P<expected_failure>\d+) Expected failures|)") "Failures(\,\ (?P<expected_failure>\d+) Expected failures|)")
...@@ -321,14 +337,6 @@ class SavedTestSuite(ERP5TypeTestSuite): ...@@ -321,14 +337,6 @@ class SavedTestSuite(ERP5TypeTestSuite):
super(SavedTestSuite, self).setup() super(SavedTestSuite, self).setup()
self.__runUnitTest('--save', self._saved_test_id) self.__runUnitTest('--save', self._saved_test_id)
class SubprocessError(EnvironmentError):
def __init__(self, status_dict):
self.status_dict = status_dict
def __getattr__(self, name):
return self.status_dict[name]
def __str__(self):
return 'Error %i' % self.status_code
sys.modules['test_suite'] = module = imp.new_module('test_suite') sys.modules['test_suite'] = module = imp.new_module('test_suite')
for var in SubprocessError, TestSuite, ERP5TypeTestSuite, ProjectTestSuite, \ for var in SubprocessError, TestSuite, ERP5TypeTestSuite, ProjectTestSuite, \
SavedTestSuite: SavedTestSuite:
......
...@@ -2,7 +2,7 @@ from setuptools import setup, find_packages ...@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
import glob import glob
import os import os
version = '0.4.6' version = '0.4.7'
name = 'erp5.util' name = 'erp5.util'
long_description = open("README.erp5.util.txt").read() + "\n" long_description = open("README.erp5.util.txt").read() + "\n"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment