123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533 |
- <?php
- namespace Nesk\Rialto\Tests;
- use Monolog\Logger;
- use Nesk\Rialto\Data\JsFunction;
- use Nesk\Rialto\Exceptions\Node;
- use Nesk\Rialto\Data\BasicResource;
- use Symfony\Component\Process\Process;
- use Nesk\Rialto\Tests\Implementation\Resources\Stats;
- use Nesk\Rialto\Tests\Implementation\{FsWithProcessDelegation, FsWithoutProcessDelegation};
- class ImplementationTest extends TestCase
- {
- const JS_FUNCTION_CREATE_DEPRECATION_PATTERN = '/^Nesk\\\\Rialto\\\\Data\\\\JsFunction::create\(\)/';
- public function setUp(): void
- {
- parent::setUp();
- $this->dirPath = realpath(__DIR__.'/resources');
- $this->filePath = "{$this->dirPath}/file";
- $this->fs = $this->canPopulateProperty('fs') ? new FsWithProcessDelegation : null;
- }
- public function tearDown(): void
- {
- $this->fs = null;
- }
- /** @test */
- public function can_call_method_and_get_its_return_value()
- {
- $content = $this->fs->readFileSync($this->filePath, 'utf8');
- $this->assertEquals('Hello world!', $content);
- }
- /** @test */
- public function can_get_property()
- {
- $constants = $this->fs->constants;
- $this->assertInternalType('array', $constants);
- }
- /** @test */
- public function can_set_property()
- {
- $this->fs->foo = 'bar';
- $this->assertEquals('bar', $this->fs->foo);
- $this->fs->foo = null;
- $this->assertNull($this->fs->foo);
- }
- /** @test */
- public function can_return_basic_resources()
- {
- $resource = $this->fs->readFileSync($this->filePath);
- $this->assertInstanceOf(BasicResource::class, $resource);
- }
- /** @test */
- public function can_return_specific_resources()
- {
- $resource = $this->fs->statSync($this->filePath);
- $this->assertInstanceOf(Stats::class, $resource);
- }
- /** @test */
- public function can_cast_resources_to_string()
- {
- $resource = $this->fs->statSync($this->filePath);
- $this->assertEquals('[object Object]', (string) $resource);
- }
- /**
- * @test
- * @dontPopulateProperties fs
- */
- public function can_omit_process_delegation()
- {
- $this->fs = new FsWithoutProcessDelegation;
- $resource = $this->fs->statSync($this->filePath);
- $this->assertInstanceOf(BasicResource::class, $resource);
- $this->assertNotInstanceOf(Stats::class, $resource);
- }
- /** @test */
- public function can_use_nested_resources()
- {
- $resources = $this->fs->multipleStatSync($this->dirPath, $this->filePath);
- $this->assertCount(2, $resources);
- $this->assertContainsOnlyInstancesOf(Stats::class, $resources);
- $isFile = $this->fs->multipleResourcesIsFile($resources);
- $this->assertFalse($isFile[0]);
- $this->assertTrue($isFile[1]);
- }
- /** @test */
- public function can_use_multiple_resources_without_confusion()
- {
- $dirStats = $this->fs->statSync($this->dirPath);
- $fileStats = $this->fs->statSync($this->filePath);
- $this->assertInstanceOf(Stats::class, $dirStats);
- $this->assertInstanceOf(Stats::class, $fileStats);
- $this->assertTrue($dirStats->isDirectory());
- $this->assertTrue($fileStats->isFile());
- }
- /** @test */
- public function can_return_multiple_times_the_same_resource()
- {
- $stats1 = $this->fs->Stats;
- $stats2 = $this->fs->Stats;
- $this->assertEquals($stats1, $stats2);
- }
- /**
- * @test
- * @group js-functions
- */
- public function can_use_js_functions_with_a_body()
- {
- $functions = [
- $this->ignoreUserDeprecation(self::JS_FUNCTION_CREATE_DEPRECATION_PATTERN, function () {
- return JsFunction::create("return 'Simple callback';");
- }),
- JsFunction::createWithBody("return 'Simple callback';"),
- ];
- foreach ($functions as $function) {
- $value = $this->fs->runCallback($function);
- $this->assertEquals('Simple callback', $value);
- }
- }
- /**
- * @test
- * @group js-functions
- */
- public function can_use_js_functions_with_parameters()
- {
- $functions = [
- $this->ignoreUserDeprecation(self::JS_FUNCTION_CREATE_DEPRECATION_PATTERN, function () {
- return JsFunction::create(['fs'], "
- return 'Callback using arguments: ' + fs.constructor.name;
- ");
- }),
- JsFunction::createWithParameters(['fs'])
- ->body("return 'Callback using arguments: ' + fs.constructor.name;"),
- ];
- foreach ($functions as $function) {
- $value = $this->fs->runCallback($function);
- $this->assertEquals('Callback using arguments: Object', $value);
- }
- }
- /**
- * @test
- * @group js-functions
- */
- public function can_use_js_functions_with_scope()
- {
- $functions = [
- $this->ignoreUserDeprecation(self::JS_FUNCTION_CREATE_DEPRECATION_PATTERN, function () {
- return JsFunction::create("
- return 'Callback using scope: ' + foo;
- ", ['foo' => 'bar']);
- }),
- JsFunction::createWithScope(['foo' => 'bar'])
- ->body("return 'Callback using scope: ' + foo;"),
- ];
- foreach ($functions as $function) {
- $value = $this->fs->runCallback($function);
- $this->assertEquals('Callback using scope: bar', $value);
- }
- }
- /**
- * @test
- * @group js-functions
- */
- public function can_use_resources_in_js_functions()
- {
- $fileStats = $this->fs->statSync($this->filePath);
- $functions = [
- JsFunction::createWithParameters(['fs', 'fileStats' => $fileStats])
- ->body("return fileStats.isFile();"),
- JsFunction::createWithScope(['fileStats' => $fileStats])
- ->body("return fileStats.isFile();"),
- ];
- foreach ($functions as $function) {
- $isFile = $this->fs->runCallback($function);
- $this->assertTrue($isFile);
- }
- }
- /**
- * @test
- * @group js-functions
- */
- public function can_use_async_with_js_functions()
- {
- $function = JsFunction::createWithAsync()
- ->body("
- await Promise.resolve();
- return true;
- ");
- $this->assertTrue($this->fs->runCallback($function));
- $function = $function->async(false);
- $this->expectException(Node\FatalException::class);
- $this->expectExceptionMessage('await is only valid in async function');
- $this->fs->runCallback($function);
- }
- /**
- * @test
- * @group js-functions
- */
- public function js_functions_are_sync_by_default()
- {
- $function = JsFunction::createWithBody('await null');
- $this->expectException(Node\FatalException::class);
- $this->expectExceptionMessage('await is only valid in async function');
- $this->fs->runCallback($function);
- }
- /** @test */
- public function can_receive_heavy_payloads_with_non_ascii_chars()
- {
- $payload = $this->fs->getHeavyPayloadWithNonAsciiChars();
- $this->assertStringStartsWith('😘', $payload);
- $this->assertStringEndsWith('😘', $payload);
- }
- /**
- * @test
- * @expectedException \Nesk\Rialto\Exceptions\Node\FatalException
- * @expectedExceptionMessage Object.__inexistantMethod__ is not a function
- */
- public function node_crash_throws_a_fatal_exception()
- {
- $this->fs->__inexistantMethod__();
- }
- /**
- * @test
- * @expectedException \Nesk\Rialto\Exceptions\Node\Exception
- * @expectedExceptionMessage Object.__inexistantMethod__ is not a function
- */
- public function can_catch_errors()
- {
- $this->fs->tryCatch->__inexistantMethod__();
- }
- /**
- * @test
- * @expectedException \Nesk\Rialto\Exceptions\Node\FatalException
- * @expectedExceptionMessage Object.__inexistantMethod__ is not a function
- */
- public function catching_a_node_exception_doesnt_catch_fatal_exceptions()
- {
- try {
- $this->fs->__inexistantMethod__();
- } catch (Node\Exception $exception) {
- //
- }
- }
- /**
- * @test
- * @dontPopulateProperties fs
- */
- public function in_debug_mode_node_exceptions_contain_stack_trace_in_message()
- {
- $this->fs = new FsWithProcessDelegation(['debug' => true]);
- $regex = '/\n\nError: "Object\.__inexistantMethod__ is not a function"\n\s+at /';
- try {
- $this->fs->tryCatch->__inexistantMethod__();
- } catch (Node\Exception $exception) {
- $this->assertRegExp($regex, $exception->getMessage());
- }
- try {
- $this->fs->__inexistantMethod__();
- } catch (Node\FatalException $exception) {
- $this->assertRegExp($regex, $exception->getMessage());
- }
- }
- /** @test */
- public function node_current_working_directory_is_the_same_as_php()
- {
- $result = $this->fs->accessSync('tests/resources/file');
- $this->assertNull($result);
- }
- /**
- * @test
- * @expectedException \Symfony\Component\Process\Exception\ProcessFailedException
- * @expectedExceptionMessageRegExp /Error Output:\n=+\n.*__inexistant_process__.*not found/
- */
- public function executable_path_option_changes_the_process_prefix()
- {
- new FsWithProcessDelegation(['executable_path' => '__inexistant_process__']);
- }
- /**
- * @test
- * @dontPopulateProperties fs
- */
- public function idle_timeout_option_closes_node_once_timer_is_reached()
- {
- $this->fs = new FsWithProcessDelegation(['idle_timeout' => 0.5]);
- $this->fs->constants;
- sleep(1);
- $this->expectException(\Nesk\Rialto\Exceptions\IdleTimeoutException::class);
- $this->expectExceptionMessageRegExp('/^The idle timeout \(0\.500 seconds\) has been exceeded/');
- $this->fs->constants;
- }
- /**
- * @test
- * @dontPopulateProperties fs
- * @expectedException \Nesk\Rialto\Exceptions\ReadSocketTimeoutException
- * @expectedExceptionMessageRegExp /^The timeout \(0\.010 seconds\) has been exceeded/
- */
- public function read_timeout_option_throws_an_exception_on_long_actions()
- {
- $this->fs = new FsWithProcessDelegation(['read_timeout' => 0.01]);
- $this->fs->wait(20);
- }
- /**
- * @test
- * @group logs
- * @dontPopulateProperties fs
- */
- public function forbidden_options_are_removed()
- {
- $this->fs = new FsWithProcessDelegation([
- 'logger' => $this->loggerMock(
- $this->at(0),
- $this->isLogLevel(),
- 'Applying options...',
- $this->callback(function ($context) {
- $this->assertArrayHasKey('read_timeout', $context['options']);
- $this->assertArrayNotHasKey('stop_timeout', $context['options']);
- $this->assertArrayNotHasKey('foo', $context['options']);
- return true;
- })
- ),
- 'read_timeout' => 5,
- 'stop_timeout' => 0,
- 'foo' => 'bar',
- ]);
- }
- /**
- * @test
- * @dontPopulateProperties fs
- */
- public function connection_delegate_receives_options()
- {
- $this->fs = new FsWithProcessDelegation([
- 'log_node_console' => true,
- 'new_option' => false,
- ]);
- $this->assertNull($this->fs->getOption('read_timeout')); // Assert this option is stripped by the supervisor
- $this->assertTrue($this->fs->getOption('log_node_console'));
- $this->assertFalse($this->fs->getOption('new_option'));
- }
- /**
- * @test
- * @dontPopulateProperties fs
- */
- public function process_status_is_tracked()
- {
- if (PHP_OS === 'WINNT') {
- $this->markTestSkipped('This test is not supported on Windows.');
- }
- if ((new Process(['which', 'pgrep']))->run() !== 0) {
- $this->markTestSkipped('The "pgrep" command is not available.');
- }
- $oldPids = $this->getPidsForProcessName('node');
- $this->fs = new FsWithProcessDelegation;
- $newPids = $this->getPidsForProcessName('node');
- $newNodeProcesses = array_values(array_diff($newPids, $oldPids));
- $newNodeProcessesCount = count($newNodeProcesses);
- $this->assertCount(
- 1,
- $newNodeProcesses,
- "One Node process should have been created instead of $newNodeProcessesCount. Try running again."
- );
- $processKilled = posix_kill($newNodeProcesses[0], SIGKILL);
- $this->assertTrue($processKilled);
- $this->expectException(\Nesk\Rialto\Exceptions\ProcessUnexpectedlyTerminatedException::class);
- $this->expectExceptionMessage('The process has been unexpectedly terminated.');
- $this->fs->foo;
- }
- /** @test */
- public function process_is_properly_shutdown_when_there_are_no_more_references()
- {
- if (!class_exists('WeakRef')) {
- $this->markTestSkipped(
- 'This test requires weak references (unavailable for PHP 7.3): http://php.net/weakref/'
- );
- }
- $ref = new \WeakRef($this->fs->getProcessSupervisor());
- $resource = $this->fs->readFileSync($this->filePath);
- $this->assertInstanceOf(BasicResource::class, $resource);
- $this->fs = null;
- unset($resource);
- $this->assertFalse($ref->valid());
- }
- /**
- * @test
- * @group logs
- * @dontPopulateProperties fs
- */
- public function logger_is_used_when_provided()
- {
- $this->fs = new FsWithProcessDelegation([
- 'logger' => $this->loggerMock(
- $this->atLeastOnce(),
- $this->isLogLevel(),
- $this->isType('string')
- ),
- ]);
- }
- /**
- * @test
- * @group logs
- * @dontPopulateProperties fs
- */
- public function node_console_calls_are_logged()
- {
- $setups = [
- [false, 'Received data on stdout:'],
- [true, 'Received a Node log:'],
- ];
- foreach ($setups as [$logNodeConsole, $startsWith]) {
- $this->fs = new FsWithProcessDelegation([
- 'log_node_console' => $logNodeConsole,
- 'logger' => $this->loggerMock(
- $this->at(5),
- $this->isLogLevel(),
- $this->stringStartsWith($startsWith)
- ),
- ]);
- $this->fs->runCallback(JsFunction::createWithBody("console.log('Hello World!')"));
- }
- }
- /**
- * @test
- * @group logs
- * @dontPopulateProperties fs
- */
- public function delayed_node_console_calls_and_data_on_standard_streams_are_logged()
- {
- $this->fs = new FsWithProcessDelegation([
- 'log_node_console' => true,
- 'logger' => $this->loggerMock([
- [$this->at(6), $this->isLogLevel(), $this->stringStartsWith('Received data on stdout:')],
- [$this->at(7), $this->isLogLevel(), $this->stringStartsWith('Received a Node log:')],
- ]),
- ]);
- $this->fs->runCallback(JsFunction::createWithBody("
- setTimeout(() => {
- process.stdout.write('Hello Stdout!');
- console.log('Hello Console!');
- });
- "));
- usleep(10000); // 10ms, to be sure the delayed instructions just above are executed.
- $this->fs = null;
- }
- }
|