Initial
This commit is contained in:
150
tests/Algorithms/ExternalGroupByTest.php
Normal file
150
tests/Algorithms/ExternalGroupByTest.php
Normal file
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SqrtSpace\SpaceTime\Tests\Algorithms;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use SqrtSpace\SpaceTime\Algorithms\ExternalGroupBy;
|
||||
use SqrtSpace\SpaceTime\SpaceTimeConfig;
|
||||
|
||||
class ExternalGroupByTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
SpaceTimeConfig::configure([
|
||||
'external_storage_path' => sys_get_temp_dir() . '/spacetime_test',
|
||||
]);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$path = sys_get_temp_dir() . '/spacetime_test';
|
||||
if (is_dir($path)) {
|
||||
array_map('unlink', glob("$path/*"));
|
||||
rmdir($path);
|
||||
}
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function testBasicGroupBy(): void
|
||||
{
|
||||
$data = [
|
||||
['category' => 'A', 'value' => 1],
|
||||
['category' => 'B', 'value' => 2],
|
||||
['category' => 'A', 'value' => 3],
|
||||
['category' => 'B', 'value' => 4],
|
||||
['category' => 'C', 'value' => 5],
|
||||
];
|
||||
|
||||
$grouped = ExternalGroupBy::groupBy($data, fn($item) => $item['category']);
|
||||
|
||||
$this->assertCount(3, $grouped);
|
||||
$this->assertCount(2, $grouped['A']);
|
||||
$this->assertCount(2, $grouped['B']);
|
||||
$this->assertCount(1, $grouped['C']);
|
||||
|
||||
$this->assertEquals(1, $grouped['A'][0]['value']);
|
||||
$this->assertEquals(3, $grouped['A'][1]['value']);
|
||||
}
|
||||
|
||||
public function testGroupByCount(): void
|
||||
{
|
||||
$data = [
|
||||
['type' => 'foo'],
|
||||
['type' => 'bar'],
|
||||
['type' => 'foo'],
|
||||
['type' => 'baz'],
|
||||
['type' => 'foo'],
|
||||
];
|
||||
|
||||
$counts = ExternalGroupBy::groupByCount($data, fn($item) => $item['type']);
|
||||
|
||||
$this->assertEquals(3, $counts['foo']);
|
||||
$this->assertEquals(1, $counts['bar']);
|
||||
$this->assertEquals(1, $counts['baz']);
|
||||
}
|
||||
|
||||
public function testGroupBySum(): void
|
||||
{
|
||||
$data = [
|
||||
['group' => 'A', 'amount' => 10],
|
||||
['group' => 'B', 'amount' => 20],
|
||||
['group' => 'A', 'amount' => 15],
|
||||
['group' => 'B', 'amount' => 25],
|
||||
];
|
||||
|
||||
$sums = ExternalGroupBy::groupBySum(
|
||||
$data,
|
||||
fn($item) => $item['group'],
|
||||
fn($item) => $item['amount']
|
||||
);
|
||||
|
||||
$this->assertEquals(25, $sums['A']);
|
||||
$this->assertEquals(45, $sums['B']);
|
||||
}
|
||||
|
||||
public function testGroupByAggregate(): void
|
||||
{
|
||||
$data = [
|
||||
['user' => 'john', 'score' => 80],
|
||||
['user' => 'jane', 'score' => 90],
|
||||
['user' => 'john', 'score' => 85],
|
||||
['user' => 'jane', 'score' => 95],
|
||||
];
|
||||
|
||||
$maxScores = ExternalGroupBy::groupByAggregate(
|
||||
$data,
|
||||
fn($item) => $item['user'],
|
||||
fn($max, $item) => max($max ?? 0, $item['score']),
|
||||
0
|
||||
);
|
||||
|
||||
$this->assertEquals(85, $maxScores['john']);
|
||||
$this->assertEquals(95, $maxScores['jane']);
|
||||
}
|
||||
|
||||
public function testGroupByStreaming(): void
|
||||
{
|
||||
$data = [];
|
||||
for ($i = 0; $i < 100; $i++) {
|
||||
$data[] = [
|
||||
'group' => chr(65 + ($i % 5)), // A-E
|
||||
'value' => $i,
|
||||
];
|
||||
}
|
||||
|
||||
$groups = [];
|
||||
foreach (ExternalGroupBy::groupByStreaming($data, fn($item) => $item['group']) as $key => $items) {
|
||||
$groups[$key] = count($items);
|
||||
}
|
||||
|
||||
$this->assertCount(5, $groups);
|
||||
$this->assertEquals(20, $groups['A']);
|
||||
$this->assertEquals(20, $groups['B']);
|
||||
}
|
||||
|
||||
public function testGroupByWithLimit(): void
|
||||
{
|
||||
$data = [];
|
||||
for ($i = 0; $i < 50; $i++) {
|
||||
$data[] = ['key' => "group_$i", 'value' => $i];
|
||||
}
|
||||
|
||||
$grouped = ExternalGroupBy::groupByWithLimit(
|
||||
$data,
|
||||
fn($item) => $item['key'],
|
||||
5 // Small limit to force external storage
|
||||
);
|
||||
|
||||
$this->assertCount(50, $grouped);
|
||||
|
||||
foreach ($grouped as $key => $items) {
|
||||
$this->assertCount(1, $items);
|
||||
$this->assertEquals($key, $items[0]['key']);
|
||||
}
|
||||
}
|
||||
}
|
||||
111
tests/Algorithms/ExternalSortTest.php
Normal file
111
tests/Algorithms/ExternalSortTest.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SqrtSpace\SpaceTime\Tests\Algorithms;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use SqrtSpace\SpaceTime\Algorithms\ExternalSort;
|
||||
use SqrtSpace\SpaceTime\SpaceTimeConfig;
|
||||
|
||||
class ExternalSortTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
SpaceTimeConfig::configure([
|
||||
'external_storage_path' => sys_get_temp_dir() . '/spacetime_test',
|
||||
]);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$path = sys_get_temp_dir() . '/spacetime_test';
|
||||
if (is_dir($path)) {
|
||||
array_map('unlink', glob("$path/*"));
|
||||
rmdir($path);
|
||||
}
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function testBasicSort(): void
|
||||
{
|
||||
$data = [5, 2, 8, 1, 9, 3, 7, 4, 6];
|
||||
$sorted = ExternalSort::sort($data);
|
||||
|
||||
$this->assertEquals([1, 2, 3, 4, 5, 6, 7, 8, 9], $sorted);
|
||||
}
|
||||
|
||||
public function testSortWithCustomComparator(): void
|
||||
{
|
||||
$data = [5, 2, 8, 1, 9, 3, 7, 4, 6];
|
||||
$sorted = ExternalSort::sort($data, fn($a, $b) => $b <=> $a);
|
||||
|
||||
$this->assertEquals([9, 8, 7, 6, 5, 4, 3, 2, 1], $sorted);
|
||||
}
|
||||
|
||||
public function testSortBy(): void
|
||||
{
|
||||
$data = [
|
||||
['name' => 'John', 'age' => 25],
|
||||
['name' => 'Jane', 'age' => 30],
|
||||
['name' => 'Bob', 'age' => 20],
|
||||
];
|
||||
|
||||
$sorted = ExternalSort::sortBy($data, fn($item) => $item['age']);
|
||||
|
||||
$this->assertEquals('Bob', $sorted[0]['name']);
|
||||
$this->assertEquals('John', $sorted[1]['name']);
|
||||
$this->assertEquals('Jane', $sorted[2]['name']);
|
||||
}
|
||||
|
||||
public function testLargeDataSet(): void
|
||||
{
|
||||
// Generate large dataset
|
||||
$data = [];
|
||||
for ($i = 0; $i < 20000; $i++) {
|
||||
$data[] = mt_rand(1, 100000);
|
||||
}
|
||||
|
||||
$sorted = ExternalSort::sort($data);
|
||||
|
||||
// Verify it's sorted
|
||||
for ($i = 1; $i < count($sorted); $i++) {
|
||||
$this->assertGreaterThanOrEqual($sorted[$i - 1], $sorted[$i]);
|
||||
}
|
||||
|
||||
// Verify same elements
|
||||
$this->assertEquals(count($data), count($sorted));
|
||||
sort($data);
|
||||
$this->assertEquals($data, $sorted);
|
||||
}
|
||||
|
||||
public function testSortObjects(): void
|
||||
{
|
||||
$objects = [
|
||||
(object)['id' => 3, 'value' => 'c'],
|
||||
(object)['id' => 1, 'value' => 'a'],
|
||||
(object)['id' => 2, 'value' => 'b'],
|
||||
];
|
||||
|
||||
$sorted = ExternalSort::sortBy($objects, fn($obj) => $obj->id);
|
||||
|
||||
$this->assertEquals(1, $sorted[0]->id);
|
||||
$this->assertEquals(2, $sorted[1]->id);
|
||||
$this->assertEquals(3, $sorted[2]->id);
|
||||
}
|
||||
|
||||
public function testStreamingSort(): void
|
||||
{
|
||||
$data = range(10, 1);
|
||||
$result = [];
|
||||
|
||||
foreach (ExternalSort::sortStreaming($data) as $item) {
|
||||
$result[] = $item;
|
||||
}
|
||||
|
||||
$this->assertEquals(range(1, 10), $result);
|
||||
}
|
||||
}
|
||||
132
tests/Batch/BatchProcessorTest.php
Normal file
132
tests/Batch/BatchProcessorTest.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SqrtSpace\SpaceTime\Tests\Batch;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use SqrtSpace\SpaceTime\Batch\BatchProcessor;
|
||||
use SqrtSpace\SpaceTime\Batch\BatchResult;
|
||||
|
||||
class BatchProcessorTest extends TestCase
|
||||
{
|
||||
public function testBasicBatchProcessing(): void
|
||||
{
|
||||
$processor = new BatchProcessor([
|
||||
'batch_size' => 3,
|
||||
'checkpoint_enabled' => false,
|
||||
]);
|
||||
|
||||
$items = range(1, 10);
|
||||
|
||||
$result = $processor->process($items, function($batch) {
|
||||
$processed = [];
|
||||
foreach ($batch as $key => $item) {
|
||||
$processed[$key] = $item * 2;
|
||||
}
|
||||
return $processed;
|
||||
});
|
||||
|
||||
$this->assertEquals(10, $result->getProcessedCount());
|
||||
$this->assertEquals(10, $result->getSuccessCount());
|
||||
$this->assertEquals(0, $result->getErrorCount());
|
||||
|
||||
// Check results
|
||||
$results = $result->getResults();
|
||||
$this->assertEquals(2, $results[0]);
|
||||
$this->assertEquals(20, $results[9]);
|
||||
}
|
||||
|
||||
public function testBatchProcessingWithErrors(): void
|
||||
{
|
||||
$processor = new BatchProcessor([
|
||||
'batch_size' => 2,
|
||||
'checkpoint_enabled' => false,
|
||||
'max_retries' => 1,
|
||||
]);
|
||||
|
||||
$items = range(1, 5);
|
||||
|
||||
$result = $processor->process($items, function($batch) {
|
||||
$processed = [];
|
||||
foreach ($batch as $key => $item) {
|
||||
if ($item === 3) {
|
||||
throw new \Exception('Error processing item 3');
|
||||
}
|
||||
$processed[$key] = $item * 2;
|
||||
}
|
||||
return $processed;
|
||||
});
|
||||
|
||||
$this->assertEquals(5, $result->getProcessedCount());
|
||||
$this->assertEquals(3, $result->getSuccessCount());
|
||||
$this->assertEquals(2, $result->getErrorCount());
|
||||
|
||||
$errors = $result->getErrors();
|
||||
$this->assertArrayHasKey(2, $errors); // Item 3 is at index 2
|
||||
}
|
||||
|
||||
public function testProgressCallback(): void
|
||||
{
|
||||
$progressCalls = [];
|
||||
|
||||
$processor = new BatchProcessor([
|
||||
'batch_size' => 2,
|
||||
'checkpoint_enabled' => false,
|
||||
'progress_callback' => function($batchNumber, $batchSize, $result) use (&$progressCalls) {
|
||||
$progressCalls[] = [
|
||||
'batch' => $batchNumber,
|
||||
'size' => $batchSize,
|
||||
'processed' => $result->getProcessedCount(),
|
||||
];
|
||||
},
|
||||
]);
|
||||
|
||||
$items = range(1, 5);
|
||||
$processor->process($items, fn($batch) => $batch);
|
||||
|
||||
$this->assertCount(3, $progressCalls); // 5 items in batches of 2
|
||||
$this->assertEquals(0, $progressCalls[0]['batch']);
|
||||
$this->assertEquals(2, $progressCalls[0]['size']);
|
||||
}
|
||||
|
||||
public function testBatchResult(): void
|
||||
{
|
||||
$result = new BatchResult();
|
||||
|
||||
$result->addSuccess('key1', 'value1');
|
||||
$result->addSuccess('key2', 'value2');
|
||||
$result->addError('key3', new \Exception('Error'));
|
||||
|
||||
$this->assertEquals(3, $result->getProcessedCount());
|
||||
$this->assertEquals(2, $result->getSuccessCount());
|
||||
$this->assertEquals(1, $result->getErrorCount());
|
||||
|
||||
$this->assertTrue($result->isProcessed('key1'));
|
||||
$this->assertFalse($result->isComplete());
|
||||
|
||||
$this->assertEquals('value1', $result->getResult('key1'));
|
||||
$this->assertNotNull($result->getError('key3'));
|
||||
|
||||
$summary = $result->getSummary();
|
||||
$this->assertEquals(3, $summary['total_processed']);
|
||||
$this->assertGreaterThan(0, $summary['execution_time']);
|
||||
}
|
||||
|
||||
public function testCheckpointingState(): void
|
||||
{
|
||||
$result = new BatchResult();
|
||||
|
||||
$result->addSuccess('a', 1);
|
||||
$result->addSuccess('b', 2);
|
||||
|
||||
$state = $result->getState();
|
||||
|
||||
$newResult = new BatchResult();
|
||||
$newResult->restore($state);
|
||||
|
||||
$this->assertEquals(2, $newResult->getProcessedCount());
|
||||
$this->assertEquals(1, $newResult->getResult('a'));
|
||||
$this->assertEquals(2, $newResult->getResult('b'));
|
||||
}
|
||||
}
|
||||
220
tests/Checkpoint/CheckpointManagerTest.php
Normal file
220
tests/Checkpoint/CheckpointManagerTest.php
Normal file
@@ -0,0 +1,220 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SqrtSpace\SpaceTime\Tests\Checkpoint;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use SqrtSpace\SpaceTime\Checkpoint\CheckpointManager;
|
||||
use SqrtSpace\SpaceTime\Checkpoint\CheckpointStorage;
|
||||
use SqrtSpace\SpaceTime\SpaceTimeConfig;
|
||||
|
||||
class CheckpointManagerTest extends TestCase
|
||||
{
|
||||
private CheckpointManager $manager;
|
||||
private CheckpointStorage $mockStorage;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// Create a mock storage
|
||||
$this->mockStorage = $this->createMock(CheckpointStorage::class);
|
||||
$this->manager = new CheckpointManager('test-checkpoint', $this->mockStorage);
|
||||
}
|
||||
|
||||
public function testShouldCheckpointReturnsFalseWhenCheckpointingDisabled(): void
|
||||
{
|
||||
// Mock the static config method
|
||||
$this->assertFalse($this->manager->shouldCheckpoint());
|
||||
}
|
||||
|
||||
public function testShouldCheckpointReturnsTrueAfterInterval(): void
|
||||
{
|
||||
// This test would need to mock SpaceTimeConfig::isCheckpointingEnabled()
|
||||
// and handle time-based logic
|
||||
$this->markTestSkipped('Requires static method mocking for SpaceTimeConfig');
|
||||
}
|
||||
|
||||
public function testSaveStoresCheckpointData(): void
|
||||
{
|
||||
$testData = ['progress' => 50, 'items_processed' => 100];
|
||||
|
||||
$this->mockStorage
|
||||
->expects($this->once())
|
||||
->method('save')
|
||||
->with(
|
||||
$this->equalTo('test-checkpoint'),
|
||||
$this->callback(function ($checkpoint) use ($testData) {
|
||||
return $checkpoint['id'] === 'test-checkpoint' &&
|
||||
isset($checkpoint['timestamp']) &&
|
||||
$checkpoint['data'] === $testData;
|
||||
})
|
||||
);
|
||||
|
||||
$this->manager->save($testData);
|
||||
}
|
||||
|
||||
public function testLoadReturnsCheckpointData(): void
|
||||
{
|
||||
$testData = ['progress' => 75, 'items_processed' => 150];
|
||||
$checkpoint = [
|
||||
'id' => 'test-checkpoint',
|
||||
'timestamp' => time(),
|
||||
'data' => $testData,
|
||||
];
|
||||
|
||||
$this->mockStorage
|
||||
->expects($this->once())
|
||||
->method('load')
|
||||
->with('test-checkpoint')
|
||||
->willReturn($checkpoint);
|
||||
|
||||
$loadedData = $this->manager->load();
|
||||
$this->assertEquals($testData, $loadedData);
|
||||
}
|
||||
|
||||
public function testLoadReturnsNullWhenNoCheckpoint(): void
|
||||
{
|
||||
$this->mockStorage
|
||||
->expects($this->once())
|
||||
->method('load')
|
||||
->with('test-checkpoint')
|
||||
->willReturn(null);
|
||||
|
||||
$this->assertNull($this->manager->load());
|
||||
}
|
||||
|
||||
public function testLoadReturnsNullWhenCheckpointHasNoData(): void
|
||||
{
|
||||
$checkpoint = [
|
||||
'id' => 'test-checkpoint',
|
||||
'timestamp' => time(),
|
||||
// No 'data' key
|
||||
];
|
||||
|
||||
$this->mockStorage
|
||||
->expects($this->once())
|
||||
->method('load')
|
||||
->with('test-checkpoint')
|
||||
->willReturn($checkpoint);
|
||||
|
||||
$this->assertNull($this->manager->load());
|
||||
}
|
||||
|
||||
public function testExistsReturnsStorageResult(): void
|
||||
{
|
||||
$this->mockStorage
|
||||
->expects($this->once())
|
||||
->method('exists')
|
||||
->with('test-checkpoint')
|
||||
->willReturn(true);
|
||||
|
||||
$this->assertTrue($this->manager->exists());
|
||||
|
||||
$this->mockStorage
|
||||
->expects($this->once())
|
||||
->method('exists')
|
||||
->with('test-checkpoint')
|
||||
->willReturn(false);
|
||||
|
||||
$this->assertFalse($this->manager->exists());
|
||||
}
|
||||
|
||||
public function testDeleteRemovesCheckpoint(): void
|
||||
{
|
||||
$this->mockStorage
|
||||
->expects($this->once())
|
||||
->method('delete')
|
||||
->with('test-checkpoint');
|
||||
|
||||
$this->manager->delete();
|
||||
}
|
||||
|
||||
public function testSetIntervalUpdatesCheckpointInterval(): void
|
||||
{
|
||||
// Test minimum interval of 1 second
|
||||
$this->manager->setInterval(0);
|
||||
// We can't directly test the interval value since it's private,
|
||||
// but we can ensure the method doesn't throw an exception
|
||||
$this->assertTrue(true);
|
||||
|
||||
$this->manager->setInterval(120);
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
public function testWrapExecutesOperationWithInitialState(): void
|
||||
{
|
||||
$initialState = ['counter' => 0];
|
||||
$expectedResult = 'operation completed';
|
||||
|
||||
$this->mockStorage
|
||||
->expects($this->once())
|
||||
->method('load')
|
||||
->willReturn(null); // No existing checkpoint
|
||||
|
||||
$this->mockStorage
|
||||
->expects($this->once())
|
||||
->method('delete')
|
||||
->with('test-checkpoint');
|
||||
|
||||
$operation = function ($state, $manager) use ($expectedResult) {
|
||||
$this->assertEquals(['counter' => 0], $state);
|
||||
$this->assertInstanceOf(CheckpointManager::class, $manager);
|
||||
return $expectedResult;
|
||||
};
|
||||
|
||||
$result = $this->manager->wrap($operation, $initialState);
|
||||
$this->assertEquals($expectedResult, $result);
|
||||
}
|
||||
|
||||
public function testWrapResumesFromCheckpoint(): void
|
||||
{
|
||||
$checkpointData = ['counter' => 50];
|
||||
$checkpoint = [
|
||||
'id' => 'test-checkpoint',
|
||||
'timestamp' => time(),
|
||||
'data' => $checkpointData,
|
||||
];
|
||||
|
||||
$this->mockStorage
|
||||
->expects($this->once())
|
||||
->method('load')
|
||||
->willReturn($checkpoint);
|
||||
|
||||
$this->mockStorage
|
||||
->expects($this->once())
|
||||
->method('delete')
|
||||
->with('test-checkpoint');
|
||||
|
||||
$operation = function ($state, $manager) {
|
||||
$this->assertEquals(['counter' => 50], $state);
|
||||
return 'resumed and completed';
|
||||
};
|
||||
|
||||
$result = $this->manager->wrap($operation, ['counter' => 0]);
|
||||
$this->assertEquals('resumed and completed', $result);
|
||||
}
|
||||
|
||||
public function testWrapPreservesCheckpointOnException(): void
|
||||
{
|
||||
$this->mockStorage
|
||||
->expects($this->once())
|
||||
->method('load')
|
||||
->willReturn(null);
|
||||
|
||||
// Delete should NOT be called when exception is thrown
|
||||
$this->mockStorage
|
||||
->expects($this->never())
|
||||
->method('delete');
|
||||
|
||||
$operation = function ($state, $manager) {
|
||||
throw new \RuntimeException('Operation failed');
|
||||
};
|
||||
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$this->expectExceptionMessage('Operation failed');
|
||||
|
||||
$this->manager->wrap($operation);
|
||||
}
|
||||
}
|
||||
135
tests/Collections/SpaceTimeArrayTest.php
Normal file
135
tests/Collections/SpaceTimeArrayTest.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SqrtSpace\SpaceTime\Tests\Collections;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use SqrtSpace\SpaceTime\Collections\SpaceTimeArray;
|
||||
use SqrtSpace\SpaceTime\SpaceTimeConfig;
|
||||
|
||||
class SpaceTimeArrayTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// Configure for testing
|
||||
SpaceTimeConfig::configure([
|
||||
'memory_limit' => '10M',
|
||||
'external_storage_path' => sys_get_temp_dir() . '/spacetime_test',
|
||||
]);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
// Clean up test files
|
||||
$path = sys_get_temp_dir() . '/spacetime_test';
|
||||
if (is_dir($path)) {
|
||||
array_map('unlink', glob("$path/*"));
|
||||
rmdir($path);
|
||||
}
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function testBasicArrayOperations(): void
|
||||
{
|
||||
$array = new SpaceTimeArray(100);
|
||||
|
||||
// Test set and get
|
||||
$array['key1'] = 'value1';
|
||||
$this->assertEquals('value1', $array['key1']);
|
||||
|
||||
// Test isset
|
||||
$this->assertTrue(isset($array['key1']));
|
||||
$this->assertFalse(isset($array['key2']));
|
||||
|
||||
// Test unset
|
||||
unset($array['key1']);
|
||||
$this->assertFalse(isset($array['key1']));
|
||||
|
||||
// Test count
|
||||
$array['a'] = 1;
|
||||
$array['b'] = 2;
|
||||
$this->assertEquals(2, count($array));
|
||||
}
|
||||
|
||||
public function testSpilloverToExternalStorage(): void
|
||||
{
|
||||
$array = new SpaceTimeArray(2); // Small threshold
|
||||
|
||||
// Add items that will stay in memory
|
||||
$array['hot1'] = 'value1';
|
||||
$array['hot2'] = 'value2';
|
||||
|
||||
// This should trigger spillover
|
||||
$array['cold1'] = 'value3';
|
||||
$array['cold2'] = 'value4';
|
||||
|
||||
// All items should still be accessible
|
||||
$this->assertEquals('value1', $array['hot1']);
|
||||
$this->assertEquals('value3', $array['cold1']);
|
||||
|
||||
// Count should include all items
|
||||
$this->assertEquals(4, count($array));
|
||||
}
|
||||
|
||||
public function testIterator(): void
|
||||
{
|
||||
$array = new SpaceTimeArray(2);
|
||||
|
||||
$data = ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4];
|
||||
foreach ($data as $key => $value) {
|
||||
$array[$key] = $value;
|
||||
}
|
||||
|
||||
// Test iteration
|
||||
$result = [];
|
||||
foreach ($array as $key => $value) {
|
||||
$result[$key] = $value;
|
||||
}
|
||||
|
||||
$this->assertEquals($data, $result);
|
||||
}
|
||||
|
||||
public function testLargeDataSet(): void
|
||||
{
|
||||
$array = new SpaceTimeArray(100);
|
||||
|
||||
// Add 1000 items
|
||||
for ($i = 0; $i < 1000; $i++) {
|
||||
$array["key_$i"] = "value_$i";
|
||||
}
|
||||
|
||||
// Verify count
|
||||
$this->assertEquals(1000, count($array));
|
||||
|
||||
// Verify random access
|
||||
$this->assertEquals('value_500', $array['key_500']);
|
||||
$this->assertEquals('value_999', $array['key_999']);
|
||||
$this->assertEquals('value_0', $array['key_0']);
|
||||
}
|
||||
|
||||
public function testArrayMethods(): void
|
||||
{
|
||||
$array = new SpaceTimeArray(10);
|
||||
|
||||
$array['a'] = 1;
|
||||
$array['b'] = 2;
|
||||
$array['c'] = 3;
|
||||
|
||||
// Test toArray
|
||||
$this->assertEquals(['a' => 1, 'b' => 2, 'c' => 3], $array->toArray());
|
||||
|
||||
// Test keys
|
||||
$this->assertEquals(['a', 'b', 'c'], $array->keys());
|
||||
|
||||
// Test values
|
||||
$this->assertEquals([1, 2, 3], $array->values());
|
||||
|
||||
// Test clear
|
||||
$array->clear();
|
||||
$this->assertEquals(0, count($array));
|
||||
}
|
||||
}
|
||||
93
tests/Memory/MemoryPressureMonitorTest.php
Normal file
93
tests/Memory/MemoryPressureMonitorTest.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SqrtSpace\SpaceTime\Tests\Memory;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use SqrtSpace\SpaceTime\Memory\MemoryPressureMonitor;
|
||||
use SqrtSpace\SpaceTime\Memory\MemoryPressureLevel;
|
||||
use SqrtSpace\SpaceTime\Memory\MemoryPressureHandler;
|
||||
|
||||
class MemoryPressureMonitorTest extends TestCase
|
||||
{
|
||||
public function testMemoryPressureLevels(): void
|
||||
{
|
||||
$monitor = new MemoryPressureMonitor('100M');
|
||||
|
||||
// Get current level
|
||||
$level = $monitor->getCurrentLevel();
|
||||
$this->assertInstanceOf(MemoryPressureLevel::class, $level);
|
||||
}
|
||||
|
||||
public function testMemoryInfo(): void
|
||||
{
|
||||
$monitor = new MemoryPressureMonitor();
|
||||
$info = $monitor->getMemoryInfo();
|
||||
|
||||
$this->assertArrayHasKey('limit', $info);
|
||||
$this->assertArrayHasKey('usage', $info);
|
||||
$this->assertArrayHasKey('percentage', $info);
|
||||
$this->assertArrayHasKey('available', $info);
|
||||
|
||||
$this->assertGreaterThan(0, $info['limit']);
|
||||
$this->assertGreaterThanOrEqual(0, $info['usage']);
|
||||
$this->assertGreaterThanOrEqual(0, $info['percentage']);
|
||||
$this->assertLessThanOrEqual(100, $info['percentage']);
|
||||
}
|
||||
|
||||
public function testHandlerRegistration(): void
|
||||
{
|
||||
$monitor = new MemoryPressureMonitor();
|
||||
|
||||
$handlerCalled = false;
|
||||
$handler = new class($handlerCalled) implements MemoryPressureHandler {
|
||||
private $called;
|
||||
|
||||
public function __construct(&$called)
|
||||
{
|
||||
$this->called = &$called;
|
||||
}
|
||||
|
||||
public function shouldHandle(MemoryPressureLevel $level): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function handle(MemoryPressureLevel $level, array $memoryInfo): void
|
||||
{
|
||||
$this->called = true;
|
||||
}
|
||||
};
|
||||
|
||||
$monitor->registerHandler($handler);
|
||||
$monitor->check();
|
||||
|
||||
$this->assertTrue($handlerCalled);
|
||||
}
|
||||
|
||||
public function testMemoryLimitParsing(): void
|
||||
{
|
||||
// Test various memory limit formats
|
||||
$testCases = [
|
||||
'256M' => 256 * 1024 * 1024,
|
||||
'1G' => 1024 * 1024 * 1024,
|
||||
'512K' => 512 * 1024,
|
||||
'1024' => 1024,
|
||||
];
|
||||
|
||||
foreach ($testCases as $limit => $expected) {
|
||||
$monitor = new MemoryPressureMonitor($limit);
|
||||
$info = $monitor->getMemoryInfo();
|
||||
$this->assertEquals($expected, $info['limit']);
|
||||
}
|
||||
}
|
||||
|
||||
public function testPressureLevelComparison(): void
|
||||
{
|
||||
$this->assertTrue(MemoryPressureLevel::HIGH->isHigherThan(MemoryPressureLevel::MEDIUM));
|
||||
$this->assertTrue(MemoryPressureLevel::CRITICAL->isHigherThan(MemoryPressureLevel::HIGH));
|
||||
$this->assertFalse(MemoryPressureLevel::LOW->isHigherThan(MemoryPressureLevel::MEDIUM));
|
||||
$this->assertFalse(MemoryPressureLevel::NONE->isHigherThan(MemoryPressureLevel::LOW));
|
||||
}
|
||||
}
|
||||
161
tests/Streams/SpaceTimeStreamTest.php
Normal file
161
tests/Streams/SpaceTimeStreamTest.php
Normal file
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SqrtSpace\SpaceTime\Tests\Streams;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use SqrtSpace\SpaceTime\Streams\SpaceTimeStream;
|
||||
|
||||
class SpaceTimeStreamTest extends TestCase
|
||||
{
|
||||
private string $testFile;
|
||||
private string $testCsv;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->testFile = sys_get_temp_dir() . '/test_stream.txt';
|
||||
file_put_contents($this->testFile, "line1\nline2\nline3\nline4\nline5");
|
||||
|
||||
$this->testCsv = sys_get_temp_dir() . '/test_stream.csv';
|
||||
file_put_contents($this->testCsv, "name,age\nJohn,25\nJane,30\nBob,20");
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
if (file_exists($this->testFile)) {
|
||||
unlink($this->testFile);
|
||||
}
|
||||
if (file_exists($this->testCsv)) {
|
||||
unlink($this->testCsv);
|
||||
}
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function testFromArray(): void
|
||||
{
|
||||
$data = [1, 2, 3, 4, 5];
|
||||
$result = SpaceTimeStream::from($data)->toArray();
|
||||
|
||||
$this->assertEquals($data, $result);
|
||||
}
|
||||
|
||||
public function testMap(): void
|
||||
{
|
||||
$data = [1, 2, 3, 4, 5];
|
||||
$result = SpaceTimeStream::from($data)
|
||||
->map(fn($x) => $x * 2)
|
||||
->toArray();
|
||||
|
||||
$this->assertEquals([2, 4, 6, 8, 10], $result);
|
||||
}
|
||||
|
||||
public function testFilter(): void
|
||||
{
|
||||
$data = [1, 2, 3, 4, 5];
|
||||
$result = SpaceTimeStream::from($data)
|
||||
->filter(fn($x) => $x % 2 === 0)
|
||||
->toArray();
|
||||
|
||||
$this->assertEquals([1 => 2, 3 => 4], $result);
|
||||
}
|
||||
|
||||
public function testChaining(): void
|
||||
{
|
||||
$data = range(1, 10);
|
||||
$result = SpaceTimeStream::from($data)
|
||||
->filter(fn($x) => $x % 2 === 0)
|
||||
->map(fn($x) => $x * 2)
|
||||
->take(3)
|
||||
->toArray();
|
||||
|
||||
$expected = [1 => 4, 3 => 8, 5 => 12];
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
public function testFromFile(): void
|
||||
{
|
||||
$lines = SpaceTimeStream::fromFile($this->testFile)->toArray();
|
||||
|
||||
$this->assertEquals(['line1', 'line2', 'line3', 'line4', 'line5'], $lines);
|
||||
}
|
||||
|
||||
public function testFromCsv(): void
|
||||
{
|
||||
$rows = SpaceTimeStream::fromCsv($this->testCsv)->toArray();
|
||||
|
||||
$expected = [
|
||||
['name' => 'John', 'age' => '25'],
|
||||
['name' => 'Jane', 'age' => '30'],
|
||||
['name' => 'Bob', 'age' => '20'],
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, $rows);
|
||||
}
|
||||
|
||||
public function testReduce(): void
|
||||
{
|
||||
$sum = SpaceTimeStream::from([1, 2, 3, 4, 5])
|
||||
->reduce(fn($acc, $x) => $acc + $x, 0);
|
||||
|
||||
$this->assertEquals(15, $sum);
|
||||
}
|
||||
|
||||
public function testCount(): void
|
||||
{
|
||||
$count = SpaceTimeStream::from(range(1, 100))
|
||||
->filter(fn($x) => $x % 2 === 0)
|
||||
->count();
|
||||
|
||||
$this->assertEquals(50, $count);
|
||||
}
|
||||
|
||||
public function testChunk(): void
|
||||
{
|
||||
$chunks = SpaceTimeStream::from(range(1, 10))
|
||||
->chunk(3)
|
||||
->toArray();
|
||||
|
||||
$this->assertCount(4, $chunks);
|
||||
$this->assertEquals([1, 2, 3], $chunks[0]);
|
||||
$this->assertEquals([4, 5, 6], $chunks[1]);
|
||||
$this->assertEquals([7, 8, 9], $chunks[2]);
|
||||
$this->assertEquals([10], $chunks[3]);
|
||||
}
|
||||
|
||||
public function testFlatMap(): void
|
||||
{
|
||||
$data = [[1, 2], [3, 4], [5]];
|
||||
$result = SpaceTimeStream::from($data)
|
||||
->flatMap(fn($arr) => $arr)
|
||||
->toArray();
|
||||
|
||||
$this->assertEquals([1, 2, 3, 4, 5], array_values($result));
|
||||
}
|
||||
|
||||
public function testSkip(): void
|
||||
{
|
||||
$result = SpaceTimeStream::from(range(1, 10))
|
||||
->skip(5)
|
||||
->toArray();
|
||||
|
||||
$this->assertEquals([6, 7, 8, 9, 10], array_values($result));
|
||||
}
|
||||
|
||||
public function testWriteToFile(): void
|
||||
{
|
||||
$outputFile = sys_get_temp_dir() . '/output_stream.txt';
|
||||
|
||||
SpaceTimeStream::from(['a', 'b', 'c'])
|
||||
->map(fn($x) => strtoupper($x))
|
||||
->toFile($outputFile);
|
||||
|
||||
$content = file_get_contents($outputFile);
|
||||
$this->assertEquals("A\nB\nC\n", $content);
|
||||
|
||||
unlink($outputFile);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user