#!/usr/bin/python3 # -*- coding: utf-8 -*- # # Copyright © 2018 Endless Mobile, Inc. # Copyright © 2025 Canonical Ltd. # # SPDX-License-Identifier: LGPL-2.1-or-later # # 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 GLib utilities.""" import os import shutil import subprocess import tempfile import unittest import sys from dataclasses import dataclass from enum import Enum class ProgramType(Enum): """Enum to define the kind of tool to use""" NATIVE = 1 INTERPRETED = 2 @dataclass class Result: """Class for keeping track of the executable result.""" info: subprocess.CompletedProcess out: str err: str subs: dict class TestProgramRunner(unittest.TestCase): """Integration test for running glib-based tools. This can be run when installed or uninstalled. When uninstalled, it requires G_TEST_BUILDDIR or _G_TEST_PROGRAM_RUNNER_PATH to be set. """ PROGRAM_NAME: str = None PROGRAM_TYPE: ProgramType = ProgramType.NATIVE INTERPRETER: str = None @classmethod def setUpClass(cls): super().setUpClass() cls.assertTrue(cls.PROGRAM_NAME, "class PROGRAM_NAME must be set") ext = "" if cls.PROGRAM_TYPE == ProgramType.NATIVE and os.name == "nt": ext = ".exe" cls._program_name = f"{cls.PROGRAM_NAME}{ext}" if "_G_TEST_PROGRAM_RUNNER_PATH" in os.environ: cls.__program = os.path.join( os.environ["_G_TEST_PROGRAM_RUNNER_PATH"], cls._program_name ) elif "G_TEST_BUILDDIR" in os.environ: cls.__program = os.path.join( os.environ["G_TEST_BUILDDIR"], cls._program_name ) else: cls.__program = os.path.join(os.path.dirname(__file__), cls._program_name) if not os.path.exists(cls.__program): cls.__program = shutil.which(cls._program_name) def setUp(self): print(f"{self.PROGRAM_NAME}: {self.__program}") self.assertTrue(os.path.exists(self.__program)) self.tmpdir = tempfile.TemporaryDirectory() self.addCleanup(self.tmpdir.cleanup) old_cwd = os.getcwd() self.addCleanup(os.chdir, old_cwd) os.chdir(self.tmpdir.name) print("tmpdir:", self.tmpdir.name) def runTestProgram( self, *args, should_fail=False, timeout_seconds=10, wrapper_args=[] ) -> Result: argv = [self.__program] argv.extend(*args) # shebang lines are not supported on native # Windows consoles if self.PROGRAM_TYPE == ProgramType.INTERPRETED and os.name == "nt": argv.insert(0, self.INTERPRETER if self.INTERPRETER else sys.executable) argv = wrapper_args + argv env = os.environ.copy() env["LC_ALL"] = "C.UTF-8" env["G_DEBUG"] = "fatal-warnings" print("Running:", argv) # We want to ensure consistent line endings... info = subprocess.run( argv, timeout=timeout_seconds, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, universal_newlines=True, text=True, encoding="utf-8", check=False, ) result = Result( info=info, out=info.stdout.strip(), err=info.stderr.strip(), subs=self._getSubs(), ) print("Return code:", result.info.returncode) print("Output:\n", result.out) print("Error:\n", result.err) if should_fail: with self.assertRaises(subprocess.CalledProcessError): info.check_returncode() else: info.check_returncode() return result def _getSubs(self) -> dict: return {}