Skip to content

Commit 6c6be6d

Browse files
committed
Merge branch 'release/0.6.37'
2 parents a58a54f + f6a4225 commit 6c6be6d

File tree

11 files changed

+389
-1
lines changed

11 files changed

+389
-1
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ build/
1212
/examples/bad/test.log
1313
/CLAUDE.md
1414
/cache/
15+
/.claude/

.version.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
"strategy": "semver",
33
"major": 0,
44
"minor": 6,
5-
"patch": 36,
5+
"patch": 37,
66
"build": 0
77
}

src/Mvc/Cache/Storage/FileCacheStorage.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ public function isExpired( string $Key ): bool
182182
*/
183183
public function gc(): int
184184
{
185+
Log::debug( "FileCacheStorage gc" );
185186
$Count = 0;
186187

187188
if( !is_dir( $this->_BasePath ) )
@@ -330,6 +331,8 @@ private function scanAndClean( string $Dir, int &$Count ): void
330331

331332
if( $MetaData && isset( $MetaData['expires'] ) && time() > $MetaData['expires'] )
332333
{
334+
Log::debug( "Cache entry expired for key: $ItemPath" );
335+
333336
// Remove the meta file
334337
@unlink( $ItemPath );
335338

src/Mvc/Cache/Storage/ICacheStorage.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,11 @@ public function clear(): bool;
5151
* @return bool
5252
*/
5353
public function isExpired( string $Key ): bool;
54+
55+
/**
56+
* Run garbage collection to remove expired cache entries
57+
*
58+
* @return int Number of entries removed
59+
*/
60+
public function gc(): int;
5461
}

src/Mvc/Cache/ViewCache.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ public function setEnabled( bool $Enabled ): void
168168
*/
169169
public function gc(): int
170170
{
171+
Log::debug( "Cache gc" );
171172
return $this->_Storage->gc();
172173
}
173174

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
namespace Mvc\Cache\Exceptions;
4+
5+
use Neuron\Mvc\Cache\Exceptions\CacheException;
6+
use PHPUnit\Framework\TestCase;
7+
8+
class CacheExceptionTest extends TestCase
9+
{
10+
public function testUnableToWrite()
11+
{
12+
$Path = '/tmp/test/cache.file';
13+
$Exception = CacheException::unableToWrite( $Path );
14+
15+
$this->assertInstanceOf( CacheException::class, $Exception );
16+
$this->assertEquals( "Unable to write cache file: $Path", $Exception->getMessage() );
17+
}
18+
19+
public function testInvalidKey()
20+
{
21+
$Key = 'invalid/key*with:chars';
22+
$Exception = CacheException::invalidKey( $Key );
23+
24+
$this->assertInstanceOf( CacheException::class, $Exception );
25+
$this->assertEquals( "Invalid cache key: $Key", $Exception->getMessage() );
26+
}
27+
28+
public function testStorageNotConfigured()
29+
{
30+
$Exception = CacheException::storageNotConfigured();
31+
32+
$this->assertInstanceOf( CacheException::class, $Exception );
33+
$this->assertEquals( "Cache storage is not properly configured", $Exception->getMessage() );
34+
}
35+
36+
public function testUnableToCreateDirectory()
37+
{
38+
$Path = '/tmp/test/cache/dir';
39+
$Exception = CacheException::unableToCreateDirectory( $Path );
40+
41+
$this->assertInstanceOf( CacheException::class, $Exception );
42+
$this->assertEquals( "Unable to create cache directory: $Path", $Exception->getMessage() );
43+
}
44+
}

tests/Mvc/Cache/FileCacheStorageTest.php

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,113 @@ public function testInvalidCacheDirectory()
134134

135135
new FileCacheStorage( $InvalidPath );
136136
}
137+
138+
public function testGarbageCollection()
139+
{
140+
// Create some cache entries with different TTLs
141+
$this->Storage->write( 'keep1', 'content1', 3600 ); // Keep for 1 hour
142+
$this->Storage->write( 'keep2', 'content2', 3600 ); // Keep for 1 hour
143+
$this->Storage->write( 'expire1', 'content3', 1 ); // Expire in 1 second
144+
$this->Storage->write( 'expire2', 'content4', 1 ); // Expire in 1 second
145+
146+
// All should exist initially
147+
$this->assertTrue( $this->Storage->exists( 'keep1' ) );
148+
$this->assertTrue( $this->Storage->exists( 'keep2' ) );
149+
$this->assertTrue( $this->Storage->exists( 'expire1' ) );
150+
$this->assertTrue( $this->Storage->exists( 'expire2' ) );
151+
152+
// Wait for some entries to expire
153+
sleep( 2 );
154+
155+
// Run garbage collection
156+
$Removed = $this->Storage->gc();
157+
$this->assertEquals( 2, $Removed );
158+
159+
// Check that only non-expired entries remain
160+
$this->assertTrue( $this->Storage->exists( 'keep1' ) );
161+
$this->assertTrue( $this->Storage->exists( 'keep2' ) );
162+
$this->assertFalse( $this->Storage->exists( 'expire1' ) );
163+
$this->assertFalse( $this->Storage->exists( 'expire2' ) );
164+
}
165+
166+
public function testGarbageCollectionOnEmptyCache()
167+
{
168+
$Removed = $this->Storage->gc();
169+
$this->assertEquals( 0, $Removed );
170+
}
171+
172+
public function testReadExpiredEntry()
173+
{
174+
$Key = 'test_read_expired';
175+
$this->Storage->write( $Key, 'content', 1 );
176+
177+
sleep( 2 );
178+
179+
// Reading expired entry should return null and delete it
180+
$this->assertNull( $this->Storage->read( $Key ) );
181+
$this->assertFalse( $this->Storage->exists( $Key ) );
182+
}
183+
184+
public function testClearWithNonExistentDirectory()
185+
{
186+
// Create storage with non-existent base path
187+
$Storage = new FileCacheStorage( vfsStream::url( 'cache/newdir' ) );
188+
189+
// Clear should still return true even if directory doesn't fully exist
190+
$this->assertTrue( $Storage->clear() );
191+
}
192+
193+
public function testIsExpiredWithMissingMetaFile()
194+
{
195+
$Key = 'test_missing_meta';
196+
197+
// Manually create cache file without meta file
198+
$Hash = md5( $Key );
199+
$SubDir = substr( $Hash, 0, 2 );
200+
$Dir = vfsStream::newDirectory( $SubDir )->at( $this->Root );
201+
vfsStream::newFile( $Hash . '.cache' )
202+
->at( $Dir )
203+
->withContent( 'content' );
204+
205+
// Should be considered expired if meta file is missing
206+
$this->assertTrue( $this->Storage->isExpired( $Key ) );
207+
}
208+
209+
public function testIsExpiredWithCorruptedMetaFile()
210+
{
211+
$Key = 'test_corrupted_meta';
212+
213+
// Manually create cache and corrupted meta file
214+
$Hash = md5( $Key );
215+
$SubDir = substr( $Hash, 0, 2 );
216+
$Dir = vfsStream::newDirectory( $SubDir )->at( $this->Root );
217+
vfsStream::newFile( $Hash . '.cache' )
218+
->at( $Dir )
219+
->withContent( 'content' );
220+
vfsStream::newFile( $Hash . '.meta' )
221+
->at( $Dir )
222+
->withContent( 'not valid json' );
223+
224+
// Should be considered expired if meta file is corrupted
225+
$this->assertTrue( $this->Storage->isExpired( $Key ) );
226+
}
227+
228+
public function testIsExpiredWithIncompleteMetaData()
229+
{
230+
$Key = 'test_incomplete_meta';
231+
232+
// Manually create cache and meta file without expires field
233+
$Hash = md5( $Key );
234+
$SubDir = substr( $Hash, 0, 2 );
235+
$Dir = vfsStream::newDirectory( $SubDir )->at( $this->Root );
236+
vfsStream::newFile( $Hash . '.cache' )
237+
->at( $Dir )
238+
->withContent( 'content' );
239+
vfsStream::newFile( $Hash . '.meta' )
240+
->at( $Dir )
241+
->withContent( json_encode( [ 'created' => time() ] ) );
242+
243+
// Should be considered expired if meta data is incomplete
244+
$this->assertTrue( $this->Storage->isExpired( $Key ) );
245+
}
137246
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
namespace Mvc\Controllers;
4+
5+
use Neuron\Core\Exceptions\NotFound;
6+
use Neuron\Mvc\Application;
7+
use Neuron\Mvc\Controllers\Factory;
8+
use Neuron\Mvc\Controllers\IController;
9+
use Neuron\Mvc\Responses\HttpResponseStatus;
10+
use Neuron\Routing\Router;
11+
use PHPUnit\Framework\TestCase;
12+
13+
class TestController implements IController
14+
{
15+
private Router $_Router;
16+
17+
public function __construct( Router $Router )
18+
{
19+
$this->_Router = $Router;
20+
}
21+
22+
public function getRouter(): Router
23+
{
24+
return $this->_Router;
25+
}
26+
27+
public function renderHtml( HttpResponseStatus $ResponseCode, array $Data = [], string $Page = "index", string $Layout = "default" ): string
28+
{
29+
return 'html';
30+
}
31+
32+
public function renderJson( HttpResponseStatus $ResponseCode, array $Data = [] ): string
33+
{
34+
return 'json';
35+
}
36+
37+
public function renderXml( HttpResponseStatus $ResponseCode, array $Data = [] ): string
38+
{
39+
return 'xml';
40+
}
41+
}
42+
43+
class InvalidController
44+
{
45+
public function __construct( Router $Router )
46+
{
47+
// This class does not implement IController
48+
}
49+
}
50+
51+
class ControllerFactoryTest extends TestCase
52+
{
53+
private Application $App;
54+
55+
protected function setUp(): void
56+
{
57+
$this->App = $this->createMock( Application::class );
58+
$this->App->method( 'getRouter' )->willReturn( new Router() );
59+
}
60+
61+
public function testCreateValidController()
62+
{
63+
$Controller = Factory::create( $this->App, TestController::class );
64+
65+
$this->assertInstanceOf( IController::class, $Controller );
66+
$this->assertInstanceOf( TestController::class, $Controller );
67+
$this->assertInstanceOf( Router::class, $Controller->getRouter() );
68+
}
69+
70+
public function testCreateNonExistentController()
71+
{
72+
$this->expectException( NotFound::class );
73+
$this->expectExceptionMessage( 'Controller NonExistentController not found.' );
74+
75+
Factory::create( $this->App, 'NonExistentController' );
76+
}
77+
78+
public function testCreateControllerNotImplementingInterface()
79+
{
80+
$this->expectException( NotFound::class );
81+
$this->expectExceptionMessage( InvalidController::class . ' does not implement IController.' );
82+
83+
Factory::create( $this->App, InvalidController::class );
84+
}
85+
}

0 commit comments

Comments
 (0)