The fork takes its own name everywhere a user can see it: binaries (voculinux, voculinux-gui), desktop entry, autostart, tray, D-Bus namespace (com.voculinux.Voculinux, no alias for the old name), config/data/cache dirs, single-instance lock, IBus engine identity, tray icons, resource prefixes, VOCULINUX_* environment names, and the update-checker user agent. The internal Python module stays vocalinux and the distribution metadata keeps the upstream URLs as provenance. On first run a previous Vocalinux install's config.json, custom dictionary, and portal restore token are copied once into the new locations (new config always wins, no merge, no write-back); models re-resolve through the pinned-checksum download path. An offline naming-sweep test proves no old-name strings remain on user-visible surfaces outside the allowlist. Refs #18
277 lines
12 KiB
Python
277 lines
12 KiB
Python
"""
|
|
Test cross-distro compatibility features.
|
|
|
|
This module tests the cross-distribution compatibility features including:
|
|
- Dynamic GI_TYPELIB_PATH detection
|
|
- Wrapper script generation with correct paths
|
|
- Desktop entry configuration
|
|
"""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
def get_repo_root() -> Path:
|
|
"""Get the repository root directory."""
|
|
# Start from current file and traverse up to find repo root
|
|
current = Path(__file__).resolve()
|
|
for parent in [current] + list(current.parents):
|
|
# Check for common repo indicators
|
|
if (parent / ".git").exists() or (parent / "install.sh").exists():
|
|
return parent
|
|
# Also check if we're in a checkout-named subdirectory (CI structure)
|
|
if parent.name in {"vocalinux", "voculinux"}:
|
|
for sibling in ("voculinux", "vocalinux"):
|
|
if (parent.parent / sibling / "install.sh").exists():
|
|
return parent.parent / sibling
|
|
# Fallback: use parent of tests directory
|
|
return current.parent.parent.parent
|
|
|
|
|
|
REPO_ROOT = get_repo_root()
|
|
|
|
|
|
class TestCrossDistroCompatibility:
|
|
"""Test cross-distro compatibility features."""
|
|
|
|
@pytest.fixture
|
|
def install_sh_content(self):
|
|
"""Fixture to load the installer entry point and sourced modules."""
|
|
install_sh_path = REPO_ROOT / "install.sh"
|
|
if not install_sh_path.exists():
|
|
pytest.skip(f"install.sh not found at {install_sh_path}")
|
|
parts = [install_sh_path, *sorted((REPO_ROOT / "install.d").glob("*.sh"))]
|
|
return "\n".join(path.read_text(encoding="utf-8") for path in parts)
|
|
|
|
@pytest.fixture
|
|
def workflow_content(self):
|
|
"""Fixture to load workflow content."""
|
|
workflow_path = REPO_ROOT / ".github" / "workflows" / "unified-pipeline.yml"
|
|
if not workflow_path.exists():
|
|
pytest.skip(f"Workflow file not found at {workflow_path}")
|
|
return workflow_path.read_text()
|
|
|
|
def test_detect_typelib_path_function_exists(self, install_sh_content):
|
|
"""Test that detect_typelib_path function exists in install.sh."""
|
|
assert (
|
|
"detect_typelib_path()" in install_sh_content
|
|
), "detect_typelib_path function should exist"
|
|
assert (
|
|
"pkg-config --variable=typelibdir" in install_sh_content
|
|
), "Should use pkg-config for detection"
|
|
|
|
def test_no_hardcoded_gi_typelib_path_in_verification(self, install_sh_content):
|
|
"""Test that package verification uses dynamic GI_TYPELIB_PATH."""
|
|
# The verification function should use the detected path, not hardcoded
|
|
assert (
|
|
"verify_package_installed()" in install_sh_content
|
|
), "verify_package_installed function should exist"
|
|
|
|
# Check that it uses GI_TYPELIB_DETECTED variable
|
|
verify_section = install_sh_content[
|
|
install_sh_content.find("verify_package_installed()") : install_sh_content.find(
|
|
"verify_package_installed()"
|
|
)
|
|
+ 500
|
|
]
|
|
assert (
|
|
"GI_TYPELIB_DETECTED" in verify_section or '"$GI_TYPELIB_DETECTED"' in verify_section
|
|
), "verify_package_installed should use GI_TYPELIB_DETECTED variable"
|
|
|
|
def test_wrapper_script_uses_detected_path(self, install_sh_content):
|
|
"""Test that wrapper scripts use the detected GI_TYPELIB_PATH."""
|
|
# Check that wrapper script creation uses GI_TYPELIB_DETECTED
|
|
assert (
|
|
"export GI_TYPELIB_PATH=$GI_TYPELIB_DETECTED" in install_sh_content
|
|
), "Wrapper script should export GI_TYPELIB_PATH with detected value"
|
|
|
|
def test_desktop_entry_uses_detected_path(self, install_sh_content):
|
|
"""Test that desktop entry uses detected GI_TYPELIB_PATH."""
|
|
# The sed command for desktop entry should use the detected path
|
|
assert (
|
|
"GI_TYPELIB_PATH=$GI_TYPELIB_DETECTED" in install_sh_content
|
|
or r"GI_TYPELIB_PATH=${GI_TYPELIB_DETECTED}" in install_sh_content
|
|
), "Desktop entry sed command should use GI_TYPELIB_DETECTED"
|
|
|
|
# Should NOT have hardcoded /usr/lib/girepository-1.0 in desktop entry section
|
|
desktop_start = install_sh_content.find("install_desktop_entry()")
|
|
if desktop_start == -1:
|
|
pytest.skip("install_desktop_entry function not found")
|
|
desktop_section = install_sh_content[desktop_start : desktop_start + 1000]
|
|
if "sed -i" in desktop_section and "voculinux.desktop" in desktop_section:
|
|
assert (
|
|
"/usr/lib/girepository-1.0"
|
|
not in desktop_section.split("sed -i")[1].split("voculinux.desktop")[0]
|
|
), "Desktop entry should not hardcode /usr/lib/girepository-1.0 path"
|
|
|
|
def test_detect_typelib_path_with_pkgconfig(self, install_sh_content):
|
|
"""Test that detect_typelib_path uses pkg-config when available."""
|
|
# Extract the detect_typelib_path function
|
|
func_start = install_sh_content.find("detect_typelib_path() {")
|
|
if func_start == -1:
|
|
pytest.skip("detect_typelib_path function not found")
|
|
func_end = install_sh_content.find("}", func_start)
|
|
func_content = install_sh_content[func_start:func_end]
|
|
|
|
# Should check for pkg-config first
|
|
assert "pkg-config" in func_content, "Should check for pkg-config"
|
|
assert "gobject-introspection-1.0" in func_content, "Should query gobject-introspection-1.0"
|
|
|
|
def test_detect_typelib_path_fallback_paths(self, install_sh_content):
|
|
"""Test that detect_typelib_path has fallback paths for various distros."""
|
|
# Should have fallback paths for different distros
|
|
expected_paths = [
|
|
"/usr/lib/x86_64-linux-gnu/girepository-1.0", # Ubuntu/Debian x86_64
|
|
"/usr/lib/aarch64-linux-gnu/girepository-1.0", # ARM64
|
|
"/usr/lib64/girepository-1.0", # Fedora/RHEL
|
|
"/usr/lib/girepository-1.0", # Arch and fallback
|
|
]
|
|
|
|
for path in expected_paths:
|
|
assert path in install_sh_content, f"Should have fallback path: {path}"
|
|
|
|
def test_opensuse_dependency_mapping_uses_tumbleweed_package_names(self, install_sh_content):
|
|
"""Test that openSUSE uses Tumbleweed-compatible dependency resolution."""
|
|
assert "suse_python_package_prefix()" in install_sh_content
|
|
assert "suse_python_package_candidates()" in install_sh_content
|
|
assert "typelib-1_0-AyatanaAppIndicator3-0_1" in install_sh_content
|
|
assert "typelib-1_0-Notify-0_7" in install_sh_content
|
|
assert "libnotify4" in install_sh_content
|
|
assert "suse_install_appindicator_runtime" in install_sh_content
|
|
zypper_line = next(
|
|
line
|
|
for line in install_sh_content.splitlines()
|
|
if "SYSTEM_PACKAGES=(gtk3 ibus " in line
|
|
)
|
|
zypper_packages = zypper_line.removeprefix("SYSTEM_PACKAGES=(").rstrip(")").split()
|
|
# ibus, not the typelib alone: ibus_engine.py spawns `ibus-daemon -x -d -r`
|
|
# and shells out to `ibus engine`, neither of which libibus provides.
|
|
assert "ibus" in zypper_packages
|
|
assert "typelib-1_0-IBus-1_0" in zypper_packages
|
|
assert "ibus-devel" not in zypper_packages
|
|
assert "python3-devel" not in zypper_line
|
|
assert "python3-virtualenv" not in zypper_line
|
|
assert "libappindicator-gtk3" not in zypper_line
|
|
assert "glslang" not in zypper_line
|
|
|
|
def test_rpm_arch_install_glslc_not_glslang(self, install_sh_content):
|
|
"""ggml Vulkan needs glslc (shaderc). glslangValidator is not a substitute (#604)."""
|
|
dnf_line = next(
|
|
line
|
|
for line in install_sh_content.splitlines()
|
|
if "SYSTEM_PACKAGES=(python3-pip python3-gobject gtk3 " in line
|
|
)
|
|
pacman_line = next(
|
|
line
|
|
for line in install_sh_content.splitlines()
|
|
if "SYSTEM_PACKAGES=(python-pip python-gobject gtk3 " in line
|
|
)
|
|
assert "glslc" in dnf_line
|
|
assert "patchelf" in dnf_line
|
|
assert "glslang" not in dnf_line
|
|
assert "shaderc" in pacman_line
|
|
assert "patchelf" in pacman_line
|
|
assert "glslang" not in pacman_line
|
|
assert (
|
|
"Fedora: sudo dnf install vulkan-loader-devel vulkan-tools glslc patchelf"
|
|
in install_sh_content
|
|
)
|
|
assert (
|
|
"Arch: sudo pacman -S vulkan-headers vulkan-tools shaderc patchelf"
|
|
in install_sh_content
|
|
)
|
|
emerge_line = next(
|
|
line
|
|
for line in install_sh_content.splitlines()
|
|
if "SYSTEM_PACKAGES=(dev-python/pygobject:3 " in line
|
|
)
|
|
assert "media-libs/shaderc" in emerge_line
|
|
assert "dev-util/patchelf" in emerge_line
|
|
assert "sys-devel/patchelf" not in emerge_line
|
|
|
|
def test_skip_system_deps_option_is_implemented(self, install_sh_content):
|
|
"""Test that the documented --skip-system-deps option is parsed and honored."""
|
|
assert "SKIP_SYSTEM_DEPS" in install_sh_content
|
|
assert "--skip-system-deps" in install_sh_content
|
|
assert "Skipping system dependency installation" in install_sh_content
|
|
|
|
def test_noninteractive_tty_fallback_does_not_abort(self, install_sh_content):
|
|
"""Test that missing controlling TTY falls back to non-interactive mode."""
|
|
assert "{ true < /dev/tty; } 2>/dev/null" in install_sh_content
|
|
# stdin must stay on the pipe while bash reads the script from it;
|
|
# prompts reach the terminal through read_prompt instead (#933).
|
|
code = "\n".join(
|
|
line for line in install_sh_content.splitlines() if not line.lstrip().startswith("#")
|
|
)
|
|
assert "exec < /dev/tty" not in code
|
|
assert "exec 0<" not in code
|
|
assert "read_prompt" in install_sh_content
|
|
assert 'NON_INTERACTIVE="yes"' in install_sh_content
|
|
|
|
def test_ci_workflow_uses_dynamic_detection(self, workflow_content):
|
|
"""Test that CI workflow uses dynamic GI_TYPELIB_PATH detection."""
|
|
# Should use pkg-config or dynamic detection
|
|
assert (
|
|
"pkg-config --variable=typelibdir" in workflow_content
|
|
), "CI workflow should use pkg-config for GI_TYPELIB_PATH"
|
|
|
|
|
|
class TestResourceManagerCrossDistro:
|
|
"""Test ResourceManager cross-distro path handling."""
|
|
|
|
def test_resource_manager_has_xdg_support(self):
|
|
"""Test that ResourceManager supports XDG directories."""
|
|
from vocalinux.utils.resource_manager import ResourceManager
|
|
|
|
rm = ResourceManager()
|
|
resources_dir = rm.resources_dir
|
|
|
|
# Should be a valid path string
|
|
assert isinstance(resources_dir, str), "resources_dir should be a string"
|
|
assert len(resources_dir) > 0, "resources_dir should not be empty"
|
|
|
|
|
|
class TestRecognitionManagerCrossDistro:
|
|
"""Test RecognitionManager cross-distro model path handling."""
|
|
|
|
def test_system_model_paths_exists(self):
|
|
"""Test that system model paths configuration exists."""
|
|
from vocalinux.speech_recognition.recognition_manager import SYSTEM_MODELS_DIRS
|
|
|
|
# Should be a list of paths
|
|
assert isinstance(SYSTEM_MODELS_DIRS, list), "SYSTEM_MODELS_DIRS should be a list"
|
|
assert len(SYSTEM_MODELS_DIRS) > 0, "SYSTEM_MODELS_DIRS should not be empty"
|
|
|
|
# Should include XDG paths
|
|
has_xdg = any("/share/" in path for path in SYSTEM_MODELS_DIRS)
|
|
assert has_xdg, "SYSTEM_MODELS_DIRS should include XDG paths"
|
|
|
|
|
|
class TestDocumentation:
|
|
"""Test that documentation is accurate."""
|
|
|
|
def test_distro_compatibility_documentation_exists(self):
|
|
"""Test that DISTRO_COMPATIBILITY.md exists and is accurate."""
|
|
docs_path = REPO_ROOT / "docs" / "DISTRO_COMPATIBILITY.md"
|
|
|
|
if not docs_path.exists():
|
|
pytest.skip(f"DISTRO_COMPATIBILITY.md not found at {docs_path}")
|
|
|
|
content = docs_path.read_text()
|
|
|
|
# Should mention the dynamic path detection
|
|
assert (
|
|
"pkg-config" in content or "detect_typelib_path" in content
|
|
), "Should document dynamic path detection"
|
|
|
|
# Should mention supported distros
|
|
expected_distros = ["Ubuntu", "Debian", "Fedora", "Arch", "openSUSE"]
|
|
for distro in expected_distros:
|
|
assert distro in content, f"Should mention {distro} support"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|