mirror of
https://github.com/miurahr/aqtinstall.git
synced 2025-12-17 04:34:37 +03:00
This change adds unit tests for the `aqt install` and `aqt install-qt` commands, and establishes a pattern that can be extended for more tests. This is intended to make it easier to increase test coverage of parts of the codebase that are not yet covered by tests. This uses the `pytest-socket` library to ensure that the tests do not use any network IO. It also mocks `multiprocessing.get_context` to prevent multiprocessing. This is necessary because multiprocessing spawns child processes that have not been monkey-patched, which would break the test. These tests use py7zr to create mock 7z archives, which are installed and patched in a temporary directory. The tests check the content of the patched files and any output to stderr.
283 lines
9.8 KiB
Python
283 lines
9.8 KiB
Python
import re
|
|
import sys
|
|
import textwrap
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from tempfile import TemporaryDirectory
|
|
from typing import Any, Callable, Dict, Iterable, List, Tuple
|
|
|
|
import py7zr
|
|
import pytest
|
|
from pytest_socket import disable_socket
|
|
|
|
from aqt.installer import Cli
|
|
|
|
|
|
class MockMultiprocessingContext:
|
|
"""
|
|
By default, monkeypatch will only patch objects in the main process.
|
|
When multiprocessing is used, child processes will not be patched.
|
|
This class forces all work to be done in the main process, so that all
|
|
patched objects remain patched.
|
|
|
|
NOTE: This probably isn't the right way to solve the problem, but it is
|
|
the only way I have found to solve it.
|
|
"""
|
|
|
|
def __init__(self, *args):
|
|
pass
|
|
|
|
class Pool:
|
|
def __init__(self, *args):
|
|
pass
|
|
|
|
def starmap(self, func: Callable, func_args: List[Tuple], *args):
|
|
for set_of_args in func_args:
|
|
func(*set_of_args)
|
|
|
|
def close(self):
|
|
pass
|
|
|
|
def join(self):
|
|
pass
|
|
|
|
def terminate(self):
|
|
assert False, "Did not expect to call terminate during unit test"
|
|
|
|
|
|
FILENAME = "filename"
|
|
UNPATCHED_CONTENT = "unpatched-content"
|
|
PATCHED_CONTENT = "expected-content"
|
|
|
|
GET_URL_TYPE = Callable[[str, Any], str]
|
|
DOWNLOAD_ARCHIVE_TYPE = Callable[[str, str, str, bytes, Any], None]
|
|
|
|
|
|
def make_mock_geturl_download_archive(
|
|
archive_filename: str,
|
|
qt_version: str,
|
|
arch: str,
|
|
arch_dir: str,
|
|
os_name: str,
|
|
updates_url: str,
|
|
compressed_files: Iterable[Dict[str, str]],
|
|
) -> Tuple[GET_URL_TYPE, DOWNLOAD_ARCHIVE_TYPE]:
|
|
"""
|
|
Returns a mock 'getUrl' and a mock 'downloadArchive' function.
|
|
"""
|
|
assert archive_filename.endswith(".7z")
|
|
|
|
def mock_getUrl(url: str, *args) -> str:
|
|
if url.endswith(updates_url):
|
|
qt_major_nodot = f"qt{qt_version[0]}.{qt_version.replace('.', '')}"
|
|
_xml = textwrap.dedent(
|
|
f"""\
|
|
<Updates>
|
|
<PackageUpdate>
|
|
<Name>qt.{qt_major_nodot}.{arch}</Name>
|
|
<Description>>Qt {qt_version} for {arch}</Description>
|
|
<Version>{qt_version}-0-{datetime.now().strftime("%Y%m%d%H%M")}</Version>
|
|
<DownloadableArchives>{archive_filename}</DownloadableArchives>
|
|
</PackageUpdate>
|
|
</Updates>
|
|
"""
|
|
)
|
|
|
|
return _xml
|
|
elif url.endswith(".sha1"):
|
|
return "" # Skip the checksum
|
|
assert False
|
|
|
|
def mock_download_archive(url: str, out: str, *args):
|
|
"""Make a mocked 7z archive at out_filename"""
|
|
assert out == archive_filename
|
|
|
|
with TemporaryDirectory() as temp_dir, py7zr.SevenZipFile(
|
|
archive_filename, "w"
|
|
) as archive:
|
|
temp_path = Path(temp_dir)
|
|
|
|
for folder in ("bin", "lib", "mkspecs"):
|
|
(temp_path / arch_dir / folder).mkdir(parents=True, exist_ok=True)
|
|
|
|
# Use `compressed_files` to write qmake binary, qmake script, QtCore binaries, etc
|
|
for file in compressed_files:
|
|
full_path = temp_path / arch_dir / file[FILENAME]
|
|
if not full_path.parent.exists():
|
|
full_path.parent.mkdir(parents=True)
|
|
full_path.write_text(file[UNPATCHED_CONTENT], "utf_8")
|
|
|
|
archive.writeall(path=temp_path, arcname=qt_version)
|
|
|
|
return mock_getUrl, mock_download_archive
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def apply_mocked_geturl(monkeypatch):
|
|
# This blocks all network connections, causing test failure if we used monkeypatch wrong
|
|
disable_socket()
|
|
|
|
# This blocks all multiprocessing, which would otherwise spawn processes that are not monkeypatched
|
|
monkeypatch.setattr(
|
|
"aqt.installer.multiprocessing.get_context",
|
|
lambda *args: MockMultiprocessingContext(),
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"cmd, host, target, version, arch, arch_dir, updates_url, files, expect_out",
|
|
(
|
|
(
|
|
"install 5.14.0 windows desktop win32_mingw73".split(),
|
|
"windows",
|
|
"desktop",
|
|
"5.14.0",
|
|
"win32_mingw73",
|
|
"mingw73_32",
|
|
"windows_x86/desktop/qt5_5140/Updates.xml",
|
|
(
|
|
{
|
|
FILENAME: "mkspecs/qconfig.pri",
|
|
UNPATCHED_CONTENT: "... blah blah blah ...\n"
|
|
"QT_EDITION = Not OpenSource\n"
|
|
"QT_LICHECK = Not Empty\n"
|
|
"... blah blah blah ...\n",
|
|
PATCHED_CONTENT: "... blah blah blah ...\n"
|
|
"QT_EDITION = OpenSource\n"
|
|
"QT_LICHECK =\n"
|
|
"... blah blah blah ...\n",
|
|
},
|
|
),
|
|
re.compile(
|
|
r"^aqtinstall\(aqt\) v.* on Python 3.*\n"
|
|
r"Warning: The command 'install' is deprecated and marked for removal in a future version of aqt.\n"
|
|
r"In the future, please use the command 'install-qt' instead.\n"
|
|
r"Downloading qtbase...\n"
|
|
r"Finished installation of qtbase-windows-win32_mingw73.7z in .*\n"
|
|
r"Finished installation\n"
|
|
r"Time elapsed: .* second"
|
|
),
|
|
),
|
|
(
|
|
"install-qt windows desktop 5.14.0 win32_mingw73".split(),
|
|
"windows",
|
|
"desktop",
|
|
"5.14.0",
|
|
"win32_mingw73",
|
|
"mingw73_32",
|
|
"windows_x86/desktop/qt5_5140/Updates.xml",
|
|
(
|
|
{
|
|
FILENAME: "mkspecs/qconfig.pri",
|
|
UNPATCHED_CONTENT: "... blah blah blah ...\n"
|
|
"QT_EDITION = Not OpenSource\n"
|
|
"QT_LICHECK = Not Empty\n"
|
|
"... blah blah blah ...\n",
|
|
PATCHED_CONTENT: "... blah blah blah ...\n"
|
|
"QT_EDITION = OpenSource\n"
|
|
"QT_LICHECK =\n"
|
|
"... blah blah blah ...\n",
|
|
},
|
|
),
|
|
re.compile(
|
|
r"^aqtinstall\(aqt\) v.* on Python 3.*\n"
|
|
r"Downloading qtbase...\n"
|
|
r"Finished installation of qtbase-windows-win32_mingw73.7z in .*\n"
|
|
r"Finished installation\n"
|
|
r"Time elapsed: .* second"
|
|
),
|
|
),
|
|
(
|
|
"install-qt windows android 6.1.0 android_armv7".split(),
|
|
"windows",
|
|
"android",
|
|
"6.1.0",
|
|
"android_armv7",
|
|
"android_armv7",
|
|
"windows_x86/android/qt6_610_armv7/Updates.xml",
|
|
(
|
|
# Qt 6 non-desktop should patch qconfig.pri, qmake script and target_qt.conf
|
|
{
|
|
FILENAME: "mkspecs/qconfig.pri",
|
|
UNPATCHED_CONTENT: "... blah blah blah ...\n"
|
|
"QT_EDITION = Not OpenSource\n"
|
|
"QT_LICHECK = Not Empty\n"
|
|
"... blah blah blah ...\n",
|
|
PATCHED_CONTENT: "... blah blah blah ...\n"
|
|
"QT_EDITION = OpenSource\n"
|
|
"QT_LICHECK =\n"
|
|
"... blah blah blah ...\n",
|
|
},
|
|
{
|
|
FILENAME: "bin/target_qt.conf",
|
|
UNPATCHED_CONTENT: "Prefix=/Users/qt/work/install/target\n"
|
|
"HostPrefix=../../\n"
|
|
"HostData=target\n",
|
|
PATCHED_CONTENT: "Prefix={base_dir}\\6.1.0\\android_armv7\\target\n"
|
|
"HostPrefix=../../mingw81_64\n"
|
|
"HostData=../android_armv7\n",
|
|
},
|
|
{
|
|
FILENAME: "bin/qmake.bat",
|
|
UNPATCHED_CONTENT: "... blah blah blah ...\n"
|
|
"/Users/qt/work/install/bin\n"
|
|
"... blah blah blah ...\n",
|
|
PATCHED_CONTENT: "... blah blah blah ...\n"
|
|
"{base_dir}\\6.1.0\\mingw81_64\\bin\n"
|
|
"... blah blah blah ...\n",
|
|
},
|
|
),
|
|
re.compile(
|
|
r"^aqtinstall\(aqt\) v.* on Python 3.*\n"
|
|
r"Downloading qtbase...\n"
|
|
r"Finished installation of qtbase-windows-android_armv7.7z in .*\n"
|
|
r"Patching .*/bin/qmake.bat\n"
|
|
r"Finished installation\n"
|
|
r"Time elapsed: .* second"
|
|
),
|
|
),
|
|
),
|
|
)
|
|
def test_install(
|
|
monkeypatch,
|
|
capsys,
|
|
cmd: List[str],
|
|
host: str,
|
|
target: str,
|
|
version: str,
|
|
arch: str,
|
|
arch_dir: str,
|
|
updates_url: str,
|
|
files: Iterable[Dict[str, str]],
|
|
expect_out: re.Pattern,
|
|
):
|
|
|
|
archive_filename = f"qtbase-{host}-{arch}.7z"
|
|
mock_get_url, mock_download_archive = make_mock_geturl_download_archive(
|
|
archive_filename, version, arch, arch_dir, host, updates_url, files
|
|
)
|
|
monkeypatch.setattr("aqt.archives.getUrl", mock_get_url)
|
|
monkeypatch.setattr("aqt.installer.getUrl", mock_get_url)
|
|
monkeypatch.setattr("aqt.installer.downloadBinaryFile", mock_download_archive)
|
|
|
|
with TemporaryDirectory() as output_dir:
|
|
cli = Cli()
|
|
cli._setup_settings()
|
|
|
|
cli.run(cmd + ["--outputdir", output_dir])
|
|
|
|
out, err = capsys.readouterr()
|
|
sys.stdout.write(out)
|
|
sys.stderr.write(err)
|
|
|
|
assert expect_out.match(err)
|
|
|
|
installed_path = Path(output_dir) / version / arch_dir
|
|
assert installed_path.is_dir()
|
|
for patched_file in files:
|
|
file_path = installed_path / patched_file[FILENAME]
|
|
assert file_path.is_file()
|
|
expect_content = patched_file[PATCHED_CONTENT].format(base_dir=output_dir)
|
|
patched_content = file_path.read_text(encoding="utf_8")
|
|
assert patched_content == expect_content
|