Repository: miurahr/aqtinstall
Pull request #375 (2021-08-24 17:45:19)

Fail installation on missing tool variants
This implements the other half of #374, so that `aqt install-tool` should fail if you tell it to install a tool variant that doesn't exist.


Patch:
diff --git a/aqt/archives.py b/aqt/archives.py
index 5f820e8..d7e16cb 100644
--- a/aqt/archives.py
+++ b/aqt/archives.py
@@ -321,7 +321,7 @@ class ToolArchives(QtArchives):
         super(ToolArchives, self).__init__(
             os_name=os_name,
             target=target,
-            version_str=version_str,
+            version_str=version_str or "0.0.1",  # dummy value
             arch=arch,
             base=base,
             timeout=timeout,
@@ -347,45 +347,47 @@ class ToolArchives(QtArchives):
         )
         update_xml_url = posixpath.join(archive_url, "Updates.xml")
         self._download_update_xml(update_xml_url)  # call super method.
-        self._parse_update_xml(archive_url, [])
+        self._parse_update_xml(archive_url, None)
 
-    def _parse_update_xml(self, archive_url, target_packages):
+    def _parse_update_xml(self, archive_url, *ignored):
         try:
             self.update_xml = ElementTree.fromstring(self.update_xml_text)
         except ElementTree.ParseError as perror:
             self.logger.error("Downloaded metadata is corrupted. {}".format(perror))
             raise ArchiveListError("Downloaded metadata is corrupted.")
 
-        for packageupdate in self.update_xml.iter("PackageUpdate"):
-            name = packageupdate.find("Name").text
-            if name != self.arch:
-                continue
-            _archives = packageupdate.find("DownloadableArchives").text
-            if _archives is not None:
-                downloadable_archives = _archives.split(", ")
-            else:
-                downloadable_archives = []
-            named_version = packageupdate.find("Version").text
-            package_desc = packageupdate.find("Description").text
-            for archive in downloadable_archives:
-                package_url = posixpath.join(
-                    # https://download.qt.io/online/qtsdkrepository/linux_x64/desktop/tools_ifw/
-                    archive_url,
-                    # qt.tools.ifw.41/
-                    name,
-                    #  4.1.1-202105261130ifw-linux-x64.7z
-                    f"{named_version}{archive}",
-                )
-                hashurl = package_url + ".sha1"
-                self.archives.append(
-                    QtPackage(
-                        name=name,
-                        archive_url=package_url,
-                        archive=archive,
-                        package_desc=package_desc,
-                        hashurl=hashurl,
-                    )
+        try:
+            packageupdate = next(filter(lambda x: x.find("Name").text == self.arch, self.update_xml.iter("PackageUpdate")))
+        except StopIteration:
+            message = f"The package {self.arch} was not found while parsing XML of package information!"
+            raise NoPackageFound(message)
+
+        name = packageupdate.find("Name").text
+        named_version = packageupdate.find("Version").text
+        package_desc = packageupdate.find("Description").text
+        downloadable_archives = packageupdate.find("DownloadableArchives").text
+        if not downloadable_archives:
+            message = f"The package {self.arch} contains no downloadable archives!"
+            raise NoPackageFound(message)
+        for archive in downloadable_archives.split(", "):
+            package_url = posixpath.join(
+                # https://download.qt.io/online/qtsdkrepository/linux_x64/desktop/tools_ifw/
+                archive_url,
+                # qt.tools.ifw.41/
+                name,
+                #  4.1.1-202105261130ifw-linux-x64.7z
+                f"{named_version}{archive}",
+            )
+            hashurl = package_url + ".sha1"
+            self.archives.append(
+                QtPackage(
+                    name=name,
+                    archive_url=package_url,
+                    archive=archive,
+                    package_desc=package_desc,
+                    hashurl=hashurl,
                 )
+            )
 
     def get_target_config(self) -> TargetConfig:
         """Get target configuration.


Test patch:
diff --git a/tests/data/mac-desktop-tools_qtcreator-expect.json b/tests/data/mac-desktop-tools_qtcreator-expect.json
index f628fdd..b164493 100644
--- a/tests/data/mac-desktop-tools_qtcreator-expect.json
+++ b/tests/data/mac-desktop-tools_qtcreator-expect.json
@@ -26,5 +26,66 @@
       "Qt Creator 4.15.1 Plugin Development",
       "Headers and libs required to develop additional plugins"
     ]
+  ],
+  "variants_metadata": [
+    {
+      "Name": "qt.tools.qtcreator",
+      "DisplayName": "Qt Creator 4.15.1",
+      "Description": "IDE for Qt application development",
+      "Version": "4.15.1-0-202106081242",
+      "ReleaseDate": "2021-06-08",
+      "Script": "installscript.qs",
+      "Dependencies": "",
+      "SortingPriority": "1000",
+      "ForcedInstallation": "true",
+      "DownloadableArchives": [
+        "qtcreator.7z",
+        "qtcreator_sdktool.7z"
+      ],
+      "UpdateFile": {
+        "CompressedSize": "136099945",
+        "UncompressedSize": "533316311",
+        "OS": "Any"
+      },
+      "UserInterfaces": [
+        "associatecommonfiletypesform.ui",
+        "launchqtcreatorcheckboxform.ui"
+      ],
+      "SHA1": "684ee4525e6412623167a5442b7c262cac52c9bb"
+    },
+    {
+      "Name": "qt.tools.qtcreatordbg",
+      "DisplayName": "Qt Creator 4.15.1 Debug Symbols",
+      "Description": "Additional symbol files required to debug Qt Creator",
+      "Version": "4.15.1-0-202106081242",
+      "ReleaseDate": "2021-06-08",
+      "SortingPriority": "994",
+      "DownloadableArchives": [
+        "qtcreator-debug.7z"
+      ],
+      "UpdateFile": {
+        "CompressedSize": "235240929",
+        "UncompressedSize": "1570679561",
+        "OS": "Any"
+      },
+      "SHA1": "c71f2e138df7232f7482e66a4cc2db5f94a3c7b4"
+    },
+    {
+      "Name": "qt.tools.qtcreatordev",
+      "DisplayName": "Qt Creator 4.15.1 Plugin Development",
+      "Description": "Headers and libs required to develop additional plugins",
+      "Version": "4.15.1-0-202106081242",
+      "ReleaseDate": "2021-06-08",
+      "SortingPriority": "990",
+      "DownloadableArchives": [
+        "qtcreator_dev.7z"
+      ],
+      "UpdateFile": {
+        "CompressedSize": "12377799",
+        "UncompressedSize": "104362249",
+        "OS": "Any"
+      },
+      "SHA1": "0e2127ef128a87f6bc04e0c9eccab677aac21ec0"
+    }
   ]
 }
\ No newline at end of file
diff --git a/tests/test_archives.py b/tests/test_archives.py
index 088a094..cca1881 100644
--- a/tests/test_archives.py
+++ b/tests/test_archives.py
@@ -1,11 +1,21 @@
+import json
 import os
+import posixpath
+import re
+from pathlib import Path
 
 import pytest
 
-from aqt.archives import QtArchives
+from aqt.archives import QtArchives, ToolArchives
+from aqt.exceptions import NoPackageFound
 from aqt.helper import Settings
 
 
+@pytest.fixture(autouse=True)
+def setup():
+    Settings.load_settings(os.path.join(os.path.dirname(__file__), "data", "settings.ini"))
+
+
 @pytest.mark.parametrize(
     "os_name,version,target,datafile",
     [
@@ -49,3 +59,54 @@ def test_parse_update_xml(monkeypatch, os_name, version, target, datafile):
 
     # Assert if list_diff contains urls without target specified
     assert unwanted_targets == []
+
+
+@pytest.mark.parametrize(
+    "tool_name, tool_variant_name, is_expect_fail",
+    (
+        ("tools_qtcreator", "qt.tools.qtcreator", False),
+        ("tools_qtcreator", "qt.tools.qtcreatordbg", False),
+        ("tools_qtcreator", "qt.tools.qtcreatordev", False),
+        ("tools_qtcreator", "qt.tools.qtifw", True),
+    ),
+)
+def test_tools_variants(monkeypatch, tool_name, tool_variant_name, is_expect_fail: bool):
+    host, target, base = "mac", "desktop", "https://example.com"
+    datafile = f"{host}-{target}-{tool_name}"
+    update_xml = (Path(__file__).parent / "data" / f"{datafile}-update.xml").read_text("utf-8")
+
+    def _mock(self, *args):
+        self.update_xml_text = update_xml
+
+    monkeypatch.setattr(QtArchives, "_download_update_xml", _mock)
+
+    if is_expect_fail:
+        with pytest.raises(NoPackageFound) as e:
+            ToolArchives(host, target, tool_name, base, arch=tool_variant_name)
+        assert e.type == NoPackageFound
+        assert tool_variant_name in str(e.value), "Message should include the missing variant"
+        return
+
+    expect_json = json.loads((Path(__file__).parent / "data" / f"{datafile}-expect.json").read_text("utf-8"))
+    expect = next(filter(lambda x: x["Name"] == tool_variant_name, expect_json["variants_metadata"]))
+    expected_7z_files = set(expect["DownloadableArchives"])
+    qt_pkgs = ToolArchives(host, target, tool_name, base, arch=tool_variant_name).archives
+    url_begin = posixpath.join(base, f"online/qtsdkrepository/mac_x64/{target}/{tool_name}")
+    expected_archive_url_pattern = re.compile(
+        r"^" + re.escape(url_begin) + "/(" + expect["Name"] + ")/" + re.escape(expect["Version"]) + r"(.+\.7z)$"
+    )
+
+    for pkg in qt_pkgs:
+        if not expect["Description"]:
+            assert not pkg.package_desc
+        else:
+            assert pkg.package_desc == expect["Description"]
+        url_match = expected_archive_url_pattern.match(pkg.archive_url)
+        assert url_match
+        actual_variant_name, archive_name = url_match.groups()
+        assert actual_variant_name == tool_variant_name
+        assert pkg.archive == archive_name
+        assert pkg.hashurl == pkg.archive_url + ".sha1"
+        assert archive_name in expected_7z_files
+        expected_7z_files.remove(archive_name)
+    assert len(expected_7z_files) == 0, f"Failed to produce QtPackages for {expected_7z_files}"