Commercial fixes, CI tests, more tests, coverage (#883)

* Add authentication flags for list-qt-commercial, add tests for coverage

* Add dry run

* Make tests really use auth since secrets have been added, fix some indents

* Fix auth issue, rename user 'email, and password 'pw'

* Fix modules param type

* Update commands names
This commit is contained in:
Alexandre Poumaroux
2025-02-22 01:56:56 +01:00
committed by GitHub
parent a30f5a3d05
commit 5d699b9ebf
13 changed files with 1172 additions and 830 deletions

View File

@@ -15,6 +15,9 @@ jobs:
check_document:
name: Check packaging 📦
runs-on: ubuntu-22.04
env:
AQT_TEST_EMAIL: ${{ secrets.AQT_TEST_EMAIL }}
AQT_TEST_PASSWORD: ${{ secrets.AQT_TEST_PASSWORD }}
steps:
- uses: actions/checkout@v4
with:

1
.gitignore vendored
View File

@@ -20,3 +20,4 @@ qtaccount.ini
.pytest_cache
.run/
.python-version
.env

View File

@@ -9,7 +9,15 @@ import requests
from defusedxml import ElementTree
from aqt.exceptions import DiskAccessNotPermitted
from aqt.helper import Settings, get_os_name, get_qt_account_path, get_qt_installer_name, safely_run, safely_run_save_output
from aqt.helper import (
Settings,
extract_auth,
get_os_name,
get_qt_account_path,
get_qt_installer_name,
safely_run,
safely_run_save_output,
)
from aqt.metadata import Version
@@ -103,19 +111,14 @@ class QtPackageManager:
version_str = self._get_version_string()
base_package = f"qt.qt{self.version.major}.{version_str}"
cmd = [
installer_path,
"--accept-licenses",
"--accept-obligations",
"--confirm-command",
"--default-answer",
"search",
base_package,
]
cmd = [installer_path, "--accept-licenses", "--accept-obligations", "--confirm-command", "--default-answer"]
if self.username and self.password:
cmd.extend(["--email", self.username, "--pw", self.password])
cmd.append("search")
cmd.append(base_package)
try:
output = safely_run_save_output(cmd, Settings.qt_installer_timeout)
@@ -190,7 +193,7 @@ class CommercialInstaller:
output_dir: Optional[str] = None,
logger: Optional[Logger] = None,
base_url: str = "https://download.qt.io",
override: Optional[list[str]] = None,
override: Optional[List[str]] = None,
modules: Optional[List[str]] = None,
no_unattended: bool = False,
):
@@ -198,14 +201,21 @@ class CommercialInstaller:
self.target = target
self.arch = arch or ""
self.version = Version(version) if version else Version("0.0.0")
self.username = username
self.password = password
self.output_dir = output_dir
self.logger = logger or getLogger(__name__)
self.base_url = base_url
self.modules = modules
self.no_unattended = no_unattended
# Extract credentials from override if present
if override:
extracted_username, extracted_password, self.override = extract_auth(override)
self.username = extracted_username or username
self.password = extracted_password or password
else:
self.username = username
self.password = password
# Set OS-specific properties
self.os_name = get_os_name()
self._installer_filename = get_qt_installer_name()
@@ -247,15 +257,15 @@ class CommercialInstaller:
if not no_unattended:
cmd.extend(["--accept-licenses", "--accept-obligations", "--confirm-command"])
# Add authentication if provided
if username and password:
cmd.extend(["--email", username, "--pw", password])
if override:
# When using override, still include unattended flags unless disabled
cmd.extend(override)
return cmd
# Add authentication if provided
if username and password:
cmd.extend(["--email", username, "--pw", password])
# Add output directory if specified
if output_dir:
cmd.extend(["--root", str(Path(output_dir).resolve())])
@@ -271,36 +281,34 @@ class CommercialInstaller:
"""Run the Qt installation process."""
if (
not self.qt_account.exists()
and not (self.username and self.password)
and (not self.username or not self.password)
and not os.environ.get("QT_INSTALLER_JWT_TOKEN")
):
raise RuntimeError(
"No Qt account credentials found. Provide username and password or ensure qtaccount.ini exists."
)
# Check output directory if specified
if self.output_dir:
output_path = Path(self.output_dir) / str(self.version)
if output_path.exists():
if Settings.qt_installer_overwritetargetdirectory.lower() == "yes":
self.logger.warning(f"Target directory {output_path} exists - removing as overwrite is enabled")
try:
import shutil
shutil.rmtree(output_path)
except (OSError, PermissionError) as e:
raise DiskAccessNotPermitted(f"Failed to remove existing target directory {output_path}: {str(e)}")
else:
msg = (
f"Target directory {output_path} already exists. "
"Set overwrite_target_directory='Yes' in settings.ini to overwrite, or select another directory."
)
raise DiskAccessNotPermitted(msg)
# Setup cache directory
cache_path = Path(Settings.qt_installer_cache_path)
cache_path.mkdir(parents=True, exist_ok=True)
# Setup output directory and validate access
output_dir = Path(self.output_dir) if self.output_dir else Path(os.getcwd()) / "Qt"
version_dir = output_dir / str(self.version)
qt_base_dir = output_dir
if qt_base_dir.exists():
if Settings.qt_installer_overwritetargetdirectory.lower() == "yes":
self.logger.warning(f"Target directory {qt_base_dir} exists - removing as overwrite is enabled")
try:
import shutil
if version_dir.exists():
shutil.rmtree(version_dir)
except (OSError, PermissionError) as e:
raise DiskAccessNotPermitted(f"Failed to remove existing version directory {version_dir}: {str(e)}")
# Setup temp directory
import shutil
temp_dir = Settings.qt_installer_temp_path
@@ -316,7 +324,16 @@ class CommercialInstaller:
try:
cmd = []
if self.override:
cmd = self.build_command(str(installer_path), override=self.override, no_unattended=self.no_unattended)
if not self.username or not self.password:
self.username, self.password, self.override = extract_auth(self.override)
cmd = self.build_command(
str(installer_path),
override=self.override,
no_unattended=self.no_unattended,
username=self.username,
password=self.password,
)
else:
# Initialize package manager and gather packages
self.package_manager.gather_packages(str(installer_path))
@@ -325,20 +342,18 @@ class CommercialInstaller:
str(installer_path.absolute()),
username=self.username,
password=self.password,
output_dir=self.output_dir,
output_dir=str(qt_base_dir.absolute()),
no_unattended=self.no_unattended,
)
cmd = [
*base_cmd,
*self.package_manager.get_install_command(self.modules, temp_dir),
]
install_cmd = self.package_manager.get_install_command(self.modules, temp_dir)
cmd = [*base_cmd, *install_cmd]
self.logger.info(f"Running: {cmd}")
safely_run(cmd, Settings.qt_installer_timeout)
except Exception as e:
self.logger.error(f"Installation failed with exit code {e.__str__()}")
self.logger.error(f"Installation failed: {str(e)}")
raise
finally:
self.logger.info("Qt installation completed successfully")

View File

@@ -459,18 +459,18 @@ class SettingsClass:
"""Path for Qt installer cache."""
config = self._get_config()
# If no cache_path or blank, return default without modifying config
if not config.has_option("qtcommercial", "cache_path") or config.get("qtcommercial", "cache_path").strip() == "":
if not config.has_option("qtofficial", "cache_path") or config.get("qtofficial", "cache_path").strip() == "":
return str(get_default_local_cache_path())
return config.get("qtcommercial", "cache_path")
return config.get("qtofficial", "cache_path")
@property
def qt_installer_temp_path(self) -> str:
"""Path for Qt installer cache."""
config = self._get_config()
# If no cache_path or blank, return default without modifying config
if not config.has_option("qtcommercial", "temp_path") or config.get("qtcommercial", "temp_path").strip() == "":
if not config.has_option("qtofficial", "temp_path") or config.get("qtofficial", "temp_path").strip() == "":
return str(get_default_local_temp_path())
return config.get("qtcommercial", "temp_path")
return config.get("qtofficial", "temp_path")
@property
def archive_download_location(self):
@@ -572,47 +572,47 @@ class SettingsClass:
@property
def qt_installer_timeout(self) -> int:
"""Timeout for Qt commercial installer operations in seconds."""
return self._get_config().getint("qtcommercial", "installer_timeout", fallback=3600)
return self._get_config().getint("qtofficial", "installer_timeout", fallback=3600)
@property
def qt_installer_operationdoesnotexisterror(self) -> str:
"""Handle OperationDoesNotExistError in Qt installer."""
return self._get_config().get("qtcommercial", "operation_does_not_exist_error", fallback="Ignore")
return self._get_config().get("qtofficial", "operation_does_not_exist_error", fallback="Ignore")
@property
def qt_installer_overwritetargetdirectory(self) -> str:
"""Handle overwriting target directory in Qt installer."""
return self._get_config().get("qtcommercial", "overwrite_target_directory", fallback="No")
return self._get_config().get("qtofficial", "overwrite_target_directory", fallback="No")
@property
def qt_installer_stopprocessesforupdates(self) -> str:
"""Handle stopping processes for updates in Qt installer."""
return self._get_config().get("qtcommercial", "stop_processes_for_updates", fallback="Cancel")
return self._get_config().get("qtofficial", "stop_processes_for_updates", fallback="Cancel")
@property
def qt_installer_installationerrorwithcancel(self) -> str:
"""Handle installation errors with cancel option in Qt installer."""
return self._get_config().get("qtcommercial", "installation_error_with_cancel", fallback="Cancel")
return self._get_config().get("qtofficial", "installation_error_with_cancel", fallback="Cancel")
@property
def qt_installer_installationerrorwithignore(self) -> str:
"""Handle installation errors with ignore option in Qt installer."""
return self._get_config().get("qtcommercial", "installation_error_with_ignore", fallback="Ignore")
return self._get_config().get("qtofficial", "installation_error_with_ignore", fallback="Ignore")
@property
def qt_installer_associatecommonfiletypes(self) -> str:
"""Handle file type associations in Qt installer."""
return self._get_config().get("qtcommercial", "associate_common_filetypes", fallback="Yes")
return self._get_config().get("qtofficial", "associate_common_filetypes", fallback="Yes")
@property
def qt_installer_telemetry(self) -> str:
"""Handle telemetry settings in Qt installer."""
return self._get_config().get("qtcommercial", "telemetry", fallback="No")
return self._get_config().get("qtofficial", "telemetry", fallback="No")
@property
def qt_installer_unattended(self) -> bool:
"""Control whether to use unattended installation flags."""
return self._get_config().getboolean("qtcommercial", "unattended", fallback=True)
return self._get_config().getboolean("qtofficial", "unattended", fallback=True)
def qt_installer_cleanup(self) -> None:
"""Control whether to use unattended installation flags."""
@@ -644,3 +644,26 @@ def safely_run_save_output(cmd: List[str], timeout: int) -> Any:
return result
except Exception:
raise
def extract_auth(args: List[str]) -> Tuple[str | None, str | None, List[str] | None]:
username = None
password = None
i = 0
while i < len(args):
if args[i] == "--email":
if i + 1 < len(args):
username = args[i + 1]
del args[i : i + 2]
else:
del args[i]
continue
elif args[i] == "--pw":
if i + 1 < len(args):
password = args[i + 1]
del args[i : i + 2]
else:
del args[i]
continue
i += 1
return username, password, args

View File

@@ -28,6 +28,7 @@ import multiprocessing
import os
import platform
import posixpath
import re
import signal
import subprocess
import sys
@@ -59,6 +60,7 @@ from aqt.helper import (
MyQueueListener,
Settings,
downloadBinaryFile,
extract_auth,
get_hash,
get_os_name,
get_qt_installer_name,
@@ -123,6 +125,7 @@ class CommonInstallArgParser(BaseArgumentParser):
internal: bool
keep: bool
archive_dest: Optional[str]
dry_run: bool
class InstallArgParser(CommonInstallArgParser):
@@ -133,8 +136,8 @@ class InstallArgParser(CommonInstallArgParser):
qt_version: str
qt_version_spec: str
version: Optional[str]
user: Optional[str]
password: Optional[str]
email: Optional[str]
pw: Optional[str]
operation_does_not_exist_error: str
overwrite_target_dir: str
stop_processes_for_updates: str
@@ -337,6 +340,7 @@ class Cli:
arch: str = self._set_arch(args.arch, os_name, target, qt_version_or_spec)
keep: bool = args.keep or Settings.always_keep_archives
archive_dest: Optional[str] = args.archive_dest
dry_run: bool = args.dry_run
output_dir = args.outputdir
if output_dir is None:
base_dir = os.getcwd()
@@ -405,7 +409,10 @@ class Cli:
with TemporaryDirectory() as temp_dir:
_archive_dest = Cli.choose_archive_dest(archive_dest, keep, temp_dir)
run_installer(qt_archives.get_packages(), base_dir, sevenzip, keep, _archive_dest)
run_installer(qt_archives.get_packages(), base_dir, sevenzip, keep, _archive_dest, dry_run=dry_run)
if dry_run:
return
if not nopatch:
Updater.update(target_config, base_path, expect_desktop_archdir)
@@ -498,7 +505,9 @@ class Cli:
)
with TemporaryDirectory() as temp_dir:
_archive_dest = Cli.choose_archive_dest(archive_dest, keep, temp_dir)
run_installer(srcdocexamples_archives.get_packages(), base_dir, sevenzip, keep, _archive_dest)
run_installer(
srcdocexamples_archives.get_packages(), base_dir, sevenzip, keep, _archive_dest, dry_run=args.dry_run
)
self.logger.info("Finished installation")
def run_install_src(self, args):
@@ -585,7 +594,7 @@ class Cli:
)
with TemporaryDirectory() as temp_dir:
_archive_dest = Cli.choose_archive_dest(archive_dest, keep, temp_dir)
run_installer(tool_archives.get_packages(), base_dir, sevenzip, keep, _archive_dest)
run_installer(tool_archives.get_packages(), base_dir, sevenzip, keep, _archive_dest, dry_run=args.dry_run)
self.logger.info("Finished installation")
self.logger.info("Time elapsed: {time:.8f} second".format(time=time.perf_counter() - start_time))
@@ -676,15 +685,19 @@ class Cli:
"""Execute commercial Qt installation"""
self.show_aqt_version()
try:
if args.override:
username, password, override_args = extract_auth(args.override)
commercial_installer = CommercialInstaller(
target="", # Empty string as placeholder
arch="",
version=None,
logger=self.logger,
base_url=args.base if args.base is not None else Settings.baseurl,
override=args.override,
override=override_args,
no_unattended=not Settings.qt_installer_unattended,
username=username or args.email,
password=password or args.pw,
)
else:
if not all([args.target, args.arch, args.version]):
@@ -694,8 +707,8 @@ class Cli:
target=args.target,
arch=args.arch,
version=args.version,
username=args.user,
password=args.password,
username=args.email,
password=args.pw,
output_dir=args.outputdir,
logger=self.logger,
base_url=args.base if args.base is not None else Settings.baseurl,
@@ -703,15 +716,12 @@ class Cli:
modules=args.modules,
)
try:
commercial_installer.install()
Settings.qt_installer_cleanup()
except DiskAccessNotPermitted:
# Let DiskAccessNotPermitted propagate up without additional logging
raise
except Exception as e:
self.logger.error(f"Commercial installation failed: {str(e)}")
raise
self.logger.error(f"Error installing official installer {str(e)}")
finally:
self.logger.info("Done")
def show_help(self, args=None):
"""Display help message"""
@@ -837,44 +847,46 @@ class Cli:
install_qt_commercial_parser.add_argument("version", nargs="?", help="Qt version", action=ConditionalRequiredAction)
install_qt_commercial_parser.add_argument(
"--user",
help="Qt account username",
"--email",
help="Qt account email",
)
install_qt_commercial_parser.add_argument(
"--password",
"--pw",
help="Qt account password",
)
install_qt_commercial_parser.add_argument(
"-m",
"--modules",
nargs="*",
help="Add modules",
)
self._set_common_options(install_qt_commercial_parser)
def _make_list_qt_commercial_parser(self, subparsers: argparse._SubParsersAction) -> None:
"""Creates a subparser for listing Qt commercial packages"""
list_parser = subparsers.add_parser(
"list-qt-commercial",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="Examples:\n"
"$ aqt list-qt-commercial # list all available packages\n"
"$ aqt list-qt-commercial gcc_64 # search for specific archs\n"
"$ aqt list-qt-commercial 6.8.1 # search for specific versions\n"
"$ aqt list-qt-commercial qtquick3d # search for specific packages\n"
"$ aqt list-qt-commercial gcc_64 6.8.1 # search for multiple terms at once\n",
def _set_list_qt_commercial_parser(self, list_qt_commercial_parser: argparse.ArgumentParser) -> None:
"""Configure parser for list-qt-official command with flexible argument handling."""
list_qt_commercial_parser.set_defaults(func=self.run_list_qt_commercial)
list_qt_commercial_parser.add_argument(
"--email",
help="Qt account email",
)
list_parser.add_argument(
list_qt_commercial_parser.add_argument(
"--pw",
help="Qt account password",
)
# Capture all remaining arguments as search terms
list_qt_commercial_parser.add_argument(
"search_terms",
nargs="*",
help="Optional search terms to pass to the installer search command. If not provided, lists all packages",
nargs="*", # Zero or more arguments
help="Search terms (all non-option arguments are treated as search terms)",
)
list_parser.set_defaults(func=self.run_list_qt_commercial)
def run_list_qt_commercial(self, args) -> None:
"""Execute Qt commercial package listing"""
"""Execute Qt commercial package listing."""
self.show_aqt_version()
# Create temporary directory to download installer
# Create temporary directory for installer
import shutil
from pathlib import Path
@@ -893,6 +905,7 @@ class Cli:
self.logger.info(f"Downloading Qt installer to {installer_path}")
base_url = Settings.baseurl
url = f"{base_url}/official_releases/online_installers/{installer_filename}"
import requests
response = requests.get(url, stream=True, timeout=Settings.qt_installer_timeout)
@@ -905,33 +918,30 @@ class Cli:
if get_os_name() != "windows":
os.chmod(installer_path, 0o500)
# Build search command
cmd = [
str(installer_path),
"--accept-licenses",
"--accept-obligations",
"--confirm-command",
"search",
"" if not args.search_terms else " ".join(args.search_terms),
]
# Build command
cmd = [str(installer_path), "--accept-licenses", "--accept-obligations", "--confirm-command"]
# Run search and display output
if args.email and args.pw:
cmd.extend(["--email", args.email, "--pw", args.pw])
cmd.append("search")
# Add all search terms if present
if args.search_terms:
cmd.extend(args.search_terms)
# Run search
output = safely_run_save_output(cmd, Settings.qt_installer_timeout)
# Process and print the output properly
if output.stdout:
# Print the actual output with proper newlines
print(output.stdout)
# If there are any errors, print them as warnings
self.logger.info(output.stdout)
if output.stderr:
for line in output.stderr.splitlines():
self.logger.warning(line)
except Exception as e:
self.logger.error(f"Failed to list Qt commercial packages: {e}")
self.logger.error(f"Failed to list Qt official packages: {e}")
finally:
# Clean up
Settings.qt_installer_cleanup()
def _warn_on_deprecated_command(self, old_name: str, new_name: str) -> None:
@@ -987,18 +997,23 @@ class Cli:
make_parser_it("install-qt", "Install Qt.", self._set_install_qt_parser, argparse.RawTextHelpFormatter)
make_parser_it("install-tool", "Install tools.", self._set_install_tool_parser, None)
make_parser_it(
"install-qt-commercial",
"Install Qt commercial.",
"install-qt-official",
"Install Qt with official installer.",
self._set_install_qt_commercial_parser,
argparse.RawTextHelpFormatter,
)
make_parser_it(
"list-qt-official",
"Search packages using Qt official installer.",
self._set_list_qt_commercial_parser,
argparse.RawTextHelpFormatter,
)
make_parser_sde("install-doc", "Install documentation.", self.run_install_doc, False)
make_parser_sde("install-example", "Install examples.", self.run_install_example, False)
make_parser_sde("install-src", "Install source.", self.run_install_src, True, is_add_modules=False)
# Create list command parsers
self._make_list_qt_parser(subparsers)
self._make_list_qt_commercial_parser(subparsers)
self._make_list_tool_parser(subparsers)
make_parser_list_sde("list-doc", "List documentation archives available (use with install-doc)", "doc")
make_parser_list_sde("list-example", "List example archives available (use with install-example)", "examples")
@@ -1018,11 +1033,16 @@ class Cli:
'$ aqt list-qt mac desktop --spec "5.9" --latest-version # print latest Qt 5.9\n'
"$ aqt list-qt mac desktop --modules 5.12.0 clang_64 # print modules for 5.12.0\n"
"$ aqt list-qt mac desktop --spec 5.9 --modules latest clang_64 # print modules for latest 5.9\n"
"$ aqt list-qt mac desktop --arch 5.9.9 # print architectures for 5.9.9/mac/desktop\n"
"$ aqt list-qt mac desktop --arch latest # print architectures for the latest Qt 5\n"
"$ aqt list-qt mac desktop --archives 5.9.0 clang_64 # list archives in base Qt installation\n"
"$ aqt list-qt mac desktop --archives 5.14.0 clang_64 debug_info # list archives in debug_info module\n"
"$ aqt list-qt all_os wasm --arch 6.8.1 # print architectures for Qt WASM 6.8.1\n",
"$ aqt list-qt mac desktop --arch 5.9.9 # print architectures for "
"5.9.9/mac/desktop\n"
"$ aqt list-qt mac desktop --arch latest # print architectures for the "
"latest Qt 5\n"
"$ aqt list-qt mac desktop --archives 5.9.0 clang_64 # list archives in base Qt "
"installation\n"
"$ aqt list-qt mac desktop --archives 5.14.0 clang_64 debug_info # list archives in debug_info "
"module\n"
"$ aqt list-qt all_os wasm --arch 6.8.1 # print architectures for Qt WASM "
"6.8.1\n",
)
list_parser.add_argument(
"host",
@@ -1182,6 +1202,11 @@ class Cli:
default=None,
help="Set the destination path for downloaded archives (temp directory by default).",
)
subparser.add_argument(
"--dry-run",
action="store_true",
help="Print what would be downloaded and installed without actually doing it",
)
def _set_module_options(self, subparser):
subparser.add_argument("-m", "--modules", nargs="*", help="Specify extra modules to install")
@@ -1354,7 +1379,28 @@ def run_installer(
sevenzip: Optional[str],
keep: bool,
archive_dest: Path,
dry_run: bool = False,
):
if dry_run:
logger = getLogger("aqt.installer")
logger.info("DRY RUN: Would download and install the following:")
for arc in archives:
line_parts = [f" - {arc.name}: {arc.archive_path}"]
if hasattr(arc, "package_desc") and arc.package_desc:
size_match = re.search(r"Size: ([^,]+)", arc.package_desc)
if size_match:
line_parts.append(f" ({size_match.group(1)})")
if arc.archive_install_path and arc.archive_install_path.strip():
line_parts.append(f" -> {arc.archive_install_path}")
logger.info("".join(line_parts))
logger.info(f"Total packages: {len(archives)}")
return
queue = multiprocessing.Manager().Queue(-1)
listener = MyQueueListener(queue)
listener.start()

View File

@@ -19,11 +19,11 @@ max_retries_to_retrieve_hash : 5
hash_algorithm : sha256
INSECURE_NOT_FOR_PRODUCTION_ignore_hash : False
[qtcommercial]
[qtofficial]
unattended : True
installer_timeout : 1800
operation_does_not_exist_error : Ignore
overwrite_target_directory : Yes
overwrite_target_directory : No
stop_processes_for_updates : Ignore
installation_error_with_cancel : Ignore
installation_error_with_ignore : Ignore

View File

@@ -3,7 +3,6 @@ This sets variables for a matrix of QT versions to test downloading against with
"""
import collections
import json
import secrets as random
import re
from itertools import product
from typing import Dict, Optional
@@ -46,7 +45,8 @@ class BuildJob:
is_autodesktop: bool = False,
tool_options: Optional[Dict[str, str]] = None,
check_output_cmd: Optional[str] = None,
emsdk_version: str = "sdk-fastcomp-1.38.27-64bit@3.1.29", # did not change for safety, created func self.emsdk_version()
emsdk_version: str = "sdk-fastcomp-1.38.27-64bit@3.1.29",
# did not change for safety, created func self.emsdk_version()
autodesk_arch_folder: Optional[str] = None,
):
self.command = command
@@ -235,10 +235,12 @@ linux_build_jobs.extend(
"install-example", "6.1.3", "linux", "desktop", "gcc_64", "gcc_64",
subarchives="qtdoc", module="qtcharts",
# Fail the job if these paths do not exist:
check_output_cmd="ls -lh ./Examples/Qt-6.1.3/charts/ ./Examples/Qt-6.1.3/demos/ ./Examples/Qt-6.1.3/tutorials/",
check_output_cmd="ls -lh ./Examples/Qt-6.1.3/charts/ ./Examples/Qt-6.1.3/demos/ "
"./Examples/Qt-6.1.3/tutorials/",
),
# test for list commands
BuildJob('list-qt', '6.1.0', 'linux', 'desktop', 'gcc_64', '', spec=">6.0,<6.1.1", list_options={'HAS_WASM': "False"}),
BuildJob('list-qt', '6.1.0', 'linux', 'desktop', 'gcc_64', '', spec=">6.0,<6.1.1",
list_options={'HAS_WASM': "False"}),
BuildJob('list-qt', '6.1.0', 'linux', 'android', 'android_armv7', '', spec=">6.0,<6.1.1", list_options={}),
]
)
@@ -271,7 +273,8 @@ windows_build_jobs.append(
# WASM post 6.7.x
linux_build_jobs.append(
BuildJob("install-qt", "6.7.3", "all_os", "wasm", "wasm_multithread", "wasm_multithread",
is_autodesktop=True, emsdk_version=f"sdk-{BuildJob.emsdk_version_for_qt("6.7.3")}-64bit", autodesk_arch_folder="gcc_64")
is_autodesktop=True, emsdk_version=f"sdk-{BuildJob.emsdk_version_for_qt("6.7.3")}-64bit",
autodesk_arch_folder="gcc_64")
)
for job_queue, host, desk_arch, target, qt_version in (
(linux_build_jobs, "all_os", "linux_gcc_64", "wasm", qt_versions[0]),
@@ -281,7 +284,8 @@ for job_queue, host, desk_arch, target, qt_version in (
for wasm_arch in ("wasm_singlethread", "wasm_multithread"):
job_queue.append(
BuildJob("install-qt", qt_version, host, target, wasm_arch, wasm_arch,
is_autodesktop=True, emsdk_version=f"sdk-{BuildJob.emsdk_version_for_qt(qt_version)}-64bit", autodesk_arch_folder=desk_arch)
is_autodesktop=True, emsdk_version=f"sdk-{BuildJob.emsdk_version_for_qt(qt_version)}-64bit",
autodesk_arch_folder=desk_arch)
)
# mobile SDK
@@ -335,6 +339,20 @@ mac_build_jobs.append(
BuildJob("install-tool", "", "mac", "desktop", "", "", tool_options=tool_options_mac)
)
# Commercial
windows_build_jobs.append(
BuildJob("install-qt-official", qt_version="6.8.1", target="desktop", arch="win64_msvc2022_64",
archdir="win64_msvc2022_64", module="qtquick3d")
)
linux_build_jobs.append(
BuildJob("install-qt-official", qt_version="6.8.1", target="desktop", arch="linux_gcc_64",
archdir="linux_gcc_64", module="qtquick3d")
)
mac_build_jobs.append(
BuildJob("install-qt-official", qt_version="6.8.1", target="desktop", arch="clang_64",
archdir="clang_64", module="qtquick3d")
)
matrices = {}
for platform_build_job in all_platform_build_jobs:

View File

@@ -1,3 +1,6 @@
env:
AQT_TEST_EMAIL: ${{ secrets.AQT_TEST_EMAIL }}
AQT_TEST_PASSWORD: ${{ secrets.AQT_TEST_PASSWORD }}
steps:
- task: UsePythonVersion@0
inputs:
@@ -11,7 +14,7 @@ steps:
- bash: |
sudo apt-get update
sudo apt-get install -y libgl1-mesa-dev libxkbcommon-x11-0
if [[ "$(SUBCOMMAND)" == "install-tool" ]]; then
if [[ "$(SUBCOMMAND)" == "install-tool" || "$(SUBCOMMAND)" == "install-qt-official" ]]; then
sudo apt-get install -y libxcb-{icccm4,image0,keysyms1,randr0,render-util0,shape0,sync1,xfixes0,xinerama0,xkb1}
fi
condition: and(eq(variables['TARGET'], 'desktop'), eq(variables['Agent.OS'], 'Linux'))
@@ -29,6 +32,10 @@ steps:
sleep $number
mkdir Qt
cd Qt
if [[ "$(SUBCOMMAND)" == "install-qt-official" ]]; then
opt+=" --email $(AQT_TEST_EMAIL) --pw $(AQT_TEST_PASSWORD)"
python -m aqt install-qt-official $opt $(TARGET) $(ARCH) $(QT_VERSION) --m $(MODULE)
fi
if [[ "$(SUBCOMMAND)" == "install-qt" ]]; then
opt=""
if [[ "$(QT_BASE_MIRROR)" != "" ]]; then

View File

@@ -1,4 +1,3 @@
import platform
import re
import sys
from pathlib import Path
@@ -16,9 +15,8 @@ from aqt.metadata import MetadataFactory, SimpleSpec, Version
def expected_help(actual, prefix=None):
expected = (
"usage: aqt [-h] [-c CONFIG]\n"
" {install-qt,install-tool,install-qt-commercial,install-doc,install-example,"
"install-src,"
"list-qt,list-qt-commercial,list-tool,list-doc,list-example,list-src,help,version}\n"
" {install-qt,install-tool,install-qt-official,list-qt-official,install-doc,install-example,"
"install-src,list-qt,list-tool,list-doc,list-example,list-src,help,version}\n"
" ...\n"
"\n"
"Another unofficial Qt Installer.\n"
@@ -34,9 +32,8 @@ def expected_help(actual, prefix=None):
" install-* subcommands are commands that install components\n"
" list-* subcommands are commands that show available components\n"
"\n"
" {install-qt,install-tool,install-qt-commercial,install-doc,install-example,"
"install-src,list-qt,list-qt-commercial,"
"list-tool,list-doc,list-example,list-src,help,version}\n"
" {install-qt,install-tool,install-qt-official,list-qt-official,install-doc,install-example,install-src,"
"list-qt,list-tool,list-doc,list-example,list-src,help,version}\n"
" Please refer to each help message by using '--help' "
"with each subcommand\n",
)
@@ -523,29 +520,3 @@ def test_get_autodesktop_dir_and_arch_non_android(
), "Expected autodesktop install message."
elif expect["instruct"]:
assert any("You can install" in line for line in err_lines), "Expected install instruction message."
@pytest.mark.parametrize(
"cmd, expected_arch, expected_err",
[
pytest.param(
"install-qt-commercial desktop {} 6.8.0",
{"windows": "win64_msvc2022_64", "linux": "linux_gcc_64", "mac": "clang_64"},
"No Qt account credentials found. Either provide --user and --password or",
),
],
)
def test_cli_login_qt_commercial(capsys, monkeypatch, cmd, expected_arch, expected_err):
"""Test commercial Qt installation command"""
# Detect current platform
current_platform = platform.system().lower()
arch = expected_arch[current_platform]
cmd = cmd.format(arch)
cli = Cli()
cli._setup_settings()
result = cli.run(cmd.split())
_, err = capsys.readouterr()
assert str(err).find(expected_err)
assert not result == 0

375
tests/test_commercial.py Normal file
View File

@@ -0,0 +1,375 @@
import os
import shutil
import sys
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Dict, List, Optional
import pytest
import requests
from aqt.commercial import CommercialInstaller, QtPackageInfo, QtPackageManager
from aqt.exceptions import DiskAccessNotPermitted
from aqt.helper import Settings, get_qt_account_path
from aqt.installer import Cli
from aqt.metadata import Version
class CompletedProcess:
def __init__(self, args, returncode):
self.args = args
self.returncode = returncode
self.stdout = None
self.stderr = None
# Test data
MOCK_XML_RESPONSE = """<?xml version="1.0" encoding="UTF-8"?>
<availablepackages>
<package name="qt.qt6.680.gcc_64" displayname="Desktop gcc" version="6.8.0-0-202312011"/>
<package name="qt.qt6.680.addons.qtquick3d" displayname="Qt Quick 3D" version="6.8.0-0-202312011"/>
</availablepackages>"""
TEST_EMAIL = os.getenv("AQT_TEST_EMAIL")
TEST_PASSWORD = os.getenv("AQT_TEST_PASSWORD")
class MockResponse:
def __init__(self, status_code: int = 200, content: bytes = b"", text: str = "", headers: Dict = None):
self.status_code = status_code
self.content = content
self.text = text
self.headers = headers or {}
self.ok = status_code == 200
def raise_for_status(self):
if not self.ok:
raise requests.HTTPError(f"HTTP Error: {self.status_code}")
def iter_content(self, chunk_size=None):
yield self.content
@pytest.fixture
def mock_settings(monkeypatch):
"""Setup test settings"""
# Instead of trying to set properties directly, we should mock the property getter
monkeypatch.setattr(Settings, "qt_installer_timeout", property(lambda self: 60))
monkeypatch.setattr(Settings, "qt_installer_cache_path", property(lambda self: str(Path.home() / ".qt" / "cache")))
monkeypatch.setattr(Settings, "qt_installer_temp_path", property(lambda self: str(Path.home() / ".qt" / "temp")))
@pytest.fixture
def commercial_installer():
return CommercialInstaller(
target="desktop",
arch="gcc_64",
version="6.8.0",
username=TEST_EMAIL,
password=TEST_PASSWORD,
output_dir="./test_output",
)
@pytest.mark.enable_socket
@pytest.mark.parametrize(
"cmd, expected_arch, expected_err",
[
pytest.param(
"install-qt-official desktop {} 6.8.0",
{"windows": "win64_msvc2022_64", "linux": "linux_gcc_64", "mac": "clang_64"},
"No Qt account credentials found",
),
],
)
def test_cli_login_qt_commercial(capsys, monkeypatch, cmd, expected_arch, expected_err):
"""Test commercial Qt installation command"""
# Detect current platform
current_platform = sys.platform.lower()
arch = expected_arch[current_platform]
cmd = cmd.format(arch)
if get_qt_account_path().exists():
os.remove(get_qt_account_path())
cli = Cli()
cli._setup_settings()
cli.run(cmd.split())
out, err = capsys.readouterr()
assert expected_err in err or expected_err in out
def test_package_manager_init():
"""Test QtPackageManager initialization"""
manager = QtPackageManager(
arch="gcc_64",
version=Version("6.8.0"),
target="desktop",
username=TEST_EMAIL,
password=TEST_PASSWORD,
)
assert manager.arch == "gcc_64"
assert str(manager.version) == "6.8.0"
assert manager.target == "desktop"
assert manager.username == TEST_EMAIL
assert manager.password == TEST_PASSWORD
@pytest.mark.parametrize(
"xml_content, expected_packages",
[
(
MOCK_XML_RESPONSE,
[
QtPackageInfo(name="qt.qt6.680.gcc_64", displayname="Desktop gcc", version="6.8.0-0-202312011"),
QtPackageInfo(name="qt.qt6.680.addons.qtquick3d", displayname="Qt Quick 3D", version="6.8.0-0-202312011"),
],
)
],
)
def test_parse_packages_xml(xml_content: str, expected_packages: List[QtPackageInfo]):
"""Test parsing of package XML data"""
manager = QtPackageManager(arch="gcc_64", version=Version("6.8.0"), target="desktop")
manager._parse_packages_xml(xml_content)
assert len(manager.packages) == len(expected_packages)
for actual, expected in zip(manager.packages, expected_packages):
assert actual.name == expected.name
assert actual.displayname == expected.displayname
assert actual.version == expected.version
def test_commercial_installer_auto_answers():
"""Test generation of auto-answer options"""
auto_answers = CommercialInstaller.get_auto_answers()
assert "OperationDoesNotExistError=Ignore" in auto_answers
assert "OverwriteTargetDirectory=No" in auto_answers
assert "telemetry-question=No" in auto_answers
@pytest.mark.parametrize(
"installer_path, override, username, password, output_dir, no_unattended, expected_cmd",
[
(
"/path/to/installer",
None,
"user",
"pass",
"./output",
False,
[
"/path/to/installer",
"--accept-licenses",
"--accept-obligations",
"--confirm-command",
"--email",
"user",
"--pw",
"pass",
"--root",
str(Path("./output").absolute()),
"--auto-answer",
CommercialInstaller.get_auto_answers(),
],
),
(
"/path/to/installer",
["--override", "arg1", "arg2"],
None,
None,
None,
True,
["/path/to/installer", "--override", "arg1", "arg2"],
),
],
)
def test_build_command(
installer_path: str,
override: Optional[List[str]],
username: Optional[str],
password: Optional[str],
output_dir: Optional[str],
no_unattended: bool,
expected_cmd: List[str],
):
"""Test building of installer command"""
cmd = CommercialInstaller.build_command(
installer_path,
override=override,
username=username,
password=password,
output_dir=output_dir,
no_unattended=no_unattended,
)
assert cmd == expected_cmd
@pytest.mark.enable_socket
def test_commercial_installer_download(monkeypatch, commercial_installer):
"""Test downloading of commercial installer"""
def mock_requests_get(*args, **kwargs):
return MockResponse(content=b"installer_content")
monkeypatch.setattr(requests, "get", mock_requests_get)
with TemporaryDirectory() as temp_dir:
target_path = Path(temp_dir) / "qt-installer"
commercial_installer.download_installer(target_path, timeout=60)
assert target_path.exists()
assert target_path.read_bytes() == b"installer_content"
@pytest.mark.parametrize(
"modules, expected_command",
[
(None, ["install", "qt.qt6.680.gcc_64"]),
(["qtquick3d"], ["install", "qt.qt6.680.gcc_64", "qt.qt6.680.addons.qtquick3d"]),
(["all"], ["install", "qt.qt6.680.gcc_64", "qt.qt6.680.addons.qtquick3d"]),
],
)
def test_get_install_command(monkeypatch, modules: Optional[List[str]], expected_command: List[str]):
"""Test generation of install commands"""
manager = QtPackageManager(arch="gcc_64", version=Version("6.8.0"), target="desktop")
def mock_gather_packages(self, installer_path: str) -> None:
self.packages = [
QtPackageInfo(name="qt.qt6.680.gcc_64", displayname="Desktop gcc", version="6.8.0"),
QtPackageInfo(name="qt.qt6.680.addons.qtquick3d", displayname="Qt Quick 3D", version="6.8.0"),
]
monkeypatch.setattr(QtPackageManager, "gather_packages", mock_gather_packages)
command = manager.get_install_command(modules, "./temp")
assert command == expected_command
@pytest.mark.enable_socket
@pytest.mark.parametrize(
"cmd, arch_dict, details, expected_command",
[
(
"install-qt-official desktop {} 6.8.1 " "--outputdir ./install-qt-official --email {} --pw {}",
{"windows": "win64_msvc2022_64", "linux": "linux_gcc_64", "mac": "clang_64"},
["./install-qt-official", "qt6", "681"],
"qt-unified-{}-x64-online.run --email ******** --pw ******** --root {} "
"--accept-licenses --accept-obligations "
"--confirm-command "
"--auto-answer OperationDoesNotExistError=Ignore,OverwriteTargetDirectory=No,"
"stopProcessesForUpdates=Cancel,installationErrorWithCancel=Cancel,installationErrorWithIgnore=Ignore,"
"AssociateCommonFiletypes=Yes,telemetry-question=No install qt.{}.{}.{}",
),
(
"install-qt-official desktop {} 6.8.1 --outputdir ./install-qt-official --email {} --pw {}",
{"windows": "win64_msvc2022_64", "linux": "linux_gcc_64", "mac": "clang_64"},
["./install-qt-official", "qt6", "681"],
"qt-unified-{}-x64-online.run --email ******** --pw ******** --root {} "
"--accept-licenses --accept-obligations "
"--confirm-command "
"--auto-answer OperationDoesNotExistError=Ignore,OverwriteTargetDirectory=Yes,"
"stopProcessesForUpdates=Cancel,installationErrorWithCancel=Cancel,installationErrorWithIgnore=Ignore,"
"AssociateCommonFiletypes=Yes,telemetry-question=No install qt.{}.{}.{}",
),
],
)
def test_install_qt_commercial(
capsys, monkeypatch, cmd: str, arch_dict: dict[str, str], details: list[str], expected_command: str
) -> None:
"""Test commercial Qt installation command"""
def mock_safely_run(*args, **kwargs):
return CompletedProcess(args=args[0], returncode=0)
monkeypatch.setattr("aqt.commercial.safely_run", mock_safely_run)
current_platform = sys.platform.lower()
arch = arch_dict[current_platform]
abs_out = Path(details[0]).absolute()
# Get the email and password from the test parameters
email = TEST_EMAIL
password = TEST_PASSWORD
formatted_cmd = cmd.format(arch, email, password)
formatted_expected = expected_command.format(current_platform, abs_out, *details[1:], arch)
cli = Cli()
cli._setup_settings()
# First test the normal installation command
try:
cli.run(formatted_cmd.split())
except AttributeError:
out = " ".join(capsys.readouterr())
assert str(out).find(formatted_expected) >= 0
abs_out.joinpath(f"6.8.{str(details[2])[-1]}").mkdir(exist_ok=True, parents=True)
# Create a new command with the temp directory
new_cmd = (
f"install-qt-official desktop {arch} 6.8.{str(details[2])[-1]} --outputdir {abs_out} --email {email} "
f"--pw {password}"
)
# This should raise DiskAccessNotPermitted only for the first test (680)
if details[2] == "680":
with pytest.raises(DiskAccessNotPermitted) as exc_info:
cli.run(new_cmd.split())
assert "Target directory" in str(exc_info.value)
assert "already exists" in str(exc_info.value)
else:
cli.run(new_cmd.split())
def modify_qt_config(content):
"""
Takes content of INI file as string and returns modified content
"""
lines = content.splitlines()
in_qt_commercial = False
modified = []
for line in lines:
# Check if we're entering qtofficial section
if line.strip() == "[qtofficial]":
in_qt_commercial = True
# If in qtofficial section, look for the target line
if in_qt_commercial and "overwrite_target_directory : No" in line:
line = "overwrite_target_directory : Yes"
elif in_qt_commercial and "overwrite_target_directory : Yes" in line:
line = "overwrite_target_directory : No"
modified.append(line)
return "\n".join(modified)
script_dir = os.path.dirname(os.path.abspath(__file__))
config_path = os.path.join(script_dir, "../aqt/settings.ini")
with open(config_path, "r") as f:
content = f.read()
modified_content = modify_qt_config(content)
with open(config_path, "w") as f:
f.write(modified_content)
Settings._initialize()
shutil.rmtree(abs_out)
@pytest.mark.parametrize(
"args, expected_error",
[
(["list-qt-official", "--bad-flag"], "usage: aqt [-h] [-c CONFIG]"),
],
)
def test_list_qt_commercial_errors(capsys, args, expected_error):
"""Test error handling in list-qt-official command"""
cli = Cli()
with pytest.raises(SystemExit):
cli.run(args)
_, err = capsys.readouterr()
assert expected_error in err

View File

@@ -2054,104 +2054,3 @@ def test_installer_passes_base_to_metadatafactory(
sys.stderr.write(err)
assert expect_out.match(err), err
class CompletedProcess:
def __init__(self, args, returncode):
self.args = args
self.returncode = returncode
self.stdout = None
self.stderr = None
@pytest.mark.enable_socket
@pytest.mark.parametrize(
"cmd, arch_dict, details, expected_command",
[
(
"install-qt-commercial desktop {} 6.8.0 " "--outputdir ./install-qt-commercial " "--user {} --password {}",
{"windows": "win64_msvc2022_64", "linux": "linux_gcc_64", "mac": "clang_64"},
["./install-qt-commercial", "qt6", "680"],
"qt-unified-{}-x64-online.run --email ******** --pw ******** --root {} "
"--accept-licenses --accept-obligations "
"--confirm-command "
"--auto-answer OperationDoesNotExistError=Ignore,OverwriteTargetDirectory=No,"
"stopProcessesForUpdates=Cancel,installationErrorWithCancel=Cancel,installationErrorWithIgnore=Ignore,"
"AssociateCommonFiletypes=Yes,telemetry-question=No install qt.{}.{}.{}",
),
(
"install-qt-commercial desktop {} 6.8.1 " "--outputdir ./install-qt-commercial " "--user {} --password {}",
{"windows": "win64_msvc2022_64", "linux": "linux_gcc_64", "mac": "clang_64"},
["./install-qt-commercial", "qt6", "681"],
"qt-unified-{}-x64-online.run --email ******** --pw ******** --root {} "
"--accept-licenses --accept-obligations "
"--confirm-command "
"--auto-answer OperationDoesNotExistError=Ignore,OverwriteTargetDirectory=Yes,"
"stopProcessesForUpdates=Cancel,installationErrorWithCancel=Cancel,installationErrorWithIgnore=Ignore,"
"AssociateCommonFiletypes=Yes,telemetry-question=No install qt.{}.{}.{}",
),
],
)
def test_install_qt_commercial(
capsys, monkeypatch, cmd: str, arch_dict: dict[str, str], details: list[str], expected_command: str
) -> None:
"""Test commercial Qt installation command"""
# Mock subprocess.run instead of run_static_subprocess_dynamically
def mock_subprocess_run(*args, **kwargs):
# This will be called instead of the real subprocess.run
return CompletedProcess(args=args[0], returncode=0)
# Patch subprocess.run directly
monkeypatch.setattr("subprocess.run", mock_subprocess_run)
current_platform = sys.platform.lower()
arch = arch_dict[current_platform]
abs_out = Path(details[0]).absolute()
formatted_cmd = cmd.format(arch, "vofab76634@gholar.com", "WxK43TdWCTmxsrrpnsWbjPfPXVq3mtLK")
formatted_expected = expected_command.format(current_platform, abs_out, *details[1:], arch)
cli = Cli()
cli._setup_settings()
try:
cli.run(formatted_cmd.split())
except AttributeError:
out = " ".join(capsys.readouterr())
assert str(out).find(formatted_expected) >= 0
def modify_qt_config(content):
"""
Takes content of INI file as string and returns modified content
"""
lines = content.splitlines()
in_qt_commercial = False
modified = []
for line in lines:
# Check if we're entering qtcommercial section
if line.strip() == "[qtcommercial]":
in_qt_commercial = True
# If in qtcommercial section, look for the target line
if in_qt_commercial and "overwrite_target_directory : No" in line:
line = "overwrite_target_directory : Yes"
elif in_qt_commercial and "overwrite_target_directory : Yes" in line:
line = "overwrite_target_directory : No"
modified.append(line)
return "\n".join(modified)
script_dir = os.path.dirname(os.path.abspath(__file__))
config_path = os.path.join(script_dir, "../aqt/settings.ini")
with open(config_path, "r") as f:
content = f.read()
modified_content = modify_qt_config(content)
with open(config_path, "w") as f:
f.write(modified_content)

View File

@@ -1336,19 +1336,3 @@ def test_find_installed_qt_mingw_dir(expected_result: str, installed_files: List
actual_result = QtRepoProperty.find_installed_desktop_qt_dir(host, base_path, Version(qt_ver))
assert (actual_result.name if actual_result else None) == expected_result
# Test error cases
@pytest.mark.parametrize(
"args, expected_error",
[
(["list-qt-commercial", "--bad-flag"], "usage: aqt [-h] [-c CONFIG]"),
],
)
def test_list_qt_commercial_errors(capsys, args, expected_error):
"""Test error handling in list-qt-commercial command"""
cli = Cli()
with pytest.raises(SystemExit):
cli.run(args)
_, err = capsys.readouterr()
assert expected_error in err