|
15 | 15 | from __future__ import annotations |
16 | 16 |
|
17 | 17 | import time |
18 | | -from unittest.mock import patch |
| 18 | +from unittest.mock import MagicMock, patch |
19 | 19 |
|
20 | 20 |
|
21 | 21 | class TestL1OnlyModeBug: |
@@ -363,6 +363,168 @@ def changing_func(x: int) -> int: |
363 | 363 | assert call_count == 2, "Should have re-executed after invalidation" |
364 | 364 |
|
365 | 365 |
|
| 366 | +class TestDefaultBackendBehavior: |
| 367 | + """ |
| 368 | + CRITICAL: Tests that @cache() WITHOUT backend=None DOES attempt provider lookup. |
| 369 | +
|
| 370 | + This is the regression test for the bug where we accidentally made ALL decorators |
| 371 | + L1-only by checking `config.backend is None` (which is the default). |
| 372 | + """ |
| 373 | + |
| 374 | + def test_default_cache_should_call_backend_provider(self): |
| 375 | + """ |
| 376 | + @cache() without backend=None SHOULD call get_backend_provider(). |
| 377 | +
|
| 378 | + This is the INVERSE of L1-only mode - verifies we didn't break default behavior. |
| 379 | + """ |
| 380 | + from cachekit.decorators import cache |
| 381 | + |
| 382 | + with patch("cachekit.decorators.wrapper.get_backend_provider") as mock_provider: |
| 383 | + # Make provider return a mock backend |
| 384 | + mock_backend = MagicMock() |
| 385 | + mock_provider.return_value.get_backend.return_value = mock_backend |
| 386 | + |
| 387 | + @cache(ttl=60) # NO backend=None - should use provider |
| 388 | + def default_func() -> str: |
| 389 | + return "result" |
| 390 | + |
| 391 | + # Call the function - this should trigger provider lookup |
| 392 | + default_func() |
| 393 | + |
| 394 | + # Backend provider SHOULD have been called |
| 395 | + mock_provider.return_value.get_backend.assert_called() |
| 396 | + |
| 397 | + def test_cache_minimal_without_backend_none_should_call_provider(self): |
| 398 | + """ |
| 399 | + @cache.minimal() without backend=None SHOULD call get_backend_provider(). |
| 400 | + """ |
| 401 | + from cachekit.decorators import cache |
| 402 | + |
| 403 | + with patch("cachekit.decorators.wrapper.get_backend_provider") as mock_provider: |
| 404 | + mock_backend = MagicMock() |
| 405 | + mock_provider.return_value.get_backend.return_value = mock_backend |
| 406 | + |
| 407 | + @cache.minimal(ttl=60) # NO backend=None |
| 408 | + def minimal_func() -> str: |
| 409 | + return "result" |
| 410 | + |
| 411 | + minimal_func() |
| 412 | + |
| 413 | + # Backend provider SHOULD have been called |
| 414 | + mock_provider.return_value.get_backend.assert_called() |
| 415 | + |
| 416 | + def test_decorator_config_default_backend_should_call_provider(self): |
| 417 | + """ |
| 418 | + DecoratorConfig() with default backend SHOULD call get_backend_provider(). |
| 419 | +
|
| 420 | + This specifically tests that DecoratorConfig.backend defaulting to None |
| 421 | + does NOT trigger L1-only mode (the bug we fixed). |
| 422 | + """ |
| 423 | + from cachekit.config import DecoratorConfig |
| 424 | + from cachekit.decorators import cache |
| 425 | + |
| 426 | + with patch("cachekit.decorators.wrapper.get_backend_provider") as mock_provider: |
| 427 | + mock_backend = MagicMock() |
| 428 | + mock_provider.return_value.get_backend.return_value = mock_backend |
| 429 | + |
| 430 | + # DecoratorConfig() has backend=None by DEFAULT - should NOT be L1-only |
| 431 | + @cache(config=DecoratorConfig(ttl=60)) |
| 432 | + def config_func() -> str: |
| 433 | + return "result" |
| 434 | + |
| 435 | + config_func() |
| 436 | + |
| 437 | + # Backend provider SHOULD have been called (default != explicit None) |
| 438 | + mock_provider.return_value.get_backend.assert_called() |
| 439 | + |
| 440 | + def test_explicit_backend_instance_should_be_used(self): |
| 441 | + """ |
| 442 | + @cache(backend=explicit_backend) should use that backend, not provider. |
| 443 | + """ |
| 444 | + from cachekit.decorators import cache |
| 445 | + |
| 446 | + with patch("cachekit.decorators.wrapper.get_backend_provider") as mock_provider: |
| 447 | + mock_provider.return_value.get_backend.side_effect = RuntimeError("Should not be called!") |
| 448 | + |
| 449 | + # Create an explicit mock backend |
| 450 | + explicit_backend = MagicMock() |
| 451 | + explicit_backend.get.return_value = None # Cache miss |
| 452 | + |
| 453 | + @cache(backend=explicit_backend, ttl=60) |
| 454 | + def explicit_func() -> str: |
| 455 | + return "result" |
| 456 | + |
| 457 | + explicit_func() |
| 458 | + |
| 459 | + # Provider should NOT be called - explicit backend provided |
| 460 | + mock_provider.return_value.get_backend.assert_not_called() |
| 461 | + |
| 462 | + def test_dev_and_test_presets_without_backend_none(self): |
| 463 | + """ |
| 464 | + @cache.dev() and @cache.test() without backend=None SHOULD call provider. |
| 465 | +
|
| 466 | + Completes coverage for all intent presets. |
| 467 | + """ |
| 468 | + from cachekit.decorators import cache |
| 469 | + |
| 470 | + with patch("cachekit.decorators.wrapper.get_backend_provider") as mock_provider: |
| 471 | + mock_backend = MagicMock() |
| 472 | + mock_provider.return_value.get_backend.return_value = mock_backend |
| 473 | + |
| 474 | + @cache.dev(ttl=60) |
| 475 | + def dev_func() -> str: |
| 476 | + return "dev" |
| 477 | + |
| 478 | + @cache.test(ttl=60) |
| 479 | + def test_func() -> str: |
| 480 | + return "test" |
| 481 | + |
| 482 | + dev_func() |
| 483 | + test_func() |
| 484 | + |
| 485 | + # Both should have triggered provider lookup |
| 486 | + assert mock_provider.return_value.get_backend.call_count >= 2 |
| 487 | + |
| 488 | + def test_dev_and_test_presets_with_backend_none(self): |
| 489 | + """ |
| 490 | + @cache.dev(backend=None) and @cache.test(backend=None) should be L1-only. |
| 491 | +
|
| 492 | + Completes L1-only coverage for all intent presets. |
| 493 | + """ |
| 494 | + from cachekit.decorators import cache |
| 495 | + |
| 496 | + with patch("cachekit.decorators.wrapper.get_backend_provider") as mock_provider: |
| 497 | + mock_provider.return_value.get_backend.side_effect = RuntimeError("Should not be called!") |
| 498 | + |
| 499 | + dev_count = 0 |
| 500 | + |
| 501 | + @cache.dev(backend=None) |
| 502 | + def dev_func() -> str: |
| 503 | + nonlocal dev_count |
| 504 | + dev_count += 1 |
| 505 | + return "dev" |
| 506 | + |
| 507 | + test_count = 0 |
| 508 | + |
| 509 | + @cache.test(backend=None) |
| 510 | + def test_func() -> str: |
| 511 | + nonlocal test_count |
| 512 | + test_count += 1 |
| 513 | + return "test" |
| 514 | + |
| 515 | + # Execute twice each - should hit L1 cache |
| 516 | + dev_func() |
| 517 | + dev_func() |
| 518 | + test_func() |
| 519 | + test_func() |
| 520 | + |
| 521 | + assert dev_count == 1, f"@cache.dev L1 miss - called {dev_count} times" |
| 522 | + assert test_count == 1, f"@cache.test L1 miss - called {test_count} times" |
| 523 | + |
| 524 | + # Provider should NEVER be called |
| 525 | + mock_provider.return_value.get_backend.assert_not_called() |
| 526 | + |
| 527 | + |
366 | 528 | class TestL1OnlyModeNoRedisWarnings: |
367 | 529 | """ |
368 | 530 | Verify that L1-only mode doesn't produce Redis connection warnings. |
|
0 commit comments