mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-14 08:18:36 +00:00
feat(pascal): Add auto-update for Pascal Language Server
- Automatically download pasls from zen010101/pascal-language-server releases - Check for updates every 24 hours with version comparison - SHA256 checksum verification for security - Atomic installation with rollback on failure - Tarfile path traversal protection - Handle nested archive directory structure CI improvements: - Fix FPC installation for Ubuntu/macOS/Windows - Add proper FPC verification step that fails on actual errors Tests: - 33 unit tests for version comparison, checksum, tarfile safety - 8 integration tests with actual pasls functionality
This commit is contained in:
1 parent
b5c49c0726
commit
0deaa923ec
3 files changed
+874
-21
No files matched your search
@@ -0,0 +1,255 @@
|
||||
"""
|
||||
Unit tests for the Pascal language server auto-update functionality.
|
||||
|
||||
These tests validate the version comparison, checksum verification,
|
||||
and other helper methods without requiring network access or the
|
||||
actual Pascal language server.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import tarfile
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp.language_servers.pascal_server import PascalLanguageServer
|
||||
|
||||
pytestmark = [pytest.mark.pascal]
|
||||
|
||||
|
||||
class TestVersionNormalization:
|
||||
"""Test version string normalization."""
|
||||
|
||||
def test_normalize_version_with_v_prefix(self) -> None:
|
||||
"""Test that 'v' prefix is stripped."""
|
||||
assert PascalLanguageServer._normalize_version("v1.0.0") == "1.0.0"
|
||||
|
||||
def test_normalize_version_with_capital_v_prefix(self) -> None:
|
||||
"""Test that 'V' prefix is stripped."""
|
||||
assert PascalLanguageServer._normalize_version("V1.0.0") == "1.0.0"
|
||||
|
||||
def test_normalize_version_without_prefix(self) -> None:
|
||||
"""Test version without prefix is unchanged."""
|
||||
assert PascalLanguageServer._normalize_version("1.0.0") == "1.0.0"
|
||||
|
||||
def test_normalize_version_with_whitespace(self) -> None:
|
||||
"""Test that whitespace is stripped."""
|
||||
assert PascalLanguageServer._normalize_version(" v1.0.0 ") == "1.0.0"
|
||||
|
||||
def test_normalize_version_empty(self) -> None:
|
||||
"""Test empty version returns empty string."""
|
||||
assert PascalLanguageServer._normalize_version("") == ""
|
||||
|
||||
def test_normalize_version_none(self) -> None:
|
||||
"""Test None returns empty string."""
|
||||
assert PascalLanguageServer._normalize_version(None) == ""
|
||||
|
||||
|
||||
class TestVersionComparison:
|
||||
"""Test version comparison logic."""
|
||||
|
||||
def test_newer_version_major(self) -> None:
|
||||
"""Test detection of newer major version."""
|
||||
assert PascalLanguageServer._is_newer_version("v2.0.0", "v1.0.0") is True
|
||||
|
||||
def test_newer_version_minor(self) -> None:
|
||||
"""Test detection of newer minor version."""
|
||||
assert PascalLanguageServer._is_newer_version("v1.1.0", "v1.0.0") is True
|
||||
|
||||
def test_newer_version_patch(self) -> None:
|
||||
"""Test detection of newer patch version."""
|
||||
assert PascalLanguageServer._is_newer_version("v1.0.1", "v1.0.0") is True
|
||||
|
||||
def test_same_version(self) -> None:
|
||||
"""Test same version returns False."""
|
||||
assert PascalLanguageServer._is_newer_version("v1.0.0", "v1.0.0") is False
|
||||
|
||||
def test_older_version(self) -> None:
|
||||
"""Test older version returns False."""
|
||||
assert PascalLanguageServer._is_newer_version("v1.0.0", "v2.0.0") is False
|
||||
|
||||
def test_latest_none_returns_false(self) -> None:
|
||||
"""Test None latest version returns False."""
|
||||
assert PascalLanguageServer._is_newer_version(None, "v1.0.0") is False
|
||||
|
||||
def test_local_none_returns_true(self) -> None:
|
||||
"""Test None local version returns True (first install)."""
|
||||
assert PascalLanguageServer._is_newer_version("v1.0.0", None) is True
|
||||
|
||||
def test_both_none_returns_false(self) -> None:
|
||||
"""Test both None returns False."""
|
||||
assert PascalLanguageServer._is_newer_version(None, None) is False
|
||||
|
||||
def test_version_with_different_lengths(self) -> None:
|
||||
"""Test versions with different number of parts."""
|
||||
assert PascalLanguageServer._is_newer_version("v1.0.1", "v1.0") is True
|
||||
assert PascalLanguageServer._is_newer_version("v1.0", "v1.0.1") is False
|
||||
|
||||
def test_version_with_prerelease(self) -> None:
|
||||
"""Test versions with prerelease suffixes."""
|
||||
# Prerelease suffix is ignored, only numeric parts are compared
|
||||
assert PascalLanguageServer._is_newer_version("v1.1.0-beta", "v1.0.0") is True
|
||||
|
||||
|
||||
class TestSHA256Checksum:
|
||||
"""Test SHA256 checksum calculation and verification."""
|
||||
|
||||
def test_calculate_sha256(self) -> None:
|
||||
"""Test SHA256 calculation for a known content."""
|
||||
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
|
||||
f.write(b"test content")
|
||||
temp_path = f.name
|
||||
|
||||
try:
|
||||
result = PascalLanguageServer._calculate_sha256(temp_path)
|
||||
expected = hashlib.sha256(b"test content").hexdigest()
|
||||
assert result == expected
|
||||
finally:
|
||||
os.unlink(temp_path)
|
||||
|
||||
def test_verify_checksum_correct(self) -> None:
|
||||
"""Test checksum verification with correct checksum."""
|
||||
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
|
||||
f.write(b"test content")
|
||||
temp_path = f.name
|
||||
|
||||
try:
|
||||
expected = hashlib.sha256(b"test content").hexdigest()
|
||||
assert PascalLanguageServer._verify_checksum(temp_path, expected) is True
|
||||
finally:
|
||||
os.unlink(temp_path)
|
||||
|
||||
def test_verify_checksum_incorrect(self) -> None:
|
||||
"""Test checksum verification with incorrect checksum."""
|
||||
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
|
||||
f.write(b"test content")
|
||||
temp_path = f.name
|
||||
|
||||
try:
|
||||
wrong_checksum = "0" * 64
|
||||
assert PascalLanguageServer._verify_checksum(temp_path, wrong_checksum) is False
|
||||
finally:
|
||||
os.unlink(temp_path)
|
||||
|
||||
def test_verify_checksum_case_insensitive(self) -> None:
|
||||
"""Test checksum verification is case insensitive."""
|
||||
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
|
||||
f.write(b"test content")
|
||||
temp_path = f.name
|
||||
|
||||
try:
|
||||
expected = hashlib.sha256(b"test content").hexdigest().upper()
|
||||
assert PascalLanguageServer._verify_checksum(temp_path, expected) is True
|
||||
finally:
|
||||
os.unlink(temp_path)
|
||||
|
||||
|
||||
class TestTarfileSafety:
|
||||
"""Test tarfile path traversal protection."""
|
||||
|
||||
def test_safe_tar_member_normal_path(self) -> None:
|
||||
"""Test normal path is considered safe."""
|
||||
member = tarfile.TarInfo(name="pasls")
|
||||
assert PascalLanguageServer._is_safe_tar_member(member, "/tmp/target") is True
|
||||
|
||||
def test_safe_tar_member_nested_path(self) -> None:
|
||||
"""Test nested path is considered safe."""
|
||||
member = tarfile.TarInfo(name="subdir/pasls")
|
||||
assert PascalLanguageServer._is_safe_tar_member(member, "/tmp/target") is True
|
||||
|
||||
def test_unsafe_tar_member_path_traversal(self) -> None:
|
||||
"""Test path traversal is detected."""
|
||||
member = tarfile.TarInfo(name="../etc/passwd")
|
||||
assert PascalLanguageServer._is_safe_tar_member(member, "/tmp/target") is False
|
||||
|
||||
def test_unsafe_tar_member_hidden_traversal(self) -> None:
|
||||
"""Test hidden path traversal in nested path."""
|
||||
member = tarfile.TarInfo(name="subdir/../../etc/passwd")
|
||||
assert PascalLanguageServer._is_safe_tar_member(member, "/tmp/target") is False
|
||||
|
||||
def test_safe_tar_member_similar_name(self) -> None:
|
||||
"""Test path containing '..' in filename (not directory) is safe."""
|
||||
member = tarfile.TarInfo(name="file..name")
|
||||
assert PascalLanguageServer._is_safe_tar_member(member, "/tmp/target") is True
|
||||
|
||||
|
||||
class TestMetadataManagement:
|
||||
"""Test metadata directory and file management."""
|
||||
|
||||
def test_meta_dir_creates_directory(self) -> None:
|
||||
"""Test _meta_dir creates directory if not exists."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
meta_path = PascalLanguageServer._meta_dir(temp_dir)
|
||||
assert os.path.exists(meta_path)
|
||||
assert meta_path == os.path.join(temp_dir, PascalLanguageServer.META_DIR)
|
||||
|
||||
def test_meta_file_returns_correct_path(self) -> None:
|
||||
"""Test _meta_file returns correct path."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
meta_file = PascalLanguageServer._meta_file(temp_dir, "version")
|
||||
expected = os.path.join(temp_dir, PascalLanguageServer.META_DIR, "version")
|
||||
assert meta_file == expected
|
||||
|
||||
|
||||
class TestUpdateCheckTiming:
|
||||
"""Test update check timing logic."""
|
||||
|
||||
def test_should_check_update_no_last_check(self) -> None:
|
||||
"""Test should check when no last_check file exists."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
assert PascalLanguageServer._should_check_update(temp_dir) is True
|
||||
|
||||
def test_should_check_update_recent_check(self) -> None:
|
||||
"""Test should not check when recently checked."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Create meta dir and last_check file with current time
|
||||
meta_dir = PascalLanguageServer._meta_dir(temp_dir)
|
||||
last_check_file = os.path.join(meta_dir, "last_check")
|
||||
with open(last_check_file, "w") as f:
|
||||
f.write(str(time.time()))
|
||||
|
||||
assert PascalLanguageServer._should_check_update(temp_dir) is False
|
||||
|
||||
def test_should_check_update_old_check(self) -> None:
|
||||
"""Test should check when last check was > 24 hours ago."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Create meta dir and last_check file with old time
|
||||
meta_dir = PascalLanguageServer._meta_dir(temp_dir)
|
||||
last_check_file = os.path.join(meta_dir, "last_check")
|
||||
old_time = time.time() - (PascalLanguageServer.UPDATE_CHECK_INTERVAL + 3600)
|
||||
with open(last_check_file, "w") as f:
|
||||
f.write(str(old_time))
|
||||
|
||||
assert PascalLanguageServer._should_check_update(temp_dir) is True
|
||||
|
||||
def test_update_last_check_creates_file(self) -> None:
|
||||
"""Test _update_last_check creates timestamp file."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
PascalLanguageServer._update_last_check(temp_dir)
|
||||
last_check_file = PascalLanguageServer._meta_file(temp_dir, "last_check")
|
||||
assert os.path.exists(last_check_file)
|
||||
|
||||
with open(last_check_file) as f:
|
||||
timestamp = float(f.read().strip())
|
||||
assert abs(timestamp - time.time()) < 5 # within 5 seconds
|
||||
|
||||
|
||||
class TestVersionPersistence:
|
||||
"""Test local version persistence."""
|
||||
|
||||
def test_save_and_get_local_version(self) -> None:
|
||||
"""Test saving and retrieving local version."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
PascalLanguageServer._save_local_version(temp_dir, "v1.0.0")
|
||||
version = PascalLanguageServer._get_local_version(temp_dir)
|
||||
assert version == "v1.0.0"
|
||||
|
||||
def test_get_local_version_not_exists(self) -> None:
|
||||
"""Test getting version when file doesn't exist."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
version = PascalLanguageServer._get_local_version(temp_dir)
|
||||
assert version is None
|
||||
Reference in new issue
Block a user