| 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() |
| 146 |