Files
core/codemaker/tests/pythonmaker/test_pythonmaker.py
Manish Bera 02f3d7e950 Unittests for pythonmaker
This patch adds the unit and integration test suite for the pythonmaker
tool.

The test is orchestrated by a Python script and uses a "golden file"
comparison method. It runs `pythonmaker` on a comprehensive test IDL
and verifies that the generated `.pyi` output exactly matches a set of
pre-validated reference stubs.

The test is integrated into the build system and runs with 'make check'.

Change-Id: If91814767779812740c543476307600f61c8a656
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/188456
Tested-by: Jenkins
Reviewed-by: Hossein <hossein@libreoffice.org>
2026-01-02 17:59:12 +01:00

246 lines
7.6 KiB
Python

#! /usr/bin/env python
# -*- tab-width: 4; indent-tabs-mode: nil; py-indent-offset: 4 -*-
#
# This file is part of the LibreOffice project.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
import unittest
import os
import subprocess
import shutil
import filecmp
import glob
import sys
from typing import Optional, List, Set
class TestPythonMaker(unittest.TestCase):
"""Test pythonmaker tool by comparing generated stubs with expected files"""
def setUp(self) -> None:
# Build environment
self.srcdir: str = os.environ.get("SRCDIR", os.getcwd())
self.builddir: str = os.environ.get("BUILDDIR", os.getcwd())
self.instdir: str = os.environ.get(
"INSTDIR", os.path.join(self.builddir, "instdir")
)
self.workdir: str = os.environ.get(
"WORKDIR", os.path.join(self.builddir, "workdir")
)
# SDK tools
self.unoidl_write: Optional[str] = None
self.pythonmaker: Optional[str] = None
sdk_patterns: List[str] = [
os.path.join(self.instdir, "sdk", "bin"),
os.path.join(self.instdir, "LibreOffice*_SDK", "bin"),
]
exe_suffix: str = ".exe" if os.name == "nt" else ""
for pattern in sdk_patterns:
for sdk_dir in glob.glob(pattern):
unoidl_path = os.path.join(sdk_dir, "unoidl-write" + exe_suffix)
pythonmaker_path = os.path.join(sdk_dir, "pythonmaker" + exe_suffix)
if os.path.exists(unoidl_path) and os.path.exists(pythonmaker_path):
self.unoidl_write = unoidl_path
self.pythonmaker = pythonmaker_path
break
if self.unoidl_write and self.pythonmaker:
break
# Program directory
self.program_dir: Optional[str]
if sys.platform == "darwin":
self.program_dir = None
app_patterns: List[str] = [
os.path.join(
self.instdir,
"LibreOfficeDev.app",
"Contents",
"Resources",
),
os.path.join(
self.instdir,
"LibreOffice.app",
"Contents",
"Resources",
),
]
for pattern in app_patterns:
if os.path.exists(pattern):
self.program_dir = pattern
break
else:
self.program_dir = os.path.join(self.instdir, "program")
# Test paths
self.golden_dir: str = os.path.join(
self.srcdir,
"codemaker",
"tests",
"pythonmaker",
"expected_pyi_stubs",
)
self.idl_file: str = os.path.join(
self.srcdir,
"codemaker",
"tests",
"pythonmaker",
"idl",
"pythontypes.idl",
)
self.test_workdir: str = os.path.join(self.workdir, "pythonmaker_test")
os.makedirs(self.test_workdir, exist_ok=True)
self.types_rdb: str = os.path.join(
self.workdir, "UnoApiTarget", "udkapi.rdb"
)
self.temp_rdb: str = os.path.join(self.test_workdir, "temptest.rdb")
self.output_dir: str = os.path.join(self.test_workdir, "generated_stubs")
def tearDown(self) -> None:
if os.path.exists(self.test_workdir):
shutil.rmtree(self.test_workdir, ignore_errors=True)
def test_pythonmaker_golden_comparison(self) -> None:
self.assertIsNotNone(self.unoidl_write)
self.assertIsNotNone(self.pythonmaker)
self.assertIsNotNone(self.program_dir)
assert self.unoidl_write is not None
assert self.pythonmaker is not None
self.assertTrue(os.path.exists(self.unoidl_write))
self.assertTrue(os.path.exists(self.pythonmaker))
self.assertTrue(os.path.exists(self.types_rdb))
self._convert_idl_to_rdb()
self._generate_python_stubs()
self._compare_with_expected_files()
def _convert_idl_to_rdb(self) -> None:
assert self.unoidl_write is not None
cmd: List[str] = [
self.unoidl_write,
self.types_rdb,
self.idl_file,
self.temp_rdb,
]
try:
subprocess.run(
cmd,
cwd=self.test_workdir,
capture_output=True,
text=True,
check=True,
)
self.assertTrue(os.path.exists(self.temp_rdb))
except subprocess.CalledProcessError as e:
self.fail(
"Failed to convert IDL to RDB:\n"
+ (e.stderr or "")
+ "\nCommand: "
+ " ".join(cmd)
)
def _generate_python_stubs(self) -> None:
assert self.pythonmaker is not None
os.makedirs(self.output_dir, exist_ok=True)
cmd: List[str] = [
self.pythonmaker,
"-O",
self.output_dir,
self.temp_rdb,
"-X",
self.types_rdb,
]
try:
subprocess.run(
cmd,
cwd=self.test_workdir,
capture_output=True,
text=True,
check=True,
)
generated_files: List[str] = []
for _, _, files in os.walk(self.output_dir):
for name in files:
if not name.lower().endswith(".tmp"):
generated_files.append(name)
self.assertGreater(len(generated_files), 0)
except subprocess.CalledProcessError as e:
self.fail(
"Failed to generate Python stubs:\n"
+ (e.stderr or "")
+ "\nCommand: "
+ " ".join(cmd)
)
def _compare_with_expected_files(self) -> None:
if not os.path.exists(self.golden_dir):
self.fail("Expected directory does not exist: " + self.golden_dir)
expected_files: List[str] = []
for root, _, files in os.walk(self.golden_dir):
for name in files:
expected_files.append(
os.path.relpath(os.path.join(root, name), self.golden_dir)
)
generated_files: List[str] = []
for root, _, files in os.walk(self.output_dir):
for name in files:
if not name.lower().endswith(".tmp"):
generated_files.append(
os.path.relpath(os.path.join(root, name), self.output_dir)
)
expected_set: Set[str] = set(expected_files)
generated_set: Set[str] = set(generated_files)
missing = expected_set - generated_set
extra = generated_set - expected_set
if missing:
self.fail("Missing generated files: " + str(missing))
if extra:
self.fail("Unexpected generated files: " + str(extra))
differences: List[str] = []
for rel_path in expected_files:
if not filecmp.cmp(
os.path.join(self.golden_dir, rel_path),
os.path.join(self.output_dir, rel_path),
shallow=False,
):
differences.append(rel_path)
if differences:
self.fail("File content differences found in: " + str(differences))
if __name__ == "__main__":
unittest.main()
# vim: set shiftwidth=4 softtabstop=4 expandtab: