Skip to content

Unit Tests

In ideal conditions, the entire unit test suite is expected to complete in just over one minute on Linux and approximately two minutes on Windows.

Unit tests are defined in the TEST_CFGS dictionary using the following structure:

  • Key: The name of the unit test.
  • Value: A tuple containing the command-line arguments for the test and the expected console output.

Unit test descriptions are retrieved via print_results and get_detail functions, using tags derived from TEST_CFGS keys plus extended ones for complex standalone tests. Tags correspond to entries in the following localization files:

  • <HUMBLE_PROJECT_ROOT>/l10n/details.txt
  • <HUMBLE_PROJECT_ROOT>/l10n/details_es.txt


Classes, Functions and unit tests of test_humble.py.

PythonVersion

Bases: NamedTuple

Python version fields for test_unsupported_python_version.

Source code in tests/test_humble.py
296
297
298
299
300
class PythonVersion(NamedTuple):
    """Python version fields for `test_unsupported_python_version`."""

    major: int
    minor: int

cleanup_analysis_history()

Truncate the test analysis history file.

After all unit tests complete, retaining only the first twenty-five lines to ensure file size stability while preserving data required for testing.

Source code in tests/test_humble.py
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
def cleanup_analysis_history():
    """Truncate the test analysis history file.

    After all unit tests complete, retaining only the first twenty-five lines to
    ensure file size stability while preserving data required for testing.
    """
    original_lines = []
    with suppress(Exception), \
         HUMBLE_TEMP_HISTORY.open(encoding="utf-8") as history_file:
        original_lines.extend(next(history_file) for _ in range(25))
    if not original_lines:
        return
    with suppress(Exception), \
         HUMBLE_TEMP_HISTORY.open("w", encoding="utf-8") as original_file:
        original_file.writelines(original_lines)
        original_file.flush()
        fsync(original_file.fileno())

delete_export_files(extension, ko_msg)

Remove temporary files from export unit tests.

Source code in tests/test_humble.py
541
542
543
544
545
546
547
548
549
550
551
552
553
554
def delete_export_files(extension, ko_msg):
    """Remove temporary files from export unit tests."""
    msgs = []
    for export_file in HUMBLE_TESTS_DIR.iterdir():
        name_lower = export_file.name.lower()
        if (name_lower.startswith(HUMBLE_TEMP_PREFIX) and
                name_lower.endswith(extension)):
            try:
                export_file.unlink()
            except OSError as cleanup_err:
                error_detail = get_detail(ko_msg, replace=True)
                msgs.append((error_detail,
                             f"({type(cleanup_err).__name__}) {export_file}"))
    return msgs

delete_pytest_caches(dir_path)

Remove PYTEST_CACHE_DIRS items following the run of unit tests.

Source code in tests/test_humble.py
557
558
559
560
561
562
563
564
565
566
567
568
def delete_pytest_caches(dir_path):
    """Remove `PYTEST_CACHE_DIRS` items following the run of unit tests."""
    msgs = []
    path_obj = Path(dir_path)
    if path_obj.is_dir():
        try:
            shutil.rmtree(path_obj)
        except OSError as rmtree_err:
            error_detail = get_detail("[test_fcache]", replace=True)
            msgs.append((error_detail,
                         f"({type(rmtree_err).__name__}) {path_obj}"))
    return msgs

delete_temp_content()

Remove the files and folders after all tests have been run.

Source code in tests/test_humble.py
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
def delete_temp_content():
    """Remove the files and folders after all tests have been run."""
    now = datetime.now().astimezone()
    current_time = now.strftime("%Y/%m/%d - %H:%M:%S")
    info_msgs = set_temp_content(current_time)
    error_msgs = [(msg, val) for msg, val in info_msgs
                  if msg.startswith("Failed")]
    info_msgs = [(msg, val) for msg, val in info_msgs
                 if not msg.startswith("Failed")]
    max_msg_len = len(get_detail("[test_tests]", replace=True))
    for message, value in error_msgs:
        print(f"[ERROR] {message.ljust(max_msg_len + 1)}: {value}")
    if error_msgs:
        print()
    for message, value in info_msgs:
        print(f"[INFO] {message.ljust(max_msg_len + 1)}: {value}")

delete_temp_coverage()

Set up session globals and clean up temporary files after testing.

Source code in tests/test_humble.py
645
646
647
648
649
650
651
652
@pytest.fixture(scope="session", autouse=True) # noqa: vulture
def delete_temp_coverage():
    """Set up session globals and clean up temporary files after testing."""
    args.lang = "en"
    l10n_main[:] = get_l10n_content()
    yield
    cleanup_analysis_history()
    delete_temp_content()

get_detail(id_mode, *, replace=False)

Print a message, optionally removing newlines.

Source code in tests/test_humble.py
303
304
305
306
307
308
309
def get_detail(id_mode, *, replace=False):
    """Print a message, optionally removing newlines."""
    for i, line in enumerate(l10n_main):
        if line.startswith(id_mode):
            return (l10n_main[i+1].replace("\n", "")) if replace else \
                l10n_main[i+1]
    return None

get_l10n_content()

Assign the lookup file to handle localized messages and errors.

Source code in tests/test_humble.py
312
313
314
315
316
317
318
319
320
def get_l10n_content():
    """Assign the lookup file to handle localized messages and errors."""
    if args.lang == "en":
        l10n_file = HUMBLE_L10N_FILE[0]
    elif args.lang == "es":
        l10n_file = HUMBLE_L10N_FILE[1]
    l10n_path = HUMBLE_TESTS_DIR / HUMBLE_L10N_DIR / l10n_file
    with l10n_path.open(encoding="utf-8") as l10n_content:
        return l10n_content.readlines()

make_test_func(cfg_key)

Generate unit test execution function.

Skips test_wrong_testssl on Windows due to the Unix-environment requirement (Cygwin, MSYS2, or Windows Subsystem for Linux) for testssl.sh

Source code in tests/test_humble.py
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
def make_test_func(cfg_key):
    """Generate unit test execution function.

    Skips `test_wrong_testssl` on Windows due to the Unix-environment
    requirement (Cygwin, MSYS2, or Windows Subsystem for Linux) for testssl.sh
    """
    def test_func():
        return run_test(*TEST_CFGS[cfg_key])

    if cfg_key == "test_wrong_testssl":
        test_func = pytest.mark.skipif(
            system().lower() == "windows",
            reason="'test_wrong_testssl' skipped on Windows",
        )(test_func)

    return test_func

parse_expected_text(output, expected_text)

Validate output against expected results after each test.

Source code in tests/test_humble.py
365
366
367
368
369
370
371
372
373
374
def parse_expected_text(output, expected_text):
    """Validate output against expected results after each test."""
    exp_msg = get_detail("[test_expected]", replace=True)
    not_found_msg = get_detail("[test_notfound]", replace=True)
    if isinstance(expected_text, list | tuple | set):
        if all(e not in output for e in expected_text):
            pytest.fail(f"{exp_msg} {expected_text} {not_found_msg}")
        return
    if expected_text not in output:
        pytest.fail(f"{exp_msg} '{expected_text}' {not_found_msg}")

print_results()

Show the description of each unit test.

Descriptions are retrieved via get_detail function using tags derived from TEST_CFGS keys plus extended ones for complex standalone unit tests.

Tags correspond to entries in the following localization files:

  • <HUMBLE_PROJECT_ROOT>/l10n/details.txt
  • <HUMBLE_PROJECT_ROOT>/l10n/details_es.txt
Source code in tests/test_humble.py
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
def print_results():
    """Show the description of each unit test.

    Descriptions are retrieved via `get_detail` function using tags derived
    from `TEST_CFGS` keys plus extended ones for complex standalone unit tests.

    Tags correspond to entries in the following localization files:

    - `<HUMBLE_PROJECT_ROOT>/l10n/details.txt`
    - `<HUMBLE_PROJECT_ROOT>/l10n/details_es.txt`
    """
    print()
    dynamic_tags = [f"[{key}]" for key in TEST_CFGS]
    all_tags = dynamic_tags + EXTENDED_TAGS
    descriptions = [(tag, get_detail(tag, replace=True)) for tag in all_tags]
    max_len = max(len(tag.strip("[]")) for tag in all_tags)
    for tag, detail in descriptions:
        label = tag.strip("[]").ljust(max_len + 1)
        print(f"{label}:{detail}")
    print()

run_test(args, expected_text, timeout=15)

Run unit test and check for expected console output.

Source code in tests/test_humble.py
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
def run_test(args, expected_text, timeout=15):
    """Run unit test and check for expected console output."""
    test_args = [TEST_URLS[9] if a is None else a for a in args]

    try:
        result = subprocess.run(
            [sys.executable, HUMBLE_MAIN_FILE, *test_args],
            capture_output=True,
            text=True,
            timeout=timeout,
            encoding="utf-8",
            errors="replace",
            check=False,
        )
        output = result.stdout + result.stderr
    except subprocess.TimeoutExpired:
        pytest.fail(get_detail("[test_timeout]", replace=True))
    parse_expected_text(output, expected_text)

set_temp_content(current_time)

Define the files and folders to be purged upon completion of tests.

Source code in tests/test_humble.py
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
def set_temp_content(current_time):
    """Define the files and folders to be purged upon completion of tests."""
    info_msgs = [(get_detail("[test_tests]", replace=True), current_time)]
    delete_extensions = [
        (".csv", "[test_fcsv]"),
        (".txt", "[test_ftxt]"),
        (".html", "[test_fhtml]"),
        (".json", "[test_fjson]"),
        (".json", "[test_fjson_brief]"),
        (".pdf", "[test_fpdf]"),
        (".xlsx", "[test_fxlsx]"),
        (".xml", "[test_fxml]"),
    ]
    for extension, ko_msg in delete_extensions:
        info_msgs.extend(delete_export_files(extension, ko_msg))
    for cache_dir in PYTEST_CACHE_DIRS:
        info_msgs.extend(delete_pytest_caches(cache_dir))
    return info_msgs

test_cicd_error(capsys)

Verify an error is displayed in CI/CD results.

Source code in tests/test_humble.py
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
def test_cicd_error(capsys):
    """Verify an error is displayed in CI/CD results."""
    with suppress(SystemExit):
        _spec.loader.exec_module(humble_module)
    humble_module.l10n_main = l10n_main
    humble_module.args = args
    with (
        patch.object(humble_module, "get_cicd_labels", side_effect=Exception),
        patch.object(humble_module, "get_detail", return_value=ASSERT_STR[1]),
    ):
        with pytest.raises(SystemExit) as wrapped_exit:
            humble_module.print_cicd_totals("any_file.tmp")
        assert wrapped_exit.value.code == 1
    captured = capsys.readouterr()
    assert ASSERT_STR[0] in captured.out.lower()

test_file_access_errors(capsys)

Verify an error is displayed related to file access.

Test whether the export or history files cannot be accessed or created.

Source code in tests/test_humble.py
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
def test_file_access_errors(capsys):
    """Verify an error is displayed related to file access.

    Test whether the export or history files cannot be accessed or created.
    """
    with suppress(SystemExit):
        _spec.loader.exec_module(humble_module)
    humble_module.l10n_main, humble_module.args = l10n_main, args
    with patch("pathlib.Path.open", side_effect=OSError), \
         patch.object(humble_module, "delete_lines"):
        _, res = humble_module.validate_file_access("f.txt", context="history")
        assert res[0] in ("Not available", "No disponible")
        with patch.object(humble_module, "get_detail",
                          return_value=HUMBLE_TEMP_HISTORY):
            humble_module.validate_file_access("f.txt", context="basic")
            out = capsys.readouterr().out.lower()
            assert str(HUMBLE_TEMP_HISTORY).lower() in out
        with patch.object(humble_module, "get_detail",
                          return_value=ASSERT_STR[1]):
            with pytest.raises(SystemExit) as wrapped_exit:
                humble_module.validate_file_access("f.txt", context="export")
            assert wrapped_exit.value.code == 1
            assert ASSERT_STR[0] in capsys.readouterr().out.lower()

test_missing_arguments()

Consolidates multiple checks for missing required arguments.

Test multiple missing argument scenarios within a single unit test.

Source code in tests/test_humble.py
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
def test_missing_arguments():
    """Consolidates multiple checks for missing required arguments.

    Test multiple missing argument scenarios within a single unit test.
    """
    expected = ["Error:", "error:", "TXT", "HTML", "Analysis"]
    run_test(["-H", "Cache-Control: no-cache"], expected)
    run_test(["-if", "humble_test.txt", "-r"], expected)
    run_test(["-if", "humble_test.txt"], expected)
    run_test(["-l", "es"], expected)
    run_test(["-of", "humble_test.txt"], expected)
    run_test(["-of", "humble_test.html", "-o", "html", "-u", TEST_URLS[9]],
             expected)
    run_test(["-b"], expected)
    run_test(["-s"], expected)

test_outdated_humble(capsys)

Verify an error is displayed related to outdated versions.

Test whether the local version of humble.py is more than 30 days older than the GitHub version.

Source code in tests/test_humble.py
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
def test_outdated_humble(capsys):
    """Verify an error is displayed related to outdated versions.

    Test whether the local version of `humble.py` is more than 30 days older
    than the GitHub version.
    """
    with suppress(SystemExit):
        _spec.loader.exec_module(humble_module)
    humble_module.l10n_main = l10n_main
    humble_module.args = args
    mock_github_version = date(2026, 3, 6)
    mock_local_version = date(2026, 1, 1)
    mock_days_diff = (mock_github_version - mock_local_version).days
    humble_module.check_updates_diff(mock_days_diff, mock_github_version,
                                     mock_local_version)
    captured = capsys.readouterr()
    assert mock_github_version.isoformat() in captured.out

test_proxy_wrong()

Consolidate missing argument checks across proxy-related features.

Source code in tests/test_humble.py
534
535
536
537
538
def test_proxy_wrong():
    """Consolidate missing argument checks across proxy-related features."""
    expected = ["Error:", "error:"]
    run_test(["-p", "https://"], expected)
    run_test(["-p", "http://127.0.0.1:test"], expected)

test_python_version()

Returns an error message related to Python version.

Test whether the current Python version does not meet the minimum requirement.

Source code in tests/test_humble.py
472
473
474
475
476
477
478
479
480
481
482
def test_python_version():
    """Returns an error message related to Python version.

    Test whether the current Python version does not meet the minimum
    requirement.
    """
    if sys.version_info[:2] < REQUIRED_PYTHON_VERSION:
        pytest.fail(
            f"{get_detail('[test_pythonm]', replace=True)} "
            f"{sys.version_info.major}.{sys.version_info.minor}",
        )

test_testssl_error(capsys)

Verify an error is displayed for TLS/SSL check exceptions.

Source code in tests/test_humble.py
441
442
443
444
445
446
447
448
449
450
def test_testssl_error(capsys):
    """Verify an error is displayed for TLS/SSL check exceptions."""
    humble_module.l10n_main = l10n_main
    humble_module.args = args
    with patch.object(humble_module, "Popen", side_effect=OSError):
        with pytest.raises(SystemExit) as wrapped_exit:
            humble_module.testssl_analysis(TESTSSL_CMD)
        assert wrapped_exit.value.code == 1
    captured = capsys.readouterr()
    assert ASSERT_STR[0] in captured.out.lower()

test_unsupported_python_version(capsys)

Verify an error is displayed related to Python version.

Test whether the Python version is below the minimum supported version.

Source code in tests/test_humble.py
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
def test_unsupported_python_version(capsys):
    """Verify an error is displayed related to Python version.

    Test whether the Python version is below the minimum supported version.
    """
    mocked_python_version = PythonVersion(3, 10)
    with suppress(SystemExit):
        _spec.loader.exec_module(humble_module)
    humble_module.l10n_main = l10n_main
    humble_module.args = args
    with patch("sys.version_info", mocked_python_version):
        with pytest.raises(SystemExit) as wrapped_exit:
            humble_module.check_python_version()
        assert wrapped_exit.value.code == 1
    captured = capsys.readouterr()
    assert "humble" in captured.out.lower()

test_updates_error(capsys)

Verify an error message is displayed if the GitHub update check fails.

Source code in tests/test_humble.py
503
504
505
506
507
508
509
510
511
512
513
514
def test_updates_error(capsys):
    """Verify an error message is displayed if the GitHub update check fails."""
    with suppress(SystemExit):
        _spec.loader.exec_module(humble_module)
    humble_module.l10n_main = l10n_main
    humble_module.args = args
    with patch("requests.get", side_effect=RequestException):
        with pytest.raises(SystemExit) as wrapped_exit:
            humble_module.check_updates(date(2026, 1, 1))
        assert wrapped_exit.value.code == 1
    captured = capsys.readouterr()
    assert ASSERT_STR[0] in captured.out.lower()