minitest/ExpectObj.hack (123 lines of code) (raw):

namespace HH\__Private\MiniTest; // Not currently using the HSL as it's used to test the HSL :) class ExpectObj<T> { public function __construct(private T $value) {} private static function assert(bool $value, ?string $message = null): void { $message ??= 'Test assertion failed'; invariant($value, '%s', $message); } public function toEqual(T $other, ?\HH\FormatString<\PlainSprintf> $message = null, mixed ...$args): void { if ($message !== null) { $message = \vsprintf($message, $args); } self::assert($this->value === $other, $message); } public function toNotEqual(T $other, ?string $message = null): void { self::assert($this->value !== $other, $message); } public function toBeNull<TInner>(): void where T = ?TInner{ self::assert($this->value === null); } public function toNotBeNull<TInner>(): TInner where T = ?TInner { self::assert($this->value !== null); return $this->value as nonnull; } public function toBeTrue(?string $message = null): void where T = bool { self::assert($this->value, $message); } public function toBeFalse(?string $message = null): void where T = bool { self::assert(!$this->value, $message); } public function toContain<Tv>(Tv $val): void where T as Container<Tv> { self::assert(\in_array($val, $this->value, /* strict = */ true)); } public function toContainKey<Tk as arraykey>(Tk $key): void where T as KeyedContainer<Tk, mixed> { self::assert(\array_key_exists($key, $this->value)); } public function toContainSubstring(string $val): void where T = string { self::assert(\strpos($this->value, $val) !== false); } public function toNotContainSubstring(string $val): void where T = string { self::assert(\strpos($this->value, $val) === false); } // FIXME this really shouldn't be used in the HSL... public function toBePHPEqual(T $other): void { self::assert($this->value == $other); } public function toEqualWithDelta(T $other, float $delta): void where T = float { $diff = \abs($this->value - $other); self::assert($diff <= $delta); } public function toAlmostEqual(T $other): void where T = float { $this->toEqualWithDelta($other, 1.19e-07 * 4); // match fbexpect and gtest } public function toBeGreaterThan(T $other): void where T as num { self::assert($this->value > $other); } public function toBeGreaterThanOrEqualTo(T $other): void where T as num { self::assert($this->value >= $other); } public function toBeLessThan(T $other): void where T as num { self::assert($this->value < $other); } public function toBeLessThanOrEqualTo(T $other): void where T as num { self::assert($this->value <= $other); } public function toThrow<TEx as \Throwable>( classname<TEx> $ex, ?string $ex_message = null, ?string $_failure_message = null, ): TEx where T = (function(): mixed) { $thrown = null; try { $f = $this->value; $ret = $f(); if ($ret is Awaitable<_>) { \HH\Asio\join($ret); } } catch (\Throwable $t) { $thrown = $t; } $thrown = expect($thrown)->toNotBeNull(); self::assert( \is_a($thrown, $ex), $_failure_message ?? 'Expected to throw "'.$ex."\", but instead got:\n".$thrown->__toString() ); if ($ex_message is nonnull) { self::assert( \strpos($thrown?->getMessage() ?? '', $ex_message) !== false, $_failure_message ?? 'Expected the exception message to be "'. $ex_message. "\", but instead got:\n". $thrown->__toString() ); } /* HH_FIXME[4110] verified with is_a() */ return $thrown; } public function toBeInstanceOf( classname<mixed> $class ): void { self::assert(\is_a($this->value, $class)); } public function notToThrow<TRet>( ): void where T = (function(): TRet) { $f = $this->value; $ret = $f(); if ($ret is Awaitable<_>) { \HH\Asio\join($ret); } } public function toHaveSameContentAs(T $other): void where T as Container<mixed> { self::assert(\count($this->value) === \count($other), 'counts differ'); foreach ($this->value as $v) { $found = false; foreach ($other as $vv) { if ($v === $vv) { $found = true; break; } } self::assert($found, 'Missing value: '.\var_export($v, true)); } } }