#!/usr/bin/env python
# coding: utf-8
# Copyright (C) 1994-2018 Altair Engineering, Inc.
# For more information, contact Altair at www.altair.com.
#
# This file is part of the PBS Professional ("PBS Pro") software.
#
# Open Source License Information:
#
# PBS Pro is free software. You can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# PBS Pro is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.
# See the GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
#
# Commercial License Information:
#
# For a copy of the commercial license terms and conditions,
# go to: (http://www.pbspro.com/UserArea/agreement.html)
# or contact the Altair Legal Department.
#
# Altair’s dual-license business model allows companies, individuals, and
# organizations to create proprietary derivative works of PBS Pro and
# distribute them - whether embedded or bundled with other software -
# under a commercial license agreement.
#
# Use of Altair’s trademarks, including but not limited to "PBS™",
# "PBS Professional®", and "PBS Pro™" and Altair’s logos is subject to Altair's
# trademark licensing policies.
import sys
import os
import getopt
import logging
import logging.config
import platform
import errno
import signal
import importlib
import ptl
import nose
from nose.plugins.base import Plugin
from ptl.lib.pbs_testlib import PtlConfig
from distutils.version import LooseVersion
from ptl.utils.pbs_cliutils import CliUtils
from ptl.utils.plugins.ptl_test_loader import PTLTestLoader
from ptl.utils.plugins.ptl_test_runner import PTLTestRunner
from ptl.utils.plugins.ptl_test_db import PTLTestDb
from ptl.utils.plugins.ptl_test_info import PTLTestInfo
from ptl.utils.plugins.ptl_test_tags import PTLTestTags
from ptl.utils.plugins.ptl_test_data import PTLTestData
# trap SIGINT and SIGPIPE
def trap_exceptions(etype, value, tb):
sys.excepthook = sys.__excepthook__
if issubclass(etype, IOError) and value.errno == errno.EPIPE:
pass
else:
sys.__excepthook__(etype, value, tb)
sys.excepthook = trap_exceptions
def sighandler(signum, frames):
signal.alarm(0)
raise KeyboardInterrupt('Signal %d received' % (signum))
# join process group of caller makes it possible to programmatically interrupt
# when run in a subshell
if os.getpgrp() != os.getpid():
os.setpgrp()
signal.signal(signal.SIGINT, sighandler)
signal.signal(signal.SIGTERM, sighandler)
def usage():
msg = []
msg += ['Usage: ' + os.path.basename(sys.argv[0]) + ' [OPTION]\n\n']
msg += [' Test harness used to run or list test suites and test ' +
'cases\n\n']
msg += ['-f : comma-separated list of file names to run\n']
msg += ['-F: set logging format to include timestamp and level\n']
msg += ['-g : path to file containing comma-separated']
msg += [' list of testsuites\n']
msg += ['-h: display usage information\n']
msg += ['-i: show test info\n']
msg += ['-l : log level\n']
msg += ['-L: display list of tests\n']
msg += ['-o : log file name\n']
msg += ['-p : test parameter. Comma-separated list of key=val']
msg += [' pairs. Note that the comma can not be used in val\n']
msg += ['-t : comma-separated list of test suites to run\n']
msg += ['--exclude=: comma-separated string of tests to exclude\n']
msg += ['--user-plugins=: comma-separated list of key=val of']
msg += [' user plugins to load, where key is module and val is']
msg += [' classname of plugin which is subclass of nose.plugins.base']
msg += ['.Plugin\n']
msg += ['--db-type=: Type of database to use.']
msg += [' can be one of "html", "file", "sqlite", "pgsql", "json".']
msg += [' Default to "file"\n']
msg += ['--db-name=: database name. Default to']
msg += [' ptl_test_results.db\n']
msg += ['--db-access=: Path to a file that defines db options '
'(PostreSQL only)\n']
msg += ['--lcov-bin=: path to lcov binary. Defaults to lcov\n']
msg += ['--genhtml-bin=: path to genhtml binary. '
'Defaults to genhtml\n']
msg += ['--lcov-data=: path to directory containig .gcno files\n']
msg += ['--lcov-out=: path to output directory\n']
msg += ['--lcov-baseurl=: use as baseurl in html report\n']
msg += ['--lcov-nosrc: don\'t include PBS source in coverage analysis.']
msg += [' Default PBS source will be included in coverage analysis\n']
msg += ['--log-conf=: logging config file\n']
msg += ['--min-pyver=: minimum Python version\n']
msg += ['--max-pyver=: maximum Python version\n']
msg += ['--param-file=: get params from file. Overrides -p\n']
msg += ['--post-analysis-data=: path to post analysis data' +
' directory\n']
msg += ['--max-postdata-threshold=: max post analysis data' +
' threshold per testsuite. Defaults to 10. =0 will' +
' disable this threshold\n']
msg += ['--tc-failure-threshold=: test case failure threshold' +
' per testsuite. Defaults to 10. =0 will disable this' +
' threshold\n']
msg += ['--cumulative-tc-failure-threshold=: cumulative test' +
' case failure threshold. Defaults to 100. =0 will' +
' disable this threshold.\n ' +
' Must be greater or equal to' +
' \'tc-failure-threshold\'\n']
msg += ['--stop-on-failure: if set, stop when one of multiple tests ' +
'fails\n']
msg += ['--timeout=: duration after which no test suites are '
'run\n']
msg += ['--follow-child: if set, walk the test hierarchy and run ' +
'each test\n']
msg += ['--tags=: Select only tests that have tag.']
msg += [' can be applied multiple times\n']
msg += [' Format: [!]tag[,tag]\n']
msg += [' Example:\n']
msg += [' smoke - This will select all tests which has']
msg += [' "smoke" tag\n']
msg += [' !smoke - This will select all tests which']
msg += [' doesn\'t have "smoke" tag\n']
msg += [' smoke,regression - This will select all tests']
msg += [' which has both "smoke" and "regression" tag\n']
msg += ['--eval-tags=\'\': Select only tests for whose']
msg += [' tags evaluates to True.']
msg += [' can be applied multiple times\n']
msg += [' Example:\n']
msg += [' \'smoke and (not regression)\' - This will select']
msg += [' all tests which has "smoke" tag and does\'t have "regression"']
msg += [' tag\n']
msg += [' \'priority>4\' - This will select all tests which']
msg += [' has "priority" tag and its value is >4\n']
msg += ['--tags-info: List all selected test suite (or test cases if ']
msg += ['--verbose applied)\n']
msg += [
' used with --tags or --eval-tags (also -t or --exclude']
msg += [' can be applied to limit selection)\n']
msg += ['--list-tags: List all currenlty used tags\n']
msg += [
'--verbose: show verbose output (used with -i, -L or --tag-info)\n']
msg += ['--version: show version number and exit\n']
print ''.join(msg)
if __name__ == '__main__':
if len(sys.argv) < 2:
usage()
sys.exit(1)
level = 'INFOCLI2'
fmt = '%(asctime)-15s %(levelname)-8s %(message)s'
outfile = None
dbtype = None
dbname = None
dbaccess = None
testfiles = None
testsuites = None
testparam = None
testgroup = None
list_test = False
showinfo = False
follow = False
excludes = None
stoponfail = False
paramfile = None
logconf = None
minpyver = None
maxpyver = None
verbose = False
lcov_data = None
lcov_bin = None
lcov_out = None
lcov_nosrc = False
lcov_baseurl = None
genhtml_bin = None
timeout = None
nosedebug = False
only_info = False
tags = []
eval_tags = []
tags_info = False
list_tags = False
post_data_dir = None
gen_ts_tree = False
tc_failure_threshold = 10
cumulative_tc_failure_threshold = 100
max_postdata_threshold = 10
user_plugins = None
PtlConfig()
largs = ['exclude=', 'log-conf=', 'timeout=']
largs += ['param-file=', 'min-pyver=', 'max-pyver=']
largs += ['db-name=', 'db-access=', 'db-type=', 'genhtml-bin=']
largs += ['lcov-bin=', 'lcov-data=', 'lcov-out=', 'lcov-nosrc']
largs += ['lcov-baseurl=', 'tags=', 'eval-tags=', 'tags-info', 'list-tags']
largs += ['version', 'verbose', 'follow-child']
largs += ['stop-on-failure', 'enable-nose-debug']
largs += ['post-analysis-data=', 'gen-ts-tree']
largs += ['tc-failure-threshold=', 'cumulative-tc-failure-threshold=']
largs += ['max-postdata-threshold=', 'user-plugins=']
try:
opts, args = getopt.getopt(
sys.argv[1:], 'f:il:t:o:p:g:hLF', largs)
except:
sys.stderr.write('Unrecognized option. Exiting\n')
usage()
sys.exit(1)
if args:
sys.stderr.write('Invalid usage. Exiting\n')
usage()
sys.exit(1)
for o, val in opts:
if o == '-i':
showinfo = True
list_test = False
gen_ts_tree = False
only_info = True
elif o == '-L':
showinfo = False
list_test = True
gen_ts_tree = False
only_info = True
elif o == '--gen-ts-tree':
showinfo = False
list_test = False
gen_ts_tree = True
only_info = True
elif o == '-l':
level = val
elif o == '-o':
outfile = CliUtils.expand_abs_path(val)
elif o == '-f':
testfiles = CliUtils.expand_abs_path(val)
elif o == '-t':
testsuites = val
elif o == '--user-plugins':
user_plugins = val
elif o == '--tags':
tags.append(val.strip())
elif o == '--eval-tags':
eval_tags.append(val.strip())
elif o == '--tags-info':
tags_info = True
elif o == '--list-tags':
list_tags = True
elif o == '--exclude':
excludes = val
elif o == '-F':
fmt = '%(asctime)-15s %(levelname)-8s %(message)s'
elif o == '-p':
testparam = val
elif o == '-g':
testgroup = val
elif o == '--timeout':
timeout = int(val)
elif o == '--db-type':
dbtype = val
elif o == '--db-name':
dbname = CliUtils.expand_abs_path(val)
elif o == '--db-access':
dbaccess = CliUtils.expand_abs_path(val)
elif o == '--genhtml-bin':
genhtml_bin = CliUtils.expand_abs_path(val)
elif o == '--lcov-bin':
lcov_bin = CliUtils.expand_abs_path(val)
elif o == '--lcov-data':
lcov_data = CliUtils.expand_abs_path(val)
elif o == '--lcov-out':
lcov_out = CliUtils.expand_abs_path(val)
elif o == '--lcov-nosrc':
lcov_nosrc = True
elif o == '--lcov-baseurl':
lcov_baseurl = val
elif o == '--param-file':
paramfile = CliUtils.expand_abs_path(val)
elif o == '--stop-on-failure':
stoponfail = True
elif o == '--follow-child':
follow = True
elif o == '--log-conf':
logconf = val
elif o == '--min-pyver':
minpyver = val
elif o == '--max-pyver':
maxpyver = val
elif o == '--enable-nose-debug':
nosedebug = True
elif o == '--verbose':
verbose = True
elif o == '--post-analysis-data':
post_data_dir = CliUtils.expand_abs_path(val)
elif o == '--tc-failure-threshold':
tc_failure_threshold = val
elif o == '--cumulative-tc-failure-threshold':
cumulative_tc_failure_threshold = val
elif o == '--max-postdata-threshold':
max_postdata_threshold = val
elif o == '-h':
usage()
sys.exit(0)
elif o == '--version':
print ptl.__version__
sys.exit(0)
else:
sys.stderr.write('Unreocgnized option %s\n' % o)
usage()
sys.exit(1)
if nosedebug:
level = 'DEBUG'
l = CliUtils.get_logging_level(level)
if logconf:
logging.config.fileConfig(logconf)
else:
logging.basicConfig(filename=outfile, filemode='w+', level=l,
format=fmt)
if outfile is not None:
stream_hdlr = logging.StreamHandler()
stream_hdlr.setLevel(l)
stream_hdlr.setFormatter(logging.Formatter(fmt))
ptl_logger = logging.getLogger('ptl')
ptl_logger.addHandler(stream_hdlr)
ptl_logger.setLevel(l)
pyver = platform.python_version()
if minpyver is not None and LooseVersion(pyver) < LooseVersion(minpyver):
logging.error('Python version ' + str(pyver) + ' does not meet ' +
'required minimum version of ' + minpyver)
sys.exit(1)
if maxpyver is not None and LooseVersion(pyver) > LooseVersion(maxpyver):
logging.error('Python version ' + str(pyver) + ' does not meet ' +
'required max version of ' + maxpyver)
sys.exit(1)
if showinfo and testsuites is None:
logging.error(
'Testsuites names require (see -t) along with -i option!')
sys.exit(1)
try:
tc_failure_threshold = int(tc_failure_threshold)
if tc_failure_threshold < 0:
raise ValueError
except:
_msg = 'Invalid value provided for testcase failure threshold, '
_msg += 'please provide integer'
logging.error(_msg)
sys.exit(1)
try:
cumulative_tc_failure_threshold = int(cumulative_tc_failure_threshold)
if cumulative_tc_failure_threshold < 0:
raise ValueError
except:
_msg = 'Invalid value provided for cumulative-tc-failure-threshold, '
_msg += 'please provide integer'
logging.error(_msg)
sys.exit(1)
if cumulative_tc_failure_threshold < tc_failure_threshold:
_msg = 'Value for cumulative-tc-failure-threshould should'
_msg += ' be greater or equal to \'tc-failure-threshold\''
logging.error(_msg)
sys.exit(1)
try:
max_postdata_threshold = int(max_postdata_threshold)
if max_postdata_threshold < 0:
raise ValueError
except:
_msg = 'Invalid value provided for max-postdata-threshold, '
_msg += 'please provide integer'
logging.error(_msg)
sys.exit(1)
if outfile is not None and not os.path.isdir(os.path.dirname(outfile)):
os.mkdir(os.path.dirname(outfile))
if timeout is not None:
signal.signal(signal.SIGALRM, sighandler)
signal.alarm(timeout)
if list_test:
excludes = None
testgroup = None
follow = True
if testfiles is not None:
tests = testfiles.split(',')
else:
tests = os.getcwd()
if testsuites is None:
testsuites = 'PBSTestSuite'
follow = True
if only_info:
testinfo = PTLTestInfo()
testinfo.set_data(testsuites, list_test,
showinfo, verbose, gen_ts_tree)
plugins = (testinfo,)
elif (tags_info or list_tags):
loader = PTLTestLoader()
testtags = PTLTestTags()
loader.set_data(testgroup, testsuites, excludes, True)
testtags.set_data(tags, eval_tags, tags_info, list_tags, verbose)
plugins = (loader, testtags)
else:
loader = PTLTestLoader()
testtags = PTLTestTags()
runner = PTLTestRunner()
db = PTLTestDb()
data = PTLTestData()
loader.set_data(testgroup, testsuites, excludes, follow)
testtags.set_data(tags, eval_tags)
runner.set_data(paramfile, testparam, lcov_bin, lcov_data, lcov_out,
genhtml_bin, lcov_nosrc, lcov_baseurl,
tc_failure_threshold, cumulative_tc_failure_threshold)
db.set_data(dbtype, dbname, dbaccess)
data.set_data(post_data_dir, max_postdata_threshold)
plugins = (loader, testtags, runner, db, data)
if user_plugins:
for plugin in user_plugins.split(','):
if '=' not in plugin:
_msg = 'Invalid value (%s)' % (plugin)
_msg += ' provided in user-plugins, it should be key value'
_msg += ' pair where key is module name and value is class'
_msg += ' name of plugin'
logging.error(_msg)
sys.exit(1)
mod, clsname = plugin.split('=', 1)
try:
loaded_mod = importlib.import_module(mod)
except ImportError:
_msg = 'Failed to load module (%s)' % mod
_msg += ' for plugin (%s)' % plugin
logging.error(_msg)
sys.exit(1)
_plugin = getattr(loaded_mod, clsname, None)
if not _plugin:
_msg = 'Could not find class named "%s"' % clsname
_msg += ' in module (%s)' % mod
logging.error(_msg)
sys.exit(1)
if not issubclass(_plugin, Plugin):
_msg = 'Plugin class (%s) should be subclass of ' % (clsname)
_msg += 'nose.plugins.base.Plugin'
logging.error(_msg)
sys.exit(1)
plugins += (_plugin(),)
test_regex = r'(^(?:[\w]+|^)Test|pbs_|^test_[\(]*)'
os.environ['NOSE_TESTMATCH'] = test_regex
if nosedebug:
os.environ['NOSE_VERBOSE'] = '7'
if outfile:
os.environ['NOSE_DEBUG_LOG'] = outfile
else:
os.environ['NOSE_VERBOSE'] = '2'
if stoponfail:
os.environ['NOSE_STOP'] = '1'
nose.main(defaultTest=tests, argv=[sys.argv[0]], plugins=plugins)