|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""Exercise the upload boundary with real Cargo tarballs and unpublished dependencies.""" |
| 3 | + |
| 4 | +import os |
| 5 | +from pathlib import Path |
| 6 | +import shutil |
| 7 | +import subprocess |
| 8 | +import tempfile |
| 9 | +import unittest |
| 10 | + |
| 11 | + |
| 12 | +SCRIPTS = Path(__file__).resolve().parent |
| 13 | + |
| 14 | + |
| 15 | +class PublishPreflightTests(unittest.TestCase): |
| 16 | + def setUp(self): |
| 17 | + real_cargo = shutil.which("cargo") |
| 18 | + self.assertIsNotNone(real_cargo, "Cargo 1.90+ must be installed to run release preflight tests") |
| 19 | + self.temporary = tempfile.TemporaryDirectory() |
| 20 | + self.addCleanup(self.temporary.cleanup) |
| 21 | + self.root = Path(self.temporary.name) |
| 22 | + self.scripts = self.root / "scripts/release" |
| 23 | + self.scripts.mkdir(parents=True) |
| 24 | + for name in ("publish-crates.sh", "validate-crate-publish-order.py"): |
| 25 | + shutil.copy2(SCRIPTS / name, self.scripts / name) |
| 26 | + (self.scripts / "crates.sh").write_text( |
| 27 | + "release_crates=(codewhale-preflight-base codewhale-preflight-app)\n" |
| 28 | + ) |
| 29 | + # These independently tested guards require a real GitHub release. The |
| 30 | + # fixture tests the subsequent Cargo boundary without contacting GitHub. |
| 31 | + for name in ("require-release-tag-checkout.sh", "verify-release-assets.sh"): |
| 32 | + guard = self.scripts / name |
| 33 | + guard.write_text("#!/bin/sh\nexit 0\n") |
| 34 | + guard.chmod(0o755) |
| 35 | + (self.root / "Cargo.toml").write_text( |
| 36 | + '[workspace]\nmembers = ["base", "app"]\nresolver = "2"\n' |
| 37 | + ) |
| 38 | + for name in ("base", "app"): |
| 39 | + crate = self.root / name |
| 40 | + (crate / "src").mkdir(parents=True) |
| 41 | + manifest = ( |
| 42 | + f'[package]\nname = "codewhale-preflight-{name}"\n' |
| 43 | + 'version = "0.0.0"\nedition = "2021"\nlicense = "MIT"\n' |
| 44 | + 'exclude = ["src/payload.txt"]\n' |
| 45 | + ) |
| 46 | + if name == "app": |
| 47 | + manifest += ( |
| 48 | + '[dependencies]\ncodewhale-preflight-base = ' |
| 49 | + '{ path = "../base", version = "=0.0.0" }\n' |
| 50 | + ) |
| 51 | + (crate / "Cargo.toml").write_text(manifest) |
| 52 | + (crate / "src/lib.rs").write_text('pub const VALUE: &str = "ok";\n') |
| 53 | + (self.root / "app/src/lib.rs").write_text( |
| 54 | + 'pub const VALUE: &str = include_str!("payload.txt");\n' |
| 55 | + ) |
| 56 | + (self.root / "app/src/payload.txt").write_text("embedded asset\n") |
| 57 | + self.uploads = self.root / "uploads" |
| 58 | + bin_dir = self.root / "bin" |
| 59 | + bin_dir.mkdir() |
| 60 | + cargo = bin_dir / "cargo" |
| 61 | + cargo.write_text( |
| 62 | + '#!/usr/bin/env bash\nset -euo pipefail\n' |
| 63 | + 'if [[ "${1:-}" == --version && -n "${TEST_CARGO_VERSION:-}" ]]; then\n' |
| 64 | + ' echo "$TEST_CARGO_VERSION"; exit 0\nfi\n' |
| 65 | + # Cargo publish --dry-run still contacts the registry. Keep its real |
| 66 | + # tarball build for the old-script regression check, fully offline. |
| 67 | + 'if [[ "${1:-}" == publish ]]; then\n' |
| 68 | + ' shift\n if [[ " $* " == *" --dry-run "* ]]; then\n' |
| 69 | + ' args=()\n for arg in "$@"; do\n' |
| 70 | + ' [[ "$arg" == --dry-run ]] || args+=("$arg")\n done\n' |
| 71 | + ' exec "$TEST_REAL_CARGO" package "${args[@]}"\n fi\n' |
| 72 | + ' echo attempted >> "$TEST_UPLOADS"\n exit 98\nfi\n' |
| 73 | + 'exec "$TEST_REAL_CARGO" "$@"\n' |
| 74 | + ) |
| 75 | + cargo.chmod(0o755) |
| 76 | + curl = bin_dir / "curl" |
| 77 | + curl.write_text("#!/bin/sh\nexit 22\n") |
| 78 | + curl.chmod(0o755) |
| 79 | + self.env = { |
| 80 | + **os.environ, |
| 81 | + "TEST_REAL_CARGO": real_cargo, |
| 82 | + "TEST_UPLOADS": str(self.uploads), |
| 83 | + "PATH": str(bin_dir) + os.pathsep + os.environ["PATH"], |
| 84 | + "CARGO_NET_OFFLINE": "true", |
| 85 | + "CARGO_TARGET_DIR": str(self.root / "target"), |
| 86 | + } |
| 87 | + self.run_command(["cargo", "generate-lockfile", "--offline"], success=True) |
| 88 | + |
| 89 | + def run_command(self, args, *, success): |
| 90 | + result = subprocess.run( |
| 91 | + args, cwd=self.root, env=self.env, capture_output=True, text=True |
| 92 | + ) |
| 93 | + output = result.stdout + result.stderr |
| 94 | + self.assertEqual(result.returncode == 0, success, output) |
| 95 | + return output |
| 96 | + |
| 97 | + def assert_missing_asset_blocks(self, mode): |
| 98 | + # The workspace compiles: only the published tarball loses the asset. |
| 99 | + self.run_command(["cargo", "check", "--locked"], success=True) |
| 100 | + output = self.run_command( |
| 101 | + ["bash", str(self.scripts / "publish-crates.sh"), mode], success=False |
| 102 | + ) |
| 103 | + self.assertFalse(self.uploads.exists(), "upload reached before all packages passed") |
| 104 | + self.assertIn("payload.txt", output) |
| 105 | + |
| 106 | + def test_old_cargo_fails_before_packaging_or_upload(self): |
| 107 | + for version in ("cargo 1.88.0 (fixture)", "cargo 1.89.0 (fixture)", "unknown"): |
| 108 | + with self.subTest(version=version): |
| 109 | + self.env["TEST_CARGO_VERSION"] = version |
| 110 | + output = self.run_command( |
| 111 | + ["bash", str(self.scripts / "publish-crates.sh"), "publish"], success=False |
| 112 | + ) |
| 113 | + self.assertIn("requires Cargo 1.90 or newer", output) |
| 114 | + self.assertFalse(self.uploads.exists()) |
| 115 | + self.assertFalse((self.root / "target/package").exists()) |
| 116 | + |
| 117 | + def test_resume_verifies_tarballs_and_skips_existing_versions(self): |
| 118 | + manifest = self.root / "app/Cargo.toml" |
| 119 | + manifest.write_text(manifest.read_text().replace('exclude = ["src/payload.txt"]\n', "")) |
| 120 | + (self.root / "bin/curl").write_text("#!/bin/sh\nexit 0\n") |
| 121 | + output = self.run_command( |
| 122 | + ["bash", str(self.scripts / "publish-crates.sh"), "publish"], success=True |
| 123 | + ) |
| 124 | + self.assertIn("Skipping codewhale-preflight-base", output) |
| 125 | + self.assertIn("Skipping codewhale-preflight-app", output) |
| 126 | + self.assertTrue((self.root / "target/package/codewhale-preflight-app-0.0.0.crate").exists()) |
| 127 | + self.assertFalse(self.uploads.exists()) |
| 128 | + |
| 129 | + def test_dry_run_builds_dependent_tarball(self): |
| 130 | + self.assert_missing_asset_blocks("dry-run") |
| 131 | + |
| 132 | + def test_publish_builds_every_tarball_before_first_upload(self): |
| 133 | + self.assert_missing_asset_blocks("publish") |
| 134 | + |
| 135 | + def test_dry_run_accepts_unpublished_workspace_dependencies(self): |
| 136 | + manifest = self.root / "app/Cargo.toml" |
| 137 | + manifest.write_text(manifest.read_text().replace('exclude = ["src/payload.txt"]\n', "")) |
| 138 | + self.run_command( |
| 139 | + ["bash", str(self.scripts / "publish-crates.sh"), "dry-run"], success=True |
| 140 | + ) |
| 141 | + self.assertFalse(self.uploads.exists()) |
| 142 | + |
| 143 | + |
| 144 | +if __name__ == "__main__": |
| 145 | + unittest.main() |
0 commit comments