Skip to content

Commit 6c4cb0f

Browse files
committed
test: Enhance cache dependency test coverage
- Add array parameter tests to RepositoryLoggerTest - Add multiple parents dependency test to CacheDependencyTest - Add unrelated resources independence test with ETag verification - Add CACHE_DEPENDENCY_TESTS.md documentation
1 parent 9edb995 commit 6c4cb0f

6 files changed

Lines changed: 259 additions & 0 deletions

File tree

tests/CACHE_DEPENDENCY_TESTS.md

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Cache Dependency Test Coverage
2+
3+
This document describes how cache dependency resolution is tested across the test suite.
4+
5+
## Dependency Patterns Tested
6+
7+
### Chain Dependencies (A → B → C)
8+
9+
When resource A embeds B, and B embeds C, purging C invalidates both B and A.
10+
11+
```
12+
LevelOne → LevelTwo → LevelThree
13+
purge(LevelThree) → LevelOne invalidated
14+
```
15+
16+
**Test:** `CacheDependencyTest::testDestroyByGrandChild`
17+
18+
### Parent-Child Dependencies
19+
20+
Purging a child invalidates its parent.
21+
22+
```
23+
LevelOne → LevelTwo
24+
purge(LevelTwo) → LevelOne invalidated
25+
```
26+
27+
**Test:** `CacheDependencyTest::testDestroyByChild`
28+
29+
### Multiple Parents Depending on Same Child
30+
31+
When multiple parents embed the same child, purging the child invalidates all parents.
32+
33+
```
34+
ParentA ──┐
35+
├──→ ChildC
36+
ParentB ──┘
37+
purge(ChildC) → ParentA, ParentB both invalidated
38+
```
39+
40+
**Test:** `CacheDependencyTest::testMultipleParentsDependOnSameChild`
41+
42+
### Unrelated Resources Independence
43+
44+
Resources in separate dependency chains do not affect each other.
45+
46+
```
47+
Chain A: LevelOne → LevelTwo → LevelThree
48+
Chain B: ChildC (independent)
49+
purge(LevelThree) → LevelOne invalidated, ChildC unchanged
50+
```
51+
52+
**Test:** `CacheDependencyTest::testUnrelatedResourcesAreIndependent`
53+
54+
### Embed-Based Dependencies with Donut Cache
55+
56+
Blog posting embeds comment; purging comment invalidates blog posting.
57+
58+
```
59+
BlogPosting → Comment
60+
purge(Comment) → BlogPosting invalidated
61+
```
62+
63+
**Test:** `DonutRepositoryTest::testCacheDependency`
64+
65+
### Tag-Based Invalidation
66+
67+
Resources can be invalidated by their URI-derived tags.
68+
69+
```
70+
invalidateTags([uriTag('page://self/html/blog-posting')]) → BlogPosting invalidated
71+
```
72+
73+
**Test:** `DonutRepositoryTest::testInvalidateTags`
74+
75+
## Component Unit Tests
76+
77+
| Component | Test File | Coverage |
78+
|-----------|-----------|----------|
79+
| `UriTag::__invoke()` | `UtiTagTest::testInvoke` | URI to tag string conversion |
80+
| `UriTag::fromAssoc()` | `UtiTagTest::testFromAssoc` | Generate tags from array data |
81+
| `SurrogateKeys` | `SurrogateKeysTest` | Aggregate tags from multiple resources |
82+
| `RepositoryLogger` | `RepositoryLoggerTest` | Log formatting with arrays |
83+
84+
## ETag Invalidation Verification
85+
86+
All dependency tests verify both resource cache and ETag invalidation:
87+
88+
| Test | ETag Assertions |
89+
|------|-----------------|
90+
| `testDestroyByChild` | Parent ETag invalidated |
91+
| `testDestroyByGrandChild` | All 3 levels' ETags invalidated |
92+
| `testUnrelatedResourcesAreIndependent` | Invalidated ETag gone, unrelated ETag preserved |
93+
| `testMultipleParentsDependOnSameChild` | Both parents' ETags invalidated |
94+
95+
## Fake Resources for Testing
96+
97+
Located in `tests/Fake/fake-app/src/Resource/Page/Dep/`:
98+
99+
| Resource | Embeds | Purpose |
100+
|----------|--------|---------|
101+
| `LevelOne` | `LevelTwo` | Top of 3-level chain |
102+
| `LevelTwo` | `LevelThree` | Middle of chain |
103+
| `LevelThree` | - | Leaf node |
104+
| `ParentA` | `ChildC` | Multiple parent test |
105+
| `ParentB` | `ChildC` | Multiple parent test |
106+
| `ChildC` | - | Shared child resource |

tests/CacheDependencyTest.php

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,74 @@ public function testDestroyByGrandChild(): void
5757
$this->assertFalse($this->storage->hasEtag($etag2));
5858
$this->assertFalse($this->storage->hasEtag($etag3));
5959
}
60+
61+
/**
62+
* Test that resources in unrelated dependency chains are independent.
63+
*
64+
* Structure:
65+
* - Chain A: LevelOne -> LevelTwo -> LevelThree
66+
* - Chain B: ChildC (completely independent)
67+
*
68+
* Purging LevelThree should invalidate LevelOne and LevelTwo but NOT ChildC.
69+
*/
70+
public function testUnrelatedResourcesAreIndependent(): void
71+
{
72+
// Access both chains independently
73+
$this->resource->get('page://self/dep/level-one');
74+
$this->resource->get('page://self/dep/child-c');
75+
76+
$levelOne = $this->repository->get(new Uri('page://self/dep/level-one'));
77+
$childC = $this->repository->get(new Uri('page://self/dep/child-c'));
78+
$this->assertInstanceOf(ResourceState::class, $levelOne);
79+
$this->assertInstanceOf(ResourceState::class, $childC);
80+
81+
// Capture ETags before purge
82+
$etagLevelOne = $levelOne->headers[Header::ETAG];
83+
$etagChildC = $childC->headers[Header::ETAG];
84+
85+
// Purge LevelThree (in Chain A)
86+
$this->repository->purge(new Uri('page://self/dep/level-three'));
87+
88+
// LevelOne should be invalidated (it depends on LevelThree via LevelTwo)
89+
$this->assertNull($this->repository->get(new Uri('page://self/dep/level-one')));
90+
$this->assertFalse($this->storage->hasEtag($etagLevelOne));
91+
92+
// ChildC should still be cached (completely unrelated chain)
93+
$childCAfterPurge = $this->repository->get(new Uri('page://self/dep/child-c'));
94+
$this->assertInstanceOf(ResourceState::class, $childCAfterPurge);
95+
$this->assertTrue($this->storage->hasEtag($etagChildC));
96+
}
97+
98+
/**
99+
* Test that purging a child invalidates all parents that depend on it.
100+
*
101+
* Structure: ParentA and ParentB both embed ChildC
102+
* Purging ChildC should invalidate both ParentA and ParentB.
103+
*/
104+
public function testMultipleParentsDependOnSameChild(): void
105+
{
106+
// Access both parents (which both embed ChildC)
107+
$this->resource->get('page://self/dep/parent-a');
108+
$this->resource->get('page://self/dep/parent-b');
109+
110+
$parentA = $this->repository->get(new Uri('page://self/dep/parent-a'));
111+
$parentB = $this->repository->get(new Uri('page://self/dep/parent-b'));
112+
$this->assertInstanceOf(ResourceState::class, $parentA);
113+
$this->assertInstanceOf(ResourceState::class, $parentB);
114+
115+
// Capture ETags before purge
116+
$etagA = $parentA->headers[Header::ETAG];
117+
$etagB = $parentB->headers[Header::ETAG];
118+
119+
// Purge the shared child
120+
$this->repository->purge(new Uri('page://self/dep/child-c'));
121+
122+
// Both parents should be invalidated
123+
$this->assertNull($this->repository->get(new Uri('page://self/dep/parent-a')));
124+
$this->assertNull($this->repository->get(new Uri('page://self/dep/parent-b')));
125+
126+
// ETags should also be invalidated
127+
$this->assertFalse($this->storage->hasEtag($etagA));
128+
$this->assertFalse($this->storage->hasEtag($etagB));
129+
}
60130
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace FakeVendor\HelloWorld\Resource\Page\Dep;
4+
5+
use BEAR\RepositoryModule\Annotation\Cacheable;
6+
use BEAR\Resource\ResourceObject;
7+
8+
#[Cacheable]
9+
class ChildC extends ResourceObject
10+
{
11+
public $body = ['child-c' => 1];
12+
13+
public function onGet()
14+
{
15+
return $this;
16+
}
17+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace FakeVendor\HelloWorld\Resource\Page\Dep;
4+
5+
use BEAR\RepositoryModule\Annotation\Cacheable;
6+
use BEAR\Resource\Annotation\Embed;
7+
use BEAR\Resource\ResourceObject;
8+
9+
#[Cacheable]
10+
class ParentA extends ResourceObject
11+
{
12+
public $body = ['parent-a' => 1];
13+
14+
#[Embed(rel: 'child', src: '/dep/child-c')]
15+
public function onGet()
16+
{
17+
return $this;
18+
}
19+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace FakeVendor\HelloWorld\Resource\Page\Dep;
4+
5+
use BEAR\RepositoryModule\Annotation\Cacheable;
6+
use BEAR\Resource\Annotation\Embed;
7+
use BEAR\Resource\ResourceObject;
8+
9+
#[Cacheable]
10+
class ParentB extends ResourceObject
11+
{
12+
public $body = ['parent-b' => 1];
13+
14+
#[Embed(rel: 'child', src: '/dep/child-c')]
15+
public function onGet()
16+
{
17+
return $this;
18+
}
19+
}

tests/RepositoryLoggerTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,32 @@ public function testLog(): void
1717
$this->assertSame('get 1
1818
put 2 3', $logString);
1919
}
20+
21+
public function testLogWithArrayValue(): void
22+
{
23+
$logger = new RepositoryLogger();
24+
$logger->log('save uri:%s tags:%s', 'app://self/user', ['etag1', 'uri-tag', 'dep-tag']);
25+
$this->assertSame('save uri:app://self/user tags:etag1 uri-tag dep-tag', (string) $logger);
26+
}
27+
28+
public function testLogWithEmptyArray(): void
29+
{
30+
$logger = new RepositoryLogger();
31+
$logger->log('invalidate tags:%s', []);
32+
$this->assertSame('invalidate tags:', (string) $logger);
33+
}
34+
35+
public function testLogWithSingleElementArray(): void
36+
{
37+
$logger = new RepositoryLogger();
38+
$logger->log('tags:%s', ['single-tag']);
39+
$this->assertSame('tags:single-tag', (string) $logger);
40+
}
41+
42+
public function testLogWithMixedParameters(): void
43+
{
44+
$logger = new RepositoryLogger();
45+
$logger->log('uri:%s tags:%s ttl:%s', 'app://self/user', ['tag1', 'tag2'], 3600);
46+
$this->assertSame('uri:app://self/user tags:tag1 tag2 ttl:3600', (string) $logger);
47+
}
2048
}

0 commit comments

Comments
 (0)