* * Full license available in separate LICENSE file */ use Kadet\Functional\Collections as c; use Kadet\Functional\Collections\Lazy as lazy; class LazyTest extends \PHPUnit\Framework\TestCase { private static function gen(iterable $entries): \Generator { foreach ($entries as $key => $value) { yield $key => $value; } } private static function naturals(): \Generator { for ($i = 1; ; $i++) { yield $i; } } public function testLazyDoesNothingUntilConsumed() { $calls = 0; $spy = static function ($value) use (&$calls) { $calls++; return $value; }; $steps = [ lazy\map($spy), lazy\filter($spy), lazy\reject($spy), lazy\uniqueBy($spy), lazy\each($spy), lazy\tap($spy), lazy\flatMap(static function ($value) use ($spy) { $spy($value); return [$value]; }), ]; foreach ($steps as $step) { $result = $step(self::gen(['a' => 1, 'b' => 2])); $this->assertInstanceOf(\Generator::class, $result); } $this->assertSame(0, $calls); iterator_to_array(lazy\map($spy)(self::gen(['a' => 1, 'b' => 2]))); $this->assertSame(2, $calls); } public function testLazyChainSingleWalk() { $walked = 0; $result = c\toArray()(lazy\take(3)(lazy\filter(static fn ($x) => $x % 2 === 0)(lazy\map(static function ($x) use (&$walked) { $walked++; return $x * 3; })(self::naturals())))); $this->assertSame([1 => 6, 3 => 12, 5 => 18], $result); $this->assertSame(6, $walked); } public static function provideEagerEquivalents(): iterable { $double = static fn ($x) => $x * 2; $isTwo = static fn ($x) => $x === 2; $inner = static fn ($x, $k) => [$x, $k]; yield 'map' => [fn () => lazy\map($double), fn () => c\map($double)]; yield 'filter' => [fn () => lazy\filter($isTwo), fn () => c\filter($isTwo)]; yield 'filter null' => [fn () => lazy\filter(), fn () => c\filter()]; yield 'reject' => [fn () => lazy\reject($isTwo), fn () => c\reject($isTwo)]; yield 'flatMap' => [fn () => lazy\flatMap($inner), fn () => c\flatMap($inner)]; yield 'values' => [fn () => lazy\values(), fn () => c\values()]; yield 'keys' => [fn () => lazy\keys(), fn () => c\keys()]; yield 'unique' => [fn () => lazy\unique(), fn () => c\unique()]; yield 'uniqueBy' => [fn () => lazy\uniqueBy($double), fn () => c\uniqueBy($double)]; yield 'slice' => [fn () => lazy\slice(1, 1), fn () => c\slice(1, 1)]; yield 'slice open' => [fn () => lazy\slice(1), fn () => c\slice(1)]; yield 'slice neg' => [fn () => lazy\slice(-2, 1), fn () => c\slice(-2, 1)]; yield 'slice negend' => [fn () => lazy\slice(0, -1), fn () => c\slice(0, -1)]; yield 'take' => [fn () => lazy\take(2), fn () => c\take(2)]; yield 'skip' => [fn () => lazy\skip(2), fn () => c\skip(2)]; yield 'prepend' => [fn () => lazy\prepend(7, 8), fn () => c\prepend(7, 8)]; yield 'append' => [fn () => lazy\append(7, 8), fn () => c\append(7, 8)]; yield 'concat' => [fn () => lazy\concat(['x' => 9]), fn () => c\concat(['x' => 9])]; yield 'each' => [fn () => lazy\each($double), fn () => c\each($double)]; yield 'tap' => [fn () => lazy\tap($double), fn () => c\tap($double)]; } #[\PHPUnit\Framework\Attributes\DataProvider('provideEagerEquivalents')] public function testLazyMatchesEagerResults(\Closure $lazy, \Closure $eager) { $fixture = ['a' => 1, 'b' => 2, 'c' => 2]; $this->assertSame($eager()($fixture), iterator_to_array($lazy()($fixture))); } public function testLazyUniqueStreams() { $walked = 0; $source = (static function () use (&$walked) { foreach ([1, 1, 2, 2, 3, 4] as $value) { $walked++; yield $value; } })(); $generator = lazy\unique()($source); $this->assertSame(0, $walked); $this->assertSame(1, $generator->current()); $this->assertSame(1, $walked); $generator->next(); $this->assertSame(2, $generator->current()); $this->assertSame(3, $walked); } public function testUniqueCanBeReusedAcrossRuns() { $step = lazy\unique(); $this->assertSame([1, 2], array_values(iterator_to_array($step([1, 1, 2])))); $this->assertSame([1, 2], array_values(iterator_to_array($step([1, 2, 2])))); } public function testFlatMapRejectsNonIterable() { $this->expectException(\UnexpectedValueException::class); $this->expectExceptionMessage('flatMap(): the callback must return an iterable, got int.'); iterator_to_array(lazy\flatMap(static fn ($x) => $x)([1])); } public function testTakeAndSkipRejectNegativeWhenBuilt() { try { lazy\take(-1); $this->fail('take(-1) should throw'); } catch (\InvalidArgumentException $e) { $this->assertSame('take(): count must be >= 0, got -1', $e->getMessage()); } $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('skip(): count must be >= 0, got -1'); lazy\skip(-1); } public function testEachYieldsEverythingThroughWithKeys() { $seen = []; $out = iterator_to_array(lazy\each(static function ($v, $k) use (&$seen) { $seen[] = [$k, $v]; })(self::gen(['a' => 1, 'b' => 2]))); $this->assertSame(['a' => 1, 'b' => 2], $out); $this->assertSame([['a', 1], ['b', 2]], $seen); } public function testStepCanBeReusedAcrossInputs() { $step = lazy\map(static fn ($x) => $x + 1); $this->assertSame([2, 3], iterator_to_array($step([1, 2]))); $this->assertSame([4], iterator_to_array($step([3]))); } public function testChunkSplitsIntoListsOfTheSize() { $this->assertSame( [[1, 2], [3, 4], [5]], iterator_to_array(lazy\chunk(2)(self::gen(['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5]))), ); } public function testChunkPreservesKeysWhenAsked() { $this->assertSame( [['a' => 1, 'b' => 2], ['c' => 3]], iterator_to_array(lazy\chunk(2, preserveKeys: true)(self::gen(['a' => 1, 'b' => 2, 'c' => 3]))), ); } public function testChunkKeepsValuesUnderRepeatedKeys() { $repeated = (function () { yield 'a' => 1; yield 'a' => 2; })(); $this->assertSame([[1, 2]], iterator_to_array(lazy\chunk(2)($repeated))); } public function testChunkYieldsEachChunkOnceFull() { $this->assertSame([1, 2, 3], lazy\chunk(3)(self::naturals())->current()); } public function testChunkOfNothingYieldsNothing() { $this->assertSame([], iterator_to_array(lazy\chunk(2)([]))); } public function testChunkRejectsSizeBelowOneWhenBuilt() { $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('chunk(): size must be >= 1, got 0'); lazy\chunk(0); } }