From 890041c42ca0a71c5bb9550a97912fc9d9007d43 Mon Sep 17 00:00:00 2001 From: Mats Wichmann Date: Mon, 12 Oct 2020 06:58:44 -0600 Subject: Fix/update global AddMethod Fixes #3028 Signed-off-by: Mats Wichmann --- CHANGES.txt | 2 + SCons/Environment.py | 46 +++------------- SCons/Environment.xml | 54 +++++++------------ SCons/EnvironmentTests.py | 14 ++++- SCons/Util.py | 132 ++++++++++++++++++++++++++-------------------- 5 files changed, 119 insertions(+), 129 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index ae754ef..3a1e25c 100755 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -63,6 +63,8 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER - Make sure cProfile is used if profiling - SCons was expecting the Util module to monkeypatch in cProfile as profile if available, but this is no longer being done. + - Cleanup in Util.AddMethod; detect environment instances and add + them using MethodWrapper to fix #3028. MethodWrapper moves to Util. From Simon Tegelid - Fix using TEMPFILE in multiple actions in an action list. Previously a builder, or command diff --git a/SCons/Environment.py b/SCons/Environment.py index 3040427..cc62679 100644 --- a/SCons/Environment.py +++ b/SCons/Environment.py @@ -54,6 +54,7 @@ import SCons.SConsign import SCons.Subst import SCons.Tool import SCons.Util +from SCons.Util import MethodWrapper import SCons.Warnings class _Null: @@ -180,48 +181,17 @@ def _delete_duplicates(l, keep_last): # Shannon at the following page (there called the "transplant" class): # # ASPN : Python Cookbook : Dynamically added methods to a class -# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/81732 +# https://code.activestate.com/recipes/81732/ # # We had independently been using the idiom as BuilderWrapper, but # factoring out the common parts into this base class, and making # BuilderWrapper a subclass that overrides __call__() to enforce specific # Builder calling conventions, simplified some of our higher-layer code. +# +# Note: MethodWrapper moved to SCons.Util as it was needed there +# and otherwise we had a circular import problem. -class MethodWrapper: - """ - A generic Wrapper class that associates a method (which can - actually be any callable) with an object. As part of creating this - MethodWrapper object an attribute with the specified (by default, - the name of the supplied method) is added to the underlying object. - When that new "method" is called, our __call__() method adds the - object as the first argument, simulating the Python behavior of - supplying "self" on method calls. - - We hang on to the name by which the method was added to the underlying - base class so that we can provide a method to "clone" ourselves onto - a new underlying object being copied (without which we wouldn't need - to save that info). - """ - def __init__(self, object, method, name=None): - if name is None: - name = method.__name__ - self.object = object - self.method = method - self.name = name - setattr(self.object, name, self) - - def __call__(self, *args, **kwargs): - nargs = (self.object,) + args - return self.method(*nargs, **kwargs) - - def clone(self, new_object): - """ - Returns an object that re-binds the underlying "method" to - the specified new object. - """ - return self.__class__(new_object, self.method, self.name) - -class BuilderWrapper(MethodWrapper): +class BuilderWrapper(SCons.Util.MethodWrapper): """ A MethodWrapper subclass that that associates an environment with a Builder. @@ -248,7 +218,7 @@ class BuilderWrapper(MethodWrapper): target = [target] if source is not None and not SCons.Util.is_List(source): source = [source] - return MethodWrapper.__call__(self, target, source, *args, **kw) + return super().__call__(target, source, *args, **kw) def __repr__(self): return '' % repr(self.name) @@ -2377,7 +2347,7 @@ class OverrideEnvironment(Base): # Environment they are being constructed with and so will not # have access to overrided values. So we rebuild them with the # OverrideEnvironment so they have access to overrided values. - if isinstance(attr, (MethodWrapper, BuilderWrapper)): + if isinstance(attr, MethodWrapper): return attr.clone(self) else: return attr diff --git a/SCons/Environment.xml b/SCons/Environment.xml index bb9b90e..a1fd8ec 100644 --- a/SCons/Environment.xml +++ b/SCons/Environment.xml @@ -286,31 +286,22 @@ until the Action object is actually used. -When called with the -AddMethod() -form, -adds the specified -function -to the specified -object -as the specified method -name. -When called using the -&f-env-AddMethod; form, -adds the specified -function -to the construction environment -env -as the specified method -name. -In both cases, if -name -is omitted or -None, -the name of the -specified -function -itself is used for the method name. +Adds function to an object as a method. +function will be called with an instance +object as the first argument as for other methods. +If name is given, it is used as +the name of the new method, else the name of +function is used. + + +When the global function &f-AddMethod; is called, +the object to add the method to must be passed as the first argument; +typically this will be &Environment;, +in order to create a method which applies to all &consenvs; +subsequently constructed. +When called using the &f-env-AddMethod; form, +the method is added to the specified &consenv; only. +Added methods propagate through &f-env-Clone; calls. @@ -318,22 +309,17 @@ Examples: -# Note that the first argument to the function to -# be attached as a method must be the object through -# which the method will be called; the Python -# convention is to call it 'self'. +# Function to add must accept an instance argument. +# The Python convention is to call this 'self'. def my_method(self, arg): print("my_method() got", arg) -# Use the global AddMethod() function to add a method -# to the Environment class. This +# Use the global function to add a method to the Environment class: AddMethod(Environment, my_method) env = Environment() env.my_method('arg') -# Add the function as a method, using the function -# name for the method call. -env = Environment() +# Use the optional name argument to set the name of the method: env.AddMethod(my_method, 'other_method_name') env.other_method_name('another arg') diff --git a/SCons/EnvironmentTests.py b/SCons/EnvironmentTests.py index 8d2704d..e308865 100644 --- a/SCons/EnvironmentTests.py +++ b/SCons/EnvironmentTests.py @@ -721,7 +721,7 @@ sys.exit(0) r = env4.func2() assert r == 'func2-4', r - # Test that clones don't re-bind an attribute that the user + # Test that clones don't re-bind an attribute that the user set. env1 = Environment(FOO = '1') env1.AddMethod(func2) def replace_func2(): @@ -731,6 +731,18 @@ sys.exit(0) r = env2.func2() assert r == 'replace_func2', r + # Test clone rebinding if using global AddMethod. + env1 = Environment(FOO='1') + SCons.Util.AddMethod(env1, func2) + r = env1.func2() + assert r == 'func2-1', r + r = env1.func2('-xxx') + assert r == 'func2-1-xxx', r + env2 = env1.Clone(FOO='2') + r = env2.func2() + assert r == 'func2-2', r + + def test_Override(self): """Test overriding construction variables""" env = SubstitutionEnvironment(ONE=1, TWO=2, THREE=3, FOUR=4) diff --git a/SCons/Util.py b/SCons/Util.py index 347395f..88aeaae 100644 --- a/SCons/Util.py +++ b/SCons/Util.py @@ -27,23 +27,19 @@ import os import sys import copy import re -import types import pprint import hashlib from collections import UserDict, UserList, UserString, OrderedDict from collections.abc import MappingView +from types import MethodType, FunctionType PYPY = hasattr(sys, 'pypy_translation_info') -# Below not used? -# InstanceType = types.InstanceType - -MethodType = types.MethodType -FunctionType = types.FunctionType - -def dictify(keys, values, result={}): - for k, v in zip(keys, values): - result[k] = v +# unused? +def dictify(keys, values, result=None): + if result is None: + result = {} + result.update(dict(zip(keys, values))) return result _altsep = os.altsep @@ -633,6 +629,41 @@ class Delegate: else: return self + +class MethodWrapper: + """A generic Wrapper class that associates a method with an object. + + As part of creating this MethodWrapper object an attribute with the + specified name (by default, the name of the supplied method) is added + to the underlying object. When that new "method" is called, our + __call__() method adds the object as the first argument, simulating + the Python behavior of supplying "self" on method calls. + + We hang on to the name by which the method was added to the underlying + base class so that we can provide a method to "clone" ourselves onto + a new underlying object being copied (without which we wouldn't need + to save that info). + """ + def __init__(self, object, method, name=None): + if name is None: + name = method.__name__ + self.object = object + self.method = method + self.name = name + setattr(self.object, name, self) + + def __call__(self, *args, **kwargs): + nargs = (self.object,) + args + return self.method(*nargs, **kwargs) + + def clone(self, new_object): + """ + Returns an object that re-binds the underlying "method" to + the specified new object. + """ + return self.__class__(new_object, self.method, self.name) + + # attempt to load the windows registry module: can_read_reg = 0 try: @@ -1096,7 +1127,7 @@ def adjustixes(fname, pre, suf, ensure_suffix=False): # From Tim Peters, -# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52560 +# https://code.activestate.com/recipes/52560 # ASPN: Python Cookbook: Remove duplicates from a sequence # (Also in the printed Python Cookbook.) @@ -1170,9 +1201,8 @@ def unique(s): return u - # From Alex Martelli, -# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52560 +# https://code.activestate.com/recipes/52560 # ASPN: Python Cookbook: Remove duplicates from a sequence # First comment, dated 2001/10/13. # (Also in the printed Python Cookbook.) @@ -1375,42 +1405,41 @@ def make_path_relative(path): return path - -# The original idea for AddMethod() and RenameFunction() come from the +# The original idea for AddMethod() came from the # following post to the ActiveState Python Cookbook: # -# ASPN: Python Cookbook : Install bound methods in an instance -# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/223613 -# -# That code was a little fragile, though, so the following changes -# have been wrung on it: +# ASPN: Python Cookbook : Install bound methods in an instance +# https://code.activestate.com/recipes/223613 # +# Changed as follows: # * Switched the installmethod() "object" and "function" arguments, # so the order reflects that the left-hand side is the thing being # "assigned to" and the right-hand side is the value being assigned. -# -# * Changed explicit type-checking to the "try: klass = object.__class__" -# block in installmethod() below so that it still works with the -# old-style classes that SCons uses. -# -# * Replaced the by-hand creation of methods and functions with use of -# the "new" module, as alluded to in Alex Martelli's response to the -# following Cookbook post: -# -# ASPN: Python Cookbook : Dynamically added methods to a class -# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/81732 +# * The instance/class detection is changed a bit, as it's all +# new-style classes now with Py3. +# * The by-hand construction of the function object from renamefunction() +# is not needed, the remaining bit is now used inline in AddMethod. def AddMethod(obj, function, name=None): - """ - Adds either a bound method to an instance or the function itself (or an unbound method in Python 2) to a class. - If name is ommited the name of the specified function - is used by default. + """Adds a method to an object. + + Adds `function` to `obj` if `obj` is a class object. + Adds `function` as a bound method if `obj` is an instance object. + If `obj` looks like an environment instance, use `MethodWrapper` + to add it. If `name` is supplied it is used as the name of `function`. + + Although this works for any class object, the intent as a public + API is to be used on Environment, to be able to add a method to all + construction environments; it is preferred to use env.AddMethod + to add to an individual environment. Example:: + class A: + ... a = A() def f(self, x, y): - self.z = x + y + self.z = x + y AddMethod(f, A, "add") a.add(2, 4) print(a.z) @@ -1420,32 +1449,24 @@ def AddMethod(obj, function, name=None): if name is None: name = function.__name__ else: - function = RenameFunction(function, name) + # "rename" + function = FunctionType( + function.__code__, function.__globals__, name, function.__defaults__ + ) - # Note the Python version checks - WLB - # Python 3.3 dropped the 3rd parameter from types.MethodType if hasattr(obj, '__class__') and obj.__class__ is not type: - # "obj" is an instance, so it gets a bound method. - if sys.version_info[:2] > (3, 2): - method = MethodType(function, obj) + # obj is an instance, so it gets a bound method. + if hasattr(obj, "added_methods"): + method = MethodWrapper(obj, function, name) + obj.added_methods.append(method) else: - method = MethodType(function, obj, obj.__class__) + method = MethodType(function, obj) else: - # Handle classes + # obj is a class method = function setattr(obj, name, method) -def RenameFunction(function, name): - """ - Returns a function identical to the specified function, but with - the specified name. - """ - return FunctionType(function.__code__, - function.__globals__, - name, - function.__defaults__) - if hasattr(hashlib, 'md5'): md5 = True @@ -1523,8 +1544,7 @@ def silent_intern(x): # From Dinu C. Gherman, # Python Cookbook, second edition, recipe 6.17, p. 277. -# Also: -# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/68205 +# Also: https://code.activestate.com/recipes/68205 # ASPN: Python Cookbook: Null Object Design Pattern class Null: -- cgit v0.12 From 7577f9454ceabb40b961301e062f3c197173f8ca Mon Sep 17 00:00:00 2001 From: Mats Wichmann Date: Wed, 14 Oct 2020 21:02:00 -0600 Subject: Update CHANGES.txt for AddMethod changes. [ci skip] Signed-off-by: Mats Wichmann --- CHANGES.txt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 3a1e25c..602e689 100755 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -63,8 +63,11 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER - Make sure cProfile is used if profiling - SCons was expecting the Util module to monkeypatch in cProfile as profile if available, but this is no longer being done. - - Cleanup in Util.AddMethod; detect environment instances and add - them using MethodWrapper to fix #3028. MethodWrapper moves to Util. + - Cleanup in SCons.Util.AddMethod. If called with an environment instance + as the object to modify, the method would not be correctly set up in + any Clone of that instance. Now tries to detect this and calls + MethodWrapper to set up the method the same way env.AddMethod does. + MethodWrapper moved to Util to avoid a circular import. Fixes #3028. From Simon Tegelid - Fix using TEMPFILE in multiple actions in an action list. Previously a builder, or command -- cgit v0.12 From dabd7986f70ef050228d1ee8e4ace67419d7efc0 Mon Sep 17 00:00:00 2001 From: Mats Wichmann Date: Mon, 26 Oct 2020 14:56:03 -0600 Subject: Fix a sider complaint in EnvironmentTests.py Signed-off-by: Mats Wichmann --- SCons/EnvironmentTests.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/SCons/EnvironmentTests.py b/SCons/EnvironmentTests.py index e308865..53dd9a7 100644 --- a/SCons/EnvironmentTests.py +++ b/SCons/EnvironmentTests.py @@ -33,7 +33,13 @@ from collections import UserDict as UD, UserList as UL import TestCmd import TestUnit -from SCons.Environment import * +from SCons.Environment import ( + Environment, + NoSubstitutionProxy, + OverrideEnvironment, + SubstitutionEnvironment, + is_valid_construction_var, +) import SCons.Warnings def diff_env(env1, env2): -- cgit v0.12 From 320981180be9b3056eb362dbb0a688324b8853ba Mon Sep 17 00:00:00 2001 From: Mats Wichmann Date: Wed, 4 Nov 2020 12:34:07 -0700 Subject: Convert test runner to use argparse Formerly used optparse + homegrown, with a comment stating migration should be done (argprse now has superseded optparse). Cleaned up a few stray bits that weren't used any longer. Collects the changes intended for os.environ into a local dict, which is then used to make a single update. Signed-off-by: Mats Wichmann --- CHANGES.txt | 2 + runtest.py | 617 ++++++++++++++++++++++++++++-------------------------------- 2 files changed, 288 insertions(+), 331 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 01a7ee7..e58ea8f 100755 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -71,6 +71,8 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER MethodWrapper to set up the method the same way env.AddMethod does. MethodWrapper moved to Util to avoid a circular import. Fixes #3028. - Some Python 2 compatibility code dropped + - Rework runtest.py to use argparse for arg handling (was a mix + of hand-coded and optparse, with a stated intent to "gradually port") From Simon Tegelid - Fix using TEMPFILE in multiple actions in an action list. Previously a builder, or command diff --git a/runtest.py b/runtest.py index f751212..ad2554a 100755 --- a/runtest.py +++ b/runtest.py @@ -2,63 +2,19 @@ # # Copyright The SCons Foundation # -# runtest.py - wrapper script for running SCons tests -# -# The SCons test suite consists of: -# -# - unit tests - included in *Tests.py files from SCons/ dir -# - end-to-end tests - these are *.py files in test/ directory that -# require custom SCons framework from testing/ -# -# This script adds SCons/ and testing/ directories to PYTHONPATH, -# performs test discovery and processes them according to options. +"""runtest - wrapper script for running SCons tests -""" -Options: - -a --all Run all tests. - -b --baseline BASE Run test scripts against baseline BASE. - -d --debug Run test scripts under the Python debugger. - -D --devmode Run tests in Python's development mode (3.7+ only) - --e2e-only Run only the end-to-end tests - -e --external Run the script in external mode (for external Tools) - -f --file FILE Only run tests listed in FILE. - -j --jobs JOBS Run tests in JOBS parallel jobs. - -k --no-progress Suppress count and percent progress messages. - -l --list List available tests and exit. - -n --no-exec No execute, just print command lines. - --nopipefiles Do not use the "file pipe" workaround for Popen() - for starting tests. WARNING: use only when too much - file traffic is giving you trouble AND you can be - sure that none of your tests create output >65K - chars! You might run into some deadlocks else. - -o --output FILE Save the output from a test run to the log file. - -P PYTHON Use the specified Python interpreter. - --passed Summarize which tests passed. - -q --quiet Don't print the test being executed. - --quit-on-failure Quit on any test failure. - --runner CLASS Alternative test runner class for unit tests. - -s --short-progress Short progress, prints only the command line. - and a percentage value, based on the total and - current number of tests. - -t --time Print test execution time. - --unit-only Run only the unit tests - -v VERSION Specify the SCons version. - --verbose=LEVEL Set verbose level: - 1 = print executed commands, - 2 = print commands and non-zero output, - 3 = print commands and all output. - -X Test script is executable, don't feed to Python. - -x --exec SCRIPT Test SCRIPT. - --xml file Save results to file in SCons XML format. - --exclude-list FILE List of tests to exclude in the current selection. - Use to exclude tests when using the -a option. +The SCons test suite consists of: -Environment Variables: - PRESERVE, PRESERVE_{PASS,FAIL,NO_RESULT}: preserve test subdirs - TESTCMD_VERBOSE: turn on verbosity in TestCommand + * unit tests - *Tests.py files from the SCons/ dir + * end-to-end tests - *.py files in the test/ directory that + require the custom SCons framework from testing/ + +This script adds SCons/ and testing/ directories to PYTHONPATH, +performs test discovery and processes tests according to options. """ -import getopt +import argparse import glob import os import re @@ -69,171 +25,177 @@ import tempfile import threading import time from abc import ABC, abstractmethod -from optparse import OptionParser, BadOptionError +from pathlib import Path from queue import Queue cwd = os.getcwd() -baseline = None -external = 0 -devmode = False -debug = '' -execute_tests = True -jobs = 1 -list_only = False -printcommand = True -print_passed_summary = False +debug = None scons = None -scons_exec = False -testlistfile = None -version = '' -print_times = False -python = None -print_progress = True catch_output = False suppress_output = False -allow_pipe_files = True -quit_on_failure = False -excludelistfile = None -e2e_only = unit_only = False -script = sys.argv[0].split("/")[-1] +script = os.path.basename(sys.argv[0]) usagestr = """\ -Usage: %(script)s [OPTIONS] [TEST ...] +%(script)s [OPTIONS] [TEST ...] %(script)s -h|--help """ % locals() -helpstr = usagestr + __doc__ - -# "Pass-through" option parsing -- an OptionParser that ignores -# unknown options and lets them pile up in the leftover argument -# list. Useful to gradually port getopt to optparse. +epilogstr = """\ +Environment Variables: + PRESERVE, PRESERVE_{PASS,FAIL,NO_RESULT}: preserve test subdirs + TESTCMD_VERBOSE: turn on verbosity in TestCommand\ +""" -class PassThroughOptionParser(OptionParser): - def _process_long_opt(self, rargs, values): - try: - OptionParser._process_long_opt(self, rargs, values) - except BadOptionError as err: - self.largs.append(err.opt_str) - def _process_short_opts(self, rargs, values): - try: - OptionParser._process_short_opts(self, rargs, values) - except BadOptionError as err: - self.largs.append(err.opt_str) - -parser = PassThroughOptionParser(add_help_option=False) -parser.add_option('-a', '--all', action='store_true', help="Run all tests.") -parser.add_option('-o', '--output', - help="Save the output from a test run to the log file.") -parser.add_option('--runner', metavar='class', - help="Test runner class for unit tests.") -parser.add_option('--xml', help="Save results to file in SCons XML format.") -(options, args) = parser.parse_args() - -# print("options:", options) -# print("args:", args) - - -opts, args = getopt.getopt( - args, - "b:dDef:hj:klnP:p:qsv:Xx:t", - [ - "baseline=", - "debug", - "devmode", - "external", - "file=", - "help", - "no-progress", - "jobs=", - "list", - "no-exec", - "nopipefiles", - "passed", - "python=", - "quiet", - "quit-on-failure", - "short-progress", - "time", - "version=", - "exec=", - "verbose=", - "exclude-list=", - "e2e-only", - "unit-only", - ], +parser = argparse.ArgumentParser( + usage=usagestr, epilog=epilogstr, allow_abbrev=False, + formatter_class=argparse.RawDescriptionHelpFormatter ) -for o, a in opts: - if o in ['-b', '--baseline']: - baseline = a - elif o in ['-d', '--debug']: - for d in sys.path: - pdb = os.path.join(d, 'pdb.py') - if os.path.exists(pdb): - debug = pdb - break - elif o in ['-D', '--devmode']: - devmode = True - elif o in ['-e', '--external']: - external = True - elif o in ['-f', '--file']: - if not os.path.isabs(a): - a = os.path.join(cwd, a) - testlistfile = a - elif o in ['-h', '--help']: - print(helpstr) - sys.exit(0) - elif o in ['-j', '--jobs']: - jobs = int(a) - # don't let tests write stdout/stderr directly if multi-job, - # or outputs will interleave and be hard to read - catch_output = True - elif o in ['-k', '--no-progress']: - print_progress = False - elif o in ['-l', '--list']: - list_only = True - elif o in ['-n', '--no-exec']: - execute_tests = False - elif o in ['--nopipefiles']: - allow_pipe_files = False - elif o in ['--passed']: - print_passed_summary = True - elif o in ['-P', '--python']: - python = a - elif o in ['-q', '--quiet']: - printcommand = False - suppress_output = catch_output = True - elif o in ['--quit-on-failure']: - quit_on_failure = True - elif o in ['-s', '--short-progress']: - print_progress = True - suppress_output = catch_output = True - elif o in ['-t', '--time']: - print_times = True - elif o in ['--verbose']: - os.environ['TESTCMD_VERBOSE'] = a - elif o in ['-v', '--version']: - version = a - elif o in ['-X']: - scons_exec = True - elif o in ['-x', '--exec']: - scons = a - elif o in ['--exclude-list']: - excludelistfile = a - elif o in ['--e2e-only']: - e2e_only = True - elif o in ['--unit-only']: - unit_only = True - - -class Unbuffered: - """ class to arrange for stdout/stderr to be unbuffered """ +# test selection options: +testsel = parser.add_argument_group(description='Test selection options:') +testsel.add_argument(metavar='TEST', nargs='*', dest='testlist', + help="Select TEST(s) (tests and/or directories) to run") +testlisting = testsel.add_mutually_exclusive_group() +testlisting.add_argument('-f', '--file', metavar='FILE', dest='testlistfile', + help="Select only tests in FILE") +testlisting.add_argument('-a', '--all', action='store_true', + help="Select all tests") +testsel.add_argument('--exclude-list', metavar="FILE", dest='excludelistfile', + help="""Exclude tests in FILE from current selection""") +testtype = testsel.add_mutually_exclusive_group() +testtype.add_argument('--e2e-only', action='store_true', + help="Exclude unit tests from selection") +testtype.add_argument('--unit-only', action='store_true', + help="Exclude end-to-end tests from selection") + +# miscellaneous options +parser.add_argument('-b', '--baseline', metavar='BASE', + help="Run test scripts against baseline BASE.") +parser.add_argument('-d', '--debug', action='store_true', + help="Run test scripts under the Python debugger.") +parser.add_argument('-D', '--devmode', action='store_true', + help="Run tests in Python's development mode (Py3.7+ only).") +parser.add_argument('-e', '--external', action='store_true', + help="Run the script in external mode (for external Tools)") +parser.add_argument('-j', '--jobs', metavar='JOBS', default=1, type=int, + help="Run tests in JOBS parallel jobs.") +parser.add_argument('-l', '--list', action='store_true', dest='list_only', + help="List available tests and exit.") +parser.add_argument('-n', '--no-exec', action='store_false', + dest='execute_tests', + help="No execute, just print command lines.") +parser.add_argument('--nopipefiles', action='store_false', + dest='allow_pipe_files', + help="""Do not use the "file pipe" workaround for Popen() + for starting tests. WARNING: use only when too much + file traffic is giving you trouble AND you can be + sure that none of your tests create output >65K + chars! You might run into some deadlocks else.""") +parser.add_argument('-P', '--python', metavar='PYTHON', + help="Use the specified Python interpreter.") +parser.add_argument('--quit-on-failure', action='store_true', + help="Quit on any test failure.") +parser.add_argument('--runner', metavar='CLASS', + help="Test runner class for unit tests.") +parser.add_argument('-X', dest='scons_exec', action='store_true', + help="Test script is executable, don't feed to Python.") +parser.add_argument('-x', '--exec', metavar="SCRIPT", + help="Test using SCRIPT as path to SCons.") + +outctl = parser.add_argument_group(description='Output control options:') +outctl.add_argument('-k', '--no-progress', action='store_false', + dest='print_progress', + help="Suppress count and progress percentage messages.") +outctl.add_argument('--passed', action='store_true', + dest='print_passed_summary', + help="Summarize which tests passed.") +outctl.add_argument('-q', '--quiet', action='store_false', + dest='printcommand', + help="Don't print the test being executed.") +outctl.add_argument('-s', '--short-progress', action='store_true', + help="""Short progress, prints only the command line + and a progress percentage.""") +outctl.add_argument('-t', '--time', action='store_true', dest='print_times', + help="Print test execution time.") +outctl.add_argument('--verbose', metavar='LEVEL', type=int, choices=range(1, 4), + help="""Set verbose level: + 1 = print executed commands, + 2 = print commands and non-zero output, + 3 = print commands and all output.""") + +logctl = parser.add_argument_group(description='Log control options:') +logctl.add_argument('-o', '--output', metavar='LOG', help="Save console output to LOG.") +logctl.add_argument('--xml', metavar='XML', help="Save results to XML in SCons XML format.") + +# process args and handle a few specific cases: +args = parser.parse_args() + +# we can't do this check with an argparse exclusive group, +# since the cmdline tests (args.testlist) are not optional +if args.testlist and (args.testlistfile or args.all): + sys.stderr.write( + parser.format_usage() + + "error: command line tests cannot be combined with -f/--file or -a/--all\n" + ) + sys.exit(1) + +if args.testlistfile: + try: + p = Path(args.testlistfile) + args.testlistfile = p.resolve(strict=True) + except FileNotFoundError: + sys.stderr.write( + parser.format_usage() + + "error: -f/--file testlist file \"%s\" not found\n" % p + ) + sys.exit(1) + +if args.excludelistfile: + try: + p = Path(args.excludelistfile) + args.excludelistfile = p.resolve(strict=True) + except FileNotFoundError: + sys.stderr.write( + parser.format_usage() + + "error: --exclude-list file \"%s\" not found\n" % p + ) + sys.exit(1) + +if args.jobs > 1: + # don't let tests write stdout/stderr directly if multi-job, + # else outputs will interleave and be hard to read + catch_output = True + +if not args.printcommand: + suppress_output = catch_output = True + +if args.verbose: + os.environ['TESTCMD_VERBOSE'] = str(args.verbose) + +if args.short_progress: + args.print_progress = True + suppress_output = catch_output = True + +if args.debug: + for d in sys.path: + pdb = os.path.join(d, 'pdb.py') + if os.path.exists(pdb): + debug = pdb + break + +if args.exec: + scons = args.exec + +# --- setup stdout/stderr --- +class Unbuffered(): def __init__(self, file): self.file = file + def write(self, arg): self.file.write(arg) self.file.flush() + def __getattr__(self, attr): return getattr(self.file, attr) @@ -243,9 +205,9 @@ sys.stderr = Unbuffered(sys.stderr) # possible alternative: switch to using print, and: # print = functools.partial(print, flush) -if options.output: - logfile = open(options.output, 'w') - class Tee: +if args.output: + logfile = open(args.output, 'w') + class Tee(): def __init__(self, openfile, stream): self.file = openfile self.stream = stream @@ -257,6 +219,7 @@ if options.output: # --- define helpers ---- if sys.platform in ('win32', 'cygwin'): + def whereis(file): pathext = [''] + os.environ['PATHEXT'].split(os.pathsep) for d in os.environ['PATH'].split(os.pathsep): @@ -268,6 +231,7 @@ if sys.platform in ('win32', 'cygwin'): return None else: + def whereis(file): for d in os.environ['PATH'].split(os.pathsep): f = os.path.join(d, file) @@ -293,12 +257,14 @@ def escape(s): if not catch_output: # Without any output suppressed, we let the subprocess # write its stuff freely to stdout/stderr. + def spawn_it(command_args, env): cp = subprocess.run(command_args, shell=False, env=env) return cp.stdout, cp.stderr, cp.returncode + else: # Else, we catch the output of both pipes... - if allow_pipe_files: + if args.allow_pipe_files: # The subprocess.Popen() suffers from a well-known # problem. Data for stdout/stderr is read into a # memory buffer of fixed size, 65K which is not very much. @@ -307,19 +273,22 @@ else: # be able to write the rest of its output. Hang! # In order to work around this, we follow a suggestion # by Anders Pearson in - # http://http://thraxil.org/users/anders/posts/2008/03/13/Subprocess-Hanging-PIPE-is-your-enemy/ + # https://thraxil.org/users/anders/posts/2008/03/13/Subprocess-Hanging-PIPE-is-your-enemy/ # and pass temp file objects to Popen() instead of the ubiquitous # subprocess.PIPE. + def spawn_it(command_args, env): # Create temporary files tmp_stdout = tempfile.TemporaryFile(mode='w+t') tmp_stderr = tempfile.TemporaryFile(mode='w+t') # Start subprocess... - cp = subprocess.run(command_args, - stdout=tmp_stdout, - stderr=tmp_stderr, - shell=False, - env=env) + cp = subprocess.run( + command_args, + stdout=tmp_stdout, + stderr=tmp_stderr, + shell=False, + env=env, + ) try: # Rewind to start of files @@ -349,12 +318,15 @@ else: # (but the subprocess isn't writing anything there). # Hence a deadlock. # Be dragons here! Better don't use this! + def spawn_it(command_args, env): - cp = subprocess.run(command_args, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - shell=False, - env=env) + cp = subprocess.run( + command_args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=False, + env=env, + ) return cp.stdout, cp.stderr, cp.returncode @@ -401,17 +373,20 @@ class PopenExecutor(RuntestBase): # For an explanation of the following 'if ... else' # and the 'allow_pipe_files' option, please check out the # definition of spawn_it() above. - if allow_pipe_files: + if args.allow_pipe_files: + def execute(self, env): # Create temporary files tmp_stdout = tempfile.TemporaryFile(mode='w+t') tmp_stderr = tempfile.TemporaryFile(mode='w+t') # Start subprocess... - cp = subprocess.run(self.command_str.split(), - stdout=tmp_stdout, - stderr=tmp_stderr, - shell=False, - env=env) + cp = subprocess.run( + self.command_str.split(), + stdout=tmp_stdout, + stderr=tmp_stderr, + shell=False, + env=env, + ) self.status = cp.returncode try: @@ -426,15 +401,16 @@ class PopenExecutor(RuntestBase): tmp_stdout.close() tmp_stderr.close() else: + def execute(self, env): - cp = subprocess.run(self.command_str.split(), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - shell=False, - env=env) - self.status = cp.returncode - self.stdout = cp.stdout - self.stderr = cp.stderr + cp = subprocess.run( + self.command_str.split(), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=False, + env=env, + ) + self.status, self.stdout, self.stderr = cp.returncode, cp.stdout, cp.stderr class XML(PopenExecutor): """ Test class for tests that will output in scons xml """ @@ -456,85 +432,68 @@ class XML(PopenExecutor): f.write(' \n' % self.total_time) f.write(' \n') -if options.xml: +if args.xml: Test = XML else: Test = SystemExecutor # --- start processing --- -sd = None -tools_dir = None -ld = None - -if not baseline or baseline == '.': - base = cwd -elif baseline == '-': +if not args.baseline or args.baseline == '.': + baseline = cwd +elif args.baseline == '-': print("This logic used to checkout from svn. It's been removed. If you used this, please let us know on devel mailing list, IRC, or discord server") sys.exit(-1) else: - base = baseline - -scons_runtest_dir = base + baseline = args.baseline +scons_runtest_dir = baseline -if not external: - scons_script_dir = sd or os.path.join(base, 'scripts') - scons_tools_dir = tools_dir or os.path.join(base, 'bin') - scons_lib_dir = ld or base +if not args.external: + scons_script_dir = os.path.join(baseline, 'scripts') + scons_tools_dir = os.path.join(baseline, 'bin') + scons_lib_dir = baseline else: - scons_script_dir = sd or '' - scons_tools_dir = tools_dir or '' - scons_lib_dir = ld or '' + scons_script_dir = '' + scons_tools_dir = '' + scons_lib_dir = '' -pythonpath_dir = scons_lib_dir +testenv = { + 'SCONS_RUNTEST_DIR': scons_runtest_dir, + 'SCONS_TOOLS_DIR': scons_tools_dir, + 'SCONS_SCRIPT_DIR': scons_script_dir, + 'SCONS_CWD': cwd, +} if scons: # Let the version of SCons that the -x option pointed to find # its own modules. - os.environ['SCONS'] = scons + testenv['SCONS'] = scons elif scons_lib_dir: # Because SCons is really aggressive about finding its modules, # it sometimes finds SCons modules elsewhere on the system. # This forces SCons to use the modules that are being tested. - os.environ['SCONS_LIB_DIR'] = scons_lib_dir + testenv['SCONS_LIB_DIR'] = scons_lib_dir -if scons_exec: - os.environ['SCONS_EXEC'] = '1' +if args.scons_exec: + testenv['SCONS_EXEC'] = '1' -if external: - os.environ['SCONS_EXTERNAL_TEST'] = '1' +if args.external: + testenv['SCONS_EXTERNAL_TEST'] = '1' -os.environ['SCONS_RUNTEST_DIR'] = scons_runtest_dir -os.environ['SCONS_SCRIPT_DIR'] = scons_script_dir -os.environ['SCONS_TOOLS_DIR'] = scons_tools_dir -os.environ['SCONS_CWD'] = cwd -os.environ['SCONS_VERSION'] = version +# Insert scons path and path for testing framework to PYTHONPATH +scriptpath = os.path.dirname(os.path.realpath(__file__)) +frameworkpath = os.path.join(scriptpath, 'testing', 'framework') +testenv['PYTHONPATH'] = os.pathsep.join((scons_lib_dir, frameworkpath)) +pythonpath = os.environ.get('PYTHONPATH') +if pythonpath: + testenv['PYTHONPATH'] = testenv['PYTHONPATH'] + os.pathsep + pythonpath -old_pythonpath = os.environ.get('PYTHONPATH') +os.environ.update(testenv) # Clear _JAVA_OPTIONS which java tools output to stderr when run breaking tests if '_JAVA_OPTIONS' in os.environ: del os.environ['_JAVA_OPTIONS'] -# FIXME: the following is necessary to pull in half of the testing -# harness from $srcdir/etc. Those modules should be transfered -# to testing/, in which case this manipulation of PYTHONPATH -# should be able to go away. -pythonpaths = [pythonpath_dir] - -scriptpath = os.path.dirname(os.path.realpath(__file__)) - -# Add path for testing framework to PYTHONPATH -pythonpaths.append(os.path.join(scriptpath, 'testing', 'framework')) - - -os.environ['PYTHONPATH'] = os.pathsep.join(pythonpaths) - -if old_pythonpath: - os.environ['PYTHONPATH'] = os.environ['PYTHONPATH'] + \ - os.pathsep + \ - old_pythonpath - # ---[ test discovery ]------------------------------------ # This section figures which tests to run. @@ -559,12 +518,9 @@ if old_pythonpath: # # Test exclusions, if specified, are then applied. -unittests = [] -endtests = [] - def scanlist(testlist): - """Process a testlist file""" + """ Process a testlist file """ tests = [t.strip() for t in testlist if not t.startswith('#')] return [t for t in tests if t] @@ -585,7 +541,6 @@ def find_unit_tests(directory): def find_e2e_tests(directory): """ Look for end-to-end tests """ result = [] - for dirpath, dirnames, filenames in os.walk(directory): # Skip folders containing a sconstest.skip file if 'sconstest.skip' in filenames: @@ -602,25 +557,24 @@ def find_e2e_tests(directory): # initial selection: -if testlistfile: - with open(testlistfile, 'r') as f: +unittests = [] +endtests = [] +if args.testlistfile: + with args.testlistfile.open() as f: tests = scanlist(f) else: testpaths = [] - if options.all: + if args.all: testpaths = ['SCons', 'test'] - elif args: - testpaths = args + elif args.testlist: + testpaths = args.testlist for tp in testpaths: # Clean up path so it can match startswith's below - # sys.stderr.write("Changed:%s->"%tp) # remove leading ./ or .\ if tp.startswith('.') and tp[1] in (os.sep, os.altsep): tp = tp[2:] - # tp = os.path.normpath(tp) - # sys.stderr.write('->%s<-'%tp) - # sys.stderr.write("to:%s\n"%tp) + for path in glob.glob(tp): if os.path.isdir(path): if path.startswith(('SCons', 'testing')): @@ -632,46 +586,49 @@ else: unittests.append(path) elif path.endswith(".py"): endtests.append(path) - tests = unittests + endtests + tests = sorted(unittests + endtests) + # Remove exclusions: -if e2e_only: +if args.e2e_only: tests = [t for t in tests if not t.endswith("Tests.py")] -if unit_only: +if args.unit_only: tests = [t for t in tests if t.endswith("Tests.py")] -if excludelistfile: - with open(excludelistfile, 'r') as f: +if args.excludelistfile: + with args.excludelistfile.open() as f: excludetests = scanlist(f) tests = [t for t in tests if t not in excludetests] if not tests: - sys.stderr.write(usagestr + """ -runtest: no tests were found. - Tests can be specified on the command line, read from a file with - the -f/--file option, or discovered with -a/--all to run all tests. + sys.stderr.write(parser.format_usage() + """ +error: no tests were found. + Tests can be specified on the command line, read from a file with + the -f/--file option, or discovered with -a/--all to run all tests. """) sys.exit(1) - # ---[ test processing ]----------------------------------- tests = [Test(t, n + 1) for n, t in enumerate(tests)] -if list_only: +if args.list_only: for t in tests: sys.stdout.write(t.path + "\n") sys.exit(0) -if not python: +if not args.python: if os.name == 'java': - python = os.path.join(sys.prefix, 'jython') + args.python = os.path.join(sys.prefix, 'jython') else: - python = sys.executable -os.environ["python_executable"] = python + args.python = sys.executable +os.environ["python_executable"] = args.python + +if args.print_times: -if print_times: def print_time(fmt, tm): sys.stdout.write(fmt % tm) + else: + def print_time(fmt, tm): pass @@ -710,7 +667,7 @@ def log_result(t, io_lock=None): if io_lock: io_lock.release() - if quit_on_failure and t.status == 1: + if args.quit_on_failure and t.status == 1: print("Exiting due to error") print(t.status) sys.exit(1) @@ -733,19 +690,18 @@ def run_test(t, io_lock=None, run_async=True): command_args = [] if debug: command_args.append(debug) - if devmode and sys.version_info >= (3, 7, 0): - command_args.append('-X dev') + if args.devmode and sys.version_info >= (3, 7, 0): + command_args.append('-X dev') command_args.append(t.path) - if options.runner and t.path in unittests: + if args.runner and t.path in unittests: # For example --runner TestUnit.TAPTestRunner - command_args.append('--runner ' + options.runner) - t.command_args = [escape(python)] + command_args + command_args.append('--runner ' + args.runner) + t.command_args = [escape(args.python)] + command_args t.command_str = " ".join(t.command_args) - if printcommand: - if print_progress: + if args.printcommand: + if args.print_progress: t.headline += "%d/%d (%.2f%s) %s\n" % ( - t.num, - total_num_tests, + t.num, total_num_tests, float(t.num) * 100.0 / float(total_num_tests), "%", t.command_str, @@ -768,7 +724,7 @@ def run_test(t, io_lock=None, run_async=True): env['FIXTURE_DIRS'] = os.pathsep.join(fixture_dirs) test_start_time = time_func() - if execute_tests: + if args.execute_tests: t.execute(env) t.test_time = time_func() - test_start_time @@ -776,12 +732,11 @@ def run_test(t, io_lock=None, run_async=True): class RunTest(threading.Thread): - """ Test Runner class + """ Test Runner class. One instance will be created for each job thread in multi-job mode """ - def __init__(self, queue=None, io_lock=None, - group=None, target=None, name=None, args=(), kwargs=None): + def __init__(self, queue=None, io_lock=None, group=None, target=None, name=None): super().__init__(group=group, target=target, name=name) self.queue = queue self.io_lock = io_lock @@ -791,14 +746,14 @@ class RunTest(threading.Thread): run_test(t, io_lock=self.io_lock, run_async=True) self.queue.task_done() -if jobs > 1: - print("Running tests using %d jobs" % jobs) +if args.jobs > 1: + print("Running tests using %d jobs" % args.jobs) testq = Queue() for t in tests: testq.put(t) testlock = threading.Lock() # Start worker threads to consume the queue - threads = [RunTest(queue=testq, io_lock=testlock) for _ in range(jobs)] + threads = [RunTest(queue=testq, io_lock=testlock) for _ in range(args.jobs)] for t in threads: t.daemon = True t.start() @@ -817,8 +772,8 @@ passed = [t for t in tests if t.status == 0] fail = [t for t in tests if t.status == 1] no_result = [t for t in tests if t.status == 2] -if len(tests) != 1 and execute_tests: - if passed and print_passed_summary: +if len(tests) != 1 and args.execute_tests: + if passed and args.print_passed_summary: if len(passed) == 1: sys.stdout.write("\nPassed the following test:\n") else: @@ -840,21 +795,21 @@ if len(tests) != 1 and execute_tests: paths = [x.path for x in no_result] sys.stdout.write("\t" + "\n\t".join(paths) + "\n") -if options.xml: - if options.xml == '-': +if args.xml: + if args.output == '-': f = sys.stdout else: - f = open(options.xml, 'w') + f = open(args.xml, 'w') tests[0].header(f) #f.write("test_result = [\n") for t in tests: t.write(f) tests[0].footer(f) #f.write("];\n") - if options.xml != '-': + if args.output != '-': f.close() -if options.output: +if args.output: if isinstance(sys.stdout, Tee): sys.stdout.file.close() if isinstance(sys.stderr, Tee): -- cgit v0.12 From f6ccf5e58ff13c0ff0482dcaa7c77aa1b0eb26dc Mon Sep 17 00:00:00 2001 From: Mats Wichmann Date: Thu, 5 Nov 2020 08:50:02 -0700 Subject: [PR #3821] accomodate Py3.5 in pathlib use the resolve() method of a Path objects takes a "strict" argument which was not defined until Py3.6. But the older behavior was "strict", so just add a version check here. Signed-off-by: Mats Wichmann --- runtest.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/runtest.py b/runtest.py index ad2554a..95dfbb1 100755 --- a/runtest.py +++ b/runtest.py @@ -15,6 +15,7 @@ performs test discovery and processes tests according to options. """ import argparse +import functools import glob import os import re @@ -143,7 +144,10 @@ if args.testlist and (args.testlistfile or args.all): if args.testlistfile: try: p = Path(args.testlistfile) - args.testlistfile = p.resolve(strict=True) + if sys.version_info.major == 3 and sys.version_info.minor < 6: + args.testlistfile = p.resolve() + else: + args.testlistfile = p.resolve(strict=True) except FileNotFoundError: sys.stderr.write( parser.format_usage() @@ -154,7 +158,11 @@ if args.testlistfile: if args.excludelistfile: try: p = Path(args.excludelistfile) - args.excludelistfile = p.resolve(strict=True) + if sys.version_info.major == 3 and sys.version_info.minor < 6: + args.testlistfile = p.resolve() + else: + args.testlistfile = p.resolve(strict=True) + args.excludelistfile = p.myresolve() except FileNotFoundError: sys.stderr.write( parser.format_usage() -- cgit v0.12 From 8a67085f0e0f2dc3425aecebe240245a9f5c3f45 Mon Sep 17 00:00:00 2001 From: Mats Wichmann Date: Thu, 5 Nov 2020 09:24:23 -0700 Subject: [PR #3821] fix editing error Signed-off-by: Mats Wichmann --- runtest.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/runtest.py b/runtest.py index 95dfbb1..2771bf2 100755 --- a/runtest.py +++ b/runtest.py @@ -15,7 +15,6 @@ performs test discovery and processes tests according to options. """ import argparse -import functools import glob import os import re @@ -162,7 +161,6 @@ if args.excludelistfile: args.testlistfile = p.resolve() else: args.testlistfile = p.resolve(strict=True) - args.excludelistfile = p.myresolve() except FileNotFoundError: sys.stderr.write( parser.format_usage() -- cgit v0.12 From 4d139db71661b8f0377f44a4af60a9b67f20bd0e Mon Sep 17 00:00:00 2001 From: Mats Wichmann Date: Thu, 5 Nov 2020 15:58:07 -0700 Subject: [PR #3851] fix cut-n-paste error in previous fix Signed-off-by: Mats Wichmann --- runtest.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/runtest.py b/runtest.py index 2771bf2..83a67d5 100755 --- a/runtest.py +++ b/runtest.py @@ -141,6 +141,7 @@ if args.testlist and (args.testlistfile or args.all): sys.exit(1) if args.testlistfile: + # args.testlistfile changes from a string to a pathlib Path object try: p = Path(args.testlistfile) if sys.version_info.major == 3 and sys.version_info.minor < 6: @@ -155,12 +156,13 @@ if args.testlistfile: sys.exit(1) if args.excludelistfile: + # args.excludelistfile changes from a string to a pathlib Path object try: p = Path(args.excludelistfile) if sys.version_info.major == 3 and sys.version_info.minor < 6: - args.testlistfile = p.resolve() + args.excludelistfile = p.resolve() else: - args.testlistfile = p.resolve(strict=True) + args.excludelistfile = p.resolve(strict=True) except FileNotFoundError: sys.stderr.write( parser.format_usage() -- cgit v0.12 From 6084c53b521cb14bce1bdbc5fe5c7c0ee6ae158e Mon Sep 17 00:00:00 2001 From: Mats Wichmann Date: Sat, 7 Nov 2020 11:06:29 -0700 Subject: runtest now writes a log of fails failed_tests.log is created if there are any fails (on by default). --retry option added to rerun tests from that file, or use -f listfile to use a file of a different name. --faillog=FILE allows saving the fails to a non-default name (in case don't want to overwrite the existing one, perhaps); --no-faillog disables the writing of the log. Two unneeded tests relating to qmtest were dropped: fallback.py didn't really test anything, and noqmtest.py was a duplicate of simple/combined.py after the qmtest specifics had been stripped out. Added tests for the added three options (git thinks two were renames). Signed-off-by: Mats Wichmann --- CHANGES.txt | 5 ++- runtest.py | 42 +++++++++++++----- test/runtest/SCons.py | 7 +-- test/runtest/faillog.py | 81 ++++++++++++++++++++++++++++++++++ test/runtest/fallback.py | 97 ----------------------------------------- test/runtest/no_faillog.py | 83 +++++++++++++++++++++++++++++++++++ test/runtest/noqmtest.py | 89 ------------------------------------- test/runtest/print_time.py | 4 -- test/runtest/python.py | 2 - test/runtest/retry.py | 69 +++++++++++++++++++++++++++++ test/runtest/simple/combined.py | 25 +++++------ test/runtest/testlistfile.py | 4 +- 12 files changed, 283 insertions(+), 225 deletions(-) create mode 100644 test/runtest/faillog.py delete mode 100644 test/runtest/fallback.py create mode 100644 test/runtest/no_faillog.py delete mode 100644 test/runtest/noqmtest.py create mode 100644 test/runtest/retry.py diff --git a/CHANGES.txt b/CHANGES.txt index e58ea8f..87b20c4 100755 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -72,7 +72,10 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER MethodWrapper moved to Util to avoid a circular import. Fixes #3028. - Some Python 2 compatibility code dropped - Rework runtest.py to use argparse for arg handling (was a mix - of hand-coded and optparse, with a stated intent to "gradually port") + of hand-coded and optparse, with a stated intent to "gradually port"). + - Add options to runtest to generate/not generate a log of failed tests, + and to rerun such tests. Useful when an error cascades through several + tests, can quickly try if a change improves all the fails. From Simon Tegelid - Fix using TEMPFILE in multiple actions in an action list. Previously a builder, or command diff --git a/runtest.py b/runtest.py index 83a67d5..df7c328 100755 --- a/runtest.py +++ b/runtest.py @@ -61,6 +61,8 @@ testlisting.add_argument('-f', '--file', metavar='FILE', dest='testlistfile', help="Select only tests in FILE") testlisting.add_argument('-a', '--all', action='store_true', help="Select all tests") +testlisting.add_argument('--retry', action='store_true', + help="Rerun the last failed tests in 'failed_tests.log'") testsel.add_argument('--exclude-list', metavar="FILE", dest='excludelistfile', help="""Exclude tests in FILE from current selection""") testtype = testsel.add_mutually_exclusive_group() @@ -87,11 +89,8 @@ parser.add_argument('-n', '--no-exec', action='store_false', help="No execute, just print command lines.") parser.add_argument('--nopipefiles', action='store_false', dest='allow_pipe_files', - help="""Do not use the "file pipe" workaround for Popen() - for starting tests. WARNING: use only when too much - file traffic is giving you trouble AND you can be - sure that none of your tests create output >65K - chars! You might run into some deadlocks else.""") + help="""Do not use the "file pipe" workaround for subprocess + for starting tests. See source code for warnings.""") parser.add_argument('-P', '--python', metavar='PYTHON', help="Use the specified Python interpreter.") parser.add_argument('--quit-on-failure', action='store_true', @@ -102,6 +101,14 @@ parser.add_argument('-X', dest='scons_exec', action='store_true', help="Test script is executable, don't feed to Python.") parser.add_argument('-x', '--exec', metavar="SCRIPT", help="Test using SCRIPT as path to SCons.") +parser.add_argument('--faillog', dest='error_log', metavar="FILE", + default='failed_tests.log', + help="Log failed tests to FILE (enabled by default, " + "default file 'failed_tests.log')") +parser.add_argument('--no-faillog', dest='error_log', + action='store_const', const=None, + default='failed_tests.log', + help="Do not log failed tests to a file") outctl = parser.add_argument_group(description='Output control options:') outctl.add_argument('-k', '--no-progress', action='store_false', @@ -123,6 +130,8 @@ outctl.add_argument('--verbose', metavar='LEVEL', type=int, choices=range(1, 4), 1 = print executed commands, 2 = print commands and non-zero output, 3 = print commands and all output.""") +# maybe add? +# outctl.add_argument('--version', action='version', version='%s 1.0' % script) logctl = parser.add_argument_group(description='Log control options:') logctl.add_argument('-o', '--output', metavar='LOG', help="Save console output to LOG.") @@ -132,14 +141,18 @@ logctl.add_argument('--xml', metavar='XML', help="Save results to XML in SCons X args = parser.parse_args() # we can't do this check with an argparse exclusive group, -# since the cmdline tests (args.testlist) are not optional -if args.testlist and (args.testlistfile or args.all): +# since the cmdline tests (args.testlist) are not optional args, +# exclusive only works with optional args. +if args.testlist and (args.testlistfile or args.all or args.retry): sys.stderr.write( parser.format_usage() - + "error: command line tests cannot be combined with -f/--file or -a/--all\n" + + "error: command line tests cannot be combined with -f/--filei -a/--all or --retry\n" ) sys.exit(1) +if args.retry: + args.testlistfile = 'failed_tests.log' + if args.testlistfile: # args.testlistfile changes from a string to a pathlib Path object try: @@ -196,7 +209,7 @@ if args.exec: scons = args.exec # --- setup stdout/stderr --- -class Unbuffered(): +class Unbuffered: def __init__(self, file): self.file = file @@ -215,7 +228,7 @@ sys.stderr = Unbuffered(sys.stderr) if args.output: logfile = open(args.output, 'w') - class Tee(): + class Tee: def __init__(self, openfile, stream): self.file = openfile self.stream = stream @@ -780,6 +793,7 @@ passed = [t for t in tests if t.status == 0] fail = [t for t in tests if t.status == 1] no_result = [t for t in tests if t.status == 2] +# print summaries, but only if multiple tests were run if len(tests) != 1 and args.execute_tests: if passed and args.print_passed_summary: if len(passed) == 1: @@ -803,6 +817,14 @@ if len(tests) != 1 and args.execute_tests: paths = [x.path for x in no_result] sys.stdout.write("\t" + "\n\t".join(paths) + "\n") +# save the fails to a file +if fail and args.error_log: + paths = [x.path for x in fail] + #print(f"DEBUG: Writing fails to {args.error_log}") + with open(args.error_log, "w") as f: + for test in paths: + print(test, file=f) + if args.xml: if args.output == '-': f = sys.stdout diff --git a/test/runtest/SCons.py b/test/runtest/SCons.py index 9bc86e8..20c4c64 100644 --- a/test/runtest/SCons.py +++ b/test/runtest/SCons.py @@ -34,9 +34,7 @@ import os import TestRuntest test = TestRuntest.TestRuntest() - -test.subdir(['SCons'], - ['SCons', 'suite']) +test.subdir(['SCons'], ['SCons', 'suite']) pythonstring = TestRuntest.pythonstring pythonflags = TestRuntest.pythonflags @@ -44,11 +42,8 @@ src_passTests_py = os.path.join('SCons', 'passTests.py') src_suite_passTests_py = os.path.join('SCons', 'suite', 'passTests.py') test.write_passing_test(['SCons', 'pass.py']) - test.write_passing_test(['SCons', 'passTests.py']) - test.write_passing_test(['SCons', 'suite', 'pass.py']) - test.write_passing_test(['SCons', 'suite', 'passTests.py']) expect_stdout = """\ diff --git a/test/runtest/faillog.py b/test/runtest/faillog.py new file mode 100644 index 0000000..e2ca67e --- /dev/null +++ b/test/runtest/faillog.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python +# +# __COPYRIGHT__ +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" + +""" +Test a list of tests in failed_tests.log to run with the --retry option +""" + +import os.path + +import TestRuntest + +pythonstring = TestRuntest.pythonstring +pythonflags = TestRuntest.pythonflags +test_fail_py = os.path.join('test', 'fail.py') +test_pass_py = os.path.join('test', 'pass.py') + +test = TestRuntest.TestRuntest() +test.subdir('test') +test.write_failing_test(test_fail_py) +test.write_passing_test(test_pass_py) + +expect_stdout = """\ +%(pythonstring)s%(pythonflags)s %(test_fail_py)s +FAILING TEST STDOUT +%(pythonstring)s%(pythonflags)s %(test_pass_py)s +PASSING TEST STDOUT + +Failed the following test: +\t%(test_fail_py)s +""" % locals() + +expect_stderr = """\ +FAILING TEST STDERR +PASSING TEST STDERR +""" + +testlist = [ + test_fail_py, + test_pass_py, +] + +test.run( + arguments='-k --faillog=fail.log %s' % ' '.join(testlist), + status=1, + stdout=expect_stdout, + stderr=expect_stderr, +) +test.must_exist('fail.log') +test.must_contain('fail.log', test_fail_py) +test.must_not_exist('failed_tests.log') + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/runtest/fallback.py b/test/runtest/fallback.py deleted file mode 100644 index b137307..0000000 --- a/test/runtest/fallback.py +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env python -# -# __COPYRIGHT__ -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY -# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" - -""" -Test that runtest.py falls back (with a warning) using --noqmtest -if it can't find qmtest on the $PATH. -""" - -import os - -import TestRuntest - -pythonstring = TestRuntest.pythonstring -pythonflags = TestRuntest.pythonflags - -test = TestRuntest.TestRuntest() - -# qmtest may be in more than one location in your path -while test.where_is('qmtest'): - qmtest=test.where_is('qmtest') - dir = os.path.split(qmtest)[0] - path = os.environ['PATH'].split(os.pathsep) - path.remove(dir) - os.environ['PATH'] = os.pathsep.join(path) - -test.subdir('test') - -test_fail_py = os.path.join('test', 'fail.py') -test_no_result_py = os.path.join('test', 'no_result.py') -test_pass_py = os.path.join('test', 'pass.py') - -test.write_failing_test(test_fail_py) -test.write_no_result_test(test_no_result_py) -test.write_passing_test(test_pass_py) - -expect_stdout = """\ -%(pythonstring)s%(pythonflags)s %(test_fail_py)s -FAILING TEST STDOUT -%(pythonstring)s%(pythonflags)s %(test_no_result_py)s -NO RESULT TEST STDOUT -%(pythonstring)s%(pythonflags)s %(test_pass_py)s -PASSING TEST STDOUT - -Failed the following test: -\t%(test_fail_py)s - -NO RESULT from the following test: -\t%(test_no_result_py)s -""" % locals() - -expect_stderr = """\ -FAILING TEST STDERR -NO RESULT TEST STDERR -PASSING TEST STDERR -""" - -testlist = [ - test_fail_py, - test_no_result_py, - test_pass_py, -] - -test.run(arguments = '-k '+' '.join(testlist), - status = 1, - stdout = expect_stdout, - stderr = expect_stderr) - -test.pass_test() - -# Local Variables: -# tab-width:4 -# indent-tabs-mode:nil -# End: -# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/runtest/no_faillog.py b/test/runtest/no_faillog.py new file mode 100644 index 0000000..db17c8e --- /dev/null +++ b/test/runtest/no_faillog.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python +# +# __COPYRIGHT__ +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" + +""" +Test a list of tests in failed_tests.log to run with the --retry option +""" + +import os.path + +import TestRuntest + +pythonstring = TestRuntest.pythonstring +pythonflags = TestRuntest.pythonflags +test_fail_py = os.path.join('test', 'fail.py') +test_pass_py = os.path.join('test', 'pass.py') + +test = TestRuntest.TestRuntest() +test.subdir('test') +test.write_failing_test(test_fail_py) +test.write_passing_test(test_pass_py) + +test.write('failed_tests.log', """\ +%(test_fail_py)s +""" % locals()) + +expect_stdout = """\ +%(pythonstring)s%(pythonflags)s %(test_fail_py)s +FAILING TEST STDOUT +%(pythonstring)s%(pythonflags)s %(test_pass_py)s +PASSING TEST STDOUT + +Failed the following test: +\t%(test_fail_py)s +""" % locals() + +expect_stderr = """\ +FAILING TEST STDERR +PASSING TEST STDERR +""" + +testlist = [ + test_fail_py, + test_pass_py, +] + +test.run( + arguments='-k --no-faillog %s' % ' '.join(testlist), + status=1, + stdout=expect_stdout, + stderr=expect_stderr, +) +test.must_not_exist('failing_tests.log') + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/runtest/noqmtest.py b/test/runtest/noqmtest.py deleted file mode 100644 index fcf7ac0..0000000 --- a/test/runtest/noqmtest.py +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env python -# -# __COPYRIGHT__ -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY -# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" - -""" -Test that by default tests are invoked directly via Python, not -using qmtest. -""" - -import os - -import TestRuntest - -pythonstring = TestRuntest.pythonstring -pythonflags = TestRuntest.pythonflags - -test = TestRuntest.TestRuntest() - -test.subdir('test') - -test_fail_py = os.path.join('test', 'fail.py') -test_no_result_py = os.path.join('test', 'no_result.py') -test_pass_py = os.path.join('test', 'pass.py') - -test.write_failing_test(test_fail_py) -test.write_no_result_test(test_no_result_py) -test.write_passing_test(test_pass_py) - -expect_stdout = """\ -%(pythonstring)s%(pythonflags)s %(test_fail_py)s -FAILING TEST STDOUT -%(pythonstring)s%(pythonflags)s %(test_no_result_py)s -NO RESULT TEST STDOUT -%(pythonstring)s%(pythonflags)s %(test_pass_py)s -PASSING TEST STDOUT - -Failed the following test: -\t%(test_fail_py)s - -NO RESULT from the following test: -\t%(test_no_result_py)s -""" % locals() - -expect_stderr = """\ -FAILING TEST STDERR -NO RESULT TEST STDERR -PASSING TEST STDERR -""" - -testlist = [ - test_fail_py, - test_no_result_py, - test_pass_py, -] - -test.run(arguments = '-k %s' % ' '.join(testlist), - status = 1, - stdout = expect_stdout, - stderr = expect_stderr) - -test.pass_test() - -# Local Variables: -# tab-width:4 -# indent-tabs-mode:nil -# End: -# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/runtest/print_time.py b/test/runtest/print_time.py index 322b88b..834d2ae 100644 --- a/test/runtest/print_time.py +++ b/test/runtest/print_time.py @@ -42,13 +42,9 @@ test_no_result_py = re.escape(os.path.join('test', 'no_result.py')) test_pass_py = re.escape(os.path.join('test', 'pass.py')) test = TestRuntest.TestRuntest(match = TestCmd.match_re) - test.subdir('test') - test.write_failing_test(['test', 'fail.py']) - test.write_no_result_test(['test', 'no_result.py']) - test.write_passing_test(['test', 'pass.py']) expect_stdout = """\ diff --git a/test/runtest/python.py b/test/runtest/python.py index da62378..499ab77 100644 --- a/test/runtest/python.py +++ b/test/runtest/python.py @@ -37,9 +37,7 @@ if not hasattr(os.path, 'pardir'): import TestRuntest test = TestRuntest.TestRuntest() - test_pass_py = os.path.join('test', 'pass.py') - head, python = os.path.split(TestRuntest.python) head, dir = os.path.split(head) diff --git a/test/runtest/retry.py b/test/runtest/retry.py new file mode 100644 index 0000000..4280152 --- /dev/null +++ b/test/runtest/retry.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python +# +# __COPYRIGHT__ +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" + +""" +Test a list of tests in failed_tests.log to run with the --retry option +""" + +import os.path + +import TestRuntest + +pythonstring = TestRuntest.pythonstring +pythonflags = TestRuntest.pythonflags +test_fail_py = os.path.join('test', 'fail.py') +test_no_result_py = os.path.join('test', 'no_result.py') +test_pass_py = os.path.join('test', 'pass.py') + +test = TestRuntest.TestRuntest() + +test.subdir('test') +test.write_failing_test(['test', 'fail.py']) +test.write_no_result_test(['test', 'no_result.py']) +test.write_passing_test(['test', 'pass.py']) + +test.write('failed_tests.log', """\ +%(test_fail_py)s +""" % locals()) + +expect_stdout = """\ +%(pythonstring)s%(pythonflags)s %(test_fail_py)s +FAILING TEST STDOUT +""" % locals() + +expect_stderr = """\ +FAILING TEST STDERR +""" + +test.run(arguments='-k --retry', status=1, stdout=expect_stdout, stderr=expect_stderr) + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/runtest/simple/combined.py b/test/runtest/simple/combined.py index ec0a1bb..a54e57c 100644 --- a/test/runtest/simple/combined.py +++ b/test/runtest/simple/combined.py @@ -1,4 +1,3 @@ - #!/usr/bin/env python # # __COPYRIGHT__ @@ -34,21 +33,17 @@ import os import TestRuntest -test = TestRuntest.TestRuntest() - pythonstring = TestRuntest.pythonstring pythonflags = TestRuntest.pythonflags test_fail_py = os.path.join('test', 'fail.py') test_no_result_py = os.path.join('test', 'no_result.py') test_pass_py = os.path.join('test', 'pass.py') +test = TestRuntest.TestRuntest() test.subdir('test') - -test.write_failing_test(['test', 'fail.py']) - -test.write_no_result_test(['test', 'no_result.py']) - -test.write_passing_test(['test', 'pass.py']) +test.write_failing_test(test_fail_py) +test.write_no_result_test(test_no_result_py) +test.write_passing_test(test_pass_py) expect_stdout = """\ %(pythonstring)s%(pythonflags)s %(test_fail_py)s @@ -71,10 +66,14 @@ NO RESULT TEST STDERR PASSING TEST STDERR """ -test.run(arguments='-k test', - status=1, - stdout=expect_stdout, - stderr=expect_stderr) +test.run( + arguments='-k test', + status=1, + stdout=expect_stdout, + stderr=expect_stderr +) +test.must_exist('failed_tests.log') +test.must_contain('failed_tests.log', test_fail_py) test.pass_test() diff --git a/test/runtest/testlistfile.py b/test/runtest/testlistfile.py index ba034e8..5c956b8 100644 --- a/test/runtest/testlistfile.py +++ b/test/runtest/testlistfile.py @@ -26,6 +26,7 @@ __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Test a list of tests to run in a file specified with the -f option. +The commented-out test should not run. """ import os.path @@ -41,11 +42,8 @@ test_pass_py = os.path.join('test', 'pass.py') test = TestRuntest.TestRuntest() test.subdir('test') - test.write_failing_test(['test', 'fail.py']) - test.write_no_result_test(['test', 'no_result.py']) - test.write_passing_test(['test', 'pass.py']) test.write('t.txt', """\ -- cgit v0.12 From 82bdb9c3423ae80461af2c5e224456e0ba181972 Mon Sep 17 00:00:00 2001 From: Mats Wichmann Date: Sun, 8 Nov 2020 06:56:13 -0700 Subject: [PR #3822] fix typo, reword comment Signed-off-by: Mats Wichmann --- CHANGES.txt | 3 ++- runtest.py | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 87b20c4..77fb110 100755 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -75,7 +75,8 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER of hand-coded and optparse, with a stated intent to "gradually port"). - Add options to runtest to generate/not generate a log of failed tests, and to rerun such tests. Useful when an error cascades through several - tests, can quickly try if a change improves all the fails. + tests, can quickly try if a change improves all the fails. Dropped + runtest test for fallback from qmtest, not needed; added new tests. From Simon Tegelid - Fix using TEMPFILE in multiple actions in an action list. Previously a builder, or command diff --git a/runtest.py b/runtest.py index df7c328..9e7cb2c 100755 --- a/runtest.py +++ b/runtest.py @@ -140,13 +140,13 @@ logctl.add_argument('--xml', metavar='XML', help="Save results to XML in SCons X # process args and handle a few specific cases: args = parser.parse_args() -# we can't do this check with an argparse exclusive group, -# since the cmdline tests (args.testlist) are not optional args, -# exclusive only works with optional args. +# we can't do this check with an argparse exclusive group, since those +# only work with optional args, and the cmdline tests (args.testlist) +# are not optional args, if args.testlist and (args.testlistfile or args.all or args.retry): sys.stderr.write( parser.format_usage() - + "error: command line tests cannot be combined with -f/--filei -a/--all or --retry\n" + + "error: command line tests cannot be combined with -f/--file, -a/--all or --retry\n" ) sys.exit(1) -- cgit v0.12 From aa11aea5e433b4d37739130774394288a906c6c5 Mon Sep 17 00:00:00 2001 From: Mats Wichmann Date: Tue, 10 Nov 2020 13:22:07 -0700 Subject: Update templates [ci skip] Signed-off-by: Mats Wichmann --- template/Tests.py | 9 ++++--- template/__init__.py | 68 ---------------------------------------------------- template/file.py | 17 +++++-------- template/test.py | 10 ++++---- 4 files changed, 17 insertions(+), 87 deletions(-) delete mode 100644 template/__init__.py diff --git a/template/Tests.py b/template/Tests.py index 4d62e0a..a615314 100644 --- a/template/Tests.py +++ b/template/Tests.py @@ -1,5 +1,6 @@ +# MIT License # -# __COPYRIGHT__ +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,9 +20,11 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +""" +Template for unit-test file. +Replace this with a description of the test. +""" import unittest diff --git a/template/__init__.py b/template/__init__.py deleted file mode 100644 index caae1ba..0000000 --- a/template/__init__.py +++ /dev/null @@ -1,68 +0,0 @@ -# -# __COPYRIGHT__ -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY -# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -"""SCons - -The main package for the SCons software construction utility. - -""" - -# -# __COPYRIGHT__ -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY -# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__REVISION__" -__version__ = "__VERSION__" -__build__ = "__BUILD__" -__buildsys__ = "__BUILDSYS__" -__date__ = "__DATE__" -__developer__ = "__DEVELOPER__" -__copyright__ = "__COPYRIGHT__" - -# make sure compatibility is always in place -import SCons.compat # noqa - -# Local Variables: -# tab-width:4 -# indent-tabs-mode:nil -# End: -# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/template/file.py b/template/file.py index f506de4..8c95d01 100644 --- a/template/file.py +++ b/template/file.py @@ -1,11 +1,6 @@ -"""${subst '/' '.' ${subst '^src/' '' ${subst '\.py$' '' $filename}}} - -XXX - -""" - +# MIT License # -# __COPYRIGHT__ +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -25,11 +20,11 @@ XXX # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" - +""" +Template for an SCons source file. +Replace this with the purpose of the file. +""" import XXX diff --git a/template/test.py b/template/test.py index 0463cbf..b60f40f 100644 --- a/template/test.py +++ b/template/test.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,12 +22,10 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ -XXX Put a description of the test here. +Template for end-to-end test file. +Replace this with a description of the test. """ import TestSCons -- cgit v0.12