From f748d31c481b7d7a62c13a27fb3321961868578f Mon Sep 17 00:00:00 2001 From: James Lucas Date: Wed, 21 Jun 2023 13:17:44 +1000 Subject: [PATCH] Add PHPUnit and add tests for TimeBucket, TimeOrderedArray and PeriodEstimator --- .gitignore | 3 +- composer.json | 5 +- phpunit.xml | 27 +++ tests/PeriodEstimatorTest.php | 33 +++ tests/TimeBucketTest.php | 189 ++++++++++++++++++ tests/{ => old_tests}/arbitaryInterval.php | 0 tests/old_tests/quicktest.php | 80 ++++++++ .../stresstest.php} | 4 +- tests/{ => old_tests}/timeSeriesFrequency.php | 0 tests/timeOrderedArrayTest.php | 187 ++++++++++++----- 10 files changed, 469 insertions(+), 59 deletions(-) create mode 100644 phpunit.xml create mode 100644 tests/PeriodEstimatorTest.php create mode 100644 tests/TimeBucketTest.php rename tests/{ => old_tests}/arbitaryInterval.php (100%) create mode 100644 tests/old_tests/quicktest.php rename tests/{timeBucketOrderTest.php => old_tests/stresstest.php} (97%) rename tests/{ => old_tests}/timeSeriesFrequency.php (100%) diff --git a/.gitignore b/.gitignore index 2f5b296..f0245ec 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ composer.phar composer.lock .idea .DS_Store -.~lock.* \ No newline at end of file +.~lock.* +.phpunit.* \ No newline at end of file diff --git a/composer.json b/composer.json index 6d6bc8b..f97002e 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,10 @@ "require": { "php": "^8.0", "ext-json": "*" - }, + }, + "require-dev": { + "phpunit/phpunit": "^9.6" + }, "autoload": { "psr-4": {"EdgeTelemetrics\\TimeBucket\\": "src/"} } diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..766495c --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,27 @@ + + + + + tests + + + + + + src + + + diff --git a/tests/PeriodEstimatorTest.php b/tests/PeriodEstimatorTest.php new file mode 100644 index 0000000..273b270 --- /dev/null +++ b/tests/PeriodEstimatorTest.php @@ -0,0 +1,33 @@ +add(new DateInterval("PT" . $i*2 . "M")); + $bucket->insert( 'test', $time); + } + + $periodEstimator = new PeriodEstimator(); + $estimatedPeriod = $periodEstimator->estimate($bucket); + + $this->assertEquals('P0Y0M0DT0H2M0S', $estimatedPeriod->format('P%yY%mM%dDT%hH%iM%sS')); + } + +} \ No newline at end of file diff --git a/tests/TimeBucketTest.php b/tests/TimeBucketTest.php new file mode 100644 index 0000000..50874dd --- /dev/null +++ b/tests/TimeBucketTest.php @@ -0,0 +1,189 @@ +assertEquals('UTC', $bucket->getTimezone()->getName()); + } + + public function testConstructorTakesTimezoneString() { + $timezone = 'Australia/Sydney'; + $bucket = new TimeBucket('second', $timezone); + $this->assertEquals($timezone, $bucket->getTimezone()->getName()); + } + + public function testConstructorTakesTimezoneObject() { + $timezone = new DateTimeZone('Australia/Sydney'); + $bucket = new TimeBucket('second', $timezone); + $this->assertEquals($timezone, $bucket->getTimezone()); + } + + public function testEmptyBucketIsEmpty() { + $bucket = new TimeBucket(); + $this->assertTrue($bucket->isEmpty()); + $this->assertFalse($bucket->nextTimeSlice()); + $this->assertNull($bucket->getTimeSlices()->getReturn()); + + $this->expectException(RuntimeException::class); + $bucket->extractTimeSlice(); + } + + public function testEmptyBucketHasNoTimeIndex() { + $bucket = new TimeBucket(); + $gen = $bucket->getTimeIndex(); + + $this->assertNull($gen->getReturn()); + } + + public function testBucketAfterSingleInsert() { + $bucket = new TimeBucket(); + $this->assertTrue($bucket->isEmpty()); + + $bucket->insert('test', 1); + $this->assertFalse($bucket->isEmpty()); + $this->assertEquals(1, $bucket->count()); + $this->assertEquals(1, $bucket->sliceCount()); + $this->assertEquals(1, $bucket->nextTimeSliceCount()); + } + + public function testNextTimeSliceReturnsOneSlice() { + $bucket = new TimeBucket('unixtime'); + + $now = time(); + $bucket->insert('correct', $now); + $bucket->insert('incorrect', $now+1); + + ['time' => $timestamp, 'data' => $data] = $bucket->nextTimeSlice(); + + $this->assertEquals(1, count($data)); + } + + public function testCorrectTimesliceRetrieval() { + $bucket = new TimeBucket('unixtime'); + + $now = time(); + $bucket->insert('slice1', $now); + $bucket->insert('slice2-1', $now+1); + $bucket->insert('slice2-2', $now+1); + $bucket->insert('slice3', $now+2); + + $slices = iterator_to_array($bucket->getTimeSlices()); + + $this->assertEquals(3, count($slices)); + $this->assertEquals(1, count($slices[0]['data'])); + $this->assertEquals(2, count($slices[1]['data'])); + $this->assertEquals(1, count($slices[2]['data'])); + + $this->assertFalse($bucket->isEmpty()); //getTimeSlices is non-destructive + $this->assertEquals(4, $bucket->count()); + } + + public function testCanInsertTimestampPriority() { + $bucket = new TimeBucket('unixtime'); + + $now = time(); + $bucket->insert('test', $now); + + ['time' => $timestamp, 'data' => $data] = $bucket->nextTimeSlice(); + + $this->assertEquals($now, $timestamp); + } + + public function testCanInsertDateTimePriority() { + $bucket = new TimeBucket('unixtime'); + + $now = new DateTime(); + $bucket->insert('test', $now); + + ['time' => $timestamp, 'data' => $data] = $bucket->nextTimeSlice(); + $this->assertEquals($now->format('U'), $timestamp); + } + + public function testCanInsertDateTimeImmutablePriority() { + $bucket = new TimeBucket('unixtime'); + + $now = new DateTimeImmutable(); + $bucket->insert('test', $now); + + ['time' => $timestamp, 'data' => $data] = $bucket->nextTimeSlice(); + $this->assertEquals($now->format('U'), $timestamp); + } + + public function testCanInsertDateTimeString() { + $bucket = new TimeBucket('unixtime'); + + $now = time(); + $bucket->insert('test', 'now'); + $then = time(); + + ['time' => $timestamp, 'data' => $data] = $bucket->nextTimeSlice(); + + $this->assertGreaterThanOrEqual($now, $timestamp); + $this->assertLessThanOrEqual($then, $timestamp); + } + + public function testConstructorSliceFormat() { + $bucket = new TimeBucket('quarter'); + + $this->assertEquals(TimeBucket::SLICE_FORMATS['quarter'], $bucket->getTimeFormat()); + } + + public function testMinuteIntervalsCanBeUser() { + $bucket = new TimeBucket('5 minute'); + + $time = new DateTimeImmutable('2023-01-01 12:31:00'); + + $bucket->insert('test', $time); + + ['time' => $timestamp, 'data' => $data] = $bucket->nextTimeSlice(); + + $this->assertEquals($time->format('Y-m-d H:30:00'), $timestamp); + } + + public function testCorrectTimesliceExtractionToEmpty() { + $bucket = new TimeBucket('unixtime'); + + $now = time(); + $bucket->insert('slice1', $now); + $bucket->insert('slice2-1', $now+1); + $bucket->insert('slice2-2', $now+1); + $bucket->insert('slice3', $now+2); + + $slices = []; + while(!$bucket->isEmpty()) { + $slices[] = $bucket->extractTimeSlice(); + } + + $this->assertEquals(3, count($slices)); + $this->assertEquals(1, count($slices[0]['data'])); + $this->assertEquals(2, count($slices[1]['data'])); + $this->assertEquals(1, count($slices[2]['data'])); + + $this->assertTrue($bucket->isEmpty()); //extractTimeSlice is destructive + } + + public function testBucketClonesNotLinked() { + $bucket = new TimeBucket('unixtime'); + + $bucket->insert('test', 1); + + $clone = clone $bucket; + $clone->insert('abc', 2); + $clone->extractTimeSlice(); + $clone->extractTimeSlice(); + + $this->assertEquals(0, $clone->count()); + $this->assertEquals(1, $bucket->count()); + } + +} \ No newline at end of file diff --git a/tests/arbitaryInterval.php b/tests/old_tests/arbitaryInterval.php similarity index 100% rename from tests/arbitaryInterval.php rename to tests/old_tests/arbitaryInterval.php diff --git a/tests/old_tests/quicktest.php b/tests/old_tests/quicktest.php new file mode 100644 index 0000000..79a653e --- /dev/null +++ b/tests/old_tests/quicktest.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +error_reporting(E_ALL); +ini_set('display_errors', "on"); + +use \EdgeTelemetrics\TimeBucket\TimeOrderedArray; +use \EdgeTelemetrics\TimeBucket\TimeOrderedQueue; + +// Load Composer +require '../vendor/autoload.php'; + +$spl = new TimeOrderedQueue(); +$spl->insert("test c", 3); +$spl->insert("test b", 2); +$spl->insert("test a", 1); +$spl->insert("test b2", 2); + +echo "TimeOrderedQueue" . PHP_EOL; +$spl->setExtractFlags(SplPriorityQueue::EXTR_DATA); +echo "Data Only: "; +print_r($spl->extract()); +$spl->setExtractFlags(SplPriorityQueue::EXTR_PRIORITY); +echo PHP_EOL . "Priority Only: "; +print_r($spl->extract()); +$spl->setExtractFlags(SplPriorityQueue::EXTR_BOTH); +echo PHP_EOL . "Both: "; +print_r($spl->extract()); +echo PHP_EOL . "Final: "; +print_r($spl->extract()); + +$bucket = new TimeOrderedArray(); + +$bucket->insert("test c", 3); +$bucket->insert("test b", 2); +$bucket->insert("test a", 1); +$bucket->insert("test b2", 2); + +echo PHP_EOL . PHP_EOL . "TimeOrderedArray" . PHP_EOL; + +$bucket->setExtractFlags(SplPriorityQueue::EXTR_DATA); +echo "Data Only: "; +print_r($bucket->extract()); +$bucket->setExtractFlags(SplPriorityQueue::EXTR_PRIORITY); +echo PHP_EOL . "Priority Only: "; +print_r($bucket->extract()); +$bucket->setExtractFlags(SplPriorityQueue::EXTR_BOTH); +echo PHP_EOL . "Both: "; +print_r($bucket->extract()); +echo PHP_EOL . "Final: "; +print_r($bucket->extract()); + +$bucket = new \EdgeTelemetrics\TimeBucket\TimeOrderedSqlite(); + +$bucket->insert("test c", 3); +$bucket->insert("test b", 2); +$bucket->insert("test a", 1); +$bucket->insert("test b2", 2); + +echo PHP_EOL . PHP_EOL . "TimeOrderedSql" . PHP_EOL; + +$bucket->setExtractFlags(SplPriorityQueue::EXTR_DATA); +echo "Data Only: "; +print_r($bucket->extract()); +$bucket->setExtractFlags(SplPriorityQueue::EXTR_PRIORITY); +echo PHP_EOL . "Priority Only: "; +print_r($bucket->extract()); +$bucket->setExtractFlags(SplPriorityQueue::EXTR_BOTH); +echo PHP_EOL . "Both: "; +print_r($bucket->extract()); +echo PHP_EOL . "Final: "; +print_r($bucket->extract()); \ No newline at end of file diff --git a/tests/timeBucketOrderTest.php b/tests/old_tests/stresstest.php similarity index 97% rename from tests/timeBucketOrderTest.php rename to tests/old_tests/stresstest.php index a2b1637..511e428 100644 --- a/tests/timeBucketOrderTest.php +++ b/tests/old_tests/stresstest.php @@ -76,7 +76,7 @@ public function __construct($array) echo '**** Validate nextTimeSlice()' . PHP_EOL; ['time' => $time, 'data' => $data] = $bucket->nextTimeSlice(); echo $time . PHP_EOL; -echo count($data) . PHP_EOL; +echo timeBucketOrderTest . phpcount($data) . PHP_EOL; echo '**** Validate getTimeSlices()' . PHP_EOL; $totalDatapoints = 0; @@ -97,7 +97,7 @@ public function __construct($array) echo "Before Extract - SliceCount: " . $bucket->sliceCount() . ", DataPoints: " . count($bucket) . ", NextSliceCount: " . $bucket->nextTimeSliceCount() . PHP_EOL; ['time' => $time, 'data' => $data] = $bucket->extractTimeSlice(); echo $time . PHP_EOL; -echo count($data) . PHP_EOL; +echo timeBucketOrderTest . phpcount($data) . PHP_EOL; echo "After Extract - SliceCount: " . $bucket->sliceCount() . ", DataPoints: " . count($bucket) . ", NextSliceCount: " . $bucket->nextTimeSliceCount() . PHP_EOL; echo "Memory before emptying TimeBucket : " . round(memory_get_usage(false) / 1024) . "KB" . PHP_EOL; diff --git a/tests/timeSeriesFrequency.php b/tests/old_tests/timeSeriesFrequency.php similarity index 100% rename from tests/timeSeriesFrequency.php rename to tests/old_tests/timeSeriesFrequency.php diff --git a/tests/timeOrderedArrayTest.php b/tests/timeOrderedArrayTest.php index b6fd06f..94b75b5 100644 --- a/tests/timeOrderedArrayTest.php +++ b/tests/timeOrderedArrayTest.php @@ -1,59 +1,136 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. +use EdgeTelemetrics\TimeBucket\TimeOrderedArray; +use PHPUnit\Framework\TestCase; + +require_once __DIR__ . '/../vendor/autoload.php'; + +/** + * @covers \EdgeTelemetrics\TimeBucket\TimeOrderedArray */ +class TimeOrderedArrayTest extends TestCase +{ + public function provideRandomDataset() + { + $dataset = []; + $now = time(); + for($i = 0; $i < 20; $i++) { + $dataset[] = [ + 'priority' => mt_rand($now - 86400, $now + 86400), + 'data' => bin2hex(random_bytes(5)), + ]; + } + + return [ + 'dataset' => [$dataset] + ]; + } + public function testQueueAfterSingleInsert() { + $queue = new TimeOrderedArray(); + + $queue->insert('test', 1); + $this->assertEquals(1, $queue->count()); + $this->assertEquals(1, $queue->peekSetCount()); + $this->assertEquals('test', $queue->current()); + } + + public function testPriorityCountAfterInsert() { + $queue = new TimeOrderedArray(); + + $queue->insert('test', 1); + $queue->insert('test', 2); + $queue->insert('test', 3); + $queue->insert('test', 3); + $queue->insert('test', 4); + $this->assertEquals(5, $queue->count()); + $this->assertEquals(1, $queue->peekSetCount()); + $this->assertEquals(4, $queue->priorityCount()); + } + + /** + * @dataProvider provideRandomDataset + */ + public function testQueueExtractionInOrder($dataset) { + $queue = new TimeOrderedArray(); + $queue->setExtractFlags(SplPriorityQueue::EXTR_PRIORITY); + + foreach($dataset as ['data' => $data, 'priority' => $priority]) { + $queue->insert($data, $priority); + } + + $this->assertEquals(count($dataset), $queue->count()); + + $prevPriority = $queue->top(); + $extracted = 0; + while (!$queue->isEmpty()) { + $priority = $queue->extract(); + $this->assertGreaterThanOrEqual($prevPriority, $priority); + ++$extracted; + } + + $this->assertEquals(count($dataset), $extracted); + $this->assertEquals(0, $queue->count()); + } + + /** + * @dataProvider provideRandomDataset + */ + public function testQueueIterationInOrder($dataset) { + $queue = new TimeOrderedArray(); + $queue->setExtractFlags(SplPriorityQueue::EXTR_PRIORITY); + + foreach($dataset as ['data' => $data, 'priority' => $priority]) { + $queue->insert($data, $priority); + } + + $this->assertEquals(count($dataset), $queue->count()); + + $prevPriority = $queue->top(); + $extracted = 0; + foreach($queue as $index => $priority) { + $this->assertGreaterThanOrEqual($prevPriority, $priority); + ++$extracted; + } + + $this->assertEquals(count($dataset), $extracted); + $this->assertEquals(0, $queue->count()); + } + + public function testEmptyQueueIsEmpty() { + $queue = new TimeOrderedArray(); + + $this->assertFalse($queue->valid()); + $this->assertTrue($queue->isEmpty()); + $this->assertEquals(0, $queue->peekSetCount()); + } + + public function testExtractEmptyQueueReturnsFalse() { + $queue = new TimeOrderedArray(); + + $this->assertFalse($queue->extract()); + $this->assertFalse($queue->top()); + } + + public function testQueueClonesNotLinked() { + $queue = new TimeOrderedArray(); + + $queue->insert('test', 1); + + $clone = clone $queue; + $clone->insert('abc', 2); + $clone->extract(); + $clone->extract(); + + $this->assertEquals(0, $clone->count()); + $this->assertEquals(1, $queue->count()); + } + + public function testCanChangeExtractFlags() { + $queue = new TimeOrderedArray(); + + $this->assertEquals(SplPriorityQueue::EXTR_DATA, $queue->getExtractFlags()); + $queue->setExtractFlags(SplPriorityQueue::EXTR_BOTH); + $this->assertEquals(SplPriorityQueue::EXTR_BOTH, $queue->getExtractFlags()); + } -error_reporting(E_ALL); -ini_set('display_errors', "on"); - -use \EdgeTelemetrics\TimeBucket\TimeOrderedArray; -use \EdgeTelemetrics\TimeBucket\TimeOrderedQueue; - -// Load Composer -require '../vendor/autoload.php'; - -$spl = new TimeOrderedQueue(); -$spl->insert("test c", 3); -$spl->insert("test b", 2); -$spl->insert("test a", 1); -$spl->insert("test b2", 2); - -echo "TimeOrderedQueue" . PHP_EOL; -$spl->setExtractFlags(SplPriorityQueue::EXTR_DATA); -echo "Data Only: "; -print_r($spl->extract()); -$spl->setExtractFlags(SplPriorityQueue::EXTR_PRIORITY); -echo PHP_EOL . "Priority Only: "; -print_r($spl->extract()); -$spl->setExtractFlags(SplPriorityQueue::EXTR_BOTH); -echo PHP_EOL . "Both: "; -print_r($spl->extract()); -echo PHP_EOL . "Final: "; -print_r($spl->extract()); - -$bucket = new TimeOrderedArray(); - -$bucket->insert("test c", 3); -$bucket->insert("test b", 2); -$bucket->insert("test a", 1); -$bucket->insert("test b2", 2); - -echo PHP_EOL . PHP_EOL . "TimeOrderedArray" . PHP_EOL; - -$bucket->setExtractFlags(SplPriorityQueue::EXTR_DATA); -echo "Data Only: "; -print_r($bucket->extract()); -$bucket->setExtractFlags(SplPriorityQueue::EXTR_PRIORITY); -echo PHP_EOL . "Priority Only: "; -print_r($bucket->extract()); -$bucket->setExtractFlags(SplPriorityQueue::EXTR_BOTH); -echo PHP_EOL . "Both: "; -print_r($bucket->extract()); -echo PHP_EOL . "Final: "; -print_r($bucket->extract()); \ No newline at end of file +} \ No newline at end of file