ImplementationTest.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. <?php
  2. namespace Nesk\Rialto\Tests;
  3. use Monolog\Logger;
  4. use Nesk\Rialto\Data\JsFunction;
  5. use Nesk\Rialto\Exceptions\Node;
  6. use Nesk\Rialto\Data\BasicResource;
  7. use Symfony\Component\Process\Process;
  8. use Nesk\Rialto\Tests\Implementation\Resources\Stats;
  9. use Nesk\Rialto\Tests\Implementation\{FsWithProcessDelegation, FsWithoutProcessDelegation};
  10. class ImplementationTest extends TestCase
  11. {
  12. const JS_FUNCTION_CREATE_DEPRECATION_PATTERN = '/^Nesk\\\\Rialto\\\\Data\\\\JsFunction::create\(\)/';
  13. protected function setUp(): void
  14. {
  15. parent::setUp();
  16. $this->dirPath = realpath(__DIR__.'/resources');
  17. $this->filePath = "{$this->dirPath}/file";
  18. $this->fs = $this->canPopulateProperty('fs') ? new FsWithProcessDelegation : null;
  19. }
  20. protected function tearDown(): void
  21. {
  22. $this->fs = null;
  23. }
  24. /** @test */
  25. public function can_call_method_and_get_its_return_value()
  26. {
  27. $content = $this->fs->readFileSync($this->filePath, 'utf8');
  28. $this->assertEquals('Hello world!', $content);
  29. }
  30. /** @test */
  31. public function can_get_property()
  32. {
  33. $constants = $this->fs->constants;
  34. $this->assertIsArray($constants);
  35. }
  36. /** @test */
  37. public function can_set_property()
  38. {
  39. $this->fs->foo = 'bar';
  40. $this->assertEquals('bar', $this->fs->foo);
  41. $this->fs->foo = null;
  42. $this->assertNull($this->fs->foo);
  43. }
  44. /** @test */
  45. public function can_return_basic_resources()
  46. {
  47. $resource = $this->fs->readFileSync($this->filePath);
  48. $this->assertInstanceOf(BasicResource::class, $resource);
  49. }
  50. /** @test */
  51. public function can_return_specific_resources()
  52. {
  53. $resource = $this->fs->statSync($this->filePath);
  54. $this->assertInstanceOf(Stats::class, $resource);
  55. }
  56. /** @test */
  57. public function can_cast_resources_to_string()
  58. {
  59. $resource = $this->fs->statSync($this->filePath);
  60. $this->assertEquals('[object Object]', (string) $resource);
  61. }
  62. /**
  63. * @test
  64. * @dontPopulateProperties fs
  65. */
  66. public function can_omit_process_delegation()
  67. {
  68. $this->fs = new FsWithoutProcessDelegation;
  69. $resource = $this->fs->statSync($this->filePath);
  70. $this->assertInstanceOf(BasicResource::class, $resource);
  71. $this->assertNotInstanceOf(Stats::class, $resource);
  72. }
  73. /** @test */
  74. public function can_use_nested_resources()
  75. {
  76. $resources = $this->fs->multipleStatSync($this->dirPath, $this->filePath);
  77. $this->assertCount(2, $resources);
  78. $this->assertContainsOnlyInstancesOf(Stats::class, $resources);
  79. $isFile = $this->fs->multipleResourcesIsFile($resources);
  80. $this->assertFalse($isFile[0]);
  81. $this->assertTrue($isFile[1]);
  82. }
  83. /** @test */
  84. public function can_use_multiple_resources_without_confusion()
  85. {
  86. $dirStats = $this->fs->statSync($this->dirPath);
  87. $fileStats = $this->fs->statSync($this->filePath);
  88. $this->assertInstanceOf(Stats::class, $dirStats);
  89. $this->assertInstanceOf(Stats::class, $fileStats);
  90. $this->assertTrue($dirStats->isDirectory());
  91. $this->assertTrue($fileStats->isFile());
  92. }
  93. /** @test */
  94. public function can_return_multiple_times_the_same_resource()
  95. {
  96. $stats1 = $this->fs->Stats;
  97. $stats2 = $this->fs->Stats;
  98. $this->assertEquals($stats1, $stats2);
  99. }
  100. /**
  101. * @test
  102. * @group js-functions
  103. */
  104. public function can_use_js_functions_with_a_body()
  105. {
  106. $functions = [
  107. $this->ignoreUserDeprecation(self::JS_FUNCTION_CREATE_DEPRECATION_PATTERN, function () {
  108. return JsFunction::create("return 'Simple callback';");
  109. }),
  110. JsFunction::createWithBody("return 'Simple callback';"),
  111. ];
  112. foreach ($functions as $function) {
  113. $value = $this->fs->runCallback($function);
  114. $this->assertEquals('Simple callback', $value);
  115. }
  116. }
  117. /**
  118. * @test
  119. * @group js-functions
  120. */
  121. public function can_use_js_functions_with_parameters()
  122. {
  123. $functions = [
  124. $this->ignoreUserDeprecation(self::JS_FUNCTION_CREATE_DEPRECATION_PATTERN, function () {
  125. return JsFunction::create(['fs'], "
  126. return 'Callback using arguments: ' + fs.constructor.name;
  127. ");
  128. }),
  129. JsFunction::createWithParameters(['fs'])
  130. ->body("return 'Callback using arguments: ' + fs.constructor.name;"),
  131. ];
  132. foreach ($functions as $function) {
  133. $value = $this->fs->runCallback($function);
  134. $this->assertEquals('Callback using arguments: Object', $value);
  135. }
  136. }
  137. /**
  138. * @test
  139. * @group js-functions
  140. */
  141. public function can_use_js_functions_with_scope()
  142. {
  143. $functions = [
  144. $this->ignoreUserDeprecation(self::JS_FUNCTION_CREATE_DEPRECATION_PATTERN, function () {
  145. return JsFunction::create("
  146. return 'Callback using scope: ' + foo;
  147. ", ['foo' => 'bar']);
  148. }),
  149. JsFunction::createWithScope(['foo' => 'bar'])
  150. ->body("return 'Callback using scope: ' + foo;"),
  151. ];
  152. foreach ($functions as $function) {
  153. $value = $this->fs->runCallback($function);
  154. $this->assertEquals('Callback using scope: bar', $value);
  155. }
  156. }
  157. /**
  158. * @test
  159. * @group js-functions
  160. */
  161. public function can_use_resources_in_js_functions()
  162. {
  163. $fileStats = $this->fs->statSync($this->filePath);
  164. $functions = [
  165. JsFunction::createWithParameters(['fs', 'fileStats' => $fileStats])
  166. ->body("return fileStats.isFile();"),
  167. JsFunction::createWithScope(['fileStats' => $fileStats])
  168. ->body("return fileStats.isFile();"),
  169. ];
  170. foreach ($functions as $function) {
  171. $isFile = $this->fs->runCallback($function);
  172. $this->assertTrue($isFile);
  173. }
  174. }
  175. /**
  176. * @test
  177. * @group js-functions
  178. */
  179. public function can_use_async_with_js_functions()
  180. {
  181. $function = JsFunction::createWithAsync()
  182. ->body("
  183. await Promise.resolve();
  184. return true;
  185. ");
  186. $this->assertTrue($this->fs->runCallback($function));
  187. $function = $function->async(false);
  188. $this->expectException(Node\FatalException::class);
  189. $this->expectExceptionMessage('await is only valid in async function');
  190. $this->fs->runCallback($function);
  191. }
  192. /**
  193. * @test
  194. * @group js-functions
  195. */
  196. public function js_functions_are_sync_by_default()
  197. {
  198. $function = JsFunction::createWithBody('await null');
  199. $this->expectException(Node\FatalException::class);
  200. $this->expectExceptionMessage('await is only valid in async function');
  201. $this->fs->runCallback($function);
  202. }
  203. /** @test */
  204. public function can_receive_heavy_payloads_with_non_ascii_chars()
  205. {
  206. $payload = $this->fs->getHeavyPayloadWithNonAsciiChars();
  207. $this->assertStringStartsWith('😘', $payload);
  208. $this->assertStringEndsWith('😘', $payload);
  209. }
  210. /**
  211. * @test
  212. */
  213. public function node_crash_throws_a_fatal_exception()
  214. {
  215. self::expectException(\Nesk\Rialto\Exceptions\Node\FatalException::class);
  216. self::expectExceptionMessage('Object.__inexistantMethod__ is not a function');
  217. $this->fs->__inexistantMethod__();
  218. }
  219. /**
  220. * @test
  221. */
  222. public function can_catch_errors()
  223. {
  224. self::expectException(\Nesk\Rialto\Exceptions\Node\Exception::class);
  225. self::expectExceptionMessage('Object.__inexistantMethod__ is not a function');
  226. $this->fs->tryCatch->__inexistantMethod__();
  227. }
  228. /**
  229. * @test
  230. */
  231. public function catching_a_node_exception_doesnt_catch_fatal_exceptions()
  232. {
  233. self::expectException(\Nesk\Rialto\Exceptions\Node\FatalException::class);
  234. self::expectExceptionMessage('Object.__inexistantMethod__ is not a function');
  235. try {
  236. $this->fs->__inexistantMethod__();
  237. } catch (Node\Exception $exception) {
  238. //
  239. }
  240. }
  241. /**
  242. * @test
  243. * @dontPopulateProperties fs
  244. */
  245. public function in_debug_mode_node_exceptions_contain_stack_trace_in_message()
  246. {
  247. $this->fs = new FsWithProcessDelegation(['debug' => true]);
  248. $regex = '/\n\nError: "Object\.__inexistantMethod__ is not a function"\n\s+at /';
  249. try {
  250. $this->fs->tryCatch->__inexistantMethod__();
  251. } catch (Node\Exception $exception) {
  252. $this->assertRegExp($regex, $exception->getMessage());
  253. }
  254. try {
  255. $this->fs->__inexistantMethod__();
  256. } catch (Node\FatalException $exception) {
  257. $this->assertRegExp($regex, $exception->getMessage());
  258. }
  259. }
  260. /** @test */
  261. public function node_current_working_directory_is_the_same_as_php()
  262. {
  263. $result = $this->fs->accessSync('tests/resources/file');
  264. $this->assertNull($result);
  265. }
  266. /**
  267. * @test
  268. */
  269. public function executable_path_option_changes_the_process_prefix()
  270. {
  271. self::expectException(\Symfony\Component\Process\Exception\ProcessFailedException::class);
  272. self::expectExceptionMessageMatches('/Error Output:\n=+\n.*__inexistant_process__.*not found/');
  273. new FsWithProcessDelegation(['executable_path' => '__inexistant_process__']);
  274. }
  275. /**
  276. * @test
  277. * @dontPopulateProperties fs
  278. */
  279. public function idle_timeout_option_closes_node_once_timer_is_reached()
  280. {
  281. $this->fs = new FsWithProcessDelegation(['idle_timeout' => 0.5]);
  282. $this->fs->constants;
  283. sleep(1);
  284. $this->expectException(\Nesk\Rialto\Exceptions\IdleTimeoutException::class);
  285. $this->expectExceptionMessageMatches('/^The idle timeout \(0\.500 seconds\) has been exceeded/');
  286. $this->fs->constants;
  287. }
  288. /**
  289. * @test
  290. * @dontPopulateProperties fs
  291. */
  292. public function read_timeout_option_throws_an_exception_on_long_actions()
  293. {
  294. self::expectException(\Nesk\Rialto\Exceptions\ReadSocketTimeoutException::class);
  295. self::expectExceptionMessageMatches('/^The timeout \(0\.010 seconds\) has been exceeded/');
  296. $this->fs = new FsWithProcessDelegation(['read_timeout' => 0.01]);
  297. $this->fs->wait(20);
  298. }
  299. /**
  300. * @test
  301. * @group logs
  302. * @dontPopulateProperties fs
  303. */
  304. public function forbidden_options_are_removed()
  305. {
  306. $this->fs = new FsWithProcessDelegation([
  307. 'logger' => $this->loggerMock(
  308. $this->at(0),
  309. $this->isLogLevel(),
  310. 'Applying options...',
  311. $this->callback(function ($context) {
  312. $this->assertArrayHasKey('read_timeout', $context['options']);
  313. $this->assertArrayNotHasKey('stop_timeout', $context['options']);
  314. $this->assertArrayNotHasKey('foo', $context['options']);
  315. return true;
  316. })
  317. ),
  318. 'read_timeout' => 5,
  319. 'stop_timeout' => 0,
  320. 'foo' => 'bar',
  321. ]);
  322. }
  323. /**
  324. * @test
  325. * @dontPopulateProperties fs
  326. */
  327. public function connection_delegate_receives_options()
  328. {
  329. $this->fs = new FsWithProcessDelegation([
  330. 'log_node_console' => true,
  331. 'new_option' => false,
  332. ]);
  333. $this->assertNull($this->fs->getOption('read_timeout')); // Assert this option is stripped by the supervisor
  334. $this->assertTrue($this->fs->getOption('log_node_console'));
  335. $this->assertFalse($this->fs->getOption('new_option'));
  336. }
  337. /**
  338. * @test
  339. * @dontPopulateProperties fs
  340. */
  341. public function process_status_is_tracked()
  342. {
  343. if (PHP_OS === 'WINNT') {
  344. $this->markTestSkipped('This test is not supported on Windows.');
  345. }
  346. if ((new Process(['which', 'pgrep']))->run() !== 0) {
  347. $this->markTestSkipped('The "pgrep" command is not available.');
  348. }
  349. $oldPids = $this->getPidsForProcessName('node');
  350. $this->fs = new FsWithProcessDelegation;
  351. $newPids = $this->getPidsForProcessName('node');
  352. $newNodeProcesses = array_values(array_diff($newPids, $oldPids));
  353. $newNodeProcessesCount = count($newNodeProcesses);
  354. $this->assertCount(
  355. 1,
  356. $newNodeProcesses,
  357. "One Node process should have been created instead of $newNodeProcessesCount. Try running again."
  358. );
  359. $processKilled = posix_kill($newNodeProcesses[0], SIGKILL);
  360. $this->assertTrue($processKilled);
  361. \usleep(10000); # To make sure the process had enough time to be killed.
  362. $this->expectException(\Nesk\Rialto\Exceptions\ProcessUnexpectedlyTerminatedException::class);
  363. $this->expectExceptionMessage('The process has been unexpectedly terminated.');
  364. $this->fs->foo;
  365. }
  366. /**
  367. * @test
  368. * @group logs
  369. * @dontPopulateProperties fs
  370. */
  371. public function logger_is_used_when_provided()
  372. {
  373. $this->fs = new FsWithProcessDelegation([
  374. 'logger' => $this->loggerMock(
  375. $this->atLeastOnce(),
  376. $this->isLogLevel(),
  377. $this->isType('string')
  378. ),
  379. ]);
  380. }
  381. /**
  382. * @test
  383. * @group logs
  384. * @dontPopulateProperties fs
  385. */
  386. public function node_console_calls_are_logged()
  387. {
  388. $setups = [
  389. [false, 'Received data on stdout:'],
  390. [true, 'Received a Node log:'],
  391. ];
  392. foreach ($setups as [$logNodeConsole, $startsWith]) {
  393. $this->fs = new FsWithProcessDelegation([
  394. 'log_node_console' => $logNodeConsole,
  395. 'logger' => $this->loggerMock(
  396. $this->at(5),
  397. $this->isLogLevel(),
  398. $this->stringStartsWith($startsWith)
  399. ),
  400. ]);
  401. $this->fs->runCallback(JsFunction::createWithBody("console.log('Hello World!')"));
  402. }
  403. }
  404. /**
  405. * @test
  406. * @group logs
  407. * @dontPopulateProperties fs
  408. */
  409. public function delayed_node_console_calls_and_data_on_standard_streams_are_logged()
  410. {
  411. $this->fs = new FsWithProcessDelegation([
  412. 'log_node_console' => true,
  413. 'logger' => $this->loggerMock([
  414. [$this->at(6), $this->isLogLevel(), $this->stringStartsWith('Received data on stdout:')],
  415. [$this->at(7), $this->isLogLevel(), $this->stringStartsWith('Received a Node log:')],
  416. ]),
  417. ]);
  418. $this->fs->runCallback(JsFunction::createWithBody("
  419. setTimeout(() => {
  420. process.stdout.write('Hello Stdout!');
  421. console.log('Hello Console!');
  422. });
  423. "));
  424. usleep(10000); // 10ms, to be sure the delayed instructions just above are executed.
  425. $this->fs = null;
  426. }
  427. }