test_release_version.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. """Tests for repository-owned Python release versions."""
  2. from __future__ import annotations
  3. import json
  4. import runpy
  5. import zipfile
  6. from pathlib import Path
  7. from types import SimpleNamespace
  8. import pytest
  9. ROOT = Path(__file__).resolve().parents[3]
  10. SCRIPT = ROOT / "scripts" / "build-python-release.py"
  11. build_python_release = SimpleNamespace(**runpy.run_path(str(SCRIPT)))
  12. def test_repository_version_matches_root_package_json() -> None:
  13. expected = json.loads((ROOT / "package.json").read_text())["version"]
  14. assert build_python_release.repository_version() == expected
  15. def test_release_tag_is_optional_for_non_release_builds() -> None:
  16. build_python_release.validate_release_tag(None, "1.2.3")
  17. def test_release_tag_must_match_repository_version() -> None:
  18. build_python_release.validate_release_tag("python-v1.2.3", "1.2.3")
  19. with pytest.raises(ValueError, match="expected 'python-v1.2.3'"):
  20. build_python_release.validate_release_tag("python-v1.2.4", "1.2.3")
  21. def test_repository_version_accepts_a_prerelease(tmp_path: Path) -> None:
  22. (tmp_path / "package.json").write_text('{"version":"1.2.3-rc.1"}\n')
  23. assert build_python_release.repository_version(tmp_path) == "1.2.3-rc.1"
  24. def test_repository_version_rejects_malformed_versions(tmp_path: Path) -> None:
  25. (tmp_path / "package.json").write_text('{"version":"v1.2"}\n')
  26. with pytest.raises(ValueError, match="must be X.Y.Z"):
  27. build_python_release.repository_version(tmp_path)
  28. def test_pep440_version_spells_a_prerelease_the_python_way() -> None:
  29. # Build backends normalize to this spelling, so the wheel filename and
  30. # metadata checks compare against it rather than the repository version.
  31. assert build_python_release.pep440_version("1.2.3") == "1.2.3"
  32. assert build_python_release.pep440_version("1.2.3-rc.1") == "1.2.3rc1"
  33. assert build_python_release.pep440_version("1.2.3-alpha.2") == "1.2.3a2"
  34. assert build_python_release.pep440_version("1.2.3-beta.10") == "1.2.3b10"
  35. with pytest.raises(ValueError, match="no PEP 440 spelling"):
  36. build_python_release.pep440_version("1.2.3-nightly")
  37. def test_macos_wheel_tag_does_not_claim_unsupported_node_platforms() -> None:
  38. assert build_python_release.PLATFORMS["macos-arm64"][0] == "macosx_14_0_arm64"
  39. assert build_python_release.PLATFORMS["macos-arm64"][1] == "deepseek-harness-sdk-runtime-macos-arm64"
  40. assert build_python_release.PLATFORMS["macos-x64"][0] == "macosx_14_0_x86_64"
  41. assert build_python_release.PLATFORMS["macos-x64"][1] == "deepseek-harness-sdk-runtime-macos-x64"
  42. def test_windows_wheel_tag_and_payload_are_x64_only() -> None:
  43. assert build_python_release.PLATFORMS["win-x64"] == (
  44. "win_amd64",
  45. "deepseek-harness-sdk-runtime-win-x64.exe",
  46. )
  47. assert not any(name.startswith("win-") and name != "win-x64" for name in build_python_release.PLATFORMS)
  48. def test_platform_manifest_rejects_incomplete_entries(tmp_path: Path) -> None:
  49. manifest = tmp_path / "platforms.json"
  50. manifest.write_text('{"macos-arm64":{"tag":"macosx_14_0_arm64"}}\n')
  51. with pytest.raises(ValueError, match="tag and executable fields"):
  52. build_python_release.load_platforms(manifest)
  53. def test_stage_sdk_keeps_distribution_module_and_runtime_pin_distinct(tmp_path: Path) -> None:
  54. destination = tmp_path / "staging"
  55. build_python_release.stage_sdk(destination, "1.2.3")
  56. pyproject = (destination / "pyproject.toml").read_text()
  57. assert 'name = "deepseek-harness-sdk"' in pyproject
  58. assert 'version = "1.2.3"' in pyproject
  59. assert 'license = "MIT"' in pyproject
  60. assert '"deepseek-harness-runtime-bin==1.2.3"' in pyproject
  61. assert 'license-files = ["LICENSE"]' in pyproject
  62. assert (destination / "LICENSE").read_bytes() == (ROOT / "LICENSE").read_bytes()
  63. assert (destination / "src" / "deepseek_harness" / "__init__.py").is_file()
  64. @pytest.mark.parametrize(
  65. ("target", "with_helper"),
  66. [("linux-x64", False), ("macos-arm64", True), ("macos-x64", True), ("win-x64.exe", False)],
  67. )
  68. def test_stage_runtime_copies_platform_payload(
  69. tmp_path: Path, target: str, with_helper: bool
  70. ) -> None:
  71. executable = tmp_path / f"deepseek-harness-sdk-runtime-{target}"
  72. executable.write_bytes(b"runtime")
  73. executable.chmod(0o755)
  74. expected = {executable.name: b"runtime"}
  75. ripgrep = (
  76. executable.with_name(f"{executable.stem}-rg.exe")
  77. if executable.suffix == ".exe"
  78. else Path(f"{executable}-rg")
  79. )
  80. ripgrep.write_bytes(b"ripgrep")
  81. ripgrep.chmod(0o755)
  82. expected[ripgrep.name] = b"ripgrep"
  83. if with_helper:
  84. spawn_helper = Path(f"{executable}-spawn-helper")
  85. spawn_helper.write_bytes(b"helper")
  86. spawn_helper.chmod(0o755)
  87. expected[spawn_helper.name] = b"helper"
  88. office = executable.parent / build_python_release.office_sidecar_name(executable.name)
  89. office_asset = office / "node_modules" / "@deepseek-ai" / "libreoffice-kit-wasm" / "assets" / "soffice.data"
  90. office_asset.parent.mkdir(parents=True)
  91. office_asset.write_bytes(b"office data")
  92. destination = tmp_path / "staging"
  93. build_python_release.stage_runtime(destination, "1.2.3", executable, executable.name)
  94. runtime_dir = destination / "src" / "deepseek_harness_runtime" / "runtime"
  95. assert {
  96. path.name: path.read_bytes()
  97. for path in runtime_dir.glob("deepseek-harness-sdk-runtime-*")
  98. if path.is_file()
  99. } == expected
  100. assert (runtime_dir / office.name / office_asset.relative_to(office)).read_bytes() == b"office data"
  101. pyproject = (destination / "pyproject.toml").read_text()
  102. assert 'license = "MIT"' in pyproject
  103. assert 'license-files = ["LICENSE", "THIRD_PARTY_NOTICES.md"]' in pyproject
  104. assert 'dsh = "deepseek_harness_runtime:main"' in pyproject
  105. assert (destination / "platforms.json").read_bytes() == (
  106. ROOT / "python" / "sdk-runtime" / "platforms.json"
  107. ).read_bytes()
  108. assert (destination / "LICENSE").read_bytes() == (ROOT / "LICENSE").read_bytes()
  109. assert (destination / "THIRD_PARTY_NOTICES.md").read_bytes() == (
  110. ROOT / "THIRD_PARTY_NOTICES.md"
  111. ).read_bytes()
  112. def test_stage_runtime_rejects_a_noncanonical_executable_name(tmp_path: Path) -> None:
  113. executable = tmp_path / "renamed.exe"
  114. executable.write_bytes(b"runtime")
  115. with pytest.raises(ValueError, match="must be named deepseek-harness-sdk-runtime-win-x64.exe"):
  116. build_python_release.stage_runtime(
  117. tmp_path / "staging",
  118. "1.2.3",
  119. executable,
  120. "deepseek-harness-sdk-runtime-win-x64.exe",
  121. )
  122. @pytest.mark.parametrize("platform_tag,selected", [
  123. ("manylinux_2_28_x86_64", "wasm"), ("macosx_14_0_arm64", "darwin-arm64"),
  124. ("macosx_14_0_x86_64", "darwin-x64"), ("win_amd64", "win32-x64"),
  125. ("manylinux_2_28_x86_64", "linux-x64"), ("macosx_14_0_arm64", "wasm"),
  126. ])
  127. @pytest.mark.parametrize("invalid", [None, "asset", "missing-engine", "foreign-engine", "helper-mode"])
  128. def test_office_wheel_requires_only_target_engine(
  129. tmp_path: Path, platform_tag: str, selected: str, invalid: str | None,
  130. ) -> None:
  131. root = "runtime-office/node_modules"
  132. wheel = tmp_path / "office.zip"
  133. native = selected != "wasm"
  134. if invalid == "helper-mode" and (not native or platform_tag == "win_amd64"):
  135. pytest.skip("executable mode applies to POSIX native helpers")
  136. engine = ({"kind": "native", "executable": "bin/helper"} if native else
  137. {"kind": "wasm", "loader": "loader.cjs", "wasm": "engine.wasm", "data": "engine.data", "metadata": "fonts.json"})
  138. with zipfile.ZipFile(wheel, "w") as archive:
  139. archive.writestr(f"{root}/@deepseek-ai/libreoffice-kit/package.json", json.dumps({
  140. "optionalDependencies": {f"@deepseek-ai/libreoffice-kit-{selected}": "0.0.1"},
  141. }))
  142. base = f"{root}/@deepseek-ai/libreoffice-kit-{selected}"
  143. if invalid != "missing-engine":
  144. archive.writestr(f"{base}/prebuilds.json", json.dumps({"engine": engine}))
  145. for field in (("executable",) if native else ("loader", "wasm", "data", "metadata")):
  146. if invalid == "asset" and field == ("executable" if native else "data"):
  147. continue
  148. asset = zipfile.ZipInfo(f"{base}/{engine[field]}")
  149. asset.external_attr = (0o644 if invalid == "helper-mode" else 0o755) << 16
  150. archive.writestr(asset, b"payload")
  151. if invalid == "foreign-engine":
  152. foreign = "wasm" if native else "darwin-arm64"
  153. archive.writestr(f"{root}/@deepseek-ai/libreoffice-kit-{foreign}/prebuilds.json", "{}")
  154. with zipfile.ZipFile(wheel) as archive:
  155. if invalid is None:
  156. build_python_release.verify_office_payload(archive, root, platform_tag)
  157. else:
  158. message = {"asset": "asset is missing", "missing-engine": "dependency is missing",
  159. "foreign-engine": "Unexpected Office engine", "helper-mode": "executable bit"}[invalid]
  160. with pytest.raises(RuntimeError, match=message):
  161. build_python_release.verify_office_payload(archive, root, platform_tag)