• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

miurahr / aqtinstall / 14106802575

27 Mar 2025 12:39PM UTC coverage: 88.493% (+0.3%) from 88.195%
14106802575

push

github

web-flow
Merge pull request #906 from Kidev/official_ux

Add --use-official-installer, fix official installer download after update 4.9

1944 of 2271 branches covered (85.6%)

Branch coverage included in aggregate %.

101 of 137 new or added lines in 5 files covered. (73.72%)

3 existing lines in 1 file now uncovered.

4170 of 4638 relevant lines covered (89.91%)

0.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.65
/tests/test_cli.py
1
import re
1✔
2
import sys
1✔
3
from pathlib import Path
1✔
4
from tempfile import TemporaryDirectory
1✔
5
from typing import Dict, List, Optional
1✔
6

7
import pytest
1✔
8

9
from aqt.exceptions import CliInputError
1✔
10
from aqt.helper import Settings
1✔
11
from aqt.installer import Cli
1✔
12
from aqt.metadata import MetadataFactory, SimpleSpec, Version
1✔
13

14

15
def expected_help(actual, prefix=None):
1✔
16
    expected = "usage: aqt [-h] [-c CONFIG]"
1✔
17
    if prefix is not None:
1!
NEW
18
        return actual.startswith(prefix + expected)
×
19
    return actual.startswith(expected)
1✔
20

21

22
def test_cli_help(capsys):
1✔
23
    cli = Cli()
1✔
24
    cli.run(["help"])
1✔
25
    out, err = capsys.readouterr()
1✔
26
    assert expected_help(out)
1✔
27

28

29
@pytest.mark.parametrize(
1✔
30
    "host, target, arch, version_or_spec, expected_version, is_bad_spec",
31
    (
32
        ("windows", "desktop", "wasm_32", "6.1", None, False),
33
        ("windows", "desktop", "wasm_32", "5.12", None, False),
34
        ("windows", "desktop", "wasm_32", "5.13", Version("5.13.2"), False),
35
        ("windows", "desktop", "wasm_32", "5", Version("5.15.2"), False),
36
        ("windows", "desktop", "wasm_32", "<5.14.5", Version("5.14.2"), False),
37
        ("windows", "desktop", "mingw32", "6.0", Version("6.0.3"), False),
38
        ("windows", "winrt", "mingw32", "6", None, False),
39
        ("windows", "winrt", "mingw32", "bad spec", None, True),
40
        ("windows", "android", "android_x86", "6", Version("6.1.0"), False),
41
        ("windows", "desktop", "android_x86", "6", Version("6.5.0"), False),  # does not validate arch
42
        ("windows", "desktop", "android_fake", "6", Version("6.5.0"), False),  # does not validate arch
43
    ),
44
)
45
def test_cli_determine_qt_version(
1✔
46
    monkeypatch, host, target, arch, version_or_spec: str, expected_version: Version, is_bad_spec: bool
47
):
48
    _html = (Path(__file__).parent / "data" / f"{host}-{target}.html").read_text("utf-8")
1✔
49
    monkeypatch.setattr(MetadataFactory, "fetch_http", lambda *args, **kwargs: _html)
1✔
50
    cli = Cli()
1✔
51
    cli._setup_settings()
1✔
52

53
    if is_bad_spec:
1✔
54
        with pytest.raises(CliInputError) as e:
1✔
55
            Cli._determine_qt_version(version_or_spec, host, target, arch)
1✔
56
        assert e.type == CliInputError
1✔
57
        assert format(e.value) == f"Invalid version or SimpleSpec: '{version_or_spec}'\n" + SimpleSpec.usage()
1✔
58
    elif not expected_version:
1✔
59
        with pytest.raises(CliInputError) as e:
1✔
60
            Cli._determine_qt_version(version_or_spec, host, target, arch)
1✔
61
        assert e.type == CliInputError
1✔
62
        expect_msg = f"No versions of Qt exist for spec={version_or_spec} with host={host}, target={target}, arch={arch}"
1✔
63
        actual_msg = format(e.value)
1✔
64
        assert actual_msg == expect_msg
1✔
65
    else:
66
        ver = Cli._determine_qt_version(version_or_spec, host, target, arch)
1✔
67
        assert ver == expected_version
1✔
68

69

70
@pytest.mark.parametrize(
1✔
71
    "invalid_version",
72
    ("5.15", "five-dot-fifteen", "5", "5.5.5.5"),
73
)
74
def test_cli_invalid_version(capsys, invalid_version):
1✔
75
    """Checks that invalid version strings are handled properly"""
76

77
    # Ensure that invalid_version cannot be a semantic_version.Version
78
    with pytest.raises(ValueError):
1✔
79
        Version(invalid_version)
1✔
80

81
    cli = Cli()
1✔
82
    cli._setup_settings()
1✔
83

84
    matcher = re.compile(
1✔
85
        #  r"^INFO    : aqtinstall\(aqt\) v.* on Python 3.*\n"
86
        r"(.*\n)*"
87
        r"ERROR   :.*Invalid version: '" + invalid_version + r"'! Please use the form '5\.X\.Y'\.\n.*"
88
    )
89

90
    for cmd in (("list-qt", "mac", "desktop", "--arch", invalid_version),):
1✔
91
        cli = Cli()
1✔
92
        assert cli.run(cmd) == 1
1✔
93
        out, err = capsys.readouterr()
1✔
94
        sys.stdout.write(out)
1✔
95
        sys.stderr.write(err)
1✔
96
        assert matcher.match(err)
1✔
97

98

99
@pytest.mark.parametrize(
1✔
100
    "version, allow_latest, allow_empty, allow_minus, expect_ok",
101
    (
102
        ("1.2.3", False, False, False, True),
103
        ("1.2.", False, False, False, False),
104
        ("latest", True, False, False, True),
105
        ("latest", False, False, False, False),
106
        ("", False, True, False, True),
107
        ("", False, False, False, False),
108
        ("1.2.3-0-123", False, False, True, True),
109
        ("1.2.3-0-123", False, False, False, False),
110
    ),
111
)
112
def test_cli_validate_version(version: str, allow_latest: bool, allow_empty: bool, allow_minus: bool, expect_ok: bool):
1✔
113
    if expect_ok:
1✔
114
        Cli._validate_version_str(version, allow_latest=allow_latest, allow_empty=allow_empty, allow_minus=allow_minus)
1✔
115
    else:
116
        with pytest.raises(CliInputError) as err:
1✔
117
            Cli._validate_version_str(version, allow_latest=allow_latest, allow_empty=allow_empty, allow_minus=allow_minus)
1✔
118
        assert err.type == CliInputError
1✔
119

120

121
def test_cli_check_mirror():
1✔
122
    cli = Cli()
1✔
123
    cli._setup_settings()
1✔
124
    assert cli._check_mirror(None)
1✔
125
    arg = ["install-qt", "linux", "desktop", "5.11.3", "-b", "https://download.qt.io/"]
1✔
126
    args = cli.parser.parse_args(arg)
1✔
127
    assert args.base == "https://download.qt.io/"
1✔
128
    assert cli._check_mirror(args.base)
1✔
129

130

131
@pytest.mark.parametrize(
1✔
132
    "arch, host, target, version, expect",
133
    (
134
        ("impossible_arch", "windows", "desktop", "6.2.0", "impossible_arch"),
135
        ("", "windows", "desktop", "6.2.0", None),
136
        (None, "windows", "desktop", "6.2.0", None),
137
        (None, "linux", "desktop", "6.2.0", "gcc_64"),
138
        (None, "mac", "desktop", "6.2.0", "clang_64"),
139
        (None, "mac", "ios", "6.2.0", "ios"),
140
        (None, "mac", "android", "6.2.0", "android"),
141
        (None, "mac", "android", "5.12.0", None),
142
        # SimpleSpec instead of Version
143
        ("impossible_arch", "windows", "desktop", "6.2", "impossible_arch"),
144
        ("", "windows", "desktop", "6.2", None),
145
        (None, "windows", "desktop", "6.2", None),
146
        (None, "linux", "desktop", "6.2", "gcc_64"),
147
        (None, "mac", "desktop", "6.2", "clang_64"),
148
        (None, "mac", "ios", "6.2", "ios"),
149
        (None, "mac", "android", "6.2", None),  # No way to determine arch for android target w/o version
150
    ),
151
)
152
def test_set_arch(arch: Optional[str], host: str, target: str, version: str, expect: Optional[str]):
1✔
153
    if not expect:
1✔
154
        with pytest.raises(CliInputError) as e:
1✔
155
            Cli._set_arch(arch, host, target, version)
1✔
156
        assert e.type == CliInputError
1✔
157
        assert format(e.value) == "Please supply a target architecture."
1✔
158
        assert e.value.should_show_help is True
1✔
159
    else:
160
        assert Cli._set_arch(arch, host, target, version) == expect
1✔
161

162

163
@pytest.mark.parametrize(
1✔
164
    "cmd, expect_msg, should_show_help",
165
    (
166
        (
167
            "install-qt mac ios 6.2.0 --base not_a_url",
168
            "The `--base` option requires a url where the path `online/qtsdkrepository` exists.",
169
            True,
170
        ),
171
        (
172
            "install-qt mac ios 6.2.0 --noarchives",
173
            "When `--noarchives` is set, the `--modules` option is mandatory.",
174
            False,
175
        ),
176
        (
177
            "install-qt mac ios 6.2.0 --noarchives --archives",
178
            "When `--noarchives` is set, the `--modules` option is mandatory.",
179
            False,
180
        ),
181
        (
182
            "install-qt mac ios 6.2.0 --noarchives --archives --modules qtcharts",
183
            "Options `--archives` and `--noarchives` are mutually exclusive.",
184
            False,
185
        ),
186
        (
187
            "install-src mac ios 6.2.0 --kde",
188
            "KDE patch: unsupported version!!",
189
            False,
190
        ),
191
    ),
192
)
193
def test_cli_input_errors(capsys, cmd, expect_msg, should_show_help):
1✔
194
    cli = Cli()
1✔
195
    cli._setup_settings()
1✔
196
    assert 1 == cli.run(cmd.split())
1✔
197
    out, err = capsys.readouterr()
1✔
198
    if should_show_help:
1✔
199
        assert expected_help(out)
1✔
200
    else:
201
        assert out == ""
1✔
202
    assert err.rstrip().endswith(expect_msg)
1✔
203

204

205
@pytest.mark.parametrize(
1✔
206
    "cmd, expect_err",
207
    (
208
        (
209
            "list-qt mac --extension wasm",
210
            "WARNING : The parameter 'extension' with value 'wasm' is deprecated "
211
            "and marked for removal in a future version of aqt.\n"
212
            "In the future, please omit this parameter.\n"
213
            "WARNING : The '--extension' flag will be ignored.\n",
214
        ),
215
        (
216
            "list-qt mac desktop --extensions 6.2.0",
217
            "WARNING : The parameter 'extensions' with value '6.2.0' is deprecated "
218
            "and marked for removal in a future version of aqt.\n"
219
            "In the future, please omit this parameter.\n"
220
            "WARNING : The '--extensions' flag will always return an empty list, "
221
            "because there are no useful arguments for the '--extension' flag.\n",
222
        ),
223
    ),
224
)
225
def test_cli_list_qt_deprecated_flags(capsys, cmd: str, expect_err: str):
1✔
226
    cli = Cli()
1✔
227
    cli._setup_settings()
1✔
228
    assert 0 == cli.run(cmd.split())
1✔
229
    out, err = capsys.readouterr()
1✔
230
    assert err == expect_err
1✔
231

232

233
def test_cli_unexpected_error(monkeypatch, capsys):
1✔
234
    def _mocked_run(*args):
1✔
235
        raise RuntimeError("Some unexpected error")
1✔
236

237
    monkeypatch.setattr("aqt.installer.Cli.run_install_qt", _mocked_run)
1✔
238

239
    cli = Cli()
1✔
240
    cli._setup_settings()
1✔
241
    assert Cli.UNHANDLED_EXCEPTION_CODE == cli.run(["install-qt", "mac", "ios", "6.2.0"])
1✔
242
    out, err = capsys.readouterr()
1✔
243
    assert err.startswith("ERROR   : Some unexpected error")
1✔
244
    assert err.rstrip().endswith(
1✔
245
        "===========================PLEASE FILE A BUG REPORT===========================\n"
246
        "You have discovered a bug in aqt.\n"
247
        "Please file a bug report at https://github.com/miurahr/aqtinstall/issues\n"
248
        "Please remember to include a copy of this program's output in your report."
249
    )
250

251

252
@pytest.mark.parametrize("external_tool_exists", (True, False))
1✔
253
def test_set_7zip_checks_external_tool_when_specified(monkeypatch, capsys, external_tool_exists: bool):
1✔
254
    cli = Cli()
1✔
255
    cli._setup_settings()
1✔
256
    external = "my_7z_extractor"
1✔
257

258
    def mock_subprocess_run(args, **kwargs):
1✔
259
        assert args[0] == external
1✔
260
        if not external_tool_exists:
1✔
261
            raise FileNotFoundError()
1✔
262

263
    monkeypatch.setattr("aqt.installer.subprocess.run", mock_subprocess_run)
1✔
264
    monkeypatch.setattr("aqt.installer.EXT7Z", False)
1✔
265
    if external_tool_exists:
1✔
266
        assert external == cli._set_sevenzip(external)
1✔
267
    else:
268
        with pytest.raises(CliInputError) as err:
1✔
269
            cli._set_sevenzip(external)
1✔
270
        assert format(err.value) == format(f"Specified 7zip command executable does not exist: '{external}'")
1✔
271
    assert capsys.readouterr()[1] == ""
1✔
272

273

274
@pytest.mark.parametrize("fallback_exists", (True, False))
1✔
275
def test_set_7zip_uses_fallback_when_py7zr_missing(monkeypatch, capsys, fallback_exists: bool):
1✔
276
    cli = Cli()
1✔
277
    cli._setup_settings()
1✔
278
    external, fallback = None, Settings.zipcmd
1✔
279

280
    def mock_subprocess_run(args, **kwargs):
1✔
281
        assert args[0] == fallback
1✔
282
        if not fallback_exists:
1✔
283
            raise FileNotFoundError()
1✔
284

285
    monkeypatch.setattr("aqt.installer.subprocess.run", mock_subprocess_run)
1✔
286
    monkeypatch.setattr("aqt.installer.EXT7Z", True)
1✔
287
    if fallback_exists:
1✔
288
        assert fallback == cli._set_sevenzip(external)
1✔
289
    else:
290
        with pytest.raises(CliInputError) as err:
1✔
291
            cli._set_sevenzip(external)
1✔
292
        assert format(err.value) == format(f"Fallback 7zip command executable does not exist: '{fallback}'")
1✔
293
    assert f"Falling back to '{fallback}'" in capsys.readouterr()[1]
1✔
294

295

296
@pytest.mark.parametrize("fallback_exists", (True, False))
1✔
297
def test_set_7zip_chooses_p7zr_when_ext_missing(monkeypatch, capsys, fallback_exists: bool):
1✔
298
    cli = Cli()
1✔
299
    cli._setup_settings()
1✔
300
    external = None
1✔
301

302
    def mock_subprocess_run(args, **kwargs):
1✔
303
        assert False, "Should not try to run anything"
×
304

305
    monkeypatch.setattr("aqt.installer.subprocess.run", mock_subprocess_run)
1✔
306
    monkeypatch.setattr("aqt.installer.EXT7Z", False)
1✔
307
    assert cli._set_sevenzip(external) is None
1✔
308
    assert capsys.readouterr()[1] == ""
1✔
309

310

311
@pytest.mark.parametrize(
1✔
312
    "archive_dest, keep, temp_dir, expect, should_make_dir",
313
    (
314
        (None, False, "temp", "temp", False),
315
        (None, True, "temp", ".", False),
316
        ("my_archives", False, "temp", "my_archives", True),
317
        ("my_archives", True, "temp", "my_archives", True),
318
    ),
319
)
320
def test_cli_choose_archive_dest(
1✔
321
    monkeypatch, archive_dest: Optional[str], keep: bool, temp_dir: str, expect: str, should_make_dir: bool
322
):
323
    enclosed = {"made_dir": False}
1✔
324

325
    def mock_mkdir(*args, **kwargs):
1✔
326
        enclosed["made_dir"] = True
1✔
327

328
    monkeypatch.setattr("aqt.installer.Path.mkdir", mock_mkdir)
1✔
329

330
    assert Cli.choose_archive_dest(archive_dest, keep, temp_dir) == Path(expect)
1✔
331
    assert enclosed["made_dir"] == should_make_dir
1✔
332

333

334
@pytest.mark.parametrize(
1✔
335
    "host, target, arch, is_auto, mocked_arches, existing_arch_dirs, expect",
336
    (
337
        (  # not installed
338
            "windows",
339
            "android",
340
            "android_armv7",
341
            False,
342
            ["win64_mingw99"],
343
            ["not_mingw"],
344
            {"install": None, "instruct": "win64_mingw99", "use_dir": "mingw99_64"},
345
        ),
346
        (  # Alt Desktop Qt already installed
347
            "windows",
348
            "android",
349
            "android_armv7",
350
            False,
351
            ["win64_mingw99"],
352
            ["mingw128_32"],
353
            {"install": None, "instruct": None, "use_dir": "mingw128_32"},
354
        ),
355
        # not installed
356
        (
357
            "linux",
358
            "android",
359
            "android_armv7",
360
            False,
361
            [],
362
            ["gcc_32"],
363
            {"install": None, "instruct": "gcc_64", "use_dir": "gcc_64"},
364
        ),
365
        (  # Desktop Qt already installed
366
            "linux",
367
            "android",
368
            "android_armv7",
369
            False,
370
            [],
371
            ["gcc_64"],
372
            {"install": None, "instruct": None, "use_dir": "gcc_64"},
373
        ),
374
        (  # not installed
375
            "windows",
376
            "android",
377
            "android_armv7",
378
            True,
379
            ["win64_mingw99"],
380
            ["not_mingw"],
381
            {"install": "win64_mingw99", "instruct": None, "use_dir": "mingw99_64"},
382
        ),
383
        (  # Alt Desktop Qt already installed
384
            "windows",
385
            "android",
386
            "android_armv7",
387
            True,
388
            ["win64_mingw99"],
389
            ["mingw128_32"],
390
            {"install": None, "instruct": None, "use_dir": "mingw128_32"},
391
        ),
392
        # not installed
393
        (
394
            "linux",
395
            "android",
396
            "android_armv7",
397
            True,
398
            [],
399
            ["gcc_32"],
400
            {"install": "gcc_64", "instruct": None, "use_dir": "gcc_64"},
401
        ),
402
        (  # Desktop Qt already installed
403
            "linux",
404
            "android",
405
            "android_armv7",
406
            True,
407
            [],
408
            ["gcc_64"],
409
            {"install": None, "instruct": None, "use_dir": "gcc_64"},
410
        ),
411
        (  # MSVC arm64 with --autodesktop: should install min64_msvc2019_64
412
            "windows",
413
            "desktop",
414
            "win64_msvc2019_arm64",
415
            True,
416
            ["win64_mingw", "win64_msvc2019_64", "win64_msvc2019_arm64", "wasm_singlethread", "wasm_multithread"],
417
            ["mingw128_32"],
418
            {"install": "win64_msvc2019_64", "instruct": None, "use_dir": "msvc2019_64"},
419
        ),
420
        (  # MSVC arm64 without --autodesktop: should point to min64_msvc2019_64
421
            "windows",
422
            "desktop",
423
            "win64_msvc2019_arm64",
424
            False,
425
            ["win64_mingw", "win64_msvc2019_64", "win64_msvc2019_arm64", "wasm_singlethread", "wasm_multithread"],
426
            ["mingw128_32"],
427
            {"install": None, "instruct": "win64_msvc2019_64", "use_dir": "msvc2019_64"},
428
        ),
429
        (  # MSVC arm64 without --autodesktop, with correct target already installed
430
            "windows",
431
            "desktop",
432
            "win64_msvc2019_arm64",
433
            False,
434
            ["win64_mingw", "win64_msvc2019_64", "win64_msvc2019_arm64", "wasm_singlethread", "wasm_multithread"],
435
            ["msvc2019_64"],
436
            {"install": None, "instruct": None, "use_dir": "msvc2019_64"},
437
        ),
438
        (  # MSVC arm64 without --autodesktop, with wrong target already installed
439
            "windows",
440
            "desktop",
441
            "win64_msvc2019_arm64",
442
            False,
443
            ["win64_mingw", "win64_msvc2019_64", "win64_msvc2019_arm64", "wasm_singlethread", "wasm_multithread"],
444
            ["mingw128_32"],
445
            {"install": None, "instruct": "win64_msvc2019_64", "use_dir": "msvc2019_64"},
446
        ),
447
    ),
448
)
449
def test_get_autodesktop_dir_and_arch_non_android(
1✔
450
    monkeypatch,
451
    capsys,
452
    host: str,
453
    target: str,
454
    arch: str,
455
    is_auto: bool,
456
    mocked_arches: List[str],
457
    existing_arch_dirs: List[str],
458
    expect: Dict[str, str],
459
):
460
    """
461
    Updated to handle version parsing and directory validation issues.
462
    """
463
    monkeypatch.setattr(MetadataFactory, "fetch_arches", lambda *args: mocked_arches)
1✔
464
    monkeypatch.setattr(Cli, "run", lambda *args: 0)
1!
465

466
    version = "6.2.3"
1✔
467
    cli = Cli()
1✔
468
    cli._setup_settings()
1✔
469

470
    flavor = "MSVC Arm64" if arch == "win64_msvc2019_arm64" else target
1✔
471

472
    with TemporaryDirectory() as temp_dir:
1✔
473
        base_dir = Path(temp_dir)
1✔
474
        for arch_dir in existing_arch_dirs:
1✔
475
            qmake = base_dir / version / arch_dir / f"bin/qmake{'.exe' if host == 'windows' else ''}"
1✔
476
            qmake.parent.mkdir(parents=True, exist_ok=True)
1✔
477
            qmake.write_text("exe file")
1✔
478

479
        autodesktop_arch_dir, autodesktop_arch_to_install = cli._get_autodesktop_dir_and_arch(
1✔
480
            is_auto, host, target, base_dir, Version(version), arch
481
        )
482

483
        # Validate directory choice and installation instructions
484
        assert autodesktop_arch_dir == expect["use_dir"], f"Expected: {expect['use_dir']}, Got: {autodesktop_arch_dir}"
1✔
485

486
        out, err = capsys.readouterr()
1✔
487
        err_lines = [line for line in err.strip().split("\n") if line]  # Remove empty lines
1!
488

489
        qmake = base_dir / version / expect["use_dir"] / f"bin/qmake{'.exe' if host == 'windows' else ''}"
1✔
490
        is_installed = qmake.exists()
1✔
491

492
        if is_installed:
1✔
493
            assert any("Found installed" in line for line in err_lines), "Expected 'Found installed' message."
1!
494
        elif expect["install"]:
1✔
495
            assert any(
1!
496
                f"You are installing the {flavor} version of Qt" in line for line in err_lines
497
            ), "Expected autodesktop install message."
498
        elif expect["instruct"]:
1!
499
            assert any("You can install" in line for line in err_lines), "Expected install instruction message."
1!
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc