databricks-cli/libs/patchwheel/patch_test.go

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

178 lines
5.2 KiB
Go
Raw Normal View History

2025-03-03 15:05:18 +00:00
package patchwheel
import (
"bytes"
"context"
2025-03-03 15:05:18 +00:00
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
2025-03-03 16:25:12 +00:00
"github.com/stretchr/testify/require"
2025-03-03 15:05:18 +00:00
)
2025-03-03 16:25:12 +00:00
// Variants -- existing env
// Clean install
// Install unpatched first
// Install patched then another patched
// Variants -- source setup.py vs pyproject
// Different build backends? setuptools vs hatchling vs flit?
// Different tools? e.g. test poetry? test pdm? test regular pip?
// Variants -- python versions
// Variants --
// verifyVersion checks that the installed package version matches the expected version
// from the patched wheel filename.
func verifyVersion(t *testing.T, tempDir, wheelPath string) {
// Extract the expected version from the wheel filename
wheelInfo, err := ParseWheelFilename(wheelPath)
require.NoError(t, err)
expectedVersion := wheelInfo.Version
// Get the actual installed version
pyExec := filepath.Join(tempDir, ".venv", "bin", "python") // Handle Windows paths appropriately
cmdOut := captureOutput(t, tempDir, pyExec, "-c", "import myproj; myproj.print_version()")
actualVersion := strings.TrimSpace(cmdOut)
// Verify the version matches what we expect
require.Equal(t, expectedVersion, actualVersion,
"Installed version doesn't match expected version from wheel filename")
// Also check that it has the expected format (starts with base version + timestamp)
require.True(t, strings.HasPrefix(actualVersion, "0.1.0+20"),
"Version should start with 0.1.0+20, got %s", actualVersion)
t.Logf("Verified installed version: %s", actualVersion)
}
2025-03-03 15:05:18 +00:00
// minimalPythonProject returns a map of file paths to their contents for a minimal Python project.
func minimalPythonProject() map[string]string {
return map[string]string{
"pyproject.toml": `[project]
name = "myproj"
version = "0.1.0"
[build-system]
requires = ["setuptools>=61.0.0", "wheel"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
2025-03-03 16:25:12 +00:00
where = ["src"]
`,
2025-03-03 16:25:12 +00:00
"src/myproj/__init__.py": `
2025-03-03 15:05:18 +00:00
def hello():
return "Hello, world!"
2025-03-03 16:25:12 +00:00
def print_version():
from importlib.metadata import version
print(version("myproj"))
2025-03-03 15:05:18 +00:00
`,
}
}
func writeProjectFiles(t *testing.T, baseDir string, files map[string]string) {
2025-03-03 15:05:18 +00:00
for path, content := range files {
fullPath := filepath.Join(baseDir, path)
if err := os.MkdirAll(filepath.Dir(fullPath), 0o755); err != nil {
t.Fatalf("Failed to create directory %s: %v", filepath.Dir(fullPath), err)
2025-03-03 15:05:18 +00:00
}
if err := os.WriteFile(fullPath, []byte(content), 0o644); err != nil {
t.Fatalf("Failed to write file %s: %v", fullPath, err)
2025-03-03 15:05:18 +00:00
}
}
}
func runCmd(t *testing.T, dir, name string, args ...string) {
out := captureOutput(t, dir, name, args...)
if len(out) > 0 {
t.Errorf("Output from %s %s:\n%s", name, args, out)
}
}
func captureOutput(t *testing.T, dir, name string, args ...string) string {
cmd := exec.Command(name, args...)
cmd.Dir = dir
var out bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &out
err := cmd.Run()
if err != nil {
t.Logf("Command failed: %s %s", name, strings.Join(args, " "))
t.Logf("Output:\n%s", out.String())
t.Fatal(err)
}
return out.String()
2025-03-03 15:05:18 +00:00
}
2025-03-03 16:25:12 +00:00
func getWheel(t *testing.T, dir string) string {
pattern := filepath.Join(dir, "*.whl")
matches, err := filepath.Glob(pattern)
if err != nil {
t.Fatalf("Error matching pattern %s: %v", pattern, err)
}
if len(matches) == 0 {
t.Fatalf("No files found matching %s", pattern)
return ""
}
if len(matches) != 1 {
t.Fatalf("Too many matches %s: %v", pattern, matches)
return ""
}
return matches[0]
}
2025-03-03 15:05:18 +00:00
func TestPatchWheel(t *testing.T) {
pythonVersions := []string{"python3.9", "python3.10", "python3.11", "python3.12"}
for _, py := range pythonVersions {
t.Run(py, func(t *testing.T) {
tempDir := t.TempDir()
2025-03-03 15:05:18 +00:00
projFiles := minimalPythonProject()
writeProjectFiles(t, tempDir, projFiles)
2025-03-03 15:05:18 +00:00
runCmd(t, tempDir, "uv", "venv", "-q", "--python", py)
2025-03-03 15:05:18 +00:00
runCmd(t, tempDir, "uv", "build", "-q", "--wheel")
2025-03-03 15:05:18 +00:00
distDir := filepath.Join(tempDir, "dist")
2025-03-03 16:25:12 +00:00
origWheel := getWheel(t, distDir)
// t.Logf("Found origWheel: %s", origWheel)
2025-03-03 15:05:18 +00:00
// First patch
2025-03-03 16:25:12 +00:00
patchedWheel, err := PatchWheel(context.Background(), origWheel, distDir)
require.NoError(t, err)
// t.Logf("origWheel=%s patchedWheel=%s", origWheel, patchedWheel)
2025-03-03 19:53:43 +00:00
// Get file info of the patched wheel
patchedInfo, err := os.Stat(patchedWheel)
require.NoError(t, err)
patchedTime := patchedInfo.ModTime()
2025-03-03 19:53:43 +00:00
// Test idempotency - patching the same wheel again should produce the same result
// and should not recreate the file (file modification time should remain the same)
patchedWheel2, err := PatchWheel(context.Background(), origWheel, distDir)
require.NoError(t, err)
require.Equal(t, patchedWheel, patchedWheel2, "PatchWheel is not idempotent")
2025-03-03 19:53:43 +00:00
// Check that the file wasn't recreated
patchedInfo2, err := os.Stat(patchedWheel2)
require.NoError(t, err)
require.Equal(t, patchedTime, patchedInfo2.ModTime(), "File was recreated when it shouldn't have been")
runCmd(t, tempDir, "uv", "pip", "install", "-q", patchedWheel)
2025-03-03 15:05:18 +00:00
// Verify the installed version matches what we expect
verifyVersion(t, tempDir, patchedWheel)
// TODO: install one more patched wheel (add an option to PatchWheel to add extra to timestamp)
2025-03-03 15:05:18 +00:00
})
}
}