|
1 | 1 | import json |
2 | 2 | import pathlib |
| 3 | +import typing |
3 | 4 | from unittest.mock import Mock, patch |
4 | 5 |
|
5 | 6 | import pytest |
@@ -549,3 +550,170 @@ def test_cache_lookup_no_cache_url_returns_none(tmp_context: WorkContext) -> Non |
549 | 550 | ) |
550 | 551 |
|
551 | 552 | assert result == (None, None) |
| 553 | + |
| 554 | + |
| 555 | +def _make_resolve_item( |
| 556 | + req: str = "testpkg", |
| 557 | + req_type: RequirementType = RequirementType.TOP_LEVEL, |
| 558 | + why_snapshot: list[tuple[RequirementType, Requirement, Version]] | None = None, |
| 559 | + parent: tuple[Requirement, Version] | None = None, |
| 560 | +) -> bootstrapper.WorkItem: |
| 561 | + return bootstrapper.WorkItem( |
| 562 | + req=Requirement(req), |
| 563 | + req_type=req_type, |
| 564 | + phase=bootstrapper.BootstrapPhase.RESOLVE, |
| 565 | + why_snapshot=why_snapshot or [], |
| 566 | + parent=parent, |
| 567 | + ) |
| 568 | + |
| 569 | + |
| 570 | +def _record_and_load( |
| 571 | + bt: bootstrapper.Bootstrapper, stack: list[bootstrapper.WorkItem] |
| 572 | +) -> list[typing.Any]: |
| 573 | + bt._record_stack_state(stack) |
| 574 | + return typing.cast(list[typing.Any], json.loads(bt._stack_filename.read_text())) |
| 575 | + |
| 576 | + |
| 577 | +def test_record_stack_state_minimal_item(tmp_context: WorkContext) -> None: |
| 578 | + """Minimal RESOLVE-phase item serializes with all optional fields None/empty.""" |
| 579 | + bt = bootstrapper.Bootstrapper(tmp_context) |
| 580 | + contents = _record_and_load(bt, [_make_resolve_item()]) |
| 581 | + |
| 582 | + result = contents[0] |
| 583 | + assert result["req"] == "testpkg" |
| 584 | + assert result["req_type"] == str(RequirementType.TOP_LEVEL) |
| 585 | + assert result["phase"] == str(bootstrapper.BootstrapPhase.RESOLVE) |
| 586 | + assert result["resolved_version"] is None |
| 587 | + assert result["source_url"] is None |
| 588 | + assert result["build_sdist_only"] is False |
| 589 | + assert result["why"] == [] |
| 590 | + assert result["parent"] is None |
| 591 | + assert result["build_system_deps"] == [] |
| 592 | + assert result["build_backend_deps"] == [] |
| 593 | + assert result["build_sdist_deps"] == [] |
| 594 | + |
| 595 | + |
| 596 | +def test_record_stack_state_full_item(tmp_context: WorkContext) -> None: |
| 597 | + """Fully-populated item serializes resolved_version, parent, why, and dep sets.""" |
| 598 | + bt = bootstrapper.Bootstrapper(tmp_context) |
| 599 | + parent_req = Requirement("parent-pkg") |
| 600 | + parent_version = Version("2.0") |
| 601 | + why_snapshot = [(RequirementType.INSTALL, parent_req, parent_version)] |
| 602 | + |
| 603 | + item = bootstrapper.WorkItem( |
| 604 | + req=Requirement("child-pkg>=1.0"), |
| 605 | + req_type=RequirementType.INSTALL, |
| 606 | + phase=bootstrapper.BootstrapPhase.BUILD, |
| 607 | + why_snapshot=why_snapshot, |
| 608 | + parent=(parent_req, parent_version), |
| 609 | + resolved_version=Version("1.5"), |
| 610 | + source_url="https://pypi.test/child-pkg-1.5.tar.gz", |
| 611 | + build_sdist_only=True, |
| 612 | + build_system_deps={Requirement("setuptools")}, |
| 613 | + build_backend_deps={Requirement("wheel")}, |
| 614 | + build_sdist_deps={Requirement("flit-core")}, |
| 615 | + ) |
| 616 | + |
| 617 | + contents = _record_and_load(bt, [item]) |
| 618 | + result = contents[0] |
| 619 | + |
| 620 | + assert result["resolved_version"] == "1.5" |
| 621 | + assert result["source_url"] == "https://pypi.test/child-pkg-1.5.tar.gz" |
| 622 | + assert result["build_sdist_only"] is True |
| 623 | + assert result["why"] == [ |
| 624 | + { |
| 625 | + "req_type": str(RequirementType.INSTALL), |
| 626 | + "req": "parent-pkg", |
| 627 | + "version": "2.0", |
| 628 | + } |
| 629 | + ] |
| 630 | + assert result["parent"] == {"req": "parent-pkg", "version": "2.0"} |
| 631 | + assert result["build_system_deps"] == ["setuptools"] |
| 632 | + assert result["build_backend_deps"] == ["wheel"] |
| 633 | + assert result["build_sdist_deps"] == ["flit-core"] |
| 634 | + |
| 635 | + |
| 636 | +def test_record_stack_state_dep_sets_are_sorted(tmp_context: WorkContext) -> None: |
| 637 | + """Mixed-order dep sets come out alphabetically sorted.""" |
| 638 | + bt = bootstrapper.Bootstrapper(tmp_context) |
| 639 | + item = bootstrapper.WorkItem( |
| 640 | + req=Requirement("mypkg"), |
| 641 | + req_type=RequirementType.TOP_LEVEL, |
| 642 | + phase=bootstrapper.BootstrapPhase.BUILD, |
| 643 | + why_snapshot=[], |
| 644 | + build_system_deps={Requirement("zzz"), Requirement("aaa"), Requirement("mmm")}, |
| 645 | + ) |
| 646 | + |
| 647 | + contents = _record_and_load(bt, [item]) |
| 648 | + assert contents[0]["build_system_deps"] == ["aaa", "mmm", "zzz"] |
| 649 | + |
| 650 | + |
| 651 | +def test_record_stack_state_writes_file(tmp_context: WorkContext) -> None: |
| 652 | + """File is created; list length matches stack size.""" |
| 653 | + bt = bootstrapper.Bootstrapper(tmp_context) |
| 654 | + stack = [_make_resolve_item("pkga"), _make_resolve_item("pkgb")] |
| 655 | + |
| 656 | + bt._record_stack_state(stack) |
| 657 | + |
| 658 | + assert bt._stack_filename.exists() |
| 659 | + contents = json.loads(bt._stack_filename.read_text()) |
| 660 | + assert isinstance(contents, list) |
| 661 | + assert len(contents) == 2 |
| 662 | + |
| 663 | + |
| 664 | +def test_record_stack_state_ordering(tmp_context: WorkContext) -> None: |
| 665 | + """Index 0 = stack[-1] (next to pop); last index = stack[0].""" |
| 666 | + bt = bootstrapper.Bootstrapper(tmp_context) |
| 667 | + stack = [ |
| 668 | + _make_resolve_item("pkga"), |
| 669 | + _make_resolve_item("pkgb"), |
| 670 | + _make_resolve_item("pkgc"), |
| 671 | + ] |
| 672 | + |
| 673 | + contents = _record_and_load(bt, stack) |
| 674 | + |
| 675 | + assert contents[0]["req"] == "pkgc" |
| 676 | + assert contents[-1]["req"] == "pkga" |
| 677 | + |
| 678 | + |
| 679 | +def test_record_stack_state_overwrites_each_call(tmp_context: WorkContext) -> None: |
| 680 | + """Second call replaces first call's content.""" |
| 681 | + bt = bootstrapper.Bootstrapper(tmp_context) |
| 682 | + |
| 683 | + bt._record_stack_state([_make_resolve_item("pkga"), _make_resolve_item("pkgb")]) |
| 684 | + first_content = bt._stack_filename.read_text() |
| 685 | + |
| 686 | + bt._record_stack_state([_make_resolve_item("pkgc")]) |
| 687 | + second_content = bt._stack_filename.read_text() |
| 688 | + |
| 689 | + assert first_content != second_content |
| 690 | + contents = json.loads(second_content) |
| 691 | + assert len(contents) == 1 |
| 692 | + assert contents[0]["req"] == "pkgc" |
| 693 | + |
| 694 | + |
| 695 | +def test_bootstrap_calls_record_stack_state(tmp_context: WorkContext) -> None: |
| 696 | + """`_record_stack_state` is called at least once during `bootstrap()`.""" |
| 697 | + bt = bootstrapper.Bootstrapper(tmp_context) |
| 698 | + call_count = {"n": 0} |
| 699 | + |
| 700 | + original = bt._record_stack_state |
| 701 | + |
| 702 | + def counting_record(stack: list[bootstrapper.WorkItem]) -> None: |
| 703 | + call_count["n"] += 1 |
| 704 | + original(stack) |
| 705 | + |
| 706 | + req = Requirement("testpkg") |
| 707 | + |
| 708 | + with ( |
| 709 | + patch.object(bt, "_record_stack_state", side_effect=counting_record), |
| 710 | + patch.object( |
| 711 | + bt._resolver, |
| 712 | + "resolve", |
| 713 | + return_value=[("https://pypi.test/testpkg-1.0.tar.gz", Version("1.0"))], |
| 714 | + ), |
| 715 | + patch.object(bt, "_phase_start", return_value=[]), |
| 716 | + ): |
| 717 | + bt.bootstrap(req=req, req_type=RequirementType.TOP_LEVEL) |
| 718 | + |
| 719 | + assert call_count["n"] >= 1 |
0 commit comments