|
1 | 1 | import unittest |
| 2 | +import pytest |
2 | 3 | from http import HTTPStatus |
3 | 4 | from unittest.mock import MagicMock, Mock, patch, call |
4 | 5 | from freezegun import freeze_time |
@@ -360,3 +361,132 @@ def test_GIVEN_empty_cache_THEN_prune_raises_value_error(self): |
360 | 361 |
|
361 | 362 | with self.assertRaises(ValueError): |
362 | 363 | cache.prune(entry_mock) |
| 364 | + |
| 365 | + |
| 366 | +@pytest.fixture(autouse=True) |
| 367 | +def connect_mock(): |
| 368 | + with patch("socket.socket") as socketMock: |
| 369 | + connectMock = socketMock() |
| 370 | + connectMock.__enter__.return_value = connectMock |
| 371 | + connectMock.connect_ex.return_value = 1 # Port not in use by default |
| 372 | + yield connectMock |
| 373 | + |
| 374 | + |
| 375 | +@pytest.fixture(autouse=True) |
| 376 | +def process_backend_mock(): |
| 377 | + with patch("cellxgene_gateway.backend_cache.process_backend") as mock: |
| 378 | + yield mock |
| 379 | + |
| 380 | + |
| 381 | +@pytest.fixture(autouse=True) |
| 382 | +def sleep_mock(): |
| 383 | + from time import sleep |
| 384 | + |
| 385 | + with patch("cellxgene_gateway.backend_cache.time.sleep") as mock: |
| 386 | + mock.side_effect = lambda x: sleep(0.01) # short sleep |
| 387 | + yield mock |
| 388 | + |
| 389 | + |
| 390 | +@freeze_time("2025-12-01 10:00:00") |
| 391 | +def test_GIVEN_empty_cache_THEN_create_entry_uses_starting_port(process_backend_mock): |
| 392 | + """create_entry should use port 8000 when cache is empty and port is free.""" |
| 393 | + key = Mock() |
| 394 | + scripts = ["script1", "script2"] |
| 395 | + |
| 396 | + cache = BackendCache() |
| 397 | + result = cache.create_entry(key, scripts) |
| 398 | + |
| 399 | + # Verify port 8000 was used |
| 400 | + assert result.port == 8000 |
| 401 | + |
| 402 | + # Verify entry was added to list |
| 403 | + assert result in cache.entry_list |
| 404 | + # Verify thread was started |
| 405 | + process_backend_mock.launch.assert_called_once() |
| 406 | + |
| 407 | + # Verify CacheEntry was created with correct key |
| 408 | + assert result.key is key |
| 409 | + assert result.status == CacheEntryStatus.loading |
| 410 | + |
| 411 | + |
| 412 | +@freeze_time("2025-12-01 10:00:00") |
| 413 | +def test_GIVEN_port_in_use_THEN_create_entry_finds_next_available_port(connect_mock): |
| 414 | + """create_entry should increment port until finding one that's free.""" |
| 415 | + # Ports 8000 and 8001 are in use, 8002 is free |
| 416 | + connect_mock.connect_ex.side_effect = [0, 0, 1] |
| 417 | + |
| 418 | + key = Mock() |
| 419 | + scripts = [] |
| 420 | + |
| 421 | + cache = BackendCache() |
| 422 | + result = cache.create_entry(key, scripts) |
| 423 | + |
| 424 | + # Verify port 8002 was used (after skipping 8000 and 8001) |
| 425 | + assert result.port == 8002 |
| 426 | + |
| 427 | + |
| 428 | +@freeze_time("2025-12-01 10:00:00") |
| 429 | +def test_GIVEN_port_exists_in_cache_THEN_create_entry_skips_it(): |
| 430 | + """create_entry should skip ports already in the cache.""" |
| 431 | + key = Mock() |
| 432 | + scripts = [] |
| 433 | + |
| 434 | + # Pre-populate cache with port 8000 |
| 435 | + existing_entry = Mock() |
| 436 | + existing_entry.port = 8000 |
| 437 | + |
| 438 | + cache = BackendCache() |
| 439 | + cache.entry_list = [existing_entry] |
| 440 | + |
| 441 | + result = cache.create_entry(key, scripts) |
| 442 | + |
| 443 | + # Verify port 8001 was used (skipping the existing 8000) |
| 444 | + assert result.port == 8001 |
| 445 | + |
| 446 | + |
| 447 | +@freeze_time("2025-12-01 10:00:00") |
| 448 | +def test_GIVEN_create_entry_THEN_background_thread_is_started(process_backend_mock): |
| 449 | + """create_entry should start a background thread with correct args.""" |
| 450 | + key = Mock() |
| 451 | + scripts = ["script1"] |
| 452 | + |
| 453 | + with patch("cellxgene_gateway.backend_cache.env") as env_mock: |
| 454 | + env_mock.cellxgene_location = "/path/to/cellxgene" |
| 455 | + |
| 456 | + cache = BackendCache() |
| 457 | + result = cache.create_entry(key, scripts) |
| 458 | + |
| 459 | + # Verify Thread was created with correct target and args |
| 460 | + process_backend_mock.launch.assert_called_once() |
| 461 | + call_kwargs = process_backend_mock.launch.call_args[0] |
| 462 | + assert call_kwargs == ("/path/to/cellxgene", ["script1"], result) |
| 463 | + |
| 464 | + |
| 465 | +@freeze_time("2025-12-01 10:00:00") |
| 466 | +def test_GIVEN_create_entry_THEN_entry_is_added_to_cache(): |
| 467 | + """create_entry should add the created entry to the cache list.""" |
| 468 | + key = Mock() |
| 469 | + scripts = [] |
| 470 | + |
| 471 | + cache = BackendCache() |
| 472 | + initial_count = len(cache.entry_list) |
| 473 | + |
| 474 | + result = cache.create_entry(key, scripts) |
| 475 | + |
| 476 | + # Verify entry was added |
| 477 | + assert len(cache.entry_list) == initial_count + 1 |
| 478 | + assert result in cache.entry_list |
| 479 | + |
| 480 | + |
| 481 | +@freeze_time("2025-12-01 10:00:00") |
| 482 | +def test_GIVEN_create_entry_THEN_returns_created_entry(): |
| 483 | + """create_entry should return the created entry.""" |
| 484 | + key = Mock() |
| 485 | + scripts = [] |
| 486 | + |
| 487 | + cache = BackendCache() |
| 488 | + result = cache.create_entry(key, scripts) |
| 489 | + |
| 490 | + # Verify the result is a CacheEntry instance |
| 491 | + assert isinstance(result, CacheEntry) |
| 492 | + assert result.status == CacheEntryStatus.loading |
0 commit comments