|
31 | 31 | from itertools import combinations |
32 | 32 | from pathlib import Path |
33 | 33 | from unittest.mock import MagicMock, patch |
| 34 | +from urllib.error import URLError |
34 | 35 |
|
35 | 36 | import pytest |
36 | 37 | import tomlrt |
@@ -732,22 +733,69 @@ def test_download_and_verify_mismatch(tmp_path): |
732 | 733 | assert not dest.exists() |
733 | 734 |
|
734 | 735 |
|
| 736 | +def test_download_and_verify_retries_transient_failure(tmp_path): |
| 737 | + """A transient network failure is retried and the next attempt succeeds.""" |
| 738 | + content = b"hello binary world" |
| 739 | + expected = hashlib.sha256(content).hexdigest() |
| 740 | + dest = tmp_path / "downloaded" |
| 741 | + |
| 742 | + with ( |
| 743 | + patch( |
| 744 | + "repomatic.tool_runner.urlopen", |
| 745 | + side_effect=[ |
| 746 | + URLError("certificate verify failed: self-signed certificate"), |
| 747 | + _urlopen_response(content), |
| 748 | + ], |
| 749 | + ) as fake_urlopen, |
| 750 | + patch("repomatic.tool_runner.time.sleep") as fake_sleep, |
| 751 | + ): |
| 752 | + _download_and_verify("https://example.com/file", expected, dest) |
| 753 | + |
| 754 | + assert fake_urlopen.call_count == 2 |
| 755 | + assert fake_sleep.call_count == 1 |
| 756 | + assert dest.read_bytes() == content |
| 757 | + |
| 758 | + |
| 759 | +def test_download_and_verify_gives_up_after_attempts(tmp_path): |
| 760 | + """A persistent network failure surfaces after the last attempt.""" |
| 761 | + dest = tmp_path / "downloaded" |
| 762 | + |
| 763 | + with ( |
| 764 | + patch( |
| 765 | + "repomatic.tool_runner.urlopen", |
| 766 | + side_effect=URLError("connection reset"), |
| 767 | + ) as fake_urlopen, |
| 768 | + patch("repomatic.tool_runner.time.sleep"), |
| 769 | + pytest.raises(URLError, match="connection reset"), |
| 770 | + ): |
| 771 | + _download_and_verify("https://example.com/file", "0" * 64, dest) |
| 772 | + |
| 773 | + assert fake_urlopen.call_count == 3 |
| 774 | + assert not dest.exists() |
| 775 | + |
| 776 | + |
735 | 777 | def test_download_and_verify_truncated(tmp_path): |
736 | 778 | """A body shorter than Content-Length raises OSError, not a mismatch. |
737 | 779 |
|
738 | 780 | A truncated transfer (proxy hiccup, dropped connection) hashes to a wrong |
739 | 781 | digest, so it used to be reported as a SHA-256 mismatch: that reads as a |
740 | 782 | stale pin or a tampered artifact when the registry checksum is correct. |
| 783 | + Truncation is retried like any network failure, so the error only |
| 784 | + surfaces once every attempt came up short. |
741 | 785 | """ |
742 | 786 | content = b"hello binary world" |
743 | 787 | dest = tmp_path / "downloaded" |
744 | 788 |
|
745 | 789 | with ( |
746 | 790 | patch( |
747 | 791 | "repomatic.tool_runner.urlopen", |
748 | | - # Advertise more bytes than the body delivers. |
749 | | - return_value=_urlopen_response(content, advertised_length=len(content) + 7), |
| 792 | + # Advertise more bytes than the body delivers, on every attempt. |
| 793 | + side_effect=[ |
| 794 | + _urlopen_response(content, advertised_length=len(content) + 7) |
| 795 | + for _ in range(3) |
| 796 | + ], |
750 | 797 | ), |
| 798 | + patch("repomatic.tool_runner.time.sleep"), |
751 | 799 | pytest.raises(OSError, match="Truncated download .* got 18 of 25 bytes"), |
752 | 800 | ): |
753 | 801 | _download_and_verify( |
|
0 commit comments