diff options
Diffstat (limited to 'testing')
-rw-r--r-- | testing/__init__.py | 50 | ||||
-rw-r--r-- | testing/blacklist.py | 61 | ||||
-rw-r--r-- | testing/buildlog.py | 97 | ||||
-rw-r--r-- | testing/command.py | 240 | ||||
-rw-r--r-- | testing/helper.py | 69 | ||||
-rw-r--r-- | testing/parser.py | 68 | ||||
-rw-r--r-- | testing/runner.py | 159 | ||||
-rw-r--r-- | testing/wheel_tester.py | 403 |
8 files changed, 510 insertions, 637 deletions
diff --git a/testing/__init__.py b/testing/__init__.py index 03c590604..62614c438 100644 --- a/testing/__init__.py +++ b/testing/__init__.py @@ -1,43 +1,5 @@ -############################################################################# -## -## Copyright (C) 2017 The Qt Company Ltd. -## Contact: https://www.qt.io/licensing/ -## -## This file is part of Qt for Python. -## -## $QT_BEGIN_LICENSE:LGPL$ -## Commercial License Usage -## Licensees holding valid commercial Qt licenses may use this file in -## accordance with the commercial license agreement provided with the -## Software or, alternatively, in accordance with the terms contained in -## a written agreement between you and The Qt Company. For licensing terms -## and conditions see https://www.qt.io/terms-conditions. For further -## information use the contact form at https://www.qt.io/contact-us. -## -## GNU Lesser General Public License Usage -## Alternatively, this file may be used under the terms of the GNU Lesser -## General Public License version 3 as published by the Free Software -## Foundation and appearing in the file LICENSE.LGPL3 included in the -## packaging of this file. Please review the following information to -## ensure the GNU Lesser General Public License version 3 requirements -## will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -## -## GNU General Public License Usage -## Alternatively, this file may be used under the terms of the GNU -## General Public License version 2.0 or (at your option) the GNU General -## Public license version 3 or any later version approved by the KDE Free -## Qt Foundation. The licenses are as published by the Free Software -## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -## included in the packaging of this file. Please review the following -## information to ensure the GNU General Public License requirements will -## be met: https://www.gnu.org/licenses/gpl-2.0.html and -## https://www.gnu.org/licenses/gpl-3.0.html. -## -## $QT_END_LICENSE$ -## -############################################################################# - -from __future__ import print_function +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only """ testing/__init__.py @@ -46,19 +8,23 @@ testing/__init__.py - define command.main as entry point """ +import builtins import sys + from . import command main = command.main # modify print so that it always flushes -__builtins__["orig_print"] = __builtins__["print"] +builtins.orig_print = builtins.print + def print_flushed(*args, **kw): orig_print(*args, **kw) sys.stdout.flush() -__builtins__["print"] = print_flushed + +builtins.print = print_flushed print = print_flushed diff --git a/testing/blacklist.py b/testing/blacklist.py index f82f2b5e7..eacb51cbb 100644 --- a/testing/blacklist.py +++ b/testing/blacklist.py @@ -1,43 +1,5 @@ -############################################################################# -## -## Copyright (C) 2017 The Qt Company Ltd. -## Contact: https://www.qt.io/licensing/ -## -## This file is part of Qt for Python. -## -## $QT_BEGIN_LICENSE:LGPL$ -## Commercial License Usage -## Licensees holding valid commercial Qt licenses may use this file in -## accordance with the commercial license agreement provided with the -## Software or, alternatively, in accordance with the terms contained in -## a written agreement between you and The Qt Company. For licensing terms -## and conditions see https://www.qt.io/terms-conditions. For further -## information use the contact form at https://www.qt.io/contact-us. -## -## GNU Lesser General Public License Usage -## Alternatively, this file may be used under the terms of the GNU Lesser -## General Public License version 3 as published by the Free Software -## Foundation and appearing in the file LICENSE.LGPL3 included in the -## packaging of this file. Please review the following information to -## ensure the GNU Lesser General Public License version 3 requirements -## will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -## -## GNU General Public License Usage -## Alternatively, this file may be used under the terms of the GNU -## General Public License version 2.0 or (at your option) the GNU General -## Public license version 3 or any later version approved by the KDE Free -## Qt Foundation. The licenses are as published by the Free Software -## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -## included in the packaging of this file. Please review the following -## information to ensure the GNU General Public License requirements will -## be met: https://www.gnu.org/licenses/gpl-2.0.html and -## https://www.gnu.org/licenses/gpl-3.0.html. -## -## $QT_END_LICENSE$ -## -############################################################################# - -from __future__ import print_function +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only """ testing/blacklist.py @@ -47,13 +9,15 @@ Take a blacklist file and build classifiers for all tests. find_matching_line() adds info using classifiers. """ -from .helper import decorate, StringIO +from io import StringIO + from .buildlog import builds +from .helper import decorate class BlackList(object): def __init__(self, blname): - if blname == None: + if not blname: f = StringIO() self.raw_data = [] else: @@ -63,8 +27,8 @@ class BlackList(object): lines = self.raw_data[:] def filtered_line(line): - if '#' in line: - line = line[0 : line.index('#')] + if "#" in line: + line = line[:line.index("#")] return line.split() # now put every bracketed line in a test @@ -78,7 +42,6 @@ class BlackList(object): # nothing supplied return - self.index = {} for idx, line in enumerate(lines): fline = filtered_line(line) if not fline: @@ -86,15 +49,15 @@ class BlackList(object): if is_test(fline): break # we have a global section - name = '' + name = "" self.tests[name] = [] for idx, line in enumerate(lines): fline = filtered_line(line) if is_test(fline): # a new name name = decorate(fline[0][1:-1]) - self.tests[name] = [] - self.index[name] = idx + # Allow for repeated sections + self.tests.setdefault(name, []) elif fline: # a known name with a new entry self.tests[name].append(fline) @@ -129,4 +92,4 @@ class BlackList(object): # found a match! return line else: - return None # nothing found + return None # nothing found diff --git a/testing/buildlog.py b/testing/buildlog.py index 216282b4c..f82191f91 100644 --- a/testing/buildlog.py +++ b/testing/buildlog.py @@ -1,43 +1,5 @@ -############################################################################# -## -## Copyright (C) 2017 The Qt Company Ltd. -## Contact: https://www.qt.io/licensing/ -## -## This file is part of Qt for Python -## -## $QT_BEGIN_LICENSE:LGPL$ -## Commercial License Usage -## Licensees holding valid commercial Qt licenses may use this file in -## accordance with the commercial license agreement provided with the -## Software or, alternatively, in accordance with the terms contained in -## a written agreement between you and The Qt Company. For licensing terms -## and conditions see https://www.qt.io/terms-conditions. For further -## information use the contact form at https://www.qt.io/contact-us. -## -## GNU Lesser General Public License Usage -## Alternatively, this file may be used under the terms of the GNU Lesser -## General Public License version 3 as published by the Free Software -## Foundation and appearing in the file LICENSE.LGPL3 included in the -## packaging of this file. Please review the following information to -## ensure the GNU Lesser General Public License version 3 requirements -## will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -## -## GNU General Public License Usage -## Alternatively, this file may be used under the terms of the GNU -## General Public License version 2.0 or (at your option) the GNU General -## Public license version 3 or any later version approved by the KDE Free -## Qt Foundation. The licenses are as published by the Free Software -## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -## included in the packaging of this file. Please review the following -## information to ensure the GNU General Public License requirements will -## be met: https://www.gnu.org/licenses/gpl-2.0.html and -## https://www.gnu.org/licenses/gpl-3.0.html. -## -## $QT_END_LICENSE$ -## -############################################################################# - -from __future__ import print_function +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only """ testing/buildlog.py @@ -48,8 +10,9 @@ BuildLog.classifiers finds the set of classifier strings. """ import os -import sys +import platform import shutil +import sys from collections import namedtuple from textwrap import dedent @@ -67,14 +30,15 @@ class BuildLog(object): For simplicity and readability, the log entries are named tuples. """ + def __init__(self): - history_dir = os.path.join(script_dir, 'build_history') + history_dir = os.path.join(script_dir, "build_history") build_history = [] for timestamp in os.listdir(history_dir): log_dir = os.path.join(history_dir, timestamp) if not os.path.isdir(log_dir): continue - fpath = os.path.join(log_dir, 'build_dir.txt') + fpath = os.path.join(log_dir, "build_dir.txt") if not os.path.exists(fpath): continue with open(fpath) as f: @@ -88,10 +52,14 @@ class BuildLog(object): build_dir = f_contents_split[0] build_classifiers = "" except IndexError: - print(dedent(""" + print( + dedent( + f""" Error: There was an issue finding the build dir and its - characteristics, in the following considered file: '{}' - """.format(fpath))) + characteristics, in the following considered file: '{fpath}' + """ + ) + ) sys.exit(1) if not os.path.exists(build_dir): @@ -102,13 +70,14 @@ class BuildLog(object): if os.path.exists(build_dir): print("Note: build_dir was probably moved.") else: - print("Warning: missing build dir %s" % build_dir) + print(f"Warning: missing build dir {build_dir}") continue entry = LogEntry(log_dir, build_dir, build_classifiers) build_history.append(entry) # we take the latest build for now. build_history.sort() self.history = build_history + self.python_version = None self._buildno = None if not is_ci: # there seems to be a timing problem in RHel 7.6, so we better don't touch it @@ -122,11 +91,13 @@ class BuildLog(object): continue lst.append(log_dir) if lst: + def warn_problem(func, path, exc_info): - cls, ins, tb = exc_info - print("rmtree({}) warning: problem with {}:\n {}: {}".format( - func.__name__, path, - cls.__name__, ins.args)) + cls, ins, _ = exc_info + print( + f"rmtree({func.__name__}) warning: problem with " + f"{path}:\n {cls.__name__}: {ins.args}" + ) lst.sort() log_dir = lst[-1] @@ -139,7 +110,7 @@ class BuildLog(object): shutil.rmtree(log_dir, onerror=warn_problem) def set_buildno(self, buildno): - self.history[buildno] # test + self.history[buildno] # test self._buildno = buildno @property @@ -153,18 +124,23 @@ class BuildLog(object): @property def classifiers(self): if not self.selected: - raise ValueError('+++ No build with the configuration found!') + raise ValueError("+++ No build with the configuration found!") # Python2 legacy: Correct 'linux2' to 'linux', recommended way. - platform = 'linux' if sys.platform.startswith('linux') else sys.platform - res = [platform] + plat_name = "linux" if sys.platform.startswith("linux") else sys.platform + res = [plat_name, "qt6"] + if is_ci: + res.append("ci") if self.selected.build_classifiers: # Use classifier string encoded into build_dir.txt file. - res.extend(self.selected.build_classifiers.split('-')) + res.extend(self.selected.build_classifiers.split("-")) else: # the rest must be guessed from the given filename path = self.selected.build_dir base = os.path.basename(path) - res.extend(base.split('-')) + res.extend(base.split("-")) + # add exact Python version + if self.python_version: + res.append("py" + ".".join(map(str, self.python_version))) # add all the python and qt subkeys for entry in res: parts = entry.split(".") @@ -172,6 +148,13 @@ class BuildLog(object): key = ".".join(parts[:idx]) if key not in res: res.append(key) + # Allow to check the processor. + # This gives "i386" or "arm" on macOS. + res.append(platform.processor()) return res + def set_python_version(self, version_triple): + self.python_version = version_triple + + builds = BuildLog() diff --git a/testing/command.py b/testing/command.py index 886ab3e74..31a48f87c 100644 --- a/testing/command.py +++ b/testing/command.py @@ -1,43 +1,5 @@ -############################################################################# -## -## Copyright (C) 2017 The Qt Company Ltd. -## Contact: https://www.qt.io/licensing/ -## -## This file is part of Qt for Python. -## -## $QT_BEGIN_LICENSE:LGPL$ -## Commercial License Usage -## Licensees holding valid commercial Qt licenses may use this file in -## accordance with the commercial license agreement provided with the -## Software or, alternatively, in accordance with the terms contained in -## a written agreement between you and The Qt Company. For licensing terms -## and conditions see https://www.qt.io/terms-conditions. For further -## information use the contact form at https://www.qt.io/contact-us. -## -## GNU Lesser General Public License Usage -## Alternatively, this file may be used under the terms of the GNU Lesser -## General Public License version 3 as published by the Free Software -## Foundation and appearing in the file LICENSE.LGPL3 included in the -## packaging of this file. Please review the following information to -## ensure the GNU Lesser General Public License version 3 requirements -## will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -## -## GNU General Public License Usage -## Alternatively, this file may be used under the terms of the GNU -## General Public License version 2.0 or (at your option) the GNU General -## Public license version 3 or any later version approved by the KDE Free -## Qt Foundation. The licenses are as published by the Free Software -## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -## included in the packaging of this file. Please review the following -## information to ensure the GNU General Public License requirements will -## be met: https://www.gnu.org/licenses/gpl-2.0.html and -## https://www.gnu.org/licenses/gpl-3.0.html. -## -## $QT_END_LICENSE$ -## -############################################################################# - -from __future__ import print_function +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only """ testrunner @@ -57,7 +19,7 @@ Building the project with something like python setup.py build --build-tests --qmake=<qmakepath> --ignore-git --debug is sufficient. The tests are run by changing into the latest build dir and there -into pyside2, then 'make test'. +into pyside6, then 'make test'. New testing policy: @@ -72,31 +34,40 @@ The full mode can be tested locally by setting export COIN_RERUN_FAILED_ONLY=0 """ +import argparse import os import sys -import argparse -from textwrap import dedent from collections import OrderedDict +from textwrap import dedent from timeit import default_timer as timer -from .helper import script_dir, decorate -from .buildlog import builds from .blacklist import BlackList -from .runner import TestRunner +from .buildlog import builds +from .helper import decorate, script_dir from .parser import TestParser +from .runner import TestRunner # Should we repeat only failed tests? COIN_RERUN_FAILED_ONLY = True -COIN_THRESHOLD = 3 # report error if >= -COIN_TESTING = 5 # number of runs +COIN_THRESHOLD = 3 # report error if >= +COIN_TESTING = 5 # number of runs +TIMEOUT = 20 * 60 -if (os.environ.get("COIN_RERUN_FAILED_ONLY", "1").lower() in - "0 f false n no".split()): +if os.environ.get("COIN_RERUN_FAILED_ONLY", "1").lower() in "0 f false n no".split(): COIN_RERUN_FAILED_ONLY = False + def test_project(project, args, blacklist, runs): ret = [] + if "pypy" in builds.classifiers: + # As long as PyPy has so many bugs, we use 1 test only... + global COIN_TESTING + COIN_TESTING = runs = 1 + # ...and extend the timeout. + global TIMEOUT + TIMEOUT = 100 * 60 + # remove files from a former run for idx in range(runs): index = idx + 1 @@ -104,11 +75,14 @@ def test_project(project, args, blacklist, runs): if os.path.exists(runner.logfile) and not args.skip: os.unlink(runner.logfile) # now start the real run + rerun_list = None for idx in range(runs): index = idx + 1 runner = TestRunner(builds.selected, project, index) + # For the full Python version we need to ask the TestRunner. + builds.set_python_version(runner.get_python_version()) print() - print("********* Start testing of %s *********" % project) + print(f"********* Start testing of {project} *********") print("Config: Using", " ".join(builds.classifiers)) print() if os.path.exists(runner.logfile) and args.skip: @@ -117,12 +91,11 @@ def test_project(project, args, blacklist, runs): if index > 1 and COIN_RERUN_FAILED_ONLY: rerun = rerun_list if not rerun: - print("--- no re-runs found, stopping before test {} ---" - .format(index)) + print(f"--- no re-runs found, stopping before test {index} ---") break else: rerun = None - runner.run("RUN {}:".format(idx + 1), rerun, 10 * 60) + runner.run(f"RUN {idx + 1}:", rerun, TIMEOUT) results = TestParser(runner.logfile) r = 5 * [0] rerun_list = [] @@ -130,12 +103,12 @@ def test_project(project, args, blacklist, runs): fatal = False for item in results.iter_blacklist(blacklist): res = item.rich_result - sharp = "#" + str(item.sharp) + sharp = f"#{item.sharp}" mod_name = decorate(item.mod_name) - print("RES {index}: Test {sharp:>4}: {res:<6} {mod_name}()".format(**locals())) + print(f"RES {index}: Test {sharp:>4}: {res:<6} {mod_name}()") r[0] += 1 if res == "PASS" else 0 r[1] += 1 if res == "FAIL!" else 0 - r[2] += 1 if res == "SKIPPED" else 0 # not yet supported + r[2] += 1 if res == "SKIPPED" else 0 # not yet supported r[3] += 1 if res == "BFAIL" else 0 r[4] += 1 if res == "BPASS" else 0 if res not in ("PASS", "BPASS"): @@ -144,67 +117,90 @@ def test_project(project, args, blacklist, runs): if item.fatal: fatal = item print() - print("Totals:", sum(r), "tests.", - "{} passed, {} failed, {} skipped, {} blacklisted, {} bpassed." - .format(*r)) + print( + f"Totals: {sum(r)} tests. " + f"{r[0]} passed, {r[1]} failed, {r[2]} skipped, {r[3]} blacklisted, {r[4]} bpassed." + ) print() - print("********* Finished testing of %s *********" % project) + print(f"********* Finished testing of {project} *********") print() ret.append(r) if fatal: print("FATAL ERROR:", fatal) print("Repetitions cancelled!") break - return ret, fatal + return ret, fatal, runs + def main(): # create the top-level command parser start_time = timer() - all_projects = "shiboken2 pyside2 pyside2-tools".split() - tested_projects = "shiboken2 pyside2".split() + all_projects = "shiboken6 pyside6".split() + tested_projects = "shiboken6 pyside6".split() + tested_projects_quoted = " ".join("'i'" for i in tested_projects) parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, - description=dedent("""\ - Run the tests for some projects, default = '{}'. + description=dedent( + """\ + Run the tests for some projects, default = {tested_projects_quoted}. - Testing is now repeated up to {rep} times, and errors are - only reported if they occur {thr} or more times. + Testing is now repeated up to {COIN_TESTING} times, and errors are + only reported if they occur {COIN_THRESHOLD} or more times. The environment variable COIN_RERUN_FAILED_ONLY controls if errors are only repeated if there are errors. The default is "1". - """.format("' '".join(tested_projects), thr=COIN_THRESHOLD, rep=COIN_TESTING))) + """ + ), + ) subparsers = parser.add_subparsers(dest="subparser_name") # create the parser for the "test" command parser_test = subparsers.add_parser("test") group = parser_test.add_mutually_exclusive_group(required=False) - blacklist_default = os.path.join(script_dir, 'build_history', 'blacklist.txt') - group.add_argument("--blacklist", "-b", type=argparse.FileType('r'), - default=blacklist_default, - help='a Qt blacklist file (default: {})'.format(blacklist_default)) - parser_test.add_argument("--skip", action='store_true', - help="skip the tests if they were run before") - parser_test.add_argument("--environ", nargs='+', - help="use name=value ... to set environment variables") - parser_test.add_argument("--buildno", default=-1, type=int, - help="use build number n (0-based), latest = -1 (default)") - parser_test.add_argument("--projects", nargs='+', type=str, - default=tested_projects, - choices=all_projects, - help="use '{}'' (default) or other projects" - .format("' '".join(tested_projects))) + blacklist_default = os.path.join(script_dir, "build_history", "blacklist.txt") + group.add_argument( + "--blacklist", + "-b", + type=argparse.FileType("r"), + default=blacklist_default, + help=f"a Qt blacklist file (default: {blacklist_default})", + ) + parser_test.add_argument( + "--skip", action="store_true", help="skip the tests if they were run before" + ) + parser_test.add_argument( + "--environ", nargs="+", help="use name=value ... to set environment variables" + ) + parser_test.add_argument( + "--buildno", + default=-1, + type=int, + help="use build number n (0-based), latest = -1 (default)", + ) + parser_test.add_argument( + "--projects", + nargs="+", + type=str, + default=tested_projects, + choices=all_projects, + help=f"use {tested_projects_quoted} (default) or other projects", + ) parser_getcwd = subparsers.add_parser("getcwd") - parser_getcwd.add_argument("filename", type=argparse.FileType('w'), - help="write the build dir name into a file") - parser_getcwd.add_argument("--buildno", default=-1, type=int, - help="use build number n (0-based), latest = -1 (default)") - parser_list = subparsers.add_parser("list") + parser_getcwd.add_argument( + "filename", type=argparse.FileType("w"), help="write the build dir name into a file" + ) + parser_getcwd.add_argument( + "--buildno", + default=-1, + type=int, + help="use build number n (0-based), latest = -1 (default)", + ) args = parser.parse_args() if hasattr(args, "buildno"): try: builds.set_buildno(args.buildno) except IndexError: - print("history out of range. Try '%s list'" % __file__) + print(f"history out of range. Try '{__file__} list'") sys.exit(1) if args.subparser_name == "getcwd": @@ -212,7 +208,7 @@ def main(): print(builds.selected.build_dir, "written to file", args.filename.name) sys.exit(0) elif args.subparser_name == "test": - pass # we do it afterwards + pass # we do it afterwards elif args.subparser_name == "list": rp = os.path.relpath print() @@ -240,17 +236,21 @@ def main(): key, value = things os.environ[key] = value - print(dedent("""\ + version = sys.version.replace("\n", " ") + print( + dedent( + f"""\ System: - Platform={platform} - Executable={executable} - Version={version_lf} - API version={api_version} + Platform={sys.platform} + Executable={sys.executable} + Version={version} + API version={sys.api_version} - Environment:""") - .format(version_lf=sys.version.replace("\n", " "), **sys.__dict__)) + Environment:""" + ) + ) for key, value in sorted(os.environ.items()): - print(" {}={}".format(key, value)) + print(f" {key}={value}") print() q = 5 * [0] @@ -260,16 +260,17 @@ def main(): # now loop over the projects and accumulate fatal = False for project in args.projects: - res, fatal = test_project(project, args, bl, runs) + res, fatal, runs = test_project(project, args, bl, runs) if fatal: runs = 1 for idx, r in enumerate(res): - q = list(map(lambda x, y: x+y, r, q)) + q = list(map(lambda x, y: x + y, r, q)) if len(args.projects) > 1: - print("All above projects:", sum(q), "tests.", - "{} passed, {} failed, {} skipped, {} blacklisted, {} bpassed." - .format(*q)) + print( + f"All above projects: {sum(q)} tests. " + f"{q[0]} passed, {q[1]} failed, {q[2]} skipped, {q[3]} blacklisted, {q[4]} bpassed." + ) print() tot_res = OrderedDict() @@ -279,7 +280,7 @@ def main(): runner = TestRunner(builds.selected, project, index) results = TestParser(runner.logfile) for item in results.iter_blacklist(bl): - key = project + ":" + item.mod_name + key = f"{project}:{item.mod_name}" tot_res.setdefault(key, []) tot_res[key].append(item.rich_result) tot_flaky = 0 @@ -312,8 +313,8 @@ def main(): continue empty = False padding = 6 * runs - txt = " ".join(("{:<{width}}".format(piece, width=5) for piece in res)) - txt = (txt + padding * " ")[:padding] + txt = " ".join((f"{piece:<5}" for piece in res)) + txt = (f"{txt}{padding * ' '}")[:padding] testpad = 36 if len(test) < testpad: test += (testpad - len(test)) * " " @@ -325,7 +326,7 @@ def main(): print("*" * 79) print() if runs > 1: - print("Total flaky tests: errors but not always = {}".format(tot_flaky)) + print(f"Total flaky tests: errors but not always = {tot_flaky}") print() else: print("For info about flaky tests, we need to perform more than one run.") @@ -334,7 +335,7 @@ def main(): # nag us about unsupported projects ap, tp = set(all_projects), set(tested_projects) if ap != tp: - print("+++++ Note: please support", " ".join(ap-tp), "+++++") + print("+++++ Note: please support", " ".join(ap - tp), "+++++") print() stop_time = timer() @@ -342,16 +343,23 @@ def main(): # Now create an error if the criterion is met: try: if fatal: - raise ValueError("FATAL format error:", fatal) - err_crit = "'FAIL! >= {}'".format(fail_crit) + raise ValueError(f"FATAL format error: {fatal}") + err_crit = f"'FAIL! >= {fail_crit}'" + fail_count = 0 for res in tot_res.values(): if res.count("FAIL!") >= fail_crit: - raise ValueError("At least one failure was not blacklisted " - "and met the criterion {}" - .format(err_crit)) - print("No test met the error criterion {}".format(err_crit)) + fail_count += 1 + if fail_count == 1: + raise ValueError(f"A test was not blacklisted and met the criterion {err_crit}") + elif fail_count > 1: + raise ValueError( + f"{fail_count} failures were not blacklisted " f"and met the criterion {err_crit}" + ) + print(f"No test met the error criterion {err_crit}") finally: print() - print("Total time of whole Python script = {:0.2f} sec".format(used_time)) + print(f"Total time of whole Python script = {used_time:0.2f} sec") print() + + # eof diff --git a/testing/helper.py b/testing/helper.py index 88da48c6e..d89f0d849 100644 --- a/testing/helper.py +++ b/testing/helper.py @@ -1,43 +1,5 @@ -############################################################################# -## -## Copyright (C) 2017 The Qt Company Ltd. -## Contact: https://www.qt.io/licensing/ -## -## This file is part of Qt for Python. -## -## $QT_BEGIN_LICENSE:LGPL$ -## Commercial License Usage -## Licensees holding valid commercial Qt licenses may use this file in -## accordance with the commercial license agreement provided with the -## Software or, alternatively, in accordance with the terms contained in -## a written agreement between you and The Qt Company. For licensing terms -## and conditions see https://www.qt.io/terms-conditions. For further -## information use the contact form at https://www.qt.io/contact-us. -## -## GNU Lesser General Public License Usage -## Alternatively, this file may be used under the terms of the GNU Lesser -## General Public License version 3 as published by the Free Software -## Foundation and appearing in the file LICENSE.LGPL3 included in the -## packaging of this file. Please review the following information to -## ensure the GNU Lesser General Public License version 3 requirements -## will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -## -## GNU General Public License Usage -## Alternatively, this file may be used under the terms of the GNU -## General Public License version 2.0 or (at your option) the GNU General -## Public license version 3 or any later version approved by the KDE Free -## Qt Foundation. The licenses are as published by the Free Software -## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -## included in the packaging of this file. Please review the following -## information to ensure the GNU General Public License requirements will -## be met: https://www.gnu.org/licenses/gpl-2.0.html and -## https://www.gnu.org/licenses/gpl-3.0.html. -## -## $QT_END_LICENSE$ -## -############################################################################# - -from __future__ import print_function +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only """ testing/helper.py @@ -46,34 +8,17 @@ Some tools that do not fit elsewhere. """ import os -import sys -from collections import namedtuple - -PY3 = sys.version_info[0] == 3 # from the six module -from subprocess import PIPE -if PY3: - from subprocess import TimeoutExpired - from io import StringIO -else: - class SubprocessError(Exception): pass - # this is a fake, just to keep the source compatible. - # timeout support is in python 3.3 and above. - class TimeoutExpired(SubprocessError): pass - from StringIO import StringIO - script_dir = os.path.dirname(os.path.dirname(__file__)) + def decorate(mod_name): """ Write the combination of "modulename_funcname" in the Qt-like form "modulename::funcname" """ - if "_" not in mod_name: - return mod_name - if "::" in mod_name: + if "_" not in mod_name or "::" in mod_name: return mod_name - name, rest = mod_name.split("_", 1) - return name + "::" + rest - -#eof + else: + name, rest = mod_name.split("_", 1) + return f"{name}::{rest}" diff --git a/testing/parser.py b/testing/parser.py index 1d5c34680..a01c4d029 100644 --- a/testing/parser.py +++ b/testing/parser.py @@ -1,48 +1,10 @@ -############################################################################# -## -## Copyright (C) 2017 The Qt Company Ltd. -## Contact: https://www.qt.io/licensing/ -## -## This file is part of Qt for Python. -## -## $QT_BEGIN_LICENSE:LGPL$ -## Commercial License Usage -## Licensees holding valid commercial Qt licenses may use this file in -## accordance with the commercial license agreement provided with the -## Software or, alternatively, in accordance with the terms contained in -## a written agreement between you and The Qt Company. For licensing terms -## and conditions see https://www.qt.io/terms-conditions. For further -## information use the contact form at https://www.qt.io/contact-us. -## -## GNU Lesser General Public License Usage -## Alternatively, this file may be used under the terms of the GNU Lesser -## General Public License version 3 as published by the Free Software -## Foundation and appearing in the file LICENSE.LGPL3 included in the -## packaging of this file. Please review the following information to -## ensure the GNU Lesser General Public License version 3 requirements -## will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -## -## GNU General Public License Usage -## Alternatively, this file may be used under the terms of the GNU -## General Public License version 2.0 or (at your option) the GNU General -## Public license version 3 or any later version approved by the KDE Free -## Qt Foundation. The licenses are as published by the Free Software -## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -## included in the packaging of this file. Please review the following -## information to ensure the GNU General Public License requirements will -## be met: https://www.gnu.org/licenses/gpl-2.0.html and -## https://www.gnu.org/licenses/gpl-3.0.html. -## -## $QT_END_LICENSE$ -## -############################################################################# - -from __future__ import print_function +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only import os import re from collections import namedtuple -from .helper import StringIO +from io import StringIO """ testing/parser.py @@ -82,7 +44,9 @@ _TEST_PAT_PRE = r""" ([0-9]+) # sharp : # colon symbol ':' """ -_TEST_PAT = _TEST_PAT_PRE + r""" +_TEST_PAT = ( + _TEST_PAT_PRE + + r""" \s+ # some WS ([\w-]+) # mod_name .*? # whatever (non greedy) @@ -98,14 +62,17 @@ _TEST_PAT = _TEST_PAT_PRE + r""" \s* # any WS $ # end """ +) # validation of our pattern: assert re.match(_TEST_PAT, _EXAMPLE.splitlines()[5], re.VERBOSE) assert len(re.match(_TEST_PAT, _EXAMPLE.splitlines()[5], re.VERBOSE).groups()) == 8 assert len(re.match(_TEST_PAT, _EXAMPLE.splitlines()[7], re.VERBOSE).groups()) == 8 -TestResult = namedtuple("TestResult", "idx n sharp mod_name passed " - "code time fatal rich_result".split()) +TestResult = namedtuple( + "TestResult", "idx n sharp mod_name passed " "code time fatal rich_result".split() +) + def _parse_tests(test_log): """ @@ -126,13 +93,13 @@ def _parse_tests(test_log): match = re.match(pat, line, re.VERBOSE) if match and line.split()[-1] != "sec": # don't change the number of lines - lines[idx : idx + 2] = [line.rstrip() + lines[idx + 1], ""] + lines[idx:idx + 2] = [line.rstrip() + lines[idx + 1], ""] pat = _TEST_PAT for line in lines: match = re.match(pat, line, re.VERBOSE) if match: - idx, n, sharp, mod_name, much_stuff, code1, code2, tim = tup = match.groups() + idx, n, sharp, mod_name, much_stuff, code1, code2, tim = match.groups() # either code1 or code2 is None code = code1 or code2 idx, n, sharp, code, tim = int(idx), int(n), int(sharp), code.lower(), float(tim) @@ -145,11 +112,10 @@ def _parse_tests(test_log): # Use "if idx + 1 != item.idx or idx == 42:" if idx + 1 != item.idx: # The numbering is disrupted. Provoke an error in this line! - passed = False - code += ", but lines are disrupted!" - result[idx] = item._replace(passed=False, - code=item.code + ", but lines are disrupted!", - fatal=True) + code = f"{code}, but lines are disrupted!" + result[idx] = item._replace( + passed=False, code=f"{item.code}, but lines are disrupted!", fatal=True + ) break return result diff --git a/testing/runner.py b/testing/runner.py index 83b7b08e6..b52ac4937 100644 --- a/testing/runner.py +++ b/testing/runner.py @@ -1,56 +1,14 @@ -############################################################################# -## -## Copyright (C) 2018 The Qt Company Ltd. -## Contact: https://www.qt.io/licensing/ -## -## This file is part of the Qt for Python project. -## -## $QT_BEGIN_LICENSE:LGPL$ -## Commercial License Usage -## Licensees holding valid commercial Qt licenses may use this file in -## accordance with the commercial license agreement provided with the -## Software or, alternatively, in accordance with the terms contained in -## a written agreement between you and The Qt Company. For licensing terms -## and conditions see https://www.qt.io/terms-conditions. For further -## information use the contact form at https://www.qt.io/contact-us. -## -## GNU Lesser General Public License Usage -## Alternatively, this file may be used under the terms of the GNU Lesser -## General Public License version 3 as published by the Free Software -## Foundation and appearing in the file LICENSE.LGPL3 included in the -## packaging of this file. Please review the following information to -## ensure the GNU Lesser General Public License version 3 requirements -## will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -## -## GNU General Public License Usage -## Alternatively, this file may be used under the terms of the GNU -## General Public License version 2.0 or (at your option) the GNU General -## Public license version 3 or any later version approved by the KDE Free -## Qt Foundation. The licenses are as published by the Free Software -## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -## included in the packaging of this file. Please review the following -## information to ensure the GNU General Public License requirements will -## be met: https://www.gnu.org/licenses/gpl-2.0.html and -## https://www.gnu.org/licenses/gpl-3.0.html. -## -## $QT_END_LICENSE$ -## -############################################################################# - -from __future__ import print_function +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +import inspect import os -import sys import re import subprocess -import inspect - -from collections import namedtuple +import sys +from subprocess import TimeoutExpired from textwrap import dedent -from .buildlog import builds -from .helper import decorate, PY3, TimeoutExpired - # Get the dir path to the utils module try: this_file = __file__ @@ -58,10 +16,11 @@ except NameError: this_file = sys.argv[0] this_file = os.path.abspath(this_file) this_dir = os.path.dirname(this_file) -build_scripts_dir = os.path.abspath(os.path.join(this_dir, '../build_scripts')) +build_scripts_dir = os.path.abspath(os.path.join(this_dir, "..")) sys.path.append(build_scripts_dir) -from utils import detect_clang +from build_scripts.utils import detect_clang + class TestRunner(object): def __init__(self, log_entry, project, index): @@ -70,23 +29,48 @@ class TestRunner(object): self.test_dir = os.path.join(built_path, project) log_dir = log_entry.log_dir if index is not None: - self.logfile = os.path.join(log_dir, project + ".{}.log".format(index)) + self.logfile = os.path.join(log_dir, f"{project}.{index}.log") else: - self.logfile = os.path.join(log_dir, project + ".log") - os.environ['CTEST_OUTPUT_ON_FAILURE'] = '1' + self.logfile = os.path.join(log_dir, f"{project}.log") + os.environ["CTEST_OUTPUT_ON_FAILURE"] = "1" self._setup_clang() self._setup() + def get_python_version(self): + """ + Finding the exact Python version. + --------------------------------- + + This is done by asking the interpreter, because it cannot reliably + be found from any file name parsing as a triple. + + Note: We need to look into the CMakeCache.txt file to find out + what CMake has found as the Python interpreter to use. + This is *not* necessarily the same Python that runs this script, + otherwise we could use the version info directly. + """ + look_python = os.path.join(self.test_dir, "CMakeCache.txt") + look_for = "PYTHON_EXECUTABLE:FILEPATH=" + with open(look_python) as f: + for line in f: + if line.startswith(look_for): + python_exec = line.split("=")[-1].strip() + res = subprocess.run([python_exec, "-c", + "import sys;print(sys.version_info[:3])"], + capture_output=True) + return eval(res.stdout.decode("utf-8")) + return None + def _setup_clang(self): if sys.platform != "win32": return clang_dir = detect_clang() if clang_dir[0]: - clang_bin_dir = os.path.join(clang_dir[0], 'bin') - path = os.environ.get('PATH') - if not clang_bin_dir in path: - os.environ['PATH'] = clang_bin_dir + os.pathsep + path - print("Adding %s as detected by %s to PATH" % (clang_bin_dir, clang_dir[1])) + clang_bin_dir = os.path.join(clang_dir[0], "bin") + path = os.environ.get("PATH") + if clang_bin_dir not in path: + os.environ["PATH"] = clang_bin_dir + os.pathsep + path + print(f"Adding {clang_bin_dir} as detected by {clang_dir[1]} to PATH") def _find_ctest_in_file(self, file_name): """ @@ -103,14 +87,16 @@ class TestRunner(object): # We have probably forgotten to build the tests. # Give a nice error message with a shortened but exact path. rel_path = os.path.relpath(file_name) - msg = dedent("""\n - {line} - ** ctest is not in '{}'. + msg = dedent( + f"""\n + {'*' * 79} + ** ctest is not in '{rel_path}'. * Did you forget to build the tests with '--build-tests' in setup.py? - """).format(rel_path, line=79 * "*") + """ + ) raise RuntimeError(msg) # the ctest program is on the left to look_for - assert line, "Did not find {}".format(look_for) + assert line, f"Did not find {look_for}" ctest = re.search(r'(\S+|"([^"]+)")\s+' + look_for, line).groups() return ctest[1] or ctest[0] @@ -130,8 +116,9 @@ class TestRunner(object): path = os.path.join(self.test_dir, candidate) if os.path.exists(path): return self._find_ctest_in_file(path) - raise RuntimeError('Cannot find any of the build system files {}.'.format( - ', '.join(candidate_files))) + raise RuntimeError( + "Cannot find any of the build system files " f"{', '.join(candidate_files)}." + ) def _setup(self): self.ctestCommand = self._find_ctest() @@ -155,16 +142,20 @@ class TestRunner(object): # without a caret are interpreted as such which leads to weirdness. # Since we have all commands with explicit paths and don't use shell # commands, this should work fine. - print(dedent("""\ - running {cmd} - in {test_dir} - """).format(**self.__dict__)) - ctest_process = subprocess.Popen(self.cmd, - cwd=self.test_dir, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) + print( + dedent( + f"""\ + running {self.cmd} + in {self.test_dir} + """ + ) + ) + ctest_process = subprocess.Popen( + self.cmd, cwd=self.test_dir, stdout=subprocess.PIPE, stderr=subprocess.STDOUT + ) + def py_tee(input, output, label): - ''' + """ A simple (incomplete) tee command in Python This script simply logs everything from input to output @@ -179,7 +170,8 @@ class TestRunner(object): The special escape is for the case of an embedded file in the output. - ''' + """ + def xprint(*args, **kw): print(*args, file=output, **kw) @@ -199,26 +191,20 @@ class TestRunner(object): if line.startswith(text_z): labelled = True - tee_src = dedent("""\ - from __future__ import print_function - import sys - {} - py_tee(sys.stdin, sys.stdout, '{label}') - """).format(dedent(inspect.getsource(py_tee)), label=label) + tee_src = dedent(inspect.getsource(py_tee)) + tee_src = f"import sys\n{tee_src}\npy_tee(sys.stdin, sys.stdout, '{label}')" tee_cmd = (sys.executable, "-E", "-u", "-c", tee_src) - tee_process = subprocess.Popen(tee_cmd, - cwd=self.test_dir, - stdin=ctest_process.stdout) + tee_process = subprocess.Popen(tee_cmd, cwd=self.test_dir, stdin=ctest_process.stdout) try: comm = tee_process.communicate - output = (comm(timeout=timeout) if PY3 else comm())[0] + _ = comm(timeout=timeout)[0] except (TimeoutExpired, KeyboardInterrupt): print() print("aborted, partial result") ctest_process.kill() - outs, errs = ctest_process.communicate() + _ = ctest_process.communicate() # ctest lists to a temp file. Move it to the log - tmp_name = self.logfile + ".tmp" + tmp_name = f"{self.logfile}.tmp" if os.path.exists(tmp_name): if os.path.exists(self.logfile): os.unlink(self.logfile) @@ -240,4 +226,3 @@ class TestRunner(object): words = "^(" + "|".join(rerun) + ")$" cmd += ("--tests-regex", words) self._run(cmd, label, timeout) -# eof diff --git a/testing/wheel_tester.py b/testing/wheel_tester.py index 100fd18f7..b36ee55a4 100644 --- a/testing/wheel_tester.py +++ b/testing/wheel_tester.py @@ -1,41 +1,5 @@ -############################################################################# -## -## Copyright (C) 2019 The Qt Company Ltd. -## Contact: https://www.qt.io/licensing/ -## -## This file is part of Qt for Python. -## -## $QT_BEGIN_LICENSE:LGPL$ -## Commercial License Usage -## Licensees holding valid commercial Qt licenses may use this file in -## accordance with the commercial license agreement provided with the -## Software or, alternatively, in accordance with the terms contained in -## a written agreement between you and The Qt Company. For licensing terms -## and conditions see https://www.qt.io/terms-conditions. For further -## information use the contact form at https://www.qt.io/contact-us. -## -## GNU Lesser General Public License Usage -## Alternatively, this file may be used under the terms of the GNU Lesser -## General Public License version 3 as published by the Free Software -## Foundation and appearing in the file LICENSE.LGPL3 included in the -## packaging of this file. Please review the following information to -## ensure the GNU Lesser General Public License version 3 requirements -## will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -## -## GNU General Public License Usage -## Alternatively, this file may be used under the terms of the GNU -## General Public License version 2.0 or (at your option) the GNU General -## Public license version 3 or any later version approved by the KDE Free -## Qt Foundation. The licenses are as published by the Free Software -## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -## included in the packaging of this file. Please review the following -## information to ensure the GNU General Public License requirements will -## be met: https://www.gnu.org/licenses/gpl-2.0.html and -## https://www.gnu.org/licenses/gpl-3.0.html. -## -## $QT_END_LICENSE$ -## -############################################################################# +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only """ This script is used by Coin (coin_test_instructions.py specifically) to @@ -51,10 +15,16 @@ looked up in your PATH. Make sure that some generated wheels already exist in the dist/ directory (e.g. setup.py bdist_wheel was already executed). """ -from __future__ import print_function, absolute_import +import os +import platform +import shutil +import sys +import tempfile +import logging from argparse import ArgumentParser, RawTextHelpFormatter -import os, sys +from pathlib import Path +from configparser import ConfigParser try: this_file = __file__ @@ -62,38 +32,37 @@ except NameError: this_file = sys.argv[0] this_file = os.path.abspath(this_file) this_dir = os.path.dirname(this_file) -setup_script_dir = os.path.abspath(os.path.join(this_dir, '..')) +setup_script_dir = os.path.abspath(os.path.join(this_dir, "..")) sys.path.append(setup_script_dir) -from build_scripts.utils import find_files_using_glob -from build_scripts.utils import find_glob_in_path -from build_scripts.utils import run_process, run_process_output -from build_scripts.utils import rmtree -import distutils.log as log -import platform +from build_scripts.utils import (find_files_using_glob, find_glob_in_path, # noqa: E402 + remove_tree, run_process, run_process_output) +from build_scripts.log import log # noqa: E402 -log.set_verbosity(1) +log.setLevel(logging.DEBUG) + +NEW_WHEELS = False def find_executable(executable, command_line_value): value = command_line_value - option_str = '--{}'.format(executable) + option_str = f"--{executable}" if value: - log.info("{} option given: {}".format(option_str, value)) + log.info(f"{option_str} option given: {value}") if not os.path.exists(value): - raise RuntimeError("No executable exists at: {}".format(value)) + raise RuntimeError(f"No executable exists at: {value}") else: - log.info("No {} option given, trying to find {} in PATH.".format(option_str, executable)) + log.info(f"No {option_str} option given, trying to find {executable} in PATH.") paths = find_glob_in_path(executable) - log.info("{} executables found in PATH: {}".format(executable, paths)) + log.info(f"{executable} executables found in PATH: {paths}") if not paths: raise RuntimeError( - "No {} option was specified and no {} was found " - "in PATH.".format(option_str, executable)) + f"No {option_str} option was specified and no {executable} was " "found in PATH." + ) else: value = paths[0] - log.info("Using {} found in PATH: {}".format(executable, value)) + log.info(f"Using {executable} found in PATH: {value}") log.info("") return value @@ -102,8 +71,8 @@ QMAKE_PATH = None CMAKE_PATH = None -def get_wheels_dir(): - return os.path.join(setup_script_dir, "dist") +def get_wheels_dir(dir_name): + return os.path.join(setup_script_dir, dir_name) def get_examples_dir(): @@ -111,9 +80,13 @@ def get_examples_dir(): def package_prefix_names(): - # Note: shiboken2_generator is not needed for compile_using_pyinstaller, + # Note: shiboken6_generator is not needed for compile_using_nuitka, # but building modules with cmake needs it. - return ["shiboken2", "shiboken2_generator", "PySide2"] + if NEW_WHEELS: + return ["shiboken6", "shiboken6_generator", "PySide6_Essentials", "PySide6_Addons", + "PySide6"] + else: + return ["shiboken6", "shiboken6_generator", "PySide6"] def clean_egg_info(): @@ -125,16 +98,16 @@ def clean_egg_info(): # safe to do so. paths = find_files_using_glob(setup_script_dir, "*.egg-info") for p in paths: - log.info("Removing {}".format(p)) - rmtree(p) + log.info(f"Removing {p}") + remove_tree(p) def install_wheel(wheel_path): - log.info("Installing wheel: {}".format(wheel_path)) + log.info(f"Installing wheel: {wheel_path}") exit_code = run_process([sys.executable, "-m", "pip", "install", wheel_path]) log.info("") if exit_code: - raise RuntimeError("Error while installing wheel {}".format(wheel_path)) + raise RuntimeError(f"Error while installing wheel {wheel_path}") def try_install_wheels(wheels_dir, py_version): @@ -143,115 +116,162 @@ def try_install_wheels(wheels_dir, py_version): all_wheels = find_files_using_glob(wheels_dir, all_wheels_pattern) if len(all_wheels) > 1: - log.info("Found the following wheels in {}: ".format(wheels_dir)) + log.info(f"Found the following wheels in {wheels_dir}: ") for wheel in all_wheels: log.info(wheel) else: - log.info("No wheels found in {}".format(wheels_dir)) + log.info(f"No wheels found in {wheels_dir}") log.info("") for p in package_prefix_names(): - log.info("Trying to install {p}:".format(**locals())) - pattern = "{}-*cp{}*.whl".format(p, int(float(py_version))) + log.info(f"Trying to install {p}:") + pattern = f"{p}-*cp{int(float(py_version))}*.whl" files = find_files_using_glob(wheels_dir, pattern) if files and len(files) == 1: wheel_path = files[0] install_wheel(wheel_path) elif len(files) > 1: - raise RuntimeError("More than one wheel found for specific {p} version." - .format(**locals())) + raise RuntimeError(f"More than one wheel found for specific {p} version.") else: - raise RuntimeError("No {p} wheels compatible with Python {py_version} found " - "for testing.".format(**locals())) - - -def is_unix(): - if sys.platform.startswith("linux") or sys.platform == "darwin": - return True - return False + raise RuntimeError( + f"No {p} wheels compatible with Python {py_version} found " f"for testing." + ) def generate_build_cmake(): - args = [CMAKE_PATH] - if is_unix(): - args.extend(["-G", "Unix Makefiles"]) - else: - args.extend(["-G", "NMake Makefiles"]) - args.append("-DCMAKE_BUILD_TYPE=Release") - args.append("-Dpython_interpreter={}".format(sys.executable)) - - # Specify prefix path so find_package(Qt5) works. - qmake_dir = os.path.abspath(os.path.join(os.path.dirname(QMAKE_PATH), "..")) - args.append("-DCMAKE_PREFIX_PATH={}".format(qmake_dir)) - - args.append("..") - + # Specify prefix path so find_package(Qt6) works. + qmake_dir = Path(QMAKE_PATH).resolve().parent.parent + args = [CMAKE_PATH, "-G", "Ninja", "-DCMAKE_BUILD_TYPE=Release", + f"-Dpython_interpreter={sys.executable}", + f"-DCMAKE_PREFIX_PATH={qmake_dir}", + ".."] exit_code = run_process(args) if exit_code: raise RuntimeError("Failure while running cmake.") log.info("") -def generate_build_qmake(): - exit_code = run_process([QMAKE_PATH, "..", "python_interpreter={}".format(sys.executable)]) - if exit_code: - raise RuntimeError("Failure while running qmake.") - log.info("") - - def raise_error_pyinstaller(msg): print() - print("PYINST: {msg}".format(**locals())) - print("PYINST: sys.version = {}".format(sys.version.splitlines()[0])) - print("PYINST: platform.platform() = {}".format(platform.platform())) + print(f"PYINST: {msg}") + print(f"PYINST: sys.version = {sys.version.splitlines()[0]}") + print(f"PYINST: platform.platform() = {platform.platform()}") print("PYINST: See the error message above.") print() for line in run_process_output([sys.executable, "-m", "pip", "list"]): - print("PyInstaller pip list: ", line) + print(f"PyInstaller pip list: {line}") print() - raise(RuntimeError(msg)) + raise (RuntimeError(msg)) def compile_using_pyinstaller(): - src_path = os.path.join("..", "hello.py") + _ = os.path.join("..", "hello.py") spec_path = os.path.join("..", "hello_app.spec") exit_code = run_process([sys.executable, "-m", "PyInstaller", spec_path]) - # to create the spec file, this setting was used: - #"--name=hello_app", "--console", "--log-level=DEBUG", src_path]) - # By using a spec file, we avoid all the probing that might disturb certain - # platforms and also save some analysis time. + # to create the spec file, this setting was used: + # "--name=hello_app", "--console", "--log-level=DEBUG", src_path]) + # By using a spec file, we avoid all the probing that might disturb certain + # platforms and also save some analysis time. if exit_code: # 2019-04-28 Raising on error is again enabled raise_error_pyinstaller("Failure while compiling script using PyInstaller.") log.info("") -def run_make(): - args = [] - if is_unix(): - executable = "make" +def test_nuitka(example): + testprog = "Nuitka" + name = os.path.splitext(os.path.basename(example))[0] + print(f"Running {testprog} test of {name}") + current_dir = os.getcwd() + result = False + tmpdirname = tempfile.mkdtemp() + try: + os.chdir(tmpdirname) + cmd = [sys.executable, "-m", "nuitka", "--run", example] # , "--standalone"] + _ = run_process(cmd) + result = True + except RuntimeError as e: + print(str(e)) + finally: + os.chdir(current_dir) + print(f"Executable is in {tmpdirname}") + return result + + +def run_nuitka_test(example): + if test_nuitka(example): + log.info("") else: - executable = "nmake" - args.append(executable) - + raise RuntimeError(f"Failure running {example} with Nuitka.") + + +def _run_deploy_test(example, tmpdirname): + """Helper for running deployment and example.""" + main_file = None + for py_file in example.glob("*.py"): + shutil.copy(py_file, tmpdirname) + if not main_file or py_file.name == "main.py": + main_file = py_file + deploy_tool = Path(sys.executable).parent / "pyside6-deploy" + cmd = [os.fspath(deploy_tool), "-f", main_file.name, "--init"] + if run_process(cmd) != 0: + raise RuntimeError("Error creating pysidedeploy.spec") + + config_file = Path(tmpdirname) / "pysidedeploy.spec" + parser = ConfigParser(comment_prefixes="/", allow_no_value=True) + parser.read(config_file) + parser.set("nuitka", "extra_args", "--verbose --assume-yes-for-downloads") + with open(config_file, "w+") as config_file_writer: + parser.write(config_file_writer, space_around_delimiters=True) + + cmd = [os.fspath(deploy_tool), "-f", "-c", os.fspath(config_file)] + if run_process(cmd) != 0: + raise RuntimeError("Error deploying") + + suffix = "exe" if sys.platform == "win32" else "bin" + + if sys.platform != "darwin": + binary = f"{tmpdirname}/{main_file.stem}.{suffix}" + else: + binary = f"{tmpdirname}/pyside_app_demo.app/Contents/MacOS/{main_file.stem}" + + if run_process([binary]) != 0: + raise RuntimeError("Error running the deployed example") + return True + + +def run_deploy_test(example): + """Test pyside6-deploy.""" + log.info(f"Running deploy test of {example}") + current_dir = Path.cwd() + result = False + with tempfile.TemporaryDirectory() as tmpdirname: + try: + os.chdir(tmpdirname) + result = _run_deploy_test(example, tmpdirname) + except RuntimeError as e: + log.error(str(e)) + raise e + finally: + os.chdir(os.fspath(current_dir)) + state = "succeeded" if result else "failed" + log.info(f"Deploy test {state}") + return result + + +def run_ninja(): + args = ["ninja"] exit_code = run_process(args) if exit_code: - raise RuntimeError("Failure while running {}.".format(executable)) + raise RuntimeError(f"Failure while running {' '.join(args)}.") log.info("") -def run_make_install(): - args = [] - if is_unix(): - executable = "make" - else: - executable = "nmake" - args.append(executable) - args.append("install") - +def run_ninja_install(): + args = ["ninja", "install"] exit_code = run_process(args) if exit_code: - raise RuntimeError("Failed while running {} install.".format(executable)) + raise RuntimeError(f"Failed while running {' '.join(args)} install.") log.info("") @@ -259,28 +279,28 @@ def run_compiled_script(binary_path): args = [binary_path] exit_code = run_process(args) if exit_code: - raise_error_pyinstaller("Failure while executing compiled script: {}".format(binary_path)) + raise_error_pyinstaller(f"Failure while executing compiled script: {binary_path}") log.info("") -def execute_script(script_path): - args = [sys.executable, script_path] +def execute_script(script_path, *extra): + args = list(map(str, (sys.executable, script_path) + extra)) exit_code = run_process(args) if exit_code: - raise RuntimeError("Failure while executing script: {}".format(script_path)) + raise RuntimeError(f"Failure while executing script: {script_path}") log.info("") def prepare_build_folder(src_path, build_folder_name): build_path = os.path.join(src_path, build_folder_name) - # The script can be called for both Python 2 and Python 3 wheels, so + # The script can be called for Python 3 wheels, so # preparing a build folder should clean any previous existing build. if os.path.exists(build_path): - log.info("Removing {}".format(build_path)) - rmtree(build_path) + log.info(f"Removing {build_path}") + remove_tree(build_path) - log.info("Creating {}".format(build_path)) + log.info(f"Creating {build_path}") os.makedirs(build_path) os.chdir(build_path) @@ -288,65 +308,102 @@ def prepare_build_folder(src_path, build_folder_name): def try_build_examples(): examples_dir = get_examples_dir() - # This script should better go to the last place, here. - # But because it is most likely to break, we put it here for now. - log.info("Attempting to build hello.py using PyInstaller.") - # PyInstaller is loaded by coin_build_instructions.py, but not when - # testing directly this script. - src_path = os.path.join(examples_dir, "installer_test") - prepare_build_folder(src_path, "pyinstaller") - - compile_using_pyinstaller() - run_compiled_script(os.path.join(src_path, - "pyinstaller", "dist", "hello_app", "hello_app")) + # Disabled PyInstaller until it supports PySide 6 + if False: + # But because it is most likely to break, we put it here for now. + log.info("Attempting to build hello.py using PyInstaller.") + # PyInstaller is loaded by coin_build_instructions.py, but not when + # testing directly this script. + src_path = os.path.join(examples_dir, "installer_test") + prepare_build_folder(src_path, "pyinstaller") + compile_using_pyinstaller() + run_compiled_script(os.path.join(src_path, "pyinstaller", "dist", "hello_app", "hello_app")) + + log.info("Attempting to build hello.py using Nuitka.") + src_path = Path(examples_dir) / "installer_test" + + # disable for windows as it Nuitka --onefile deployment in Windows + # requires DependencyWalker. Download and installing will slow down + # Coin + if sys.platform != "win32": + run_deploy_test(src_path) + + if False: # pre 6.4.1, kept for reference + # Nuitka is loaded by coin_build_instructions.py, but not when + # testing directly this script. + run_nuitka_test(os.fspath(src_path / "hello.py")) log.info("Attempting to build and run samplebinding using cmake.") src_path = os.path.join(examples_dir, "samplebinding") prepare_build_folder(src_path, "cmake") generate_build_cmake() - run_make() - run_make_install() + run_ninja() + run_ninja_install() execute_script(os.path.join(src_path, "main.py")) log.info("Attempting to build scriptableapplication using cmake.") src_path = os.path.join(examples_dir, "scriptableapplication") prepare_build_folder(src_path, "cmake") generate_build_cmake() - run_make() - - log.info("Attempting to build scriptableapplication using qmake.") - src_path = os.path.join(examples_dir, "scriptableapplication") - prepare_build_folder(src_path, "qmake") - generate_build_qmake() - run_make() - - -def run_wheel_tests(install_wheels): - wheels_dir = get_wheels_dir() - py_version = "{v.major}.{v.minor}".format(v=sys.version_info) + run_ninja() + + if sys.version_info[:2] >= (3, 7): + log.info("Checking Python Interface Files in Python 3 with all features selected") + with tempfile.TemporaryDirectory() as tmpdirname: + src_path = Path(tmpdirname) / "pyi_test" + pyi_script_dir = Path(setup_script_dir) / "sources" / "pyside6" / "PySide6" / "support" + execute_script( + pyi_script_dir / "generate_pyi.py", + "all", + "--outpath", + src_path, + "--feature", + "snake_case", + "true_property", + ) + from PySide6 import __all__ as modules + + for modname in modules: + # PYSIDE-1735: pyi files are no longer compatible with Python. + # XXX Maybe add a test with Mypy here? + pass # execute_script(src_path / f"{modname}.pyi") + + +def run_wheel_tests(install_wheels, wheels_dir_name): + wheels_dir = get_wheels_dir(wheels_dir_name) + py_version = f"{sys.version_info.major}.{sys.version_info.minor}" if install_wheels: log.info("Attempting to install wheels.\n") try_install_wheels(wheels_dir, py_version) log.info("Attempting to build examples.\n") - try_build_examples() + bin_dir = os.fspath(Path(sys.executable).parent) + path = os.environ["PATH"] + if bin_dir not in path: + log.info(f"Adding {bin_dir} to PATH...") + os.environ["PATH"] = f"{bin_dir}{os.pathsep}{path}" + try_build_examples() log.info("All tests passed!") if __name__ == "__main__": - parser = ArgumentParser(description="wheel_tester", - formatter_class=RawTextHelpFormatter) - parser.add_argument('--no-install-wheels', '-n', action='store_true', - help='Do not install wheels' - ' (for developer builds with virtualenv)') - parser.add_argument("--qmake", type=str, - help="Path to qmake") - parser.add_argument("--cmake", type=str, - help="Path to cmake") + parser = ArgumentParser(description="wheel_tester", formatter_class=RawTextHelpFormatter) + parser.add_argument( + "--no-install-wheels", + "-n", + action="store_true", + help="Do not install wheels" " (for developer builds with virtualenv)", + ) + parser.add_argument("--qmake", type=str, help="Path to qmake") + parser.add_argument("--cmake", type=str, help="Path to cmake") + parser.add_argument("--wheels-dir", type=str, help="Path to where the wheels are", + default="dist") + parser.add_argument("--new", action="store_true", help="Option to test new wheels") options = parser.parse_args() - QMAKE_PATH = find_executable('qmake', options.qmake) - CMAKE_PATH = find_executable('cmake', options.cmake) + QMAKE_PATH = find_executable("qmake", options.qmake) + CMAKE_PATH = find_executable("cmake", options.cmake) + NEW_WHEELS = options.new - run_wheel_tests(not options.no_install_wheels) + run_wheel_tests(not options.no_install_wheels, options.wheels_dir) |