From 207b8cb8a50d68e207d28b59e588311a5cbd9772 Mon Sep 17 00:00:00 2001 From: Emmanuel Fleury Date: Tue, 21 Jun 2022 18:49:25 +0200 Subject: [PATCH 1/2] Convert tests/assert-msg-test* to glib/tests/assert-msg-test* Closes issue #1434 --- {tests => glib/tests}/assert-msg-test.c | 0 glib/tests/assert-msg-test.py | 155 +++++++++++++++++++ glib/tests/meson.build | 41 ++++++ glib/tests/taptestrunner.py | 188 ++++++++++++++++++++++++ meson.build | 3 - tests/assert-msg-test.gdb | 5 - tests/meson.build | 29 ---- tests/run-assert-msg-test.sh | 49 ------ 8 files changed, 384 insertions(+), 86 deletions(-) rename {tests => glib/tests}/assert-msg-test.c (100%) create mode 100755 glib/tests/assert-msg-test.py create mode 100644 glib/tests/taptestrunner.py delete mode 100644 tests/assert-msg-test.gdb delete mode 100644 tests/meson.build delete mode 100755 tests/run-assert-msg-test.sh diff --git a/tests/assert-msg-test.c b/glib/tests/assert-msg-test.c similarity index 100% rename from tests/assert-msg-test.c rename to glib/tests/assert-msg-test.c diff --git a/glib/tests/assert-msg-test.py b/glib/tests/assert-msg-test.py new file mode 100755 index 000000000..2d54a5619 --- /dev/null +++ b/glib/tests/assert-msg-test.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright © 2022 Emmanuel Fleury +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301 USA + +""" Integration tests for g_assert() functions. """ + +import collections +import os +import shutil +import subprocess +import sys +import tempfile +import unittest + +import taptestrunner + +Result = collections.namedtuple("Result", ("info", "out", "err")) + +GDB_SCRIPT = """ +# Work around https://sourceware.org/bugzilla/show_bug.cgi?id=22501 +set confirm off +set print elements 0 +set auto-load safe-path / +run +print *((char**) &__glib_assert_msg) +quit +""" + + +class TestAssertMessage(unittest.TestCase): + """Integration test for throwing message on g_assert(). + + This can be run when installed or uninstalled. When uninstalled, + it requires G_TEST_BUILDDIR and G_TEST_SRCDIR to be set. + + The idea with this test harness is to test if g_assert() prints + an error message when called, and that it saves this error + message in a global variable accessible to gdb, so that developers + and automated tools can more easily debug assertion failures. + """ + + def setUp(self): + self.__gdb = shutil.which("gdb") + self.timeout_seconds = 10 # seconds per test + + if "G_TEST_BUILDDIR" in os.environ: + self.__assert_msg_test = os.path.join( + os.environ["G_TEST_BUILDDIR"], "assert-msg-test" + ) + else: + self.__assert_msg_test = shutil.which("assert-msg-test") + print("assert-msg-test:", self.__assert_msg_test) + + def runAssertMessage(self, *args): + argv = [self.__assert_msg_test] + # shebang lines are not supported on native + # Windows consoles + if os.name == "nt": + argv.insert(0, sys.executable) + argv.extend(args) + print("Running:", argv) + + env = os.environ.copy() + env["LC_ALL"] = "C.UTF-8" + print("Environment:", env) + + # We want to ensure consistent line endings... + info = subprocess.run( + argv, + timeout=self.timeout_seconds, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=env, + universal_newlines=True, + ) + out = info.stdout.strip() + err = info.stderr.strip() + + result = Result(info, out, err) + + print("Output:", result.out) + return result + + def runGdbAssertMessage(self, *args): + if self.__gdb is None: + return Result(None, "", "") + + argv = ["gdb", "--batch"] + argv.extend(args) + print("Running:", argv) + + env = os.environ.copy() + env["LC_ALL"] = "C.UTF-8" + print("Environment:", env) + + # We want to ensure consistent line endings... + info = subprocess.run( + argv, + timeout=self.timeout_seconds, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=env, + universal_newlines=True, + ) + info.check_returncode() + out = info.stdout.strip() + err = info.stderr.strip() + + result = Result(info, out, err) + + print("Output:", result.out) + return result + + def test_gassert(self): + """Test running g_assert() and fail the program.""" + result = self.runAssertMessage() + + self.assertEqual(result.info.returncode, -6) + self.assertIn("assertion failed: (42 < 0)", result.out) + + def test_gdb_gassert(self): + """Test running g_assert() within gdb and fail the program.""" + if self.__gdb is None: + self.skipTest("GDB is not installed, skipping this test!") + + with tempfile.NamedTemporaryFile( + prefix="assert-msg-test-", suffix=".gdb", mode="w" + ) as tmp: + tmp.write(GDB_SCRIPT) + tmp.flush() + + result = self.runGdbAssertMessage("-x", tmp.name, self.__assert_msg_test) + self.assertEqual(result.info.returncode, 0) + self.assertIn("$1 = 0x", result.out) + self.assertIn("assertion failed: (42 < 0)", result.out) + + +if __name__ == "__main__": + unittest.main(testRunner=taptestrunner.TAPTestRunner()) diff --git a/glib/tests/meson.build b/glib/tests/meson.build index d16a071e5..9b3b3bfa4 100644 --- a/glib/tests/meson.build +++ b/glib/tests/meson.build @@ -282,6 +282,47 @@ if installed_tests_enabled ) endif +python_tests = [ + 'assert-msg-test.py', +] + +executable('assert-msg-test', ['assert-msg-test.c'], + c_args : test_cargs, + dependencies : test_deps, + install_dir : installed_tests_execdir, + install : installed_tests_enabled, + win_subsystem : extra_args.get('win_subsystem', 'console'), +) + +foreach test_name : python_tests + test( + test_name, + python, + args: ['-B', files(test_name)], + env: test_env, + suite: ['glib', 'no-valgrind'], + ) + + if installed_tests_enabled + install_data( + files(test_name), + install_dir: installed_tests_execdir, + install_mode: 'rwxr-xr-x', + ) + + test_conf = configuration_data() + test_conf.set('installed_tests_dir', installed_tests_execdir) + test_conf.set('program', test_name) + test_conf.set('env', '') + configure_file( + input: installed_tests_template_tap, + output: test_name + '.test', + install_dir: installed_tests_metadir, + configuration: test_conf, + ) + endif +endforeach + executable('spawn-path-search-helper', 'spawn-path-search-helper.c', c_args : test_cargs, dependencies : test_deps, diff --git a/glib/tests/taptestrunner.py b/glib/tests/taptestrunner.py new file mode 100644 index 000000000..9adbd8daa --- /dev/null +++ b/glib/tests/taptestrunner.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python +# coding=utf-8 + +# Copyright (c) 2015 Remko Tronçon (https://el-tramo.be) +# Copied from https://github.com/remko/pycotap/ +# +# SPDX-License-Identifier: MIT +# +# Released under the MIT license +# +# 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. + + +import unittest +import sys +import base64 +from io import StringIO + + +# Log modes +class LogMode(object): + LogToError, LogToDiagnostics, LogToYAML, LogToAttachment = range(4) + + +class TAPTestResult(unittest.TestResult): + def __init__(self, output_stream, error_stream, message_log, test_output_log): + super(TAPTestResult, self).__init__(self, output_stream) + self.output_stream = output_stream + self.error_stream = error_stream + self.orig_stdout = None + self.orig_stderr = None + self.message = None + self.test_output = None + self.message_log = message_log + self.test_output_log = test_output_log + self.output_stream.write("TAP version 13\n") + self._set_streams() + + def printErrors(self): + self.print_raw("1..%d\n" % self.testsRun) + self._reset_streams() + + def _set_streams(self): + self.orig_stdout = sys.stdout + self.orig_stderr = sys.stderr + if self.message_log == LogMode.LogToError: + self.message = self.error_stream + else: + self.message = StringIO() + if self.test_output_log == LogMode.LogToError: + self.test_output = self.error_stream + else: + self.test_output = StringIO() + + if self.message_log == self.test_output_log: + self.test_output = self.message + sys.stdout = sys.stderr = self.test_output + + def _reset_streams(self): + sys.stdout = self.orig_stdout + sys.stderr = self.orig_stderr + + def print_raw(self, text): + self.output_stream.write(text) + self.output_stream.flush() + + def print_result(self, result, test, directive=None): + self.output_stream.write("%s %d %s" % (result, self.testsRun, test.id())) + if directive: + self.output_stream.write(" # " + directive) + self.output_stream.write("\n") + self.output_stream.flush() + + def ok(self, test, directive=None): + self.print_result("ok", test, directive) + + def not_ok(self, test): + self.print_result("not ok", test) + + def startTest(self, test): + super(TAPTestResult, self).startTest(test) + + def stopTest(self, test): + super(TAPTestResult, self).stopTest(test) + if self.message_log == self.test_output_log: + logs = [(self.message_log, self.message, "output")] + else: + logs = [ + (self.test_output_log, self.test_output, "test_output"), + (self.message_log, self.message, "message"), + ] + for log_mode, log, log_name in logs: + if log_mode != LogMode.LogToError: + output = log.getvalue() + if len(output): + if log_mode == LogMode.LogToYAML: + self.print_raw(" ---\n") + self.print_raw(" " + log_name + ": |\n") + self.print_raw( + " " + output.rstrip().replace("\n", "\n ") + "\n" + ) + self.print_raw(" ...\n") + elif log_mode == LogMode.LogToAttachment: + self.print_raw(" ---\n") + self.print_raw(" " + log_name + ":\n") + self.print_raw(" File-Name: " + log_name + ".txt\n") + self.print_raw(" File-Type: text/plain\n") + self.print_raw( + " File-Content: " + base64.b64encode(output) + "\n" + ) + self.print_raw(" ...\n") + else: + self.print_raw( + "# " + output.rstrip().replace("\n", "\n# ") + "\n" + ) + # Truncate doesn't change the current stream position. + # Seek to the beginning to avoid extensions on subsequent writes. + log.seek(0) + log.truncate(0) + + def addSuccess(self, test): + super(TAPTestResult, self).addSuccess(test) + self.ok(test) + + def addError(self, test, err): + super(TAPTestResult, self).addError(test, err) + self.message.write(self.errors[-1][1] + "\n") + self.not_ok(test) + + def addFailure(self, test, err): + super(TAPTestResult, self).addFailure(test, err) + self.message.write(self.failures[-1][1] + "\n") + self.not_ok(test) + + def addSkip(self, test, reason): + super(TAPTestResult, self).addSkip(test, reason) + self.ok(test, "SKIP " + reason) + + def addExpectedFailure(self, test, err): + super(TAPTestResult, self).addExpectedFailure(test, err) + self.ok(test) + + def addUnexpectedSuccess(self, test): + super(TAPTestResult, self).addUnexpectedSuccess(test) + self.message.write("Unexpected success" + "\n") + self.not_ok(test) + + +class TAPTestRunner(object): + def __init__( + self, + message_log=LogMode.LogToYAML, + test_output_log=LogMode.LogToDiagnostics, + output_stream=sys.stdout, + error_stream=sys.stderr, + ): + self.output_stream = output_stream + self.error_stream = error_stream + self.message_log = message_log + self.test_output_log = test_output_log + + def run(self, test): + result = TAPTestResult( + self.output_stream, + self.error_stream, + self.message_log, + self.test_output_log, + ) + test(result) + result.printErrors() + + return result diff --git a/meson.build b/meson.build index e44bad35b..5c85db9b2 100644 --- a/meson.build +++ b/meson.build @@ -2346,9 +2346,6 @@ subdir('gthread') subdir('gmodule') subdir('gio') subdir('fuzzing') -if build_tests - subdir('tests') -endif subdir('tools') # xgettext is optional (on Windows for instance) diff --git a/tests/assert-msg-test.gdb b/tests/assert-msg-test.gdb deleted file mode 100644 index dbecaaf2a..000000000 --- a/tests/assert-msg-test.gdb +++ /dev/null @@ -1,5 +0,0 @@ -run -set print elements 0 -# Work around https://sourceware.org/bugzilla/show_bug.cgi?id=22501 -print *((char**) &__glib_assert_msg) -quit diff --git a/tests/meson.build b/tests/meson.build deleted file mode 100644 index c2d927854..000000000 --- a/tests/meson.build +++ /dev/null @@ -1,29 +0,0 @@ -# tests - -test_env = environment() -test_env.set('G_TEST_SRCDIR', meson.current_source_dir()) -test_env.set('G_TEST_BUILDDIR', meson.current_build_dir()) -test_env.set('G_DEBUG', 'gc-friendly') -test_env.set('MALLOC_CHECK_', '2') - -test_cargs = ['-DG_LOG_DOMAIN="GLib"', '-UG_DISABLE_ASSERT'] - -test_extra_programs = { - 'assert-msg-test' : {}, -} - -common_c_args = test_cargs + ['-DGLIB_DISABLE_DEPRECATION_WARNINGS'] -common_deps = [libm, thread_dep, libglib_dep] - -foreach program_name, extra_args : test_extra_programs - source = extra_args.get('source', program_name + '.c') - extra_sources = extra_args.get('extra_sources', []) - install = installed_tests_enabled and extra_args.get('install', true) - executable(program_name, [source, extra_sources], - c_args : common_c_args, - dependencies : common_deps + extra_args.get('dependencies', []), - install_dir : installed_tests_execdir, - install : install, - win_subsystem : extra_args.get('win_subsystem', 'console'), - ) -endforeach diff --git a/tests/run-assert-msg-test.sh b/tests/run-assert-msg-test.sh deleted file mode 100755 index 88f86f1e2..000000000 --- a/tests/run-assert-msg-test.sh +++ /dev/null @@ -1,49 +0,0 @@ -#! /bin/sh - -fail () -{ - echo "Test failed: $*" - exit 1 -} - -echo_v () -{ - if [ "$verbose" = "1" ]; then - echo "$*" - fi -} - -error_out=/dev/null -if [ "$1" = "-v" ]; then - verbose=1 - error_out=/dev/stderr -fi - -if [ -z "$LIBTOOL" ]; then - if [ -f ../libtool ]; then - LIBTOOL=../libtool - else - LIBTOOL=libtool - fi -fi - -echo_v "Running assert-msg-test" -OUT=$(./assert-msg-test 2>&1) && fail "assert-msg-test should abort" -echo "$OUT" | grep -q '^GLib:ERROR:.*assert-msg-test.c:.*:.*main.*: assertion failed: (42 < 0)' || \ - fail "does not print assertion message" - -if ! type gdb >/dev/null 2>&1; then - echo_v "Skipped (no gdb installed)" - exit 0 -fi - -echo_v "Running gdb on assert-msg-test" -OUT=$($LIBTOOL --mode=execute gdb --batch -x "${srcdir:-.}/assert-msg-test.gdb" ./assert-msg-test 2> $error_out) || fail "failed to run gdb" - -echo_v "Checking if assert message is in __glib_assert_msg" -# shellcheck disable=SC2016 -if ! echo "$OUT" | grep -q '^$1.*"GLib:ERROR:.*assert-msg-test.c:.*:.*main.*: assertion failed: (42 < 0)"'; then - fail "__glib_assert_msg does not have assertion message" -fi - -echo_v "All tests passed." From 5699b7b1691c15ac7eaafe71f6d08d649f75c046 Mon Sep 17 00:00:00 2001 From: Emmanuel Fleury Date: Thu, 23 Jun 2022 18:41:31 +0200 Subject: [PATCH 2/2] Fix some coding style issues in python tests pointed out by black and flake8 --- gio/tests/gengiotypefuncs.py | 4 ++-- gobject/tests/gobject-query.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/gio/tests/gengiotypefuncs.py b/gio/tests/gengiotypefuncs.py index ab99df66a..ae25817f7 100644 --- a/gio/tests/gengiotypefuncs.py +++ b/gio/tests/gengiotypefuncs.py @@ -25,9 +25,9 @@ for filename in in_files: with open(filename, "rb") as f: for line in f: line = line.rstrip(b"\n").rstrip(b"\r") - match = re.search(br"\bg_[a-zA-Z0-9_]*_get_type\b", line) + match = re.search(rb"\bg_[a-zA-Z0-9_]*_get_type\b", line) if match: - func = match.group(0).decode('utf-8') + func = match.group(0).decode("utf-8") if func not in funcs: funcs.append(func) if debug: diff --git a/gobject/tests/gobject-query.py b/gobject/tests/gobject-query.py index 9eba8bc84..094f37d3c 100644 --- a/gobject/tests/gobject-query.py +++ b/gobject/tests/gobject-query.py @@ -25,7 +25,6 @@ import os import shutil import subprocess import sys -from textwrap import dedent import unittest import taptestrunner