Signed-off-by: Change72 <changg@nvidia.com> Co-authored-by: Codex <codex@openai.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
620 lines
21 KiB
Python
620 lines
21 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
"""
|
|
Mock-based unit tests for ObjectStoreSecondaryTierManager.
|
|
|
|
These tests replace the NIXL backend with an in-memory mock so they run
|
|
without S3 credentials or a live object store. They verify the manager's
|
|
state machine: job submission, transfer completion polling, and lookup.
|
|
"""
|
|
|
|
import time
|
|
import uuid
|
|
from collections.abc import Callable
|
|
from types import SimpleNamespace
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import numpy as np
|
|
import pytest
|
|
import torch
|
|
|
|
from vllm.v1.kv_offload.base import (
|
|
Locality,
|
|
LookupResult,
|
|
OffloadingKVEventsConfig,
|
|
OffloadKey,
|
|
ReqContext,
|
|
ScheduleEndContext,
|
|
make_offload_key,
|
|
)
|
|
from vllm.v1.kv_offload.config import (
|
|
OffloadingCacheConfig,
|
|
OffloadingConfig,
|
|
OffloadingModelConfig,
|
|
OffloadingParallelConfig,
|
|
)
|
|
from vllm.v1.kv_offload.tiering.base import JobMetadata, JobResult
|
|
from vllm.v1.kv_offload.tiering.obj.config import ObjStoreConfig
|
|
from vllm.v1.kv_offload.tiering.obj.manager import ObjectStoreSecondaryTierManager
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Shared stubs
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _make_offloading_config(enable_kv_cache_events: bool) -> OffloadingConfig:
|
|
return OffloadingConfig(
|
|
groups=(),
|
|
worker_kv_bytes_per_block=0,
|
|
enable_kv_cache_events=enable_kv_cache_events,
|
|
extra_config={},
|
|
engine_id="test-engine",
|
|
model=OffloadingModelConfig(name="test/model", dtype="float16"),
|
|
cache=OffloadingCacheConfig(tokens_per_hash=16, blocks_per_chunk=1),
|
|
parallel=OffloadingParallelConfig(
|
|
rank=0,
|
|
world_size=1,
|
|
tp_size=1,
|
|
pp_size=1,
|
|
pcp_size=1,
|
|
dcp_size=1,
|
|
data_parallel_index=0,
|
|
is_parallelism_agnostic=False,
|
|
),
|
|
)
|
|
|
|
|
|
_OFFLOADING_SPEC = SimpleNamespace(
|
|
config=_make_offloading_config(enable_kv_cache_events=False),
|
|
)
|
|
|
|
_STORE_CONFIG = {
|
|
"bucket": "mock-bucket",
|
|
"endpoint_override": "mock:9000",
|
|
"access_key": "mock-access",
|
|
"secret_key": "mock-secret",
|
|
}
|
|
|
|
_BLOCK_ELEMENTS = 256
|
|
_DTYPE = torch.float32
|
|
_RUN_PREFIX = f"test/{uuid.uuid4().hex[:8]}"
|
|
_CTX = ReqContext(req_id="test-req")
|
|
|
|
|
|
def key(n: int) -> OffloadKey:
|
|
return make_offload_key(n.to_bytes(8, "big"), 0)
|
|
|
|
|
|
def make_job(
|
|
job_id: int,
|
|
keys: list[OffloadKey],
|
|
block_ids: list[int] | None = None,
|
|
) -> JobMetadata:
|
|
if block_ids is None:
|
|
block_ids = list(range(len(keys)))
|
|
return JobMetadata(
|
|
job_id=job_id,
|
|
keys=keys,
|
|
block_ids=np.array(block_ids, dtype=np.int64),
|
|
is_promotion=False,
|
|
req_context=_CTX,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Mock NIXL agent
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class MockNixlAgent:
|
|
"""In-memory NIXL agent. Tracks stored object keys and simulates async
|
|
transfers: transfer() returns PROC, check_xfer_state() returns DONE and
|
|
commits the write to the in-memory key set.
|
|
|
|
The four methods overridden by tests (register_memory, make_prepped_xfer,
|
|
check_xfer_state, query_memory) are stored as Callable instance attributes
|
|
so mypy allows reassignment in tests.
|
|
"""
|
|
|
|
# Callable attributes — tests may reassign these on instances.
|
|
register_memory: Callable
|
|
make_prepped_xfer: Callable
|
|
check_xfer_state: Callable
|
|
query_memory: Callable
|
|
|
|
def __init__(self):
|
|
self._stored_obj_keys: set[str] = set()
|
|
# handle_id -> (op, [obj_keys])
|
|
self._pending: dict[int, tuple[str, list[str]]] = {}
|
|
self._handle_counter = 0
|
|
self._last_obj_keys: list[str] = []
|
|
# Bind default implementations as instance attributes.
|
|
self.register_memory = self._register_memory
|
|
self.make_prepped_xfer = self._make_prepped_xfer
|
|
self.check_xfer_state = self._check_xfer_state
|
|
self.query_memory = self._query_memory
|
|
|
|
def create_backend(self, backend_type, params):
|
|
pass
|
|
|
|
def _register_memory(self, descs, mem_type=None, backends=None):
|
|
mock = MagicMock()
|
|
mock.trim.return_value = MagicMock()
|
|
# Capture obj_keys from OBJ 4-tuples: (addr, len, dev_id, obj_key)
|
|
if mem_type == "OBJ" and descs:
|
|
self._last_obj_keys = [d[3] for d in descs if d[3]]
|
|
return mock
|
|
|
|
def deregister_memory(self, desc):
|
|
pass
|
|
|
|
def prep_xfer_dlist(self, agent_name, descs, mem_type=None, backends=None):
|
|
return MagicMock()
|
|
|
|
def _make_prepped_xfer(
|
|
self,
|
|
op,
|
|
local_handle,
|
|
local_indices,
|
|
remote_handle,
|
|
remote_indices,
|
|
notif_msg=b"",
|
|
backends=None,
|
|
skip_desc_merge=False,
|
|
):
|
|
handle = MagicMock()
|
|
handle._id = self._handle_counter
|
|
self._pending[self._handle_counter] = (op, list(self._last_obj_keys))
|
|
self._handle_counter += 1
|
|
return handle
|
|
|
|
def transfer(self, handle):
|
|
return "PROC"
|
|
|
|
def _check_xfer_state(self, handle):
|
|
entry = self._pending.pop(handle._id, None)
|
|
if entry:
|
|
op, obj_keys = entry
|
|
if op == "WRITE":
|
|
self._stored_obj_keys.update(obj_keys)
|
|
return "DONE"
|
|
|
|
def release_xfer_handle(self, handle):
|
|
pass
|
|
|
|
def release_dlist_handle(self, handle):
|
|
pass
|
|
|
|
def _query_memory(self, queries, mem_type, agent_name):
|
|
return [object() if q[3] in self._stored_obj_keys else None for q in queries]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixture
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _make_events_spec(enable_kv_cache_events: bool) -> SimpleNamespace:
|
|
"""Offloading spec stub with an explicit global KV events flag."""
|
|
return SimpleNamespace(
|
|
config=_make_offloading_config(enable_kv_cache_events),
|
|
kv_events_config=OffloadingKVEventsConfig(
|
|
enable_kv_cache_events=enable_kv_cache_events,
|
|
self_describing_kv_events=False,
|
|
),
|
|
)
|
|
|
|
|
|
def _make_tier(
|
|
num_blocks: int = 4,
|
|
offloading_spec: SimpleNamespace = _OFFLOADING_SPEC,
|
|
**tier_kwargs,
|
|
) -> tuple[ObjectStoreSecondaryTierManager, MockNixlAgent]:
|
|
"""Create a tier backed by a fresh MockNixlAgent."""
|
|
mock_agent = MockNixlAgent()
|
|
tensor = torch.zeros((num_blocks, _BLOCK_ELEMENTS), dtype=_DTYPE)
|
|
view = memoryview(tensor.numpy())
|
|
with (
|
|
patch("vllm.v1.kv_offload.tiering.obj.manager.nixl_agent_config"),
|
|
patch(
|
|
"vllm.v1.kv_offload.tiering.obj.manager.nixl_agent",
|
|
return_value=mock_agent,
|
|
),
|
|
):
|
|
tier = ObjectStoreSecondaryTierManager(
|
|
offloading_spec=offloading_spec,
|
|
primary_kv_view=view,
|
|
tier_type="obj",
|
|
store_config=_STORE_CONFIG,
|
|
prefix=_RUN_PREFIX,
|
|
**tier_kwargs,
|
|
)
|
|
return tier, mock_agent
|
|
|
|
|
|
def drain(
|
|
tier: ObjectStoreSecondaryTierManager, max_rounds: int = 20
|
|
) -> list[JobResult]:
|
|
"""Poll get_finished_jobs() until all in-flight jobs resolve."""
|
|
results: list[JobResult] = []
|
|
for _ in range(max_rounds):
|
|
results.extend(tier.get_finished_jobs())
|
|
if not tier._transfers:
|
|
break
|
|
return results
|
|
|
|
|
|
def lookup_and_wait(
|
|
tier: ObjectStoreSecondaryTierManager,
|
|
keys: list[OffloadKey],
|
|
ctx: ReqContext = _CTX,
|
|
timeout: float = 1.0,
|
|
) -> list[bool]:
|
|
"""Perform a full async lookup cycle and return resolved results."""
|
|
for k in keys:
|
|
tier.lookup(k, ctx)
|
|
tier.on_schedule_end(ScheduleEndContext(new_req_ids=[], preempted_req_ids=()))
|
|
deadline = time.monotonic() + timeout
|
|
while time.monotonic() < deadline:
|
|
if not tier._lookup_manager._pending_results.empty():
|
|
break
|
|
time.sleep(0.01)
|
|
return [tier.lookup(k, ctx) for k in keys]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.parametrize("locality", ["local", ""])
|
|
def test_invalid_locality_raises_at_construction(locality):
|
|
with pytest.raises(ValueError, match="Locality"):
|
|
_make_tier(locality=locality)
|
|
|
|
|
|
class TestMockObjTierBasic:
|
|
def setup_method(self):
|
|
self.tier, self.agent = _make_tier(num_blocks=4)
|
|
|
|
def test_lookup_empty_tier(self):
|
|
assert lookup_and_wait(self.tier, [key(1)]) == [LookupResult.MISS]
|
|
|
|
def test_store_and_lookup(self):
|
|
self.tier.submit_store(make_job(1, [key(1)], [0]))
|
|
results = drain(self.tier)
|
|
assert len(results) == 1
|
|
assert results[0].success
|
|
assert lookup_and_wait(self.tier, [key(1)]) == [LookupResult.HIT]
|
|
|
|
def test_lookup_unrelated_key_returns_false(self):
|
|
self.tier.submit_store(make_job(1, [key(1)], [0]))
|
|
drain(self.tier)
|
|
assert lookup_and_wait(self.tier, [key(999)]) == [LookupResult.MISS]
|
|
|
|
def test_store_then_load_roundtrip(self):
|
|
self.tier.submit_store(make_job(1, [key(1), key(2)], [0, 1]))
|
|
results = drain(self.tier)
|
|
assert results[0].success
|
|
|
|
self.tier.submit_load(make_job(2, [key(1), key(2)], [0, 1]))
|
|
results = drain(self.tier)
|
|
assert len(results) == 1
|
|
assert results[0].success
|
|
|
|
def test_multiple_jobs_tracked_independently(self):
|
|
self.tier.submit_store(make_job(1, [key(1)], [0]))
|
|
self.tier.submit_store(make_job(2, [key(2)], [1]))
|
|
results = drain(self.tier)
|
|
assert len(results) == 2
|
|
assert all(r.success for r in results)
|
|
|
|
def test_failed_transfer_reported(self):
|
|
self.agent.check_xfer_state = lambda h: "ERR"
|
|
self.tier.submit_store(make_job(1, [key(1)], [0]))
|
|
results = drain(self.tier)
|
|
assert len(results) == 1
|
|
assert not results[0].success
|
|
|
|
def test_pending_transfer_not_returned_until_done(self):
|
|
# First poll returns PROC; second poll returns DONE.
|
|
call_count = [0]
|
|
original = self.agent.check_xfer_state
|
|
|
|
def delayed(h):
|
|
call_count[0] += 1
|
|
return "PROC" if call_count[0] == 1 else original(h)
|
|
|
|
self.agent.check_xfer_state = delayed
|
|
|
|
self.tier.submit_store(make_job(1, [key(1)], [0]))
|
|
assert list(self.tier.get_finished_jobs()) == []
|
|
results = list(self.tier.get_finished_jobs())
|
|
assert len(results) == 1
|
|
assert results[0].success
|
|
|
|
def test_drain_jobs_polls_until_transfers_complete(self):
|
|
"""drain_jobs must keep polling check_xfer_state until every
|
|
in-flight transfer finishes. A buggy implementation that only
|
|
polled once would return with _transfers still populated.
|
|
"""
|
|
call_count = [0]
|
|
original = self.agent.check_xfer_state
|
|
|
|
def delayed(h):
|
|
call_count[0] += 1
|
|
# Stay in PROC for the first 2 polls, then DONE.
|
|
return "PROC" if call_count[0] < 3 else original(h)
|
|
|
|
self.agent.check_xfer_state = delayed
|
|
|
|
self.tier.submit_store(make_job(1, [key(1)], [0]))
|
|
assert self.tier._transfers # in flight
|
|
|
|
self.tier.drain_jobs()
|
|
|
|
assert not self.tier._transfers # fully drained
|
|
assert call_count[0] >= 3 # polled past the initial PROC responses
|
|
# Result is buffered for the next get_finished_jobs() call.
|
|
results = list(self.tier.get_finished_jobs())
|
|
assert len(results) == 1
|
|
assert results[0].success
|
|
|
|
|
|
class TestMockObjTierMultiBlock:
|
|
def test_store_multiple_blocks(self):
|
|
tier, _ = _make_tier(num_blocks=8)
|
|
keys = [key(i) for i in range(8)]
|
|
tier.submit_store(make_job(1, keys, list(range(8))))
|
|
results = drain(tier)
|
|
assert len(results) == 1
|
|
assert results[0].success
|
|
assert lookup_and_wait(tier, keys) == [LookupResult.HIT] * 8
|
|
|
|
def test_partial_block_lookup(self):
|
|
tier, _ = _make_tier(num_blocks=4)
|
|
tier.submit_store(make_job(1, [key(0), key(1)], [0, 1]))
|
|
drain(tier)
|
|
assert lookup_and_wait(tier, [key(0), key(1), key(2)]) == [
|
|
LookupResult.HIT,
|
|
LookupResult.HIT,
|
|
LookupResult.MISS,
|
|
]
|
|
|
|
|
|
class TestMockObjTierFailures:
|
|
def test_lookup_exception_returns_false(self):
|
|
tier, agent = _make_tier(num_blocks=4)
|
|
agent.query_memory = lambda *a, **k: (_ for _ in ()).throw(
|
|
RuntimeError("backend error")
|
|
)
|
|
assert lookup_and_wait(tier, [key(1)]) == [LookupResult.MISS]
|
|
|
|
def test_submit_store_register_memory_failure_reported_in_get_finished(self):
|
|
tier, agent = _make_tier(num_blocks=4)
|
|
agent.register_memory = lambda *a, **k: None
|
|
tier.submit_store(make_job(1, [key(1)], [0]))
|
|
results = list(tier.get_finished_jobs())
|
|
assert len(results) == 1
|
|
assert results[0].job_id == 1
|
|
assert not results[0].success
|
|
|
|
def test_submit_load_register_memory_failure_reported_in_get_finished(self):
|
|
tier, agent = _make_tier(num_blocks=4)
|
|
agent.register_memory = lambda *a, **k: None
|
|
tier.submit_load(make_job(2, [key(1)], [0]))
|
|
results = list(tier.get_finished_jobs())
|
|
assert len(results) == 1
|
|
assert results[0].job_id == 2
|
|
assert not results[0].success
|
|
|
|
def test_submit_store_make_prepped_xfer_failure_reported_in_get_finished(self):
|
|
tier, agent = _make_tier(num_blocks=4)
|
|
agent.make_prepped_xfer = lambda *a, **k: None
|
|
tier.submit_store(make_job(3, [key(1)], [0]))
|
|
results = list(tier.get_finished_jobs())
|
|
assert len(results) == 1
|
|
assert results[0].job_id == 3
|
|
assert not results[0].success
|
|
|
|
def test_failure_and_success_both_returned_by_get_finished(self):
|
|
# One job fails at submission, another succeeds in flight.
|
|
tier, agent = _make_tier(num_blocks=4)
|
|
original_register = agent.register_memory
|
|
call_count = [0]
|
|
|
|
def register_once_fail(*a, **k):
|
|
call_count[0] += 1
|
|
return None if call_count[0] == 1 else original_register(*a, **k)
|
|
|
|
agent.register_memory = register_once_fail
|
|
|
|
tier.submit_store(make_job(1, [key(1)], [0])) # fails immediately
|
|
tier.submit_store(make_job(2, [key(2)], [1])) # succeeds
|
|
results = drain(tier)
|
|
assert len(results) == 2
|
|
by_id = {r.job_id: r for r in results}
|
|
assert not by_id[1].success
|
|
assert by_id[2].success
|
|
|
|
|
|
class TestMockObjTierShutdown:
|
|
def test_shutdown_clears_in_flight_transfers(self):
|
|
tier, agent = _make_tier(num_blocks=4)
|
|
# Keep transfer in flight by never completing it
|
|
agent.check_xfer_state = lambda h: "PROC"
|
|
tier.submit_store(make_job(1, [key(1)], [0]))
|
|
assert len(tier._transfers) == 1
|
|
tier.shutdown()
|
|
assert len(tier._transfers) == 0
|
|
assert tier._dram_prepped_handle is None
|
|
assert tier._primary_reg is None
|
|
|
|
def test_shutdown_idempotent(self):
|
|
tier, _ = _make_tier(num_blocks=4)
|
|
tier.shutdown()
|
|
tier.shutdown() # must not raise
|
|
|
|
|
|
class TestObjTierKVEvents:
|
|
def setup_method(self):
|
|
self.tier, self.agent = _make_tier(
|
|
offloading_spec=_make_events_spec(enable_kv_cache_events=True),
|
|
enable_kv_events=True,
|
|
locality="REMOTE",
|
|
)
|
|
|
|
def test_successful_store_emits_stored_event(self):
|
|
"""A completed store transfer emits one stored event with the job's keys."""
|
|
keys = [key(1), key(2)]
|
|
self.tier.submit_store(make_job(1, keys, [0, 1]))
|
|
assert all(r.success for r in drain(self.tier))
|
|
|
|
events = list(self.tier.take_events())
|
|
assert len(events) == 1
|
|
assert events[0].keys == keys
|
|
# Literal medium pins the wire contract, not just the constant choice.
|
|
assert events[0].medium == "OBJ"
|
|
assert events[0].locality is Locality.REMOTE
|
|
assert not events[0].removed
|
|
# take_events drains the buffer.
|
|
assert list(self.tier.take_events()) == []
|
|
|
|
@pytest.mark.parametrize(
|
|
("locality", "expected"),
|
|
[(None, None), ("LOCAL", Locality.LOCAL)],
|
|
)
|
|
def test_store_event_uses_configured_locality(self, locality, expected):
|
|
locality_config = {} if locality is None else {"locality": locality}
|
|
tier, _ = _make_tier(
|
|
offloading_spec=_make_events_spec(enable_kv_cache_events=True),
|
|
enable_kv_events=True,
|
|
**locality_config,
|
|
)
|
|
try:
|
|
tier.submit_store(make_job(1, [key(1)], [0]))
|
|
assert all(r.success for r in drain(tier))
|
|
|
|
events = list(tier.take_events())
|
|
assert len(events) == 1
|
|
assert events[0].locality is expected
|
|
finally:
|
|
tier.shutdown()
|
|
|
|
def test_mixed_job_results_emit_event_only_for_successful_job(self):
|
|
"""With a failed and a successful store job resolving in the same
|
|
poll, exactly one event is emitted and its keys belong to the
|
|
successful job."""
|
|
original = self.agent.check_xfer_state
|
|
self.agent.check_xfer_state = lambda h: "ERR" if h._id == 0 else original(h)
|
|
self.tier.submit_store(make_job(1, [key(1)], [0])) # handle 0: fails
|
|
self.tier.submit_store(make_job(2, [key(2)], [1])) # handle 1: succeeds
|
|
results = drain(self.tier)
|
|
by_id = {r.job_id: r for r in results}
|
|
assert not by_id[1].success
|
|
assert by_id[2].success
|
|
|
|
events = list(self.tier.take_events())
|
|
assert len(events) == 1
|
|
assert events[0].keys == [key(2)]
|
|
assert self.tier._store_job_keys == {}
|
|
|
|
def test_load_job_emits_no_event(self):
|
|
self.tier.submit_store(make_job(1, [key(1)], [0]))
|
|
results = drain(self.tier)
|
|
assert len(results) == 1
|
|
assert results[0].success
|
|
list(self.tier.take_events())
|
|
|
|
self.tier.submit_load(make_job(2, [key(1)], [0]))
|
|
results = drain(self.tier)
|
|
assert len(results) == 1
|
|
assert results[0].success
|
|
assert list(self.tier.take_events()) == []
|
|
|
|
def test_failed_transfer_emits_no_event(self):
|
|
self.agent.check_xfer_state = lambda h: "ERR"
|
|
self.tier.submit_store(make_job(1, [key(1)], [0]))
|
|
results = drain(self.tier)
|
|
assert not results[0].success
|
|
assert list(self.tier.take_events()) == []
|
|
assert self.tier._store_job_keys == {}
|
|
|
|
def test_submission_failure_emits_no_event(self):
|
|
self.agent.make_prepped_xfer = lambda *a, **k: None
|
|
self.tier.submit_store(make_job(1, [key(1)], [0]))
|
|
results = list(self.tier.get_finished_jobs())
|
|
assert not results[0].success
|
|
assert list(self.tier.take_events()) == []
|
|
assert self.tier._store_job_keys == {}
|
|
|
|
def test_events_disabled_by_default(self):
|
|
tier, _ = _make_tier()
|
|
tier.submit_store(make_job(1, [key(1)], [0]))
|
|
results = drain(tier)
|
|
assert len(results) == 1
|
|
assert results[0].success
|
|
assert tier.events is None
|
|
assert tier._store_job_keys == {}
|
|
assert list(tier.take_events()) == []
|
|
|
|
def test_events_require_global_kv_events_flag(self):
|
|
"""Tier-level opt-in alone is not enough; the global flag gates events."""
|
|
tier, _ = _make_tier(
|
|
offloading_spec=_make_events_spec(enable_kv_cache_events=False),
|
|
enable_kv_events=True,
|
|
)
|
|
tier.submit_store(make_job(1, [key(1)], [0]))
|
|
results = drain(tier)
|
|
assert len(results) == 1
|
|
assert results[0].success
|
|
assert tier.events is None
|
|
assert tier._store_job_keys == {}
|
|
assert list(tier.take_events()) == []
|
|
|
|
|
|
class TestObjStoreConfig:
|
|
def test_explicit_credentials_included(self):
|
|
cfg = ObjStoreConfig(
|
|
bucket="b",
|
|
endpoint_override="ep",
|
|
access_key="ak",
|
|
secret_key="sk",
|
|
)
|
|
params = cfg.to_nixl_params()
|
|
assert params["access_key"] == "ak"
|
|
assert params["secret_key"] == "sk"
|
|
|
|
def test_credentials_omitted_when_empty(self):
|
|
cfg = ObjStoreConfig(bucket="b", endpoint_override="ep")
|
|
params = cfg.to_nixl_params()
|
|
assert "access_key" not in params
|
|
assert "secret_key" not in params
|
|
assert "session_token" not in params
|
|
assert "region" not in params
|
|
assert params["bucket"] == "b"
|
|
assert params["endpoint_override"] == "ep"
|
|
|
|
def test_session_token_and_region_included(self):
|
|
cfg = ObjStoreConfig(
|
|
bucket="b",
|
|
endpoint_override="ep",
|
|
access_key="ak",
|
|
secret_key="sk",
|
|
session_token="tok",
|
|
region="us-east-1",
|
|
)
|
|
params = cfg.to_nixl_params()
|
|
assert params["session_token"] == "tok"
|
|
assert params["region"] == "us-east-1"
|
|
|
|
def test_ca_bundle_included_when_set(self):
|
|
cfg = ObjStoreConfig(
|
|
bucket="b",
|
|
endpoint_override="ep",
|
|
ca_bundle="/path/to/ca.pem",
|
|
)
|
|
params = cfg.to_nixl_params()
|
|
assert params["ca_bundle"] == "/path/to/ca.pem"
|
|
assert "access_key" not in params
|