diff --git a/src/PostgresDriver/CursorException.php b/src/PostgresDriver/CursorException.php index e5d24d56769e4f3d03bc6fd184943aec4480e061..70e641e5c19a6b8ba1b769eb0464ae21b45a05a0 100644 --- a/src/PostgresDriver/CursorException.php +++ b/src/PostgresDriver/CursorException.php @@ -27,36 +27,70 @@ class CursorException extends \LogicException parent::__construct($message, $code, $previous); } - public static function noDataToFetch() + public static function noDataToFetch() : self { return new static(self::MESSAGE_NO_DATA_TO_FETCH); } - public static function cursorOverflow() + public static function cursorOverflow() : self { return new static(self::MESSAGE_OVERFLOW); } - public static function noDataCursorHasBeenSetToZero() + public static function noDataCursorHasBeenSetToZero() : self { return new static(self::MESSAGE_NO_DATA_CURSOR_RESET); } - public static function cannotRecoverCursorIntoOriginalPosition(\Throwable $previous) + public static function cannotRecoverCursorIntoOriginalPosition(\Throwable $previous) : self { return new static(self::MESSAGE_CANNOT_RECOVER_TO_ORIGINAL_POSITION, 0, $previous); } - public static function cannotMoveToTheEnd() + public static function cannotMoveToTheEnd() : self { return new static(self::MESSAGE_CANNOT_REACH_THE_END); } - public static function untraceableValue($moreInfo) + public static function cursorPosition_invalidStartingPoint() : self + { + return new static("Invalid starting point value. Use constants defined in value object."); + } + + public static function cursorPosition_invalidIndexValue(int $value) : self + { + return new static( + sprintf("Index value %i is not valid.", $value) + ); + } + + public static function untraceableValue($moreInfo) : self { return new static( sprintf(self::MESSAGE_UNTRACEABLE_VALUE, $moreInfo) ); } + public static function unexpectedRowCountReturned(int $rowCount) : self + { + return new static( + sprintf("Underlying driver failed. Returned unexpected number (%i) of rows.", $rowCount) + ); + } + + public static function cursorPosition_unknownError() : self + { + return new static("Moving cursor failed due to unknown error."); + } + + public static function noResultsAvailable_hitBeginning() : self + { + return new static("No results available. You've hit the beginning."); + } + + public static function noResultsAvailable_hitEnd() : self + { + return new static("No results available. You've hit the end."); + } + } \ No newline at end of file diff --git a/src/PostgresDriver/CursorPosition.php b/src/PostgresDriver/CursorPosition.php new file mode 100644 index 0000000000000000000000000000000000000000..5745aae40e4f68420c6fefa94fc177a99722b2d6 --- /dev/null +++ b/src/PostgresDriver/CursorPosition.php @@ -0,0 +1,96 @@ +<?php declare(strict_types = 1); +/** + * This file is part of mappi/store. + */ + +namespace Grifart\Mappi\Store\PostgresDriver; + +/** + * Value object for TrackedCursor position + * + * @link https://github.com/nicolopignatelli/valueobjects (inspiration) + * @package Grifart\Mappi\Store\PostgresDriver + */ +final class CursorPosition +{ + /** @var CursorPositionOrigin */ + private $origin; + + /** @var int the cursor position */ + private $position = 0; + + /** + * @param CursorPositionOrigin $origin from left or from right? + * @param int $position which position + */ + public function __construct(CursorPositionOrigin $origin, int $position) + { + $this->setPosition($origin, $position); + } + + public static function fromLeft(int $position) : self + { + return new self( + CursorPositionOrigin::get(CursorPositionOrigin::FROM_LEFT), + $position + ); + } + + public static function fromRight(int $position) : self + { + return new self( + CursorPositionOrigin::get(CursorPositionOrigin::FROM_RIGHT), + $position + ); + } + + public function setPositionFromLeft(int $position) + { + $this->setPosition(CursorPositionOrigin::get(CursorPositionOrigin::FROM_LEFT), $position); + } + + public function setPositionFromRight(int $position) + { + $this->setPosition(CursorPositionOrigin::get(CursorPositionOrigin::FROM_RIGHT), $position); + } + + private function setPosition(CursorPositionOrigin $origin, int $position) + { + if($position < 0) { + throw CursorException::cursorPosition_invalidIndexValue($position); + } + $this->origin = $origin; + $this->position = $position; + } + + /** + * @param int $by negative = left; positive = right + */ + public function movePositionBy(int $by) + { + $modifier = 1; + if ($this->origin->is(CursorPositionOrigin::FROM_RIGHT)) { + $modifier = -1; + } + $this->position += $modifier * $by; + } + + /** + * From which side is position counted? + * @return CursorPositionOrigin + */ + public function getOrigin() + { + return $this->origin; + } + + /** + * Cursor position from origin + * @return int + */ + public function getPosition() + { + return $this->position; + } + +} \ No newline at end of file diff --git a/src/PostgresDriver/CursorPositionOrigin.php b/src/PostgresDriver/CursorPositionOrigin.php new file mode 100644 index 0000000000000000000000000000000000000000..01447b8bcd4052bc30c4a7acea4242930b756a37 --- /dev/null +++ b/src/PostgresDriver/CursorPositionOrigin.php @@ -0,0 +1,14 @@ +<?php declare(strict_types = 1); +/** + * This file is part of mappi/store. + */ + +namespace Grifart\Mappi\Store\PostgresDriver; + +use MabeEnum\Enum; + +class CursorPositionOrigin extends Enum +{ + const FROM_LEFT = 0; + const FROM_RIGHT = 1; +} diff --git a/src/PostgresDriver/TrackedCursor.php b/src/PostgresDriver/TrackedCursor.php index 79d3f161fd862d4e6e3e966d82a5dcdd11e8ef3a..4ef618bc330e78b8f00cb4c09abbbdaaac08e43b 100644 --- a/src/PostgresDriver/TrackedCursor.php +++ b/src/PostgresDriver/TrackedCursor.php @@ -7,33 +7,38 @@ namespace Grifart\Mappi\Store\PostgresDriver; use Dibi\Connection; -class TrackedCursor implements ICursor +class TrackedCursor implements ICursorDriver { - /** @var int */ private $total; /** - * null-based index of current cursor state - * @var int + * @var CursorPosition */ - private $position = 0; + private $position; - /** @var ICursor */ private $cursor; + /** @var Connection */ + private $connection; + /** - * @param ICursor $cursor Cursor in initial state (index=0) - * Tip: if you are not sure that cursor will be in initial state, call ->moveToBeginning() after initialization. + * Tip: if you are not sure that cursor will be in initial state, call + * ->moveToBeginning() after initialization. + * @param Connection $connection + * @param ICursorDriver $cursor CursorDriver in initial state (index=0) + * @param CursorPosition $initialPosition */ - public function __construct(ICursor $cursor) + public function __construct(Connection $connection, ICursorDriver $cursor, CursorPosition $initialPosition) { $this->cursor = $cursor; + $this->connection = $connection; + $this->position = $initialPosition; } /** - * @return int + * @return CursorPosition */ public function getPosition() { @@ -42,7 +47,7 @@ class TrackedCursor implements ICursor public function getConnection() : Connection { - return $this->cursor->getConnection(); + return $this->connection; } public function getName() : string @@ -50,191 +55,140 @@ class TrackedCursor implements ICursor return $this->cursor->getName(); } - public function moveTo(int $index) + public function isOnRecord() : bool { - if($index < 0) { - throw CursorException::untraceableValue("Given index is bellow zero. Use moveToEnd() + moveBy() combination."); - } - if($this->handleSpecialCases($index)) { // this was special case and was handled by special handling bellow - return; - } - $this->cursor->moveTo($index); - $this->position = $index; + return $this->cursor->isOnRecord(); } - public function moveBy(int $rows) + public function moveTo(int $index) { - $originalPosition = $this->getPosition(); - $index = $originalPosition + $rows; - //$this->moveTo($index); // todo: is this efficient? + $this->cursor->moveTo($index); + if ($this->cursor->isOnRecord()) { + $this->position->setPositionFromLeft( + $index + ); + return; + } - if($this->handleSpecialCases($originalPosition + $rows)) { // this was special case and was handled by special handling bellow + if ($index === 0) { + $this->position->setPositionFromLeft(0); // BEGINNING return; } - try { - $this->cursor->moveBy($rows); - $this->position += $rows; - } catch (CursorException $e) { - $this->tryToRecoverFromError( - CursorException::cursorOverflow(), - $originalPosition - ); + if ($index > 0 ) { + $this->position->setPositionFromRight(0); // END + return; } + throw CursorException::cursorPosition_unknownError(); } - public function moveToBeginning() + public function moveFromEndTo(int $index) { - $this->cursor->moveToBeginning(); - $this->position = 0; - } - - public function moveToFirst() - { - $this->cursor->moveToFirst(); - $this->position = 1; - } - - public function moveToLast() - { - $this->moveToEnd(); - $this->moveBy(-1); - } - - public function moveToEnd() - { - $rowsSkipped = $this->getConnection()->query( - "MOVE FORWARD ALL IN %n", - $this->getName() - ); - if ($rowsSkipped === 0) { - // edge case: when already in END or LAST position - if ($this->total !== NULL) - { - $indexOfEnd = $this->total + 1; - $indexOfLast = $this->total; - if ( - $this->position === $indexOfLast || - $this->position === $indexOfEnd - ) - { - $this->position = $indexOfEnd; - return; - } - - } else { - throw CursorException::cannotMoveToTheEnd(); - } + $this->cursor->moveFromEndTo($index); + if ($this->cursor->isOnRecord()) { + $this->position->setPositionFromRight( + $index + ); + return; } - $this->position += $rowsSkipped + 1; - $this->total = $this->position - 1; // end is one step after last item - } - public function fetch(int $rows) - { - $forward = $rows > 0; - $rows = $this->cursor->fetch($rows); - $this->position += count($rows) * ($forward ? 1 : -1); - return $rows; + if ($index === 0) { + $this->position->setPositionFromRight(0); // END + return; + } + if ($index > 0) { + $this->position->setPositionFromLeft(0); // BEGINNING + return; + } + throw CursorException::cursorPosition_unknownError(); } - public function fetchNext() + public function moveBy(int $rows) { - $row = $this->cursor->fetchNext(); - if($row !== FALSE) { - $this->position += 1; + // todo: use MOVE FORWARD n IN ... which returns number of rows read + $this->cursor->moveBy($rows); + if ($this->cursor->isOnRecord()) { + $this->position->movePositionBy($rows); + return; + } + if ($rows < 0) { + $this->position->setPositionFromLeft(0); // BEGINNING + return; } - return $row; + if ($rows > 0) { + $this->position->setPositionFromRight(0); // END + return; + } + if ($rows === 0) { + return; // already on one of ends + } + throw CursorException::cursorPosition_unknownError(); } - public function fetchCurrent() + public function fetchRange(int $rows): array { - return $this->cursor->fetchCurrent(); - } + // todo: fixme overriding parameter + $result = $this->cursor->fetchRange($rows); - public function fetchNextSingle() - { - $value = $this->cursor->fetchNextSingle(); - $this->position += 1; - return $value; - } + if($rows !== 0) { // zero does not move cursor + $forward = $rows > 0; + $this->position->movePositionBy( + count($result) * ($forward ? 1 : -1) + ); + } - public function fetchCurrentSingle() - { - return $this->cursor->fetchCurrentSingle(); + return $result; } - public function fetchOneAt(int $index) + public function fetchOneAt(int $index) : array { - $originalPosition = $this->getPosition(); $row = $this->cursor->fetchOneAt($index); - if($row !== FALSE) { - $this->position = $index; - } else { - $this->tryToRecoverFromError( - CursorException::noDataCursorHasBeenSetToZero(), - $originalPosition - ); + if ($row !== NULL) { + if($index < 0) { + $this->position->setPositionFromRight(abs($index)); + } else /* >= 0 */ { + $this->position->setPositionFromLeft($index); + } + return $row; } - return $row; - } - public function fetchOneBy(int $rows) - { - $originalPosition = $this->getPosition(); - $row = $this->cursor->fetchOneBy($rows); - if($row !== FALSE) { - $this->position += $rows; - } else { - $this->tryToRecoverFromError( - CursorException::noDataCursorHasBeenSetToZero(), - $originalPosition - ); + if ($index === 0) { + $this->position->setPositionFromLeft(0); // BEGINNING + return NULL; } - return $row; - } - public function fetchRemaining() - { - $data = $this->cursor->fetchRemaining(); - $this->position += count($data); - return $data; - } + if ($index < 0) { // went from right to left -> hit beginning + $this->position->setPositionFromLeft(0); + return NULL; + } + if ($index > 0) { // went left->right -> hit end + $this->position->setPositionFromRight(0); + return NULL; + } - public function fetchForegoing() - { - $data = $this->cursor->fetchForegoing(); - $this->position -= count($data); - return $data; + throw CursorException::cursorPosition_unknownError(); } - private function handleSpecialCases($index) : bool + public function fetchOneBy(int $rows): array { - // if requested position was BEGINNING - if($index === 0) { - $this->moveToBeginning(); - return TRUE; + $row = $this->cursor->fetchOneBy($rows); + if ($row !== NULL) { + $this->position->movePositionBy($rows); + return $row; } - // if requested position was the END - if($this->total !== NULL) { - if($this->total + 1 === $index) { - $this->moveToEnd(); - return TRUE; - } + if ($rows < 0) { + $this->position->setPositionFromLeft(0); // BEGINNING + return NULL; + } + if ($rows > 0) { + $this->position->setPositionFromRight(0); // END + return NULL; + } + if ($rows === 0) { + return NULL; // already on one of ends } - // wasn't a special case - return FALSE; + throw CursorException::cursorPosition_unknownError(); } - private function tryToRecoverFromError(\Throwable $throwable, $moveTo) - { - try { - $this->moveTo($moveTo); - } catch (\Throwable $e) { - throw CursorException::cannotRecoverCursorIntoOriginalPosition($throwable); - } - - throw $throwable; - } } \ No newline at end of file diff --git a/tests/Store/PostgresDriver/TrackedCursorTest.phpt b/tests/Store/PostgresDriver/TrackedCursorTest.phpt index 450538418d135c0ec19c4bbb65ce5fb75f404fee..2d05d39ee484cc8bbc9f03a7cefcc929f292deb1 100644 --- a/tests/Store/PostgresDriver/TrackedCursorTest.phpt +++ b/tests/Store/PostgresDriver/TrackedCursorTest.phpt @@ -1,20 +1,27 @@ -<?php +<?php declare(strict_types = 1); /** * @testCase */ +// TODO: Add boundary checks! +// TODO: moveBy() ->isOnRecord() ->moveBy() ->isOnRecord() (now I see state before and after command) +// TODO: Cursor should have dedicated method for overriding data processing +// TODO: Maybe construct Cursor class in TrackedCursor constructor? +// TODO: Consistency check of inner cursor (headOnRecord === FALSE) + namespace Grifart\Mappi\Tests\Store\Store\PostgresDriver; -use Grifart\Mappi\Store\PostgresDriver\CursorException; -use Grifart\Mappi\Store\PostgresDriver\Cursor; -use Grifart\Mappi\Store\PostgresDriver\CursorFactory; +use Grifart\Mappi\Store\PostgresDriver\CursorDriverFactory; +use Grifart\Mappi\Store\PostgresDriver\CursorPosition; +use Grifart\Mappi\Store\PostgresDriver\ICursorDriver; use Grifart\Mappi\Store\PostgresDriver\TrackedCursor; +use Grifart\Mappi\Tests\Store\BaseTest; use Tester\Assert; require_once __DIR__ . "/../../bootstrap.php"; require_once __DIR__ . "/CursorInterfaceTest.php"; -class TrackedCursorTest extends CursorInterfaceTest +class TrackedCursorTest extends BaseTest { /** @link https://en.wikipedia.org/wiki/42_(number)#Hitchhiker.27s_Guide_to_the_Galaxy */ const THE_MAGIC_NUMBER = 42; @@ -27,9 +34,11 @@ class TrackedCursorTest extends CursorInterfaceTest global $connection, $SQL_thousandRowsAscending; $connection->begin(); - $factory = new CursorFactory($connection); + $factory = new CursorDriverFactory($connection); $this->uut = new TrackedCursor( - $factory->create($SQL_thousandRowsAscending, true) + $connection, + $factory->create($SQL_thousandRowsAscending, TRUE), + CursorPosition::fromLeft(0) ); parent::setUp(); } @@ -42,121 +51,412 @@ class TrackedCursorTest extends CursorInterfaceTest parent::tearDown(); } - public function test_givenBeginningPosition_whenMoveLeft_thenGetError() + private function helper_fetchNextSingle() { - $this->uut->moveToBeginning(); + $rows = $this->uut->fetchRange(1); + if (count($rows) === 0) { + return NULL; + } + return $rows[0]["n"]; + } - Assert::exception(function() { - // todo: what to do with this non-uniformity with original Cursor? - $this->uut->moveBy(-1); - }, CursorException::class, CursorException::MESSAGE_OVERFLOW); + private function helper_fetchCurrentSingle() + { + $rows = $this->uut->fetchRange(0); + if (count($rows) === 0) { + return NULL; + } + return $rows[0]["n"]; + } - $this->uut->moveBy(1); - Assert::equal(1, $this->uut->fetchCurrentSingle()); + private function helper_fetchPrevSingle() + { + $rows = $this->uut->fetchRange(-1); + if (count($rows) === 0) { + return NULL; + } + return $rows[0]["n"]; + } + + // initial position + public function test_givenCursor_whenGetCurrentRow_thenGetNothing() + { + Assert::count(0, $this->uut->fetchRange(0)); // must fail because initial position is 0 + Assert::count(0, $this->uut->fetchRange(-1)); + Assert::count(1, $this->uut->fetchRange(1)); } - public function test_givenEndPosition_whenMoveToRight_thenGetError() + public function test_givenInitialPosition_whenFetch_thenGetFirstRow() { - $this->uut->moveToEnd(); + Assert::same(1, $this->helper_fetchNextSingle()); + } - Assert::exception(function() { - // todo: what to do with this non-uniformity with original Cursor? - $this->uut->moveBy(1); - }, CursorException::class, CursorException::MESSAGE_OVERFLOW); + // fetch( x == 0 ) + public function test_givenSecondPosition_whenFetch0_thenGetCurrentRow() + { + // head: . + // index: 0 1 2 3 4 + // value: 1 2 3 4 + $this->uut->moveTo(3); + Assert::same(3, $this->helper_fetchCurrentSingle()); - $this->uut->moveBy(-1); - Assert::equal(1000, $this->uut->fetchCurrentSingle()); + $result = $this->uut->fetchRange(0); // rows backwards + read current + + Assert::count(1, $result); + Assert::same(3, current($result[0])); } - // corrected behaviour of original cursor - public function test_givenFirstPosition_whenMoveToLeft_thenImOnFirstValue() + // fetch( x > 0 ) + public function test_givenSecondPosition_whenFetchForward_thenGetRowsAfterCursor() { - $this->uut->moveToFirst(); + // head: . + // index: 0 1 2 3 4 + // value: 1 2 3 4 + $this->uut->moveTo(3); + Assert::same(3, $this->helper_fetchCurrentSingle()); + + // head: . + // index: 0 1 2 3 4 5 6 + // value: 1 2 3 4 5 6 + $result = $this->uut->fetchRange(2); // rows backwards + read current + // todo: split into fetch decorator for those special fetch* methods + + Assert::count(2, $result); + Assert::same(4, current($result[0])); + Assert::same(5, current($result[1])); + Assert::same(5, $this->helper_fetchCurrentSingle()); + } + // fetch( x < 0 ) + public function test_givenSecondPosition_whenFetchBackwards_thenGetRowsBeforeCursor() + { + // head: . + // index: 0 1 2 3 4 + // value: 1 2 3 4 + $this->uut->moveTo(3); + Assert::same(3, $this->helper_fetchCurrentSingle()); + + // head: . + // index: 0 1 2 3 4 + // value: 1 2 3 4 + $result = $this->uut->fetchRange(-2); // rows backwards + read current + // todo: split into fetch decorator for those special fetch* methods + + Assert::count(2, $result); + Assert::same(2, current($result[0])); + Assert::same(1, current($result[1])); + Assert::same(1, $this->helper_fetchCurrentSingle()); + } + + // moveToBeginning() + public function test_givenLastPosition_whenMoveToTheBeginning_thenFetchingNextRowWillBeFirstRow() + { + // Arrange + $this->uut->moveFromEndTo(1); + + // Act + $this->uut->moveTo(0); + + //Assert + Assert::same(1, $this->helper_fetchNextSingle()); + } + + // moveToFirst() + public function test_givenLastPosition_whenMoveToTheFirst_thenFetchingCurrentWillBeFirstRow() + { + // Arrange + $this->uut->moveFromEndTo(1); + + // Act + $this->uut->moveTo(1); + + //Assert + Assert::same(1, $this->helper_fetchCurrentSingle()); + } + + // moveToEnd() + public function test_givenInitialPosition_whenMoveToEnd_thenFetchingOneBacwardsWillBeTheLastRow() + { + $this->uut->moveFromEndTo(0); + + Assert::null($this->helper_fetchCurrentSingle()); + + $data = $this->helper_fetchPrevSingle(); + Assert::same(1000, $data); + } + + // moveToLast() + public function test_givenInitialPosition_whenMoveToLast_thenFetchingCurrentWillBeLastRow() + { + $this->uut->moveFromEndTo(1); + Assert::same(1000, $this->helper_fetchCurrentSingle()); + + Assert::null($this->helper_fetchNextSingle()); + } + + // moveBy() + public function test_givenInitialPosition_whenMoveBy2_thenGetSecondValue() + { + $this->uut->moveBy(2); + Assert::same(2, $this->helper_fetchCurrentSingle()); + } + + public function test_givenSecondPosition_whenMoveOneBack_thenGetFirstValue() + { + $this->uut->moveTo(2); $this->uut->moveBy(-1); - // todo: what to do with this non-uniformity with original Cursor? - Assert::equal(1, $this->uut->fetchNextSingle()); + Assert::same(1, $this->helper_fetchCurrentSingle()); + $this->uut->moveBy(-1); + Assert::null($this->helper_fetchCurrentSingle()); } - public function test_givenLastPosition_whenMoveToRight_thenGetError() + // fetchOneAt() + public function test_givenInitialPosition_whenFetch5_thenGetFive() { - // todo: what to do with this non-uniformity with original Cursor? - $this->uut->moveToLast(); - $this->uut->moveBy(1); // here is ok, now in original cursor + $data = $this->uut->fetchOneAt(5); + Assert::same(5, $data["n"]); + } - $this->uut->moveBy(-1); // todo: make decorator which will make this nicer - Assert::equal(1000, $this->uut->fetchCurrentSingle()); + public function test_givenNonInitialPosition_whenFetch5_thenGetFive() + { + $this->uut->moveTo(234); // wherever + + $data = $this->uut->fetchOneAt(5); + Assert::same(5, $data["n"]); } + // fetchRemaining() + public function test_given5BeforeEndPosition_whenFetchRemaining_thenGetLastFile() + { + $this->uut->moveTo(995); + $result = $this->uut->fetchRange(ICursorDriver::FETCH_REMAINING); - // ->getPosition() tests: + Assert::count(5, $result); + Assert::same(996, current($result[0])); + Assert::same(997, current($result[1])); + Assert::same(998, current($result[2])); + Assert::same(999, current($result[3])); + Assert::same(1000, current($result[4])); - // moveToBeginning() - public function test_givenInitialPosition_whenGetPosition_thenGetZero() + Assert::null($this->helper_fetchCurrentSingle()); + Assert::equal(1000, $this->helper_fetchPrevSingle()); + } + + // fetchRemaining() + public function test_given5AfterStart_whenFetchForegoing_thenGetFirstFive() { - Assert::equal(0, $this->uut->getPosition()); + $this->uut->moveTo(6); - $this->uut->moveToBeginning(); - Assert::equal(0, $this->uut->getPosition()); + $result = $this->uut->fetchRange(ICursorDriver::FETCH_FOREGOING); - $this->uut->moveToFirst(); - Assert::equal(1, $this->uut->getPosition()); + Assert::count(5, $result); + Assert::same(5, current($result[0])); + Assert::same(4, current($result[1])); + Assert::same(3, current($result[2])); + Assert::same(2, current($result[3])); + Assert::same(1, current($result[4])); + + Assert::null($this->helper_fetchCurrentSingle()); } - public function test_givenMiddlePosition_whenMoveAround_thenGetCorrectPosition() + // edge cases: + public function test_givenInitialPosition_whenMoveToLeft_thenGetError() { - $this->uut->moveTo(self::THE_MAGIC_NUMBER); - $this->uut->moveTo(self::THE_MAGIC_NUMBER + self::THE_MAGIC_NUMBER); - $this->uut->moveBy(-self::THE_MAGIC_NUMBER); - $this->uut->moveBy(+self::THE_MAGIC_NUMBER); - $this->uut->moveBy(-self::THE_MAGIC_NUMBER); - Assert::equal(self::THE_MAGIC_NUMBER, $this->uut->getPosition()); + Assert::false($this->uut->isOnRecord()); // initial position + + // start: index === 0 + $this->uut->moveBy(-1); + Assert::false($this->uut->isOnRecord()); + + // index === 0 + $data = $this->uut->fetchRange(-1); + Assert::count(0, $data); + + $firstValue = $this->helper_fetchNextSingle(); + Assert::equal(1, $firstValue); + // index === 1 } - public function test_givenEndPosition_whenGetPosition_thenGetTotalPlusOne() + public function test_givenEndPosition_whenMoveToRight_thenStayInPlace() { - $this->uut->moveToEnd(); - Assert::equal(1001, $this->uut->getPosition()); + $this->uut->moveFromEndTo(0); + + Assert::null($this->helper_fetchNextSingle()); + Assert::null($this->helper_fetchCurrentSingle()); + Assert::equal(1000, $this->helper_fetchPrevSingle()); } - public function test_givenInitialPosition_whenMoveToLast_thenGetTotal() + public function test_giveSomePosition_whenMoveToAfterEnd_thenWillBeAtTheEnd() { - $this->uut->moveToLast(); - Assert::equal(1000, $this->uut->getPosition()); + $this->uut->moveTo(25); // some position + + $this->uut->moveTo(9999); + + Assert::null($this->helper_fetchCurrentSingle()); + Assert::equal(1000, $this->helper_fetchPrevSingle()); } - // nasty edge cases: - public function test_givenLastPosition_whenMoveToEnd_thenGetEndPosition() + public function test_givenFirstPosition_whenMoveToLeft_thenWillBeAtBeginning() { - $this->uut->moveToLast(); - $this->uut->moveToEnd(); - Assert::equal(1001, $this->uut->getPosition()); + // Arrange + Assert::false($this->uut->isOnRecord()); + $this->uut->moveTo(1); + Assert::true($this->uut->isOnRecord()); + + // Act + $this->uut->moveBy(-1); + + // Assert + Assert::false($this->uut->isOnRecord()); + Assert::null($this->helper_fetchCurrentSingle()); + Assert::null($this->helper_fetchPrevSingle()); - // regression test: - $this->uut->moveToEnd(); - Assert::equal(1001, $this->uut->getPosition()); + Assert::equal(1, $this->helper_fetchNextSingle()); + Assert::true($this->uut->isOnRecord()); } - public function test_givenEndPosition_whenMoveToLast_thenGetLastPosition() + public function test_givenLastPosition_whenMoveToRight_thenReachTheEnd() { - $this->uut->moveToEnd(); - $this->uut->moveToLast(); - Assert::equal(1000, $this->uut->getPosition()); + $this->uut->moveFromEndTo(1); + Assert::true($this->uut->isOnRecord()); - // regression test: - $this->uut->moveToLast(); - Assert::equal(1000, $this->uut->getPosition()); + $this->uut->moveBy(1); + + Assert::false($this->uut->isOnRecord()); + Assert::null($this->helper_fetchCurrentSingle()); + Assert::null($this->helper_fetchNextSingle()); + + Assert::equal(1000, $this->helper_fetchPrevSingle()); + Assert::true($this->uut->isOnRecord()); } - public function test_giveSomePosition_whenMoveToBeginning_thenGetToBeginning() + public function test_giveSomePosition_whenMoveToBeginning_thenWillBeAtBeginning() { $this->uut->moveTo(42); $this->uut->moveTo(0); - - Assert::equal(0, $this->uut->getPosition()); - Assert::equal(1, $this->uut->fetchNextSingle()); + Assert::equal(1, $this->helper_fetchNextSingle()); } + + + + +// +// public function test_givenBeginningPosition_whenMoveLeft_thenGetError() +// { +// $this->uut->moveToBeginning(); +// +// Assert::exception(function() { +// // todo: what to do with this non-uniformity with original CursorDriver? +// $this->uut->moveBy(-1); +// }, CursorException::class, CursorException::MESSAGE_OVERFLOW); +// +// $this->uut->moveBy(1); +// Assert::equal(1, $this->uut->fetchCurrentSingle()); +// } +// +// public function test_givenEndPosition_whenMoveToRight_thenGetError() +// { +// $this->uut->moveToEnd(); +// +// Assert::exception(function() { +// // todo: what to do with this non-uniformity with original CursorDriver? +// $this->uut->moveBy(1); +// }, CursorException::class, CursorException::MESSAGE_OVERFLOW); +// +// $this->uut->moveBy(-1); +// Assert::equal(1000, $this->uut->fetchCurrentSingle()); +// } +// +// // corrected behaviour of original cursor +// public function test_givenFirstPosition_whenMoveToLeft_thenImOnFirstValue() +// { +// $this->uut->moveToFirst(); +// +// $this->uut->moveBy(-1); +// // todo: what to do with this non-uniformity with original CursorDriver? +// Assert::equal(1, $this->uut->fetchNextSingle()); +// } +// +// public function test_givenLastPosition_whenMoveToRight_thenGetError() +// { +// // todo: what to do with this non-uniformity with original CursorDriver? +// $this->uut->moveToLast(); +// $this->uut->moveBy(1); // here is ok, now in original cursor +// +// $this->uut->moveBy(-1); // todo: make decorator which will make this nicer +// Assert::equal(1000, $this->uut->fetchCurrentSingle()); +// } +// +// +// +// // ->getPosition() tests: +// +// // moveToBeginning() +// public function test_givenInitialPosition_whenGetPosition_thenGetZero() +// { +// Assert::equal(0, $this->uut->getPosition()); +// +// $this->uut->moveToBeginning(); +// Assert::equal(0, $this->uut->getPosition()); +// +// $this->uut->moveToFirst(); +// Assert::equal(1, $this->uut->getPosition()); +// } +// +// public function test_givenMiddlePosition_whenMoveAround_thenGetCorrectPosition() +// { +// $this->uut->moveTo(self::THE_MAGIC_NUMBER); +// $this->uut->moveTo(self::THE_MAGIC_NUMBER + self::THE_MAGIC_NUMBER); +// $this->uut->moveBy(-self::THE_MAGIC_NUMBER); +// $this->uut->moveBy(+self::THE_MAGIC_NUMBER); +// $this->uut->moveBy(-self::THE_MAGIC_NUMBER); +// Assert::equal(self::THE_MAGIC_NUMBER, $this->uut->getPosition()); +// } +// +// public function test_givenEndPosition_whenGetPosition_thenGetTotalPlusOne() +// { +// $this->uut->moveToEnd(); +// Assert::equal(1001, $this->uut->getPosition()); +// } +// +// public function test_givenInitialPosition_whenMoveToLast_thenGetTotal() +// { +// $this->uut->moveToLast(); +// Assert::equal(1000, $this->uut->getPosition()); +// } +// +// // nasty edge cases: +// public function test_givenLastPosition_whenMoveToEnd_thenGetEndPosition() +// { +// $this->uut->moveToLast(); +// $this->uut->moveToEnd(); +// Assert::equal(1001, $this->uut->getPosition()); +// +// // regression test: +// $this->uut->moveToEnd(); +// Assert::equal(1001, $this->uut->getPosition()); +// } +// +// public function test_givenEndPosition_whenMoveToLast_thenGetLastPosition() +// { +// $this->uut->moveToEnd(); +// $this->uut->moveToLast(); +// Assert::equal(1000, $this->uut->getPosition()); +// +// // regression test: +// $this->uut->moveToLast(); +// Assert::equal(1000, $this->uut->getPosition()); +// } +// +// public function test_giveSomePosition_whenMoveToBeginning_thenGetToBeginning() +// { +// $this->uut->moveTo(42); +// $this->uut->moveTo(0); +// +// Assert::equal(0, $this->uut->getPosition()); +// Assert::equal(1, $this->uut->fetchNextSingle()); +// } } (new TrackedCursorTest())->run(); \ No newline at end of file