# -*- coding: utf-8 -*-
"""Runs unit tests of on multiple implementations and platforms.
Provides some essential features for multi-platform development:
* easy selection of the used implementation
* detailed information of the actual called implementation for the test cases
"""
from __future__ import absolute_import
from __future__ import print_function
import os
import sys
import distutils.cmd
import yapyutils.config.capabilities
import setuplibcore
from setuptestx import SetupTestXError
from pythonids import PYVxyz, PYV27
__author__ = 'Arno-Can Uestuensoez'
__author_email__ = 'acue_sf2@sourceforge.net'
__license__ = "Artistic-License-2.0 + Forced-Fairplay-Constraints"
__copyright__ = "Copyright (C) 2019 Arno-Can Uestuensoez @Ingenieurbuero Arno-Can Uestuensoez"
__uuid__ = "1936395c-9621-42df-b5ec-9c4df4f1ff49"
__version__ = "01.01.042"
__product_family__ = "setuplib"
__product__ = "testx"
__product_component__ = "testx"
[docs]class TestX(distutils.cmd.Command):
"""Calls *unittest* in the subdirectory *tests*."""
description = "Calls 'unittest' in the subdirectory 'tests'"
user_options = [
('abs', 'a', "change all paths where possible to absolute"),
('prog=', 'p', "call program for tests, default 'CallCase.py'"),
('implementation=', 'i', "the Python implementation, default is 'python' - CPython,"
" change to e.g. 'jython' - needs eventually additional options. "
"Tested values are: python, jython, ipython, ipy(IronPython), pypy"),
('name=', None, "The name of the package. "
"Default: attribute of derived class self.name"),
('start=', 's', "start package, e.g. 'tests' or 'tests.setuplib'"),
('testlib=', 'm', "test library, default 'unittest discover'"),
('noexec', 'n', "print only, do not execute"),
('print-env', None, "print subprocess environment after an exec call"),
('print-ver', None, "print the version of executed Python implementation"),
('print-vinfo', None, "print information about executed implementation"),
('verbose', None, "pass verbose flag, e.g. 'jython -v' or 'python -v'"),
('quiet', 'q', "silence the default verbosity, e.g. 'jython -q' or 'python -q'"),
('exe', 'c', """call provided string, e.g. python -c "print 'hello'" """),
('coff', 'C', "JYTHON: set cache off, sets '-Dpython.cachedir.skip=true'"),
('jyjar=', 'j', "JYTHON: switch to call of 'java -jar <jython.jar>', "
"requires absolute path, "
"default is 'jython', "
"for 'jython' the default names are 'java -jar jython.jar', "
"for a valid jar-file 'java -jar /my/path/jython.jar'"
"- enabled by '-i jython',"),
('jyjvm=', 'J', "JYTHON: Java JVM options for Jyhon, "
"e.g. '-J-Xmx512m' - enabled by '-i jython',"),
('jyprop=', 'D', "JYTHON: properties '<prop>=<value>', "
"e.g '-Dpython.path=/my/path', enabled by '-i jython'"),
]
#: The provided capabilities of the builder and it's components.
#: The values are loaded dynamically during bootstrap where the
#: enabled packages are loaded and initialized.
#: Each component is responsible for it's own data which is dynamically
#: updated in place.
#: Only activated components are loaded, which is dynamically defined
#: by the command line parameters.
#: When missing the *default* item defines the active component.
#: At least one subcommand component is required.
capabilities = yapyutils.config.capabilities.Capability(
{
"master_data": {
"author": __author__,
"author_email": __author_email__,
"license": __license__,
"copyright": __copyright__,
"uuid": __uuid__,
"product_family": __product_family__,
"product": __product__,
"product_component": __product_component__,
"component_version": __version__,
"product_version": __version__,
},
"components": { # see also 'user_options'
"default": "testx", # call when no parameters are provided
"testx": "testx", # call unittest in subdirectory "tests"
},
"testx": {
"status": True, # enables the component
"package": None, # the implementation
"exit": False, # exit after list
"entry": True, # List entry points.
"local": True, # List local defined commands.
"standard": True, # List standard commands.
"long": False, # List long format, similar to shell command 'ls -l'.
"format": ( # Define display format.
),
}
}
)
[docs] def initialize_options(self):
self.debug = None
self.noexec = None
self.quiet = None
self.verbose = None
self.break_on_err = None
self.abs = None
self.call = None
self.calljy = None
self.coff = None
self.exe = None
self.implementation = None
self.jyjar = None
self.jyjvm = None
self.jyprop = None
self.name = None
self.print_env = None
self.print_ver = None
self.print_vinfo = None
self.prog = None
self.start = None
self.testlib = None
self.postfix = None
[docs] def finalize_options(self):
# scan for any context-help request
_help_request = setuplibcore.check_for_context_help(self)
if _help_request:
print(_help_request)
sys.exit(0)
# quick-and-dirty hack to resolve the inconsistency of
# global and local verbose values of distutils
try:
# The context option is actually not set by the framework,
# instead the global option is reset and intialized to
# the number of occurances and passes to the initialization
# of the memeber 'self.verbose'.
# Thus the poll fails, while the value is already set via the framework.
# See code distutils.dist.Distribution.
# Anyhow...keeping it as a reminder.
_v_opts = self.distribution.get_option_dict('build_docx')['verbose'][1]
if _v_opts:
self.verbose += 1
except:
# fallback to the slightly erroneous behavior when the interface
# of distutils changes
pass
# global and local verbose values of distutils
try:
# See verbose for description of the global option quiet.
# See code distutils.dist.Distribution.
_q_opts = self.distribution.get_option_dict('build_docx')['quiet'][1]
if _q_opts:
self.quiet += 1
except:
# fallback to the slightly erroneous behavior when the interface
# of distutils changes
if self.quiet == None:
self.quiet = 0
pass
if self.verbose != None:
if os.name == 'java':
# Jython - however - re-initializes the 'None' to '1',
# when missing verbose parameter
if not (set(sys.argv) & set(('-v', '--verbose'))):
self.verbose -= 1
if self.quiet != None:
# quiet dominates
self.quiet = True
self.verbose = 0
# debug
if self.debug == None:
self.debug = 0
# if self.verbose_ext == None:
# self.verbose_ext = 0
if self.abs:
self.abs = True
if self.print_env != None:
# environment
self.print_env = True
if self.print_ver != None:
# version
self.print_ver = True
if self.print_vinfo != None:
# sounding info
self.print_vinfo = True
if self.postfix == None:
self.postfix = '_cases'
#FIXME:
# print("4TEST:sys.implementation = " + str(self.implementation))
if not self.implementation:
# default is to use current called executable
self.implementation = sys.executable
# this is None in case of Jython!!!
if not sys.executable and os.name == 'java':
self.implementation = 'jython'
#
# check implementation to be used for tests via subprocess call
#
if (
#
# hard-coded for standard names for now
# egg-and-chicken for the process to be started
# not doing it twice :-)
#
self.implementation.endswith('jython')
or
self.implementation.endswith('jython.exe')
or
self.implementation.endswith('.jar')
):
self.calljy = True
if self.calljy:
# call Jython for subprocess - enables Jython and Java specific options
if self.jyjar == None:
if self.abs:
self.implementation = os.path.abspath('jython')
else:
self.implementation = 'jython'
elif self.jyjar:
if self.abs:
self.implementation = os.path.abspath('java') + ' -jar ' + str(self.jyjar)
else:
self.implementation = 'java -jar ' + str(self.jyjar)
else:
if self.abs:
self.implementation = os.path.abspath('java') + ' -jar jython.jar '
else:
self.implementation = 'java -jar jython.jar'
else:
# prohibit Jyhton and Java specific options
if(
self.jyjar
or
self.jyjvm
or
self.jyprop
):
# has options for Java
raise SetupTestXError("Java options require '-i=jython'")
if not self.testlib:
self.testlib = 'unittest discover'
if self.noexec != None:
self.noexec = True
#
# name of current package - serves as default for package subdiretory etc.
#
if self.name == None:
self.name = self.distribution.metadata.name
if not self.start:
if os.path.exists('tests' + os.sep + self.name):
# try literally
self.start = 'tests.' + self.name
elif os.path.exists('tests' + os.sep + 'py' + self.name + self.postfix):
# check for python specific
self.start = 'tests.py' + self.name
else:
# provoke the sounding error message
self.start = 'tests.' + self.name
if not self.prog:
self.prog = 'CallCase.py'
[docs] def run(self):
"""Run command."""
command = []
command.append(str(self.implementation))
if self.calljy:
if self.jyjvm:
command.append("-J" + str(self.jyjvm))
if self.jyprop:
command.append("-D" + str(self.jyprop))
if self.coff:
command.append("-Dpython.cachedir.skip=true")
for s in range(self.verbose):
# want each separate
command.append("-v")
if self.print_env:
if (PYVxyz&PYV27) == PYV27:
command.append('''-c "import os;\nfor k in sorted(os.environ.keys()):\n\tprint '%-30s = %s' % (str(k), str(os.environ[k]))"''')
else:
command.append('''-c "import os;\nfor k in sorted(os.environ.keys()):\n\tprint('%-30s = %s' % (str(k), str(os.environ[k])))"''')
elif self.print_ver:
command.append('''--version''')
elif self.print_vinfo:
if (PYVxyz&PYV27) == PYV27:
command.append('''-c "from pythonids.pythondist import PYDIST_DATA;print PYDIST_DATA.__str__()"''')
else:
command.append('''-c "from pythonids.pythondist import PYDIST_DATA;print(PYDIST_DATA.__str__())"''')
elif self.call:
command.append('-c "' + self.call + '"')
elif self.exe:
command.append('-c "' + self.exe + '"')
else:
command.append("-m " + str(self.testlib))
command.append("-s " + str(self.start))
command.append("-p " + str(self.prog))
if self.noexec:
print(' '.join(command))
exit_code = 0
else:
exit_code = os.system(' '.join(command))
sys.exit(exit_code)