mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-21 05:45:37 +08:00
Replace offset-based and security.encode_id() pagination with generalized keyset abstraction supporting both single-ID and composite keysets. New abstraction (lib/galaxy/model/keyset_token_pagination.py): - KeysetToken Protocol: polymorphic via to_values()/from_values() - SingleKeysetToken: single ID keyset for runs pagination - KeysetPagination: base64+JSON encoder/decoder - TaskKeysetToken (wes.py): composite (step_order, job_index) keyset Changes to WES pagination: - list_runs: SingleKeysetToken with WHERE id < last_id (unchanged query) - get_run_tasks: TaskKeysetToken with tuple comparison WHERE (step_order, job_index) > (last_step, last_idx) - Removed offset-based _encode_page_token/_decode_page_token - Removed _encode_keyset_token/_decode_keyset_token Benefits: - Cursor-stable pagination under concurrent changes - Efficient composite keyset filtering on UNION queries - Consistent token encoding across all WES endpoints - Protocol-based design - no type checking needed Tests: 9 unit tests for keyset pagination abstraction (all passing) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
93 lines
3.1 KiB
Python
93 lines
3.1 KiB
Python
"""Tests for keyset token pagination."""
|
|
|
|
import pytest
|
|
|
|
from galaxy import exceptions
|
|
from galaxy.model.keyset_token_pagination import (
|
|
KeysetPagination,
|
|
SingleKeysetToken,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def pagination():
|
|
"""Provide KeysetPagination instance."""
|
|
return KeysetPagination()
|
|
|
|
|
|
class TestSingleKeysetToken:
|
|
"""Test SingleKeysetToken implementation."""
|
|
|
|
def test_to_values(self):
|
|
"""Test converting token to values."""
|
|
token = SingleKeysetToken(last_id=42)
|
|
assert token.to_values() == [42]
|
|
|
|
def test_from_values(self):
|
|
"""Test reconstructing token from values."""
|
|
token = SingleKeysetToken.from_values([42])
|
|
assert token.last_id == 42
|
|
|
|
def test_from_values_multiple(self):
|
|
"""Test from_values uses first value only."""
|
|
token = SingleKeysetToken.from_values([42, 100, 200])
|
|
assert token.last_id == 42
|
|
|
|
def test_from_values_empty_raises(self):
|
|
"""Test from_values raises on empty values."""
|
|
with pytest.raises(ValueError, match="requires at least 1 value"):
|
|
SingleKeysetToken.from_values([])
|
|
|
|
|
|
class TestKeysetPagination:
|
|
"""Test KeysetPagination encoder/decoder."""
|
|
|
|
def test_encode_decode_roundtrip(self, pagination):
|
|
"""Test encoding and decoding roundtrip."""
|
|
original = SingleKeysetToken(last_id=123)
|
|
encoded = pagination.encode_token(original)
|
|
decoded = pagination.decode_token(encoded, token_class=SingleKeysetToken)
|
|
assert decoded is not None
|
|
assert decoded.last_id == 123
|
|
|
|
def test_decode_none_token_returns_none(self, pagination):
|
|
"""Test decoding None returns None."""
|
|
result = pagination.decode_token(None, token_class=SingleKeysetToken)
|
|
assert result is None
|
|
|
|
def test_decode_empty_string_returns_none(self, pagination):
|
|
"""Test decoding empty string returns None."""
|
|
result = pagination.decode_token("", token_class=SingleKeysetToken)
|
|
assert result is None
|
|
|
|
def test_decode_invalid_token_raises(self, pagination):
|
|
"""Test decoding invalid token raises MessageException."""
|
|
with pytest.raises(exceptions.MessageException, match="Invalid page_token"):
|
|
pagination.decode_token("invalid_token_!@#", token_class=SingleKeysetToken)
|
|
|
|
def test_encode_multiple_values(self, pagination):
|
|
"""Test encoding handles multiple values."""
|
|
# Create custom token class with multiple values
|
|
from dataclasses import dataclass
|
|
|
|
@dataclass
|
|
class MultiValueToken:
|
|
val1: int
|
|
val2: int
|
|
val3: int
|
|
|
|
def to_values(self):
|
|
return [self.val1, self.val2, self.val3]
|
|
|
|
@classmethod
|
|
def from_values(cls, values):
|
|
return cls(val1=values[0], val2=values[1], val3=values[2])
|
|
|
|
original = MultiValueToken(val1=1, val2=2, val3=3)
|
|
encoded = pagination.encode_token(original)
|
|
decoded = pagination.decode_token(encoded, token_class=MultiValueToken)
|
|
assert decoded is not None
|
|
assert decoded.val1 == 1
|
|
assert decoded.val2 == 2
|
|
assert decoded.val3 == 3
|