返回 CodeWhale
publish-crates.test.py
根目录 / scripts / release / publish-crates.test.py
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
146 lines PYTHON