241 lines
8.8 KiB
PHP
241 lines
8.8 KiB
PHP
|
<?php
|
||
|
/**
|
||
|
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||
|
* @license MIT
|
||
|
*/
|
||
|
|
||
|
declare(strict_types=1);
|
||
|
|
||
|
namespace Test;
|
||
|
|
||
|
use BitBadger\InspiredByFSharp\Option;
|
||
|
use InvalidArgumentException;
|
||
|
use PHPUnit\Framework\Attributes\TestDox;
|
||
|
use PHPUnit\Framework\TestCase;
|
||
|
|
||
|
/**
|
||
|
* Unit tests for the Option class
|
||
|
*/
|
||
|
class OptionTest extends TestCase
|
||
|
{
|
||
|
#[TestDox('Get succeeds for Some')]
|
||
|
public function testGetSucceedsForSome(): void
|
||
|
{
|
||
|
$it = Option::Some(9);
|
||
|
$this->assertTrue(Option::isSome($it), 'The option should have been "Some"');
|
||
|
$this->assertEquals(9, $it->get(), 'The value was incorrect');
|
||
|
}
|
||
|
|
||
|
#[TestDox('Get fails for None')]
|
||
|
public function testGetFailsForNone(): void
|
||
|
{
|
||
|
$this->expectException(InvalidArgumentException::class);
|
||
|
Option::None()->get();
|
||
|
}
|
||
|
|
||
|
public function testSomeSucceedsWithValue(): void
|
||
|
{
|
||
|
$it = Option::Some('hello');
|
||
|
$this->assertTrue(Option::isSome($it), 'The option should have been "Some"');
|
||
|
}
|
||
|
|
||
|
public function testSomeFailsWithNull(): void
|
||
|
{
|
||
|
$this->expectException(InvalidArgumentException::class);
|
||
|
Option::Some(null);
|
||
|
}
|
||
|
|
||
|
public function testNoneSucceeds(): void
|
||
|
{
|
||
|
$it = Option::None();
|
||
|
$this->assertTrue(Option::isNone($it), 'The option should have been "None"');
|
||
|
}
|
||
|
|
||
|
public function testOfSucceedsWithNull(): void
|
||
|
{
|
||
|
$it = Option::of(null);
|
||
|
$this->assertTrue(Option::isNone($it), '"null" should have created a "None" option');
|
||
|
}
|
||
|
|
||
|
public function testOfSucceedsWithNonNull(): void
|
||
|
{
|
||
|
$it = Option::of('test');
|
||
|
$this->assertTrue(Option::isSome($it), 'A non-null value should have created a "Some" option');
|
||
|
$this->assertEquals('test', $it->get(), 'The value was not assigned correctly');
|
||
|
}
|
||
|
|
||
|
#[TestDox('IsNone succeeds with None')]
|
||
|
public function testIsNoneSucceedsWithNone(): void
|
||
|
{
|
||
|
$this->assertTrue(Option::isNone(Option::None()), '"None" should return true');
|
||
|
}
|
||
|
|
||
|
#[TestDox('IsNone succeeds with Some')]
|
||
|
public function testIsNoneSucceedsWithSome(): void
|
||
|
{
|
||
|
$this->assertFalse(Option::isNone(Option::Some(8)), '"Some" should return false');
|
||
|
}
|
||
|
|
||
|
#[TestDox('IsSome succeeds with None')]
|
||
|
public function testIsSomeSucceedsWithNone(): void
|
||
|
{
|
||
|
$this->assertFalse(Option::isSome(Option::None()), '"None" should return false');
|
||
|
}
|
||
|
|
||
|
#[TestDox('IsSome succeeds with Some')]
|
||
|
public function testIsSomeSucceedsWithSome(): void
|
||
|
{
|
||
|
$this->assertTrue(Option::isSome(Option::Some('boo')), '"Some" should return true');
|
||
|
}
|
||
|
|
||
|
#[TestDox('DefaultValue succeeds with None')]
|
||
|
public function testDefaultValueSucceedsWithNone(): void
|
||
|
{
|
||
|
$this->assertEquals('yes', Option::defaultValue('yes', Option::None()), 'Value should have been default');
|
||
|
}
|
||
|
|
||
|
#[TestDox('DefaultValue succeeds with Some')]
|
||
|
public function testDefaultValueSucceedsWithSome(): void
|
||
|
{
|
||
|
$this->assertEquals('no', Option::defaultValue('yes', Option::Some('no')),
|
||
|
'Value should have been from option');
|
||
|
}
|
||
|
|
||
|
#[TestDox('GetOrCall succeeds with None')]
|
||
|
public function testGetOrCallSucceedsWithNone(): void
|
||
|
{
|
||
|
$value = Option::getOrCall(new class { public function __invoke(): string { return 'called'; } },
|
||
|
Option::None());
|
||
|
$this->assertEquals('called', $value, 'The value should have been obtained from the callable');
|
||
|
}
|
||
|
|
||
|
#[TestDox('GetOrCall succeeds with Some')]
|
||
|
public function testGetOrCallSucceedsWithSome(): void
|
||
|
{
|
||
|
$value = Option::getOrCall(new class { public function __invoke(): string { return 'called'; } },
|
||
|
Option::Some('passed'));
|
||
|
$this->assertEquals('passed', $value, 'The value should have been obtained from the option');
|
||
|
}
|
||
|
|
||
|
#[TestDox('Map succeeds with None')]
|
||
|
public function testMapSucceedsWithNone(): void
|
||
|
{
|
||
|
$tattle = new class { public bool $called = false; };
|
||
|
$none = Option::None();
|
||
|
$mapped = Option::map(function ($ignored) use ($tattle)
|
||
|
{
|
||
|
$tattle->called = true;
|
||
|
return 'hello';
|
||
|
}, $none);
|
||
|
$this->assertTrue(Option::isNone($mapped), 'The mapped option should be "None"');
|
||
|
$this->assertFalse($tattle->called, 'The mapping function should not have been called');
|
||
|
$this->assertNotSame($none, $mapped, 'There should have been a new "None" instance returned');
|
||
|
}
|
||
|
|
||
|
#[TestDox('Map succeeds with Some')]
|
||
|
public function testMapSucceedsWithSome(): void
|
||
|
{
|
||
|
$mapped = Option::map(fn($it) => str_repeat($it, 2), Option::Some('abc '));
|
||
|
$this->assertTrue(Option::isSome($mapped), 'The mapped option should be "Some"');
|
||
|
$this->assertEquals('abc abc ', $mapped->get(), 'The mapping function was not called correctly');
|
||
|
}
|
||
|
|
||
|
#[TestDox('Map fails with Some when mapping is null')]
|
||
|
public function testMapFailsWithSomeWhenMappingIsNull(): void
|
||
|
{
|
||
|
$this->expectException(InvalidArgumentException::class);
|
||
|
Option::map(fn($it) => null, Option::Some('oof'));
|
||
|
}
|
||
|
|
||
|
#[TestDox('Iter succeeds with None')]
|
||
|
public function testIterSucceedsWithNone(): void
|
||
|
{
|
||
|
$target = new class { public mixed $called = null; };
|
||
|
Option::iter(function ($ignored) use ($target) { $target->called = 'uh oh'; }, Option::None());
|
||
|
$this->assertNull($target->called, 'The function should not have been called');
|
||
|
}
|
||
|
|
||
|
#[TestDox('Iter succeeds with Some')]
|
||
|
public function testIterSucceedsWithSome(): void
|
||
|
{
|
||
|
$target = new class { public mixed $called = null; };
|
||
|
Option::iter(function ($it) use ($target) { $target->called = $it; }, Option::Some(33));
|
||
|
$this->assertEquals(33, $target->called, 'The function should have been called with the "Some" value');
|
||
|
}
|
||
|
|
||
|
#[TestDox('Filter succeeds with None')]
|
||
|
public function testFilterSucceedsWithNone(): void
|
||
|
{
|
||
|
$tattle = new class { public bool $called = false; };
|
||
|
$none = Option::None();
|
||
|
$filtered = Option::filter(function ($ignored) use ($tattle)
|
||
|
{
|
||
|
$tattle->called = true;
|
||
|
return true;
|
||
|
}, $none);
|
||
|
$this->assertTrue(Option::isNone($filtered), 'The filtered option should have been "None"');
|
||
|
$this->assertFalse($tattle->called, 'The callable should not have been called');
|
||
|
}
|
||
|
|
||
|
#[TestDox('Filter succeeds with Some when true')]
|
||
|
public function testFilterSucceedsWithSomeWhenTrue(): void
|
||
|
{
|
||
|
$some = Option::Some(12);
|
||
|
$filtered = Option::filter(fn($it) => $it % 2 === 0, $some);
|
||
|
$this->assertTrue(Option::isSome($filtered), 'The filtered option should have been "Some"');
|
||
|
$this->assertEquals(12, $filtered->get(), 'The filtered option value is incorrect');
|
||
|
$this->assertNotSame($some, $filtered, 'There should have been a new option instance returned');
|
||
|
}
|
||
|
|
||
|
#[TestDox('Filter succeeds with Some when false')]
|
||
|
public function testFilterSucceedsWithSomeWhenFalse(): void
|
||
|
{
|
||
|
$some = Option::Some(23);
|
||
|
$filtered = Option::filter(fn($it) => $it % 2 === 0, $some);
|
||
|
$this->assertTrue(Option::isNone($filtered), 'The filtered option should have been "None"');
|
||
|
}
|
||
|
|
||
|
#[TestDox('Is succeeds with None')]
|
||
|
public function testIsSucceedsWithNone(): void
|
||
|
{
|
||
|
$this->assertFalse(Option::is(null, Option::None()), '"None" should always return false');
|
||
|
}
|
||
|
|
||
|
#[TestDox('Is succeeds with Some when strictly equal')]
|
||
|
public function testIsSucceedsWithSomeWhenStrictlyEqual(): void
|
||
|
{
|
||
|
$this->assertTrue(Option::is(3, Option::Some(3)), '"Some" with strict equality should be true');
|
||
|
}
|
||
|
|
||
|
#[TestDox('Is succeeds with Some when strictly unequal')]
|
||
|
public function testIsSucceedsWithSomeWhenStrictlyUnequal(): void
|
||
|
{
|
||
|
$this->assertFalse(Option::is(3, Option::Some('3')), '"Some" with strict equality should be false');
|
||
|
}
|
||
|
|
||
|
#[TestDox('Is succeeds with Some when loosely equal')]
|
||
|
public function testIsSucceedsWithSomeWhenLooselyEqual(): void
|
||
|
{
|
||
|
$this->assertTrue(Option::is(3, Option::Some('3'), strict: false), '"Some" with loose equality should be true');
|
||
|
}
|
||
|
|
||
|
#[TestDox('Is succeeds with Some when loosely unequal')]
|
||
|
public function testIsSucceedsWithSomeWhenLooselyUnequal(): void
|
||
|
{
|
||
|
$this->assertFalse(Option::is(4, Option::Some('3'), strict: false),
|
||
|
'"Some" with loose equality should be false');
|
||
|
}
|
||
|
|
||
|
#[TestDox('Unwrap succeeds with None')]
|
||
|
public function testUnwrapSucceedsWithNone(): void
|
||
|
{
|
||
|
$this->assertNull(Option::unwrap(Option::None()), '"None" should return null');
|
||
|
}
|
||
|
|
||
|
#[TestDox('Unwrap succeeds with Some')]
|
||
|
public function testUnwrapSucceedsWithSome(): void
|
||
|
{
|
||
|
$this->assertEquals('boy howdy', Option::unwrap(Option::Some('boy howdy')), '"Some" should return its value');
|
||
|
}
|
||
|
}
|