Skip to content
This repository has been archived by the owner on Aug 1, 2023. It is now read-only.

Unsortable has same contents as #44

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ jobs:
# Run tests on all OS's and HHVM versions, even if one fails
fail-fast: false
matrix:
os: [ ubuntu ]
os: [ ubuntu-20.04 ]
hhvm:
- '4.128'
- latest
- nightly
runs-on: ${{matrix.os}}-latest
- '4.153'
- '4.168'
runs-on: ${{matrix.os}}
steps:
- uses: actions/checkout@v2
- name: Create branch for version alias
Expand Down
120 changes: 116 additions & 4 deletions src/Assert.hack
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,23 @@ abstract class Assert {
);
}

public function assertContainsStrict(
mixed $needle,
Container<mixed> $haystack,
string $message,
)[]: void {
if (!C\contains($haystack, $needle)) {
throw new ExpectationFailedException(
Str\format(
"%s\nFailed asserting that %s contains %s",
$message,
\var_export_pure($haystack),
\var_export_pure($needle),
),
);
}
}

public function assertNotContains(
mixed $needle,
mixed $haystack,
Expand Down Expand Up @@ -587,7 +604,46 @@ abstract class Assert {
Traversable<T> $actual,
string $msg = '',
): void {
$this->assertEquals(Vec\sort($expected), Vec\sort($actual), $msg);
$expected = vec($expected);
$actual = vec($actual);

list($e_null_count, $e_strings, $e_ints, $e_floats, $e_bools, $e_rest) =
self::segregateByType($expected);

list($a_null_count, $a_strings, $a_ints, $a_floats, $a_bools, $a_rest) =
self::segregateByType($actual);

$this->assertEquals(
$e_null_count,
$a_null_count,
'Amount of nulls comparison: '.$msg,
);
$this->assertEquals(
Vec\sort($e_strings),
Vec\sort($a_strings),
'Checking strings only: '.$msg,
);
$this->assertEquals(
Vec\sort($e_ints),
Vec\sort($a_ints),
'Checking ints only: '.$msg,
);
$this->assertEquals(
Vec\sort($e_floats),
Vec\sort($a_floats),
'Checking floats only: '.$msg,
);
$this->assertEquals(
Vec\sort($e_bools),
Vec\sort($a_bools),
'Checking bools only: '.$msg,
);

$this->assertContentsEqualSlowPath(
$e_rest,
$a_rest,
'Checking non-scalars: '.$msg,
);
}
/**
* Checks that a collection is sorted according to some criterion.
Expand Down Expand Up @@ -640,9 +696,8 @@ abstract class Assert {
\var_export($pair[1], true),
);

throw new ExpectationFailedException(
$main_message.': '.$failure_detail,
);
throw
new ExpectationFailedException($main_message.': '.$failure_detail);
}

$index++;
Expand Down Expand Up @@ -690,4 +745,61 @@ abstract class Assert {
return $out;
}

/**
* Each returned vec, except for the last one is naively sortable.
* The number of nulls is returned, since there is only one value of this type.
* Returning a `vec<null>`, just to sort and count them would be rather silly...
*/
private static function segregateByType(vec<mixed> $values)[]: (
int /*number of nulls*/,
vec<string>,
vec<int>,
vec<float>,
vec<bool>,
vec<mixed>,
) {
$number_of_nulls = 0;
$strings = vec[];
$ints = vec[];
$floats = vec[];
$bools = vec[];
$rest = vec[];

foreach ($values as $v) {
if ($v is null) {
++$number_of_nulls;
} else if ($v is string) {
$strings[] = $v;
} else if ($v is int) {
$ints[] = $v;
} else if ($v is float) {
$floats[] = $v;
} else if ($v is bool) {
$bools[] = $v;
} else {
$rest[] = $v;
}
}

return tuple($number_of_nulls, $strings, $ints, $floats, $bools, $rest);
}

private function assertContentsEqualSlowPath(
vec<mixed> $expected,
vec<mixed> $actual,
string $msg,
): void {
$this->assertEquals(C\count($expected), C\count($actual), $msg);

// O(n^2), be prepared to spin here for a while...
foreach ($expected as $e) {
$this->assertContainsStrict($e, $actual, $msg);
}

// This test needs to be reflected.
// vec[A, A, A] compare to vec[A, B, C] passes the loop above.
foreach ($actual as $a) {
$this->assertContainsStrict($a, $expected, $msg);
}
}
}
18 changes: 17 additions & 1 deletion src/ExpectObj.hack
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use type Facebook\HackTest\ExpectationFailedException;

/* HHAST_IGNORE_ERROR[FinalOrAbstractClass] Intentional non-final for backward compatibility */
class ExpectObj<T> extends Assert {
public function __construct(private T $var) {}
public function __construct(private T $var)[] {}

/**************************************
**************************************
Expand Down Expand Up @@ -55,6 +55,22 @@ class ExpectObj<T> extends Assert {
$this->assertEquals($expected, $this->var, $msg);
}

public function toBeOfType<<<__Enforceable>> reify Treified>(
string $msg = '',
mixed ...$args
)[]: Treified {
$v = $this->var;
if (!$v is Treified) {
throw new ExpectationFailedException(Str\format(
"%s\nFailed to assert that %s was of the expected type.",
\vsprintf($msg, $args),
\var_export_pure($v),
));
}

return $v;
}

/**
* Float comparison can give false positives - this will only error if $actual
* and $expected are not within $delta of each other.
Expand Down
2 changes: 1 addition & 1 deletion src/expect.hack
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ namespace Facebook\FBExpect;
* - Function Call Assertions
* - Function Exception Assertions
*/
function expect<T>(T $obj): ExpectObj<T> {
function expect<T>(T $obj)[]: ExpectObj<T> {
return new ExpectObj($obj);
}
36 changes: 36 additions & 0 deletions tests/ExpectObjTest.hack
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,21 @@ final class ExpectObjTest extends HackTest {
});

expect(2)->toEqualWithDelta(1.99, 0.01);

// Optimized cases
expect(vec[2, 1])->toHaveSameContentAs(vec[1, 2]);
expect(vec['2', '1'])->toHaveSameContentAs(vec['1', '2']);
expect(vec[2., 1.])->toHaveSameContentAs(vec[1., 2.]);
expect(vec[true, false])->toHaveSameContentAs(vec[false, true]);
expect(vec[null, null])->toHaveSameContentAs(vec[null, null]);
expect(vec[null, true, 1., '1', 1])->toHaveSameContentAs(
vec[1, '1', 1., true, null],
);

// Slow path cases
expect(vec[dict['nested' => $o], $o, $o2])->toHaveSameContentAs(
vec[$o, $o2, dict['nested' => $o]],
);
}

/**
Expand Down Expand Up @@ -181,6 +196,13 @@ final class ExpectObjTest extends HackTest {
dict['k1' => 'v1', 'k2' => 'v2'],
dict['k1' => 'v2'],
],

vec['toHaveSameContentAs', vec[true], vec[1]],
vec['toHaveSameContentAs', vec['1'], vec[1]],
vec['toHaveSameContentAs', vec[1.], vec[1]],
vec['toHaveSameContentAs', vec[null], vec[0]],
vec['toHaveSameContentAs', vec[$o, $o], vec[$o, $o, $o]],
vec['toHaveSameContentAs', vec[$o, $o, $this], vec[$o, $o, $o]],
];
}

Expand Down Expand Up @@ -549,11 +571,25 @@ final class ExpectObjTest extends HackTest {
expect(fun('time'))->notToThrow();
}

public function testAssertToBeOfType(): void {
expect(1)->toBeOfType<int>() |> self::takesT<int>($$);
expect(new \stdClass())->toBeOfType<\stdClass>()
|> self::takesT<\stdClass>($$);
expect(1)->toBeOfType<?int>() |> self::takesT<?int>($$);

expect(() ==> {
// inferred type is the generic type of toBeOfType<T>(), not the passed type.
expect(1)->toBeOfType<string>() |> self::takesT<string>($$);
})->toThrow(ExpectationFailedException::class, 'the expected type');
}

public static function exampleStaticCallable(): void {
throw new \Exception('Static method called!');
}

public function exampleInstanceCallable(): void {
throw new \Exception('Instance method called!');
}

private static function takesT<T>(T $_)[]: void {}
}