#!/usr/bin/env python
# Copyright Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT
"""Unit tests for generate_local_index.py.
Tests verify that index generation creates correct HTML structure with proper
relative paths for local and parent files, and that multi-arch mode correctly
processes directory structures.
"""
import os
import sys
import tempfile
import unittest
from contextlib import redirect_stderr
from io import StringIO
from pathlib import Path
# Add build_tools/packaging/python to path so generate_local_index is importable.
sys.path.insert(0, os.fspath(Path(__file__).parent.parent / "python"))
from generate_local_index import (
generate_multiarch_indexes,
generate_simple_index,
main,
)
class TestGenerateSimpleIndex(unittest.TestCase):
"""Tests for generate_simple_index()."""
def test_local_files_only(self):
"""Test index with only local files."""
with tempfile.TemporaryDirectory() as tmp:
output = Path(tmp) / "index.html"
local_files = [
Path(tmp) / "package1-1.0.whl",
Path(tmp) / "package2-2.0.tar.gz",
]
generate_simple_index(output, local_files)
content = output.read_text()
self.assertIn("", content)
self.assertIn("
Package Index", content)
self.assertIn('package1-1.0.whl', content)
self.assertIn(
'package2-2.0.tar.gz', content
)
def test_parent_files_only(self):
"""Test index with only parent files."""
with tempfile.TemporaryDirectory() as tmp:
output = Path(tmp) / "index.html"
parent_files = [
Path(tmp) / "parent1-1.0.whl",
Path(tmp) / "parent2-2.0.tar.gz",
]
generate_simple_index(output, [], parent_files=parent_files)
content = output.read_text()
self.assertIn('parent1-1.0.whl', content)
self.assertIn(
'parent2-2.0.tar.gz', content
)
def test_mixed_local_and_parent_files(self):
"""Test index with both local and parent files."""
with tempfile.TemporaryDirectory() as tmp:
output = Path(tmp) / "index.html"
local_files = [Path(tmp) / "local-1.0.whl"]
parent_files = [Path(tmp) / "parent-1.0.whl"]
generate_simple_index(output, local_files, parent_files=parent_files)
content = output.read_text()
self.assertIn('local-1.0.whl', content)
self.assertIn('parent-1.0.whl', content)
def test_custom_title(self):
"""Test index with custom title."""
with tempfile.TemporaryDirectory() as tmp:
output = Path(tmp) / "index.html"
generate_simple_index(output, [], title="Custom Title")
content = output.read_text()
self.assertIn("Custom Title", content)
self.assertIn("Custom Title
", content)
def test_empty_lists(self):
"""Test index with no files."""
with tempfile.TemporaryDirectory() as tmp:
output = Path(tmp) / "index.html"
generate_simple_index(output, [])
content = output.read_text()
self.assertIn("", content)
self.assertIn("Package Index", content)
# Should have no tags
self.assertNotIn("", content)
self.assertIn("", content)
self.assertIn("", content)
self.assertIn('', content)
self.assertIn("", content)
self.assertIn("", content)
self.assertIn("", content)
self.assertIn("", content)
# Check it ends with newline
self.assertTrue(content.endswith("\n"))
def test_plus_in_filename_is_url_encoded(self):
"""Test that '+' in filenames is percent-encoded in hrefs.
PEP 440 local versions produce filenames like
'pkg-1.0+localver-py3-none-any.whl'. S3 interprets a literal '+'
in URL paths as a space, so hrefs must encode '+' as '%2B'.
"""
with tempfile.TemporaryDirectory() as tmp:
output = Path(tmp) / "index.html"
local_files = [
Path(tmp) / "rocm_sdk_core-7.0.0+abc123-py3-none-linux_x86_64.whl",
]
parent_files = [
Path(tmp) / "rocm_sdk_libs-7.0.0+abc123-py3-none-linux_x86_64.whl",
]
generate_simple_index(output, local_files, parent_files=parent_files)
content = output.read_text()
# href should have %2B, not literal +
self.assertIn(
'href="./rocm_sdk_core-7.0.0%2Babc123-py3-none-linux_x86_64.whl"',
content,
)
self.assertIn(
'href="../rocm_sdk_libs-7.0.0%2Babc123-py3-none-linux_x86_64.whl"',
content,
)
# Display text should still have literal +
self.assertIn(
">rocm_sdk_core-7.0.0+abc123-py3-none-linux_x86_64.whl",
content,
)
class TestGenerateMultiarchIndexes(unittest.TestCase):
"""Tests for generate_multiarch_indexes()."""
def test_generates_per_family_indexes(self):
"""Test that indexes are generated for each family subdirectory."""
with tempfile.TemporaryDirectory() as tmp:
dist_dir = Path(tmp)
# Create generic packages at top level
(dist_dir / "rocm_sdk_core-1.0.whl").write_bytes(b"core")
# Create family subdirectories with packages
(dist_dir / "gfx94X-dcgpu").mkdir()
(dist_dir / "gfx94X-dcgpu" / "rocm_libs_gfx94x-1.0.whl").write_bytes(
b"gfx94x"
)
(dist_dir / "gfx120X-all").mkdir()
(dist_dir / "gfx120X-all" / "rocm_libs_gfx120x-1.0.whl").write_bytes(
b"gfx120x"
)
generate_multiarch_indexes(dist_dir)
# Verify indexes were created
self.assertTrue((dist_dir / "gfx94X-dcgpu" / "index.html").is_file())
self.assertTrue((dist_dir / "gfx120X-all" / "index.html").is_file())
def test_family_index_includes_local_and_parent(self):
"""Test that family indexes include both local and parent files."""
with tempfile.TemporaryDirectory() as tmp:
dist_dir = Path(tmp)
# Create generic packages
(dist_dir / "rocm_sdk_core-1.0.whl").write_bytes(b"core")
(dist_dir / "rocm_sdk_devel-1.0.whl").write_bytes(b"devel")
# Create family directory
(dist_dir / "gfx94X-dcgpu").mkdir()
(dist_dir / "gfx94X-dcgpu" / "rocm_libs_gfx94x-1.0.whl").write_bytes(
b"gfx94x"
)
generate_multiarch_indexes(dist_dir)
content = (dist_dir / "gfx94X-dcgpu" / "index.html").read_text()
# Should have local file with ./ prefix
self.assertIn(
'rocm_libs_gfx94x-1.0.whl',
content,
)
# Should have parent files with ../ prefix
self.assertIn(
'rocm_sdk_core-1.0.whl', content
)
self.assertIn(
'rocm_sdk_devel-1.0.whl',
content,
)
def test_custom_title_per_family(self):
"""Test that each family gets a custom title."""
with tempfile.TemporaryDirectory() as tmp:
dist_dir = Path(tmp)
(dist_dir / "gfx94X-dcgpu").mkdir()
(dist_dir / "gfx120X-all").mkdir()
generate_multiarch_indexes(dist_dir)
content_94x = (dist_dir / "gfx94X-dcgpu" / "index.html").read_text()
content_120x = (dist_dir / "gfx120X-all" / "index.html").read_text()
self.assertIn("ROCm Python Packages - gfx94X-dcgpu", content_94x)
self.assertIn("ROCm Python Packages - gfx120X-all", content_120x)
def test_no_subdirectories_raises(self):
"""Test that missing family subdirectories raises an error."""
with tempfile.TemporaryDirectory() as tmp:
dist_dir = Path(tmp)
(dist_dir / "rocm_sdk_core-1.0.whl").write_bytes(b"core")
with self.assertRaises(FileNotFoundError):
generate_multiarch_indexes(dist_dir)
def test_empty_family_directory(self):
"""Test family directory with no packages."""
with tempfile.TemporaryDirectory() as tmp:
dist_dir = Path(tmp)
(dist_dir / "rocm_sdk_core-1.0.whl").write_bytes(b"core")
(dist_dir / "gfx94X-dcgpu").mkdir() # Empty directory
generate_multiarch_indexes(dist_dir)
# Index should still be created, just with no local files
content = (dist_dir / "gfx94X-dcgpu" / "index.html").read_text()
# Should have parent file
self.assertIn(
'rocm_sdk_core-1.0.whl', content
)
# Should not have any ./ links (no local files)
self.assertNotIn('rocm_sdk_core-1.0.whl',
content,
)
self.assertIn(
'rocm_sdk_libraries-1.0.whl',
content,
)
self.assertIn(
'rocm_sdk_device_gfx942-1.0.whl',
content,
)
def test_default_honors_output(self):
with tempfile.TemporaryDirectory() as tmp:
dist_dir = Path(tmp)
output = dist_dir / "custom.html"
(dist_dir / "rocm_sdk_core-1.0.whl").write_bytes(b"core")
main([os.fspath(dist_dir), "--output", os.fspath(output)])
self.assertTrue(output.is_file())
self.assertFalse((dist_dir / "index.html").exists())
def test_default_honors_patterns(self):
with tempfile.TemporaryDirectory() as tmp:
dist_dir = Path(tmp)
(dist_dir / "rocm_sdk_core-1.0.whl").write_bytes(b"core")
(dist_dir / "rocm-1.0.tar.gz").write_bytes(b"sdist")
main([os.fspath(dist_dir), "--patterns", "*.whl"])
content = (dist_dir / "index.html").read_text()
self.assertIn("rocm_sdk_core-1.0.whl", content)
self.assertNotIn("rocm-1.0.tar.gz", content)
def test_default_url_encodes_plus(self):
with tempfile.TemporaryDirectory() as tmp:
dist_dir = Path(tmp)
filename = "rocm_sdk_core-7.0.0+abc123-py3-none-any.whl"
(dist_dir / filename).write_bytes(b"core")
main([os.fspath(dist_dir)])
content = (dist_dir / "index.html").read_text()
self.assertIn(
'href="./rocm_sdk_core-7.0.0%2Babc123-py3-none-any.whl"',
content,
)
self.assertIn(f">{filename}", content)
def test_per_family_indexes_generates_subdirectory_indexes(self):
with tempfile.TemporaryDirectory() as tmp:
dist_dir = Path(tmp)
(dist_dir / "rocm_sdk_core-1.0.whl").write_bytes(b"core")
family_dir = dist_dir / "gfx94X-dcgpu"
family_dir.mkdir()
(family_dir / "rocm_sdk_libraries_gfx94x_dcgpu-1.0.whl").write_bytes(
b"libs"
)
main([os.fspath(dist_dir), "--per-family-indexes"])
self.assertTrue((family_dir / "index.html").is_file())
self.assertFalse((dist_dir / "index.html").exists())
def test_per_family_indexes_rejects_output(self):
with tempfile.TemporaryDirectory() as tmp:
dist_dir = Path(tmp)
(dist_dir / "gfx94X-dcgpu").mkdir()
with redirect_stderr(StringIO()), self.assertRaises(SystemExit):
main(
[
os.fspath(dist_dir),
"--per-family-indexes",
"--output",
os.fspath(dist_dir / "index.html"),
]
)
def test_per_family_indexes_rejects_parent_dir(self):
with tempfile.TemporaryDirectory() as tmp:
dist_dir = Path(tmp)
parent_dir = dist_dir / "parent"
(dist_dir / "gfx94X-dcgpu").mkdir()
with redirect_stderr(StringIO()), self.assertRaises(SystemExit):
main(
[
os.fspath(dist_dir),
"--per-family-indexes",
"--parent-dir",
os.fspath(parent_dir),
]
)
if __name__ == "__main__":
unittest.main()