1<?php 2 3namespace FPM; 4 5use Adoy\FastCGI\Client; 6 7require_once 'fcgi.inc'; 8require_once 'logtool.inc'; 9require_once 'response.inc'; 10 11class Tester 12{ 13 /** 14 * Config directory for included files. 15 */ 16 const CONF_DIR = __DIR__ . '/conf.d'; 17 18 /** 19 * File extension for access log. 20 */ 21 const FILE_EXT_LOG_ACC = 'acc.log'; 22 23 /** 24 * File extension for error log. 25 */ 26 const FILE_EXT_LOG_ERR = 'err.log'; 27 28 /** 29 * File extension for slow log. 30 */ 31 const FILE_EXT_LOG_SLOW = 'slow.log'; 32 33 /** 34 * File extension for PID file. 35 */ 36 const FILE_EXT_PID = 'pid'; 37 38 /** 39 * @var array 40 */ 41 static private $supportedFiles = [ 42 self::FILE_EXT_LOG_ACC, 43 self::FILE_EXT_LOG_ERR, 44 self::FILE_EXT_LOG_SLOW, 45 self::FILE_EXT_PID, 46 'src.php', 47 'ini', 48 'skip.ini', 49 '*.sock', 50 ]; 51 52 /** 53 * @var array 54 */ 55 static private $filesToClean = ['.user.ini']; 56 57 /** 58 * @var bool 59 */ 60 private $debug; 61 62 /** 63 * @var array 64 */ 65 private $clients; 66 67 /** 68 * @var LogTool 69 */ 70 private $logTool; 71 72 /** 73 * Configuration template 74 * 75 * @var string 76 */ 77 private $configTemplate; 78 79 /** 80 * The PHP code to execute 81 * 82 * @var string 83 */ 84 private $code; 85 86 /** 87 * @var array 88 */ 89 private $options; 90 91 /** 92 * @var string 93 */ 94 private $fileName; 95 96 /** 97 * @var resource 98 */ 99 private $masterProcess; 100 101 /** 102 * @var resource 103 */ 104 private $outDesc; 105 106 /** 107 * @var array 108 */ 109 private $ports = []; 110 111 /** 112 * @var string 113 */ 114 private $error; 115 116 /** 117 * The last response for the request call 118 * 119 * @var Response 120 */ 121 private $response; 122 123 /** 124 * Clean all the created files up 125 * 126 * @param int $backTraceIndex 127 */ 128 static public function clean($backTraceIndex = 1) 129 { 130 $filePrefix = self::getCallerFileName($backTraceIndex); 131 if (substr($filePrefix, -6) === 'clean.') { 132 $filePrefix = substr($filePrefix, 0, -6); 133 } 134 135 $filesToClean = array_merge( 136 array_map( 137 function($fileExtension) use ($filePrefix) { 138 return $filePrefix . $fileExtension; 139 }, 140 self::$supportedFiles 141 ), 142 array_map( 143 function($fileExtension) { 144 return __DIR__ . '/' . $fileExtension; 145 }, 146 self::$filesToClean 147 ) 148 ); 149 // clean all the root files 150 foreach ($filesToClean as $filePattern) { 151 foreach (glob($filePattern) as $filePath) { 152 unlink($filePath); 153 } 154 } 155 // clean config files 156 if (is_dir(self::CONF_DIR)) { 157 foreach(glob(self::CONF_DIR . '/*.conf') as $name) { 158 unlink($name); 159 } 160 rmdir(self::CONF_DIR); 161 } 162 } 163 164 /** 165 * @param int $backTraceIndex 166 * @return string 167 */ 168 static private function getCallerFileName($backTraceIndex = 1) 169 { 170 $backtrace = debug_backtrace(); 171 if (isset($backtrace[$backTraceIndex]['file'])) { 172 $filePath = $backtrace[$backTraceIndex]['file']; 173 } else { 174 $filePath = __FILE__; 175 } 176 177 return substr($filePath, 0, -strlen(pathinfo($filePath, PATHINFO_EXTENSION))); 178 } 179 180 /** 181 * @return bool|string 182 */ 183 static public function findExecutable() 184 { 185 $phpPath = getenv("TEST_PHP_EXECUTABLE"); 186 for ($i = 0; $i < 2; $i++) { 187 $slashPosition = strrpos($phpPath, "/"); 188 if ($slashPosition) { 189 $phpPath = substr($phpPath, 0, $slashPosition); 190 } else { 191 break; 192 } 193 } 194 195 if ($phpPath && is_dir($phpPath)) { 196 if (file_exists($phpPath."/fpm/php-fpm") && is_executable($phpPath."/fpm/php-fpm")) { 197 /* gotcha */ 198 return $phpPath."/fpm/php-fpm"; 199 } 200 $phpSbinFpmi = $phpPath."/sbin/php-fpm"; 201 if (file_exists($phpSbinFpmi) && is_executable($phpSbinFpmi)) { 202 return $phpSbinFpmi; 203 } 204 } 205 206 // try local php-fpm 207 $fpmPath = dirname(__DIR__) . '/php-fpm'; 208 if (file_exists($fpmPath) && is_executable($fpmPath)) { 209 return $fpmPath; 210 } 211 212 return false; 213 } 214 215 /** 216 * Skip test if any of the supplied files does not exist. 217 * 218 * @param mixed $files 219 */ 220 static public function skipIfAnyFileDoesNotExist($files) 221 { 222 if (!is_array($files)) { 223 $files = array($files); 224 } 225 foreach ($files as $file) { 226 if (!file_exists($file)) { 227 die("skip File $file does not exist"); 228 } 229 } 230 } 231 232 /** 233 * Skip test if config file is invalid. 234 * 235 * @param string $configTemplate 236 * @throws \Exception 237 */ 238 static public function skipIfConfigFails(string $configTemplate) 239 { 240 $tester = new self($configTemplate, '', [], self::getCallerFileName()); 241 $testResult = $tester->testConfig(); 242 if ($testResult !== null) { 243 self::clean(2); 244 die("skip $testResult"); 245 } 246 } 247 248 /** 249 * Skip test if IPv6 is not supported. 250 */ 251 static public function skipIfIPv6IsNotSupported() 252 { 253 @stream_socket_client('tcp://[::1]:0', $errno); 254 if ($errno != 111) { 255 die('skip IPv6 is not supported.'); 256 } 257 } 258 259 /** 260 * Skip if running on Travis. 261 * 262 * @param $message 263 */ 264 static public function skipIfTravis($message) 265 { 266 if (getenv("TRAVIS")) { 267 die('skip Travis: ' . $message); 268 } 269 } 270 271 /** 272 * Tester constructor. 273 * 274 * @param string|array $configTemplate 275 * @param string $code 276 * @param array $options 277 * @param string $fileName 278 */ 279 public function __construct( 280 $configTemplate, 281 string $code = '', 282 array $options = [], 283 $fileName = null 284 ) { 285 $this->configTemplate = $configTemplate; 286 $this->code = $code; 287 $this->options = $options; 288 $this->fileName = $fileName ?: self::getCallerFileName(); 289 $this->logTool = new LogTool(); 290 $this->debug = (bool) getenv('TEST_FPM_DEBUG'); 291 } 292 293 /** 294 * @param string $ini 295 */ 296 public function setUserIni(string $ini) 297 { 298 $iniFile = __DIR__ . '/.user.ini'; 299 file_put_contents($iniFile, $ini); 300 } 301 302 /** 303 * Test configuration file. 304 * 305 * @return null|string 306 * @throws \Exception 307 */ 308 public function testConfig() 309 { 310 $configFile = $this->createConfig(); 311 $cmd = self::findExecutable() . ' -t -y ' . $configFile . ' 2>&1'; 312 exec($cmd, $output, $code); 313 if ($code) { 314 return preg_replace("/\[.+?\]/", "", $output[0]); 315 } 316 317 return null; 318 } 319 320 /** 321 * Start PHP-FPM master process 322 * 323 * @param string $extraArgs 324 * @return bool 325 * @throws \Exception 326 */ 327 public function start(string $extraArgs = '') 328 { 329 $configFile = $this->createConfig(); 330 $desc = $this->outDesc ? [] : [1 => array('pipe', 'w')]; 331 $asRoot = getenv('TEST_FPM_RUN_AS_ROOT') ? '--allow-to-run-as-root' : ''; 332 $cmd = self::findExecutable() . " $asRoot -F -O -y $configFile $extraArgs"; 333 /* Since it's not possible to spawn a process under linux without using a 334 * shell in php (why?!?) we need a little shell trickery, so that we can 335 * actually kill php-fpm */ 336 $this->masterProcess = proc_open( 337 "killit () { kill \$child 2> /dev/null; }; " . 338 "trap killit TERM; $cmd 2>&1 & child=\$!; wait", 339 $desc, 340 $pipes 341 ); 342 register_shutdown_function( 343 function($masterProcess) use($configFile) { 344 @unlink($configFile); 345 if (is_resource($masterProcess)) { 346 @proc_terminate($masterProcess); 347 while (proc_get_status($masterProcess)['running']) { 348 usleep(10000); 349 } 350 } 351 }, 352 $this->masterProcess 353 ); 354 if (!$this->outDesc !== false) { 355 $this->outDesc = $pipes[1]; 356 } 357 358 return true; 359 } 360 361 /** 362 * Run until needle is found in the log. 363 * 364 * @param string $needle 365 * @param int $max 366 * @return bool 367 * @throws \Exception 368 */ 369 public function runTill(string $needle, $max = 10) 370 { 371 $this->start(); 372 $found = false; 373 for ($i = 0; $i < $max; $i++) { 374 $line = $this->getLogLine(); 375 if (is_null($line)) { 376 break; 377 } 378 if (preg_match($needle, $line) === 1) { 379 $found = true; 380 break; 381 } 382 } 383 $this->close(true); 384 385 if (!$found) { 386 return $this->error("The search pattern not found"); 387 } 388 389 return true; 390 } 391 392 /** 393 * Check if connection works. 394 * 395 * @param string $host 396 * @param null|string $successMessage 397 * @param null|string $errorMessage 398 * @param int $attempts 399 * @param int $delay 400 */ 401 public function checkConnection( 402 $host = '127.0.0.1', 403 $successMessage = null, 404 $errorMessage = 'Connection failed', 405 $attempts = 20, 406 $delay = 50000 407 ) { 408 $i = 0; 409 do { 410 if ($i > 0 && $delay > 0) { 411 usleep($delay); 412 } 413 $fp = @fsockopen($host, $this->getPort()); 414 } while ((++$i < $attempts) && !$fp); 415 416 if ($fp) { 417 $this->message($successMessage); 418 fclose($fp); 419 } else { 420 $this->message($errorMessage); 421 } 422 } 423 424 425 /** 426 * Execute request with parameters ordered for better checking. 427 * 428 * @param string $address 429 * @param string|null $successMessage 430 * @param string|null $errorMessage 431 * @param string $uri 432 * @param string $query 433 * @param array $headers 434 * @return Response 435 */ 436 public function checkRequest( 437 string $address, 438 string $successMessage = null, 439 string $errorMessage = null, 440 $uri = '/ping', 441 $query = '', 442 $headers = [] 443 ) { 444 return $this->request($query, $headers, $uri, $address, $successMessage, $errorMessage); 445 } 446 447 /** 448 * Execute and check ping request. 449 * 450 * @param string $address 451 * @param string $pingPath 452 * @param string $pingResponse 453 */ 454 public function ping( 455 string $address = '{{ADDR}}', 456 string $pingResponse = 'pong', 457 string $pingPath = '/ping' 458 ) { 459 $response = $this->request('', [], $pingPath, $address); 460 $response->expectBody($pingResponse, 'text/plain'); 461 } 462 463 /** 464 * Execute and check status request(s). 465 * 466 * @param array $expectedFields 467 * @param string|null $address 468 * @param string $statusPath 469 * @param mixed $formats 470 * @throws \Exception 471 */ 472 public function status( 473 array $expectedFields, 474 string $address = null, 475 string $statusPath = '/status', 476 $formats = ['plain', 'html', 'xml', 'json'] 477 ) { 478 if (!is_array($formats)) { 479 $formats = [$formats]; 480 } 481 482 require_once "status.inc"; 483 $status = new Status(); 484 foreach ($formats as $format) { 485 $query = $format === 'plain' ? '' : $format; 486 $response = $this->request($query, [], $statusPath, $address); 487 $status->checkStatus($response, $expectedFields, $format); 488 } 489 } 490 491 /** 492 * Execute request. 493 * 494 * @param string $query 495 * @param array $headers 496 * @param string|null $uri 497 * @param string|null $address 498 * @param string|null $successMessage 499 * @param string|null $errorMessage 500 * @param bool $connKeepAlive 501 * @return Response 502 */ 503 public function request( 504 string $query = '', 505 array $headers = [], 506 string $uri = null, 507 string $address = null, 508 string $successMessage = null, 509 string $errorMessage = null, 510 bool $connKeepAlive = false 511 ) { 512 if ($this->hasError()) { 513 return new Response(null, true); 514 } 515 if (is_null($uri)) { 516 $uri = $this->makeSourceFile(); 517 } 518 519 $params = array_merge( 520 [ 521 'GATEWAY_INTERFACE' => 'FastCGI/1.0', 522 'REQUEST_METHOD' => 'GET', 523 'SCRIPT_FILENAME' => $uri, 524 'SCRIPT_NAME' => $uri, 525 'QUERY_STRING' => $query, 526 'REQUEST_URI' => $uri . ($query ? '?'.$query : ""), 527 'DOCUMENT_URI' => $uri, 528 'SERVER_SOFTWARE' => 'php/fcgiclient', 529 'REMOTE_ADDR' => '127.0.0.1', 530 'REMOTE_PORT' => '7777', 531 'SERVER_ADDR' => '127.0.0.1', 532 'SERVER_PORT' => '80', 533 'SERVER_NAME' => php_uname('n'), 534 'SERVER_PROTOCOL' => 'HTTP/1.1', 535 'DOCUMENT_ROOT' => __DIR__, 536 'CONTENT_TYPE' => '', 537 'CONTENT_LENGTH' => 0 538 ], 539 $headers 540 ); 541 try { 542 $this->response = new Response( 543 $this->getClient($address, $connKeepAlive)->request_data($params, false) 544 ); 545 $this->message($successMessage); 546 } catch (\Exception $exception) { 547 if ($errorMessage === null) { 548 $this->error("Request failed", $exception); 549 } else { 550 $this->message($errorMessage); 551 } 552 $this->response = new Response(); 553 } 554 if ($this->debug) { 555 $this->response->debugOutput(); 556 } 557 return $this->response; 558 } 559 560 /** 561 * Get client. 562 * 563 * @param string $address 564 * @param bool $keepAlive 565 * @return Client 566 */ 567 private function getClient(string $address = null, $keepAlive = false) 568 { 569 $address = $address ? $this->processTemplate($address) : $this->getAddr(); 570 if ($address[0] === '/') { // uds 571 $host = 'unix://' . $address; 572 $port = -1; 573 } elseif ($address[0] === '[') { // ipv6 574 $addressParts = explode(']:', $address); 575 $host = $addressParts[0]; 576 if (isset($addressParts[1])) { 577 $host .= ']'; 578 $port = $addressParts[1]; 579 } else { 580 $port = $this->getPort(); 581 } 582 } else { // ipv4 583 $addressParts = explode(':', $address); 584 $host = $addressParts[0]; 585 $port = $addressParts[1] ?? $this->getPort(); 586 } 587 588 if (!$keepAlive) { 589 return new Client($host, $port); 590 } 591 592 if (!isset($this->clients[$host][$port])) { 593 $client = new Client($host, $port); 594 $client->setKeepAlive(true); 595 $this->clients[$host][$port] = $client; 596 } 597 598 return $this->clients[$host][$port]; 599 } 600 601 /** 602 * Display logs 603 * 604 * @param int $number 605 * @param string $ignore 606 */ 607 public function displayLog(int $number = 1, string $ignore = 'systemd') 608 { 609 /* Read $number lines or until EOF */ 610 while ($number > 0 || ($number < 0 && !feof($this->outDesc))) { 611 $a = fgets($this->outDesc); 612 if (empty($ignore) || !strpos($a, $ignore)) { 613 echo $a; 614 $number--; 615 } 616 } 617 } 618 619 /** 620 * Get a single log line 621 * 622 * @return null|string 623 */ 624 private function getLogLine() 625 { 626 $read = [$this->outDesc]; 627 $write = null; 628 $except = null; 629 if (stream_select($read, $write, $except, 2 )) { 630 return fgets($this->outDesc); 631 } else { 632 return null; 633 } 634 } 635 636 /** 637 * Get log lines 638 * 639 * @param int $number 640 * @param bool $skipBlank 641 * @param string $ignore 642 * @return array 643 */ 644 public function getLogLines(int $number = 1, bool $skipBlank = false, string $ignore = 'systemd') 645 { 646 $lines = []; 647 /* Read $n lines or until EOF */ 648 while ($number > 0 || ($number < 0 && !feof($this->outDesc))) { 649 $line = $this->getLogLine(); 650 if (is_null($line)) { 651 break; 652 } 653 if ((empty($ignore) || !strpos($line, $ignore)) && (!$skipBlank || strlen(trim($line)) > 0)) { 654 $lines[] = $line; 655 $number--; 656 } 657 } 658 659 return $lines; 660 } 661 662 /** 663 * @return mixed|string 664 */ 665 public function getLastLogLine() 666 { 667 $lines = $this->getLogLines(); 668 669 return $lines[0] ?? ''; 670 } 671 672 /** 673 * Send signal to the supplied PID or the server PID. 674 * 675 * @param string $signal 676 * @param int|null $pid 677 * @return string 678 */ 679 public function signal($signal, int $pid = null) 680 { 681 if (is_null($pid)) { 682 $pid = $this->getPid(); 683 } 684 685 return exec("kill -$signal $pid"); 686 } 687 688 /** 689 * Terminate master process 690 */ 691 public function terminate() 692 { 693 proc_terminate($this->masterProcess); 694 } 695 696 /** 697 * Close all open descriptors and process resources 698 * 699 * @param bool $terminate 700 */ 701 public function close($terminate = false) 702 { 703 if ($terminate) { 704 $this->terminate(); 705 } 706 fclose($this->outDesc); 707 proc_close($this->masterProcess); 708 } 709 710 /** 711 * Create a config file. 712 * 713 * @param string $extension 714 * @return string 715 * @throws \Exception 716 */ 717 private function createConfig($extension = 'ini') 718 { 719 if (is_array($this->configTemplate)) { 720 $configTemplates = $this->configTemplate; 721 if (!isset($configTemplates['main'])) { 722 throw new \Exception('The config template array has to have main config'); 723 } 724 $mainTemplate = $configTemplates['main']; 725 unset($configTemplates['main']); 726 if (!is_dir(self::CONF_DIR)) { 727 mkdir(self::CONF_DIR); 728 } 729 foreach ($configTemplates as $name => $configTemplate) { 730 $this->makeFile( 731 'conf', 732 $this->processTemplate($configTemplate), 733 self::CONF_DIR, 734 $name 735 ); 736 } 737 } else { 738 $mainTemplate = $this->configTemplate; 739 } 740 741 return $this->makeFile($extension, $this->processTemplate($mainTemplate)); 742 } 743 744 /** 745 * Process template string. 746 * 747 * @param string $template 748 * @return string 749 */ 750 private function processTemplate(string $template) 751 { 752 $vars = [ 753 'FILE:LOG:ACC' => ['getAbsoluteFile', self::FILE_EXT_LOG_ACC], 754 'FILE:LOG:ERR' => ['getAbsoluteFile', self::FILE_EXT_LOG_ERR], 755 'FILE:LOG:SLOW' => ['getAbsoluteFile', self::FILE_EXT_LOG_SLOW], 756 'FILE:PID' => ['getAbsoluteFile', self::FILE_EXT_PID], 757 'RFILE:LOG:ACC' => ['getRelativeFile', self::FILE_EXT_LOG_ACC], 758 'RFILE:LOG:ERR' => ['getRelativeFile', self::FILE_EXT_LOG_ERR], 759 'RFILE:LOG:SLOW' => ['getRelativeFile', self::FILE_EXT_LOG_SLOW], 760 'RFILE:PID' => ['getRelativeFile', self::FILE_EXT_PID], 761 'ADDR:IPv4' => ['getAddr', 'ipv4'], 762 'ADDR:IPv4:ANY' => ['getAddr', 'ipv4-any'], 763 'ADDR:IPv6' => ['getAddr', 'ipv6'], 764 'ADDR:IPv6:ANY' => ['getAddr', 'ipv6-any'], 765 'ADDR:UDS' => ['getAddr', 'uds'], 766 'PORT' => ['getPort', 'ip'], 767 'INCLUDE:CONF' => self::CONF_DIR . '/*.conf', 768 ]; 769 $aliases = [ 770 'ADDR' => 'ADDR:IPv4', 771 'FILE:LOG' => 'FILE:LOG:ERR', 772 ]; 773 foreach ($aliases as $aliasName => $aliasValue) { 774 $vars[$aliasName] = $vars[$aliasValue]; 775 } 776 777 return preg_replace_callback( 778 '/{{([a-zA-Z0-9:]+)(\[\w+\])?}}/', 779 function ($matches) use ($vars) { 780 $varName = $matches[1]; 781 if (!isset($vars[$varName])) { 782 $this->error("Invalid config variable $varName"); 783 return 'INVALID'; 784 } 785 $pool = $matches[2] ?? 'default'; 786 $varValue = $vars[$varName]; 787 if (is_string($varValue)) { 788 return $varValue; 789 } 790 $functionName = array_shift($varValue); 791 $varValue[] = $pool; 792 return call_user_func_array([$this, $functionName], $varValue); 793 }, 794 $template 795 ); 796 } 797 798 /** 799 * @param string $type 800 * @param string $pool 801 * @return string 802 */ 803 public function getAddr(string $type = 'ipv4', $pool = 'default') 804 { 805 $port = $this->getPort($type, $pool, true); 806 if ($type === 'uds') { 807 return $this->getFile($port . '.sock'); 808 } 809 810 return $this->getHost($type) . ':' . $port; 811 } 812 813 /** 814 * @param string $type 815 * @param string $pool 816 * @param bool $useAsId 817 * @return int 818 */ 819 public function getPort(string $type = 'ip', $pool = 'default', $useAsId = false) 820 { 821 if ($type === 'uds' && !$useAsId) { 822 return -1; 823 } 824 825 if (isset($this->ports['values'][$pool])) { 826 return $this->ports['values'][$pool]; 827 } 828 $port = ($this->ports['last'] ?? 9000 + PHP_INT_SIZE - 1) + 1; 829 $this->ports['values'][$pool] = $this->ports['last'] = $port; 830 831 return $port; 832 } 833 834 /** 835 * @param string $type 836 * @return string 837 */ 838 public function getHost(string $type = 'ipv4') 839 { 840 switch ($type) { 841 case 'ipv6-any': 842 return '[::]'; 843 case 'ipv6': 844 return '[::1]'; 845 case 'ipv4-any': 846 return '0.0.0.0'; 847 default: 848 return '127.0.0.1'; 849 } 850 } 851 852 /** 853 * Get listen address. 854 * 855 * @param string|null $template 856 * @return string 857 */ 858 public function getListen($template = null) 859 { 860 return $template ? $this->processTemplate($template) : $this->getAddr(); 861 } 862 863 /** 864 * Get PID. 865 * 866 * @return int 867 */ 868 public function getPid() 869 { 870 $pidFile = $this->getFile('pid'); 871 if (!is_file($pidFile)) { 872 return (int) $this->error("PID file has not been created"); 873 } 874 $pidContent = file_get_contents($pidFile); 875 if (!is_numeric($pidContent)) { 876 return (int) $this->error("PID content '$pidContent' is not integer"); 877 } 878 879 return (int) $pidContent; 880 } 881 882 883 /** 884 * @param string $extension 885 * @param string|null $dir 886 * @param string|null $name 887 * @return string 888 */ 889 private function getFile(string $extension, $dir = null, $name = null) 890 { 891 $fileName = (is_null($name) ? $this->fileName : $name . '.') . $extension; 892 893 return is_null($dir) ? $fileName : $dir . '/' . $fileName; 894 } 895 896 /** 897 * @param string $extension 898 * @return string 899 */ 900 private function getAbsoluteFile(string $extension) 901 { 902 return $this->getFile($extension); 903 } 904 905 /** 906 * @param string $extension 907 * @return string 908 */ 909 private function getRelativeFile(string $extension) 910 { 911 $fileName = rtrim(basename($this->fileName), '.'); 912 913 return $this->getFile($extension, null, $fileName); 914 } 915 916 /** 917 * @param string $extension 918 * @param string $prefix 919 * @return string 920 */ 921 private function getPrefixedFile(string $extension, string $prefix = null) 922 { 923 $fileName = rtrim($this->fileName, '.'); 924 if (!is_null($prefix)) { 925 $fileName = $prefix . '/' . basename($fileName); 926 } 927 928 return $this->getFile($extension, null, $fileName); 929 } 930 931 /** 932 * @param string $extension 933 * @param string $content 934 * @param string|null $dir 935 * @param string|null $name 936 * @return string 937 */ 938 private function makeFile(string $extension, string $content = '', $dir = null, $name = null) 939 { 940 $filePath = $this->getFile($extension, $dir, $name); 941 file_put_contents($filePath, $content); 942 943 return $filePath; 944 } 945 946 /** 947 * @return string 948 */ 949 public function makeSourceFile() 950 { 951 return $this->makeFile('src.php', $this->code); 952 } 953 954 /** 955 * @param string|null $msg 956 */ 957 private function message($msg) 958 { 959 if ($msg !== null) { 960 echo "$msg\n"; 961 } 962 } 963 964 /** 965 * @param string $msg 966 * @param \Exception|null $exception 967 */ 968 private function error($msg, \Exception $exception = null) 969 { 970 $this->error = 'ERROR: ' . $msg; 971 if ($exception) { 972 $this->error .= '; EXCEPTION: ' . $exception->getMessage(); 973 } 974 $this->error .= "\n"; 975 976 echo $this->error; 977 } 978 979 /** 980 * @return bool 981 */ 982 private function hasError() 983 { 984 return !is_null($this->error) || !is_null($this->logTool->getError()); 985 } 986 987 /** 988 * Expect file with a supplied extension to exist. 989 * 990 * @param string $extension 991 * @param string $prefix 992 * @return bool 993 */ 994 public function expectFile(string $extension, $prefix = null) 995 { 996 $filePath = $this->getPrefixedFile($extension, $prefix); 997 if (!file_exists($filePath)) { 998 return $this->error("The file $filePath does not exist"); 999 } 1000 1001 return true; 1002 } 1003 1004 /** 1005 * Expect file with a supplied extension to not exist. 1006 * 1007 * @param string $extension 1008 * @param string $prefix 1009 * @return bool 1010 */ 1011 public function expectNoFile(string $extension, $prefix = null) 1012 { 1013 $filePath = $this->getPrefixedFile($extension, $prefix); 1014 if (file_exists($filePath)) { 1015 return $this->error("The file $filePath exists"); 1016 } 1017 1018 return true; 1019 } 1020 1021 /** 1022 * Expect message to be written to FastCGI error stream. 1023 * 1024 * @param string $message 1025 * @param int $limit 1026 * @param int $repeat 1027 */ 1028 public function expectFastCGIErrorMessage( 1029 string $message, 1030 int $limit = 1024, 1031 int $repeat = 0 1032 ) { 1033 $this->logTool->setExpectedMessage($message, $limit, $repeat); 1034 $this->logTool->checkTruncatedMessage($this->response->getErrorData()); 1035 } 1036 1037 /** 1038 * Expect starting lines to be logged. 1039 */ 1040 public function expectLogStartNotices() 1041 { 1042 $this->logTool->expectStartingLines($this->getLogLines(2)); 1043 } 1044 1045 /** 1046 * Expect terminating lines to be logged. 1047 */ 1048 public function expectLogTerminatingNotices() 1049 { 1050 $this->logTool->expectTerminatorLines($this->getLogLines(-1)); 1051 } 1052 1053 /** 1054 * Expect log message that can span multiple lines. 1055 * 1056 * @param string $message 1057 * @param int $limit 1058 * @param int $repeat 1059 * @param bool $decorated 1060 * @param bool $wrapped 1061 */ 1062 public function expectLogMessage( 1063 string $message, 1064 int $limit = 1024, 1065 int $repeat = 0, 1066 bool $decorated = true, 1067 bool $wrapped = true 1068 ) { 1069 $this->logTool->setExpectedMessage($message, $limit, $repeat); 1070 if ($wrapped) { 1071 $logLines = $this->getLogLines(-1, true); 1072 $this->logTool->checkWrappedMessage($logLines, true, $decorated); 1073 } else { 1074 $logLines = $this->getLogLines(1, true); 1075 $this->logTool->checkTruncatedMessage($logLines[0] ?? ''); 1076 } 1077 if ($this->debug) { 1078 $this->message("-------------- LOG LINES: -------------"); 1079 var_dump($logLines); 1080 $this->message("---------------------------------------\n"); 1081 } 1082 } 1083 1084 /** 1085 * Expect a single log line. 1086 * 1087 * @param string $message 1088 * @return bool 1089 */ 1090 public function expectLogLine(string $message, bool $is_stderr = true) 1091 { 1092 $messageLen = strlen($message); 1093 $limit = $messageLen > 1024 ? $messageLen + 16 : 1024; 1094 $this->logTool->setExpectedMessage($message, $limit); 1095 $logLines = $this->getLogLines(1, true); 1096 if ($this->debug) { 1097 $this->message("LOG LINE: " . ($logLines[0] ?? '')); 1098 } 1099 1100 return $this->logTool->checkWrappedMessage($logLines, false, true, $is_stderr); 1101 } 1102 1103 /** 1104 * Expect a log debug message. 1105 * 1106 * @param string $message 1107 * @param string|null $pool 1108 * @return bool 1109 */ 1110 public function expectLogDebug(string $message, $pool = null) 1111 { 1112 return $this->logTool->expectDebug($this->getLastLogLine(), $message, $pool); 1113 } 1114 1115 /** 1116 * Expect a log notice. 1117 * 1118 * @param string $message 1119 * @param string|null $pool 1120 * @return bool 1121 */ 1122 public function expectLogNotice(string $message, $pool = null) 1123 { 1124 return $this->logTool->expectNotice($this->getLastLogLine(), $message, $pool); 1125 } 1126 1127 /** 1128 * Expect a log warning. 1129 * 1130 * @param string $message 1131 * @param string|null $pool 1132 * @return bool 1133 */ 1134 public function expectLogWarning(string $message, $pool = null) 1135 { 1136 return $this->logTool->expectWarning($this->getLastLogLine(), $message, $pool); 1137 } 1138 1139 /** 1140 * Expect a log error. 1141 * 1142 * @param string $message 1143 * @param string|null $pool 1144 * @return bool 1145 */ 1146 public function expectLogError(string $message, $pool = null) 1147 { 1148 return $this->logTool->expectError($this->getLastLogLine(), $message, $pool); 1149 } 1150 1151 /** 1152 * Expect a log alert. 1153 * 1154 * @param string $message 1155 * @param string|null $pool 1156 * @return bool 1157 */ 1158 public function expectLogAlert(string $message, $pool = null) 1159 { 1160 return $this->logTool->expectAlert($this->getLastLogLine(), $message, $pool); 1161 } 1162 1163 /** 1164 * Expect no log lines to be logged. 1165 * 1166 * @return bool 1167 */ 1168 public function expectNoLogMessages() 1169 { 1170 $logLines = $this->getLogLines(-1, true); 1171 if (!empty($logLines)) { 1172 return $this->error( 1173 "Expected no log lines but following lines logged:\n" . implode("\n", $logLines) 1174 ); 1175 } 1176 1177 return true; 1178 } 1179 1180 /** 1181 * Print content of access log. 1182 */ 1183 public function printAccessLog() 1184 { 1185 $accessLog = $this->getFile('acc.log'); 1186 if (is_file($accessLog)) { 1187 print file_get_contents($accessLog); 1188 } 1189 } 1190} 1191