1<?php 2/* Copyright 2012-present Facebook, Inc. 3 * Licensed under the Apache License, Version 2.0 */ 4 5if (!defined('PHP_BINARY')) { 6 define('PHP_BINARY', 'php'); 7} 8 9function w_unlink($name) { 10 if (phutil_is_windows()) { 11 for ($i = 0; $i < 10; $i++) { 12 $x = @unlink($name); 13 if ($x) return true; 14 usleep(200000); 15 } 16 } 17 return unlink($name); 18} 19 20function w_normalize_filename($a) { 21 if ($a === null) { 22 return null; 23 } 24 $a = str_replace('/', DIRECTORY_SEPARATOR, $a); 25 if (DIRECTORY_SEPARATOR == '\\') { 26 return strtolower($a); 27 } 28 return $a; 29} 30 31function w_is_same_filename($a, $b) { 32 return w_normalize_filename($a) == w_normalize_filename($b); 33} 34 35function w_is_file_in_file_list($filename, $list) { 36 $list = w_normalize_file_list($list); 37 $filename = w_normalize_filename($filename); 38 return in_array($filename, $list); 39} 40 41function w_normalize_file_list($a) { 42 return array_map('w_normalize_filename', $a); 43} 44 45function w_is_same_file_list($a, $b) { 46 $a = w_normalize_file_list($a); 47 $b = w_normalize_file_list($b); 48 return $a == $b; 49} 50 51function w_find_subdata_containing_file($subdata, $filename) { 52 if (!is_array($subdata)) { 53 return null; 54 } 55 $filename = w_normalize_filename($filename); 56 foreach ($subdata as $sub) { 57 if (in_array($filename, $sub['files'])) { 58 return $sub; 59 } 60 } 61 return null; 62} 63 64class TestSkipException extends Exception {} 65 66class WatchmanTestCase { 67 protected $root; 68 protected $watchman_instance; 69 private $use_cli = false; 70 private $cli_args = null; 71 private $watches = array(); 72 static $test_number = 1; 73 74 // If this returns false, we can run this test case using 75 // the CLI instead of via a unix socket 76 function needsLiveConnection() { 77 return false; 78 } 79 80 function isUsingCLI() { 81 return $this->use_cli; 82 } 83 84 function useCLI($args) { 85 $this->use_cli = true; 86 $this->cli_args = $args; 87 } 88 89 function setRoot($root) { 90 $this->root = $root; 91 } 92 93 function getRoot() { 94 return $this->root; 95 } 96 97 // This can be overridden if your test requires specific global config options 98 function getGlobalConfig() { 99 return array(); 100 } 101 102 function setWatchmanInstance($instance) { 103 $this->watchman_instance = $instance; 104 } 105 106 function watchProject($root) { 107 $res = $this->watchmanCommand('watch-project', $root); 108 if (!is_array($res)) { 109 $err = $res; 110 } else { 111 $err = idx($res, 'error'); 112 } 113 if (!$err) { 114 // Remember the watched dir 115 $this->watches[$root] = idx($res, 'watch'); 116 } 117 return $res; 118 } 119 120 function watch($root, $assert = true) { 121 $root = w_normalize_filename($root); 122 $res = $this->watchmanCommand('watch', $root); 123 $this->watches[$root] = $res; 124 if ($assert) { 125 if (!is_array($res)) { 126 $err = $res; 127 } else { 128 $err = idx($res, 'error', w_normalize_filename(idx($res, 'watch'))); 129 } 130 $this->assertEqual(w_normalize_filename($root), $err); 131 } 132 return $res; 133 } 134 135 function trigger() { 136 $args = func_get_args(); 137 array_unshift($args, 'trigger'); 138 if (is_string($args[2])) { 139 $id = $args[2]; 140 } else { 141 $id = $args[2]['name']; 142 } 143 $out = call_user_func_array(array($this, 'watchmanCommand'), $args); 144 if (!is_array($out)) { 145 $err = $out; 146 } else { 147 $err = idx($out, 'error', idx($out, 'triggerid'), 'unpossible'); 148 } 149 $def = json_encode($args); 150 $output = json_encode($out); 151 $message = "trigger definition: $def, output was $output"; 152 $this->assertEqual($id, $err, $message); 153 return $out; 154 } 155 156 private function computeWatchmanTestCaseName($test_method_name = '') { 157 $cls = get_class($this); 158 if ($test_method_name) { 159 $cls .= "::$test_method_name"; 160 } 161 return $cls; 162 } 163 164 private function logTestInfo($msg, $test_method_name = '') { 165 try { 166 $name = $this->computeWatchmanTestCaseName($test_method_name); 167 $this->watchmanCommand( 168 'log', 169 'debug', 170 "TEST: $msg $name\n\n" 171 ); 172 } catch (Exception $e) { 173 printf( 174 "logTestInfo %s %s failed: %s\n", 175 $msg, 176 $name, 177 $e->getMessage() 178 ); 179 } 180 } 181 182 function didRunOneTest($test_method_name) { 183 if (!$this->use_cli) { 184 $this->watchman_instance->stopLogging(); 185 } 186 chdir($this->root); 187 $this->logTestInfo('end', $test_method_name); 188 } 189 190 function willRunOneTest($test_method_name) { 191 chdir($this->root); 192 $this->logTestInfo('begin', $test_method_name); 193 } 194 195 function willRunTests() { 196 $this->logTestInfo('willRun'); 197 } 198 199 function didRunTests() { 200 $this->logTestInfo('didRun'); 201 202 try { 203 $this->watchmanCommand('watch-del-all'); 204 } catch (Exception $e) { 205 // Swallow 206 } 207 208 $this->watches = array(); 209 } 210 211 function watchmanCommand() { 212 $args = func_get_args(); 213 214 if ($this->use_cli) { 215 $future = new WatchmanQueryFuture( 216 $this->watchman_instance->getFullSockName(), 217 $this->root, 218 $this->cli_args, 219 $args 220 ); 221 return $future->resolve(); 222 } 223 224 return call_user_func_array( 225 array($this->watchman_instance, 'request'), 226 $args); 227 } 228 229 function assertRegex($pattern, $subject, $message = null) { 230 if (!$message) { 231 $message = "Failed to assert that $subject matches $pattern"; 232 } 233 $this->assertTrue(preg_match($pattern, $subject) === 1, $message); 234 } 235 236 /** 237 * Suspends the watchman process. 238 * 239 * This is useful when testing to try to force batching or coalescing in the 240 * kernel notification layer. You must have a matching resumeProcess() call. 241 */ 242 function suspendWatchman() { 243 $this->watchman_instance->suspendProcess(); 244 } 245 246 /** 247 * Resumes the watchman process. This is meant to be called while the watchman 248 * process is suspended. 249 */ 250 function resumeWatchman() { 251 $this->watchman_instance->resumeProcess(); 252 } 253 254 function assertLiveConnection() { 255 $this->assertTrue( 256 $this->needsLiveConnection(), 257 "you must override needsLiveConnection and make it return true" 258 ); 259 } 260 261 function startLogging($level) { 262 $this->assertLiveConnection(); 263 $out = $this->watchman_instance->startLogging($level); 264 $this->assertEqual($level, $out['log_level'], "set log level to $level"); 265 } 266 267 function stopLogging() { 268 $this->assertLiveConnection(); 269 $out = $this->watchman_instance->stopLogging(); 270 $this->assertEqual('off', $out['log_level'], "set log level to 'off'"); 271 } 272 273 function waitForSub($subname, $callable, $timeout = 5) { 274 return $this->watchman_instance->waitForSub($subname, $callable, $timeout); 275 } 276 277 function getSubData($subname) { 278 return $this->watchman_instance->getSubData($subname); 279 } 280 281 function waitForLog($criteria, $timeout = 5) { 282 $this->assertLiveConnection(); 283 // Can't use the generic waitFor routine here because 284 // we're delegating to a more efficient mechanism in 285 // the instance class. 286 return $this->watchman_instance->waitForLog($criteria, $timeout); 287 } 288 289 function assertWaitForLog($criteria, $timeout = 5) { 290 list($ok, $line, $matches) = $this->waitForLog($criteria, $timeout); 291 if (!$ok) { 292 $this->assertFailure( 293 "did not find $criteria in log output within $timeout seconds"); 294 } 295 return array($ok, $line, $matches); 296 } 297 298 function waitForLogOutput($criteria, $timeout = 5) { 299 // Can't use the generic waitFor routine here because 300 // we're delegating to a more efficient mechanism in 301 // the instance class. 302 return $this->watchman_instance->waitForLogOutput($criteria, $timeout); 303 } 304 305 function assertWaitForLogOutput($criteria, $timeout = 5) { 306 list($ok, $line, $matches) = $this->waitForLogOutput($criteria, $timeout); 307 if (!$ok) { 308 $this->assertFailure( 309 "did not find $criteria in log file within $timeout seconds"); 310 } 311 return array($ok, $line, $matches); 312 } 313 314 // Generic waiting assertion; continually invokes $callable 315 // until timeout is hit. Returns the returned value from 316 // $callable if it is truthy. 317 // Asserts failure if no truthy value is encountered within 318 // the timeout 319 function waitForNoThrow($callable, $timeout = 10) { 320 $current_time = time(); 321 $deadline = $current_time + $timeout; 322 $res = null; 323 do { 324 try { 325 $res = $callable(); 326 if ($res) { 327 return array(true, $res); 328 } 329 } catch (Exception $e) { 330 $res = $e->getMessage(); 331 break; 332 } 333 usleep(30000); 334 $current_time = time(); 335 } while ($current_time < $deadline); 336 return array(false, $res); 337 } 338 339 function waitFor($callable, $timeout = 10, $message = null) { 340 list($ok, $res) = $this->waitForNoThrow($callable, $timeout); 341 342 if ($ok) { 343 return $res; 344 } 345 346 if ($message === null) { 347 $message = "Condition [$callable] was not met in $timeout seconds"; 348 } 349 if (is_callable($message)) { 350 $message = call_user_func($message); 351 } 352 if (is_string($res)) { 353 $message .= " $res"; 354 } 355 $this->assertFailure($message); 356 } 357 358 // Wait for a watchman command to return output that matches 359 // some criteria. 360 // Returns the command output. 361 // $have_data is a callable that returns a boolean result 362 // to indicate that the criteria have been met. 363 // timeout is the timeout in seconds. 364 function waitForWatchmanNoThrow(array $command, $have_data, $timeout = 10) { 365 $last_output = null; 366 367 $instance = $this->watchman_instance; 368 list($ok, $res) = $this->waitForNoThrow( 369 function () use ($instance, $command, $have_data, &$last_output) { 370 $out = call_user_func_array( 371 array($instance, 'request'), 372 $command); 373 if ($out === false) { 374 // Connection terminated 375 $last_output = "watchman went away"; 376 throw new Exception($last_output); 377 } 378 $last_output = $out; 379 if ($have_data($out)) { 380 return $out; 381 } 382 return false; 383 }, 384 $timeout 385 ); 386 387 if ($ok) { 388 return array(true, $res); 389 } 390 return array(false, $last_output); 391 } 392 393 function waitForWatchman(array $command, $have_data, 394 $timeout = 10, $message = null) 395 { 396 list($ok, $res) = $this->waitForWatchmanNoThrow( 397 $command, $have_data, $timeout); 398 if ($ok) { 399 return $res; 400 } 401 402 if ($message === null) { 403 $where = debug_backtrace(); 404 $where = array_shift($where); 405 $where = sprintf("at line %d in file %s", 406 idx($where, 'line'), 407 basename(idx($where, 'file'))); 408 409 $cmd_text = json_encode($command); 410 411 $message = "watchman [$cmd_text] didn't yield expected results " . 412 "within $timeout seconds\n" . json_encode($res) . "\n" . 413 $where; 414 } 415 416 $this->assertFailure($message); 417 } 418 419 function assertFileListUsingSince($root, $cursor, array $files, 420 array $files_via_since = null, $message = null) { 421 422 if ($cursor) { 423 if ($files_via_since === null) { 424 $files_via_since = $files; 425 } 426 sort($files_via_since); 427 } 428 429 sort($files); 430 431 $sort_func = function ($list) { 432 $files = array(); 433 if (!is_array($list)) { 434 return $files; 435 } 436 foreach ($list as $ent) { 437 if ($ent['exists']) { 438 $files[] = $ent['name']; 439 } 440 } 441 sort($files); 442 return $files; 443 }; 444 445 list($ok, $out) = $this->waitForWatchmanNoThrow( 446 array('find', $root), 447 function ($out) use ($sort_func, $files) { 448 return w_is_same_file_list( 449 $sort_func(idx($out, 'files', array())), $files); 450 }, 451 0 // timeout 452 ); 453 454 if ($ok) { 455 456 if (!$cursor) { 457 // we've already gotten all the files we care about 458 $this->assertTrue(true); 459 return; 460 } 461 462 $since = $this->watchmanCommand('since', $root, $cursor); 463 464 $since_files = $sort_func(idx($since, 'files')); 465 if (w_is_same_file_list($since_files, $files_via_since)) { 466 $this->assertTrue(true); 467 return $since; 468 } 469 470 if ($message === null) { 471 $where = debug_backtrace(); 472 $where = array_shift($where); 473 $where = sprintf("at line %d in file %s", 474 idx($where, 'line'), 475 basename(idx($where, 'file'))); 476 477 $message = "\nwatchman since vs. find result mismatch\n" . 478 json_encode($out) . "\n" . 479 json_encode($since) . "\n" . 480 $where; 481 482 $message .= "\nsince_files = " . json_encode($since_files) . 483 "\ngot_files = " . json_encode($files) . "\n"; 484 485 } 486 487 $got = $since_files; 488 } elseif (is_array($out)) { 489 $error = idx($out, 'error'); 490 if ($error) { 491 throw new Exception($error); 492 } 493 $got = $sort_func(idx($out, 'files')); 494 } else { 495 $got = $out; 496 } 497 498 if ($message === null) { 499 $where = debug_backtrace(); 500 $where = array_shift($where); 501 $where = sprintf("at line %d in file %s", 502 idx($where, 'line'), 503 basename(idx($where, 'file'))); 504 505 $message = "\nwatchman didn't yield expected file list " . 506 json_encode($out) . "\n" . $where; 507 } 508 509 $this->assertEqualFileList($files, $got, $message); 510 } 511 512 function assertEqualFileList($a, $b, $message = null) { 513 if ($message === null) { 514 $where = debug_backtrace(); 515 $where = array_shift($where); 516 $where = sprintf("at line %d in file %s", 517 idx($where, 'line'), 518 basename(idx($where, 'file'))); 519 520 $message = "\nfile lists are not equal $where"; 521 } 522 $a = w_normalize_file_list($a); 523 sort($a); 524 $b = w_normalize_file_list($b); 525 sort($b); 526 $this->assertEqual($a, $b, $message); 527 } 528 529 function assertFileList($root, array $files, $message = null) { 530 $this->assertFileListUsingSince($root, null, $files, null, $message); 531 } 532 533 private function secondLevelSort(array $objs) { 534 $ret = array(); 535 536 foreach ($objs as $obj) { 537 ksort($obj); 538 $ret[] = $obj; 539 } 540 541 return $ret; 542 } 543 544 function assertTriggerList($root, $trig_list) { 545 $triggers = $this->watchmanCommand('trigger-list', $root); 546 547 $triggers = $triggers['triggers']; 548 usort($triggers, function ($a, $b) { 549 return strcmp($a['name'], $b['name']); 550 }); 551 $this->assertEqual( 552 $this->secondLevelSort($trig_list), 553 $this->secondLevelSort($triggers) 554 ); 555 } 556 557 function waitForFileContents($filename, $content, $timeout = 5) { 558 $this->waitFor( 559 function () use ($filename, $content) { 560 $got = @file_get_contents($filename); 561 return $got === $content; 562 }, 563 $timeout, 564 function () use ($filename, $content) { 565 $got = @file_get_contents($filename); 566 return "Wanted: $content\nGot: $got\n". 567 "wait for $filename to hold a certain content"; 568 } 569 ); 570 return @file_get_contents($filename); 571 } 572 573 function assertFileContents($filename, $content, $timeout = 5) { 574 $got = $this->waitForFileContents($filename, $content, $timeout); 575 $this->assertEqual($got, $content, 576 "waiting for $filename to have a certain content"); 577 } 578 579 function waitForFileToHaveNLines($filename, $nlines, $timeout = 5) { 580 $this->waitFor( 581 function () use ($filename, $nlines) { 582 return count(@file($filename)) == $nlines; 583 }, 584 $timeout, 585 function () use ($filename, $nlines) { 586 $lines = count(@file($filename)); 587 return "wait for $filename to hold $nlines lines, got $lines"; 588 } 589 ); 590 return @file($filename, FILE_IGNORE_NEW_LINES); 591 } 592 593 function waitForJsonInput($log, $timeout = 5) { 594 $this->waitFor( 595 function () use ($log) { 596 $data = @file_get_contents($log); 597 if (!strlen($data)) { 598 return false; 599 } 600 $obj = @json_decode($data, true); 601 return is_array($obj); 602 }, 603 $timeout, 604 "waiting for $log to hold a JSON object" 605 ); 606 607 $obj = json_decode(file_get_contents($log), true); 608 $this->assertTrue(is_array($obj), "got JSON object in $log"); 609 610 return $obj; 611 } 612 613 function isCaseInsensitive() { 614 static $insensitive = null; 615 if ($insensitive === null) { 616 $dir = new WatchmanDirectoryFixture(); 617 $path = $dir->getPath(); 618 touch("$path/a"); 619 $insensitive = file_exists("$path/A"); 620 } 621 return $insensitive; 622 } 623 624 function run() { 625 $ref = new ReflectionClass($this); 626 $methods = $ref->getMethods(); 627 shuffle($methods); 628 $this->willRunTests(); 629 foreach ($methods as $method) { 630 $name = $method->getName(); 631 if (!preg_match('/^test/', $name)) { 632 continue; 633 } 634 635 try { 636 $this->willRunOneTest($name); 637 638 call_user_func(array($this, $name)); 639 640 try { 641 $this->didRunOneTest($name); 642 } catch (Exception $e) { 643 $this->failException($e); 644 } 645 646 } catch (TestSkipException $e) { 647 // Continue with next 648 } catch (Exception $e) { 649 $this->failException($e); 650 } 651 } 652 $this->didRunTests(); 653 } 654 655 function failException($e) { 656 $this->fail(sprintf("%s: %s\n%s", 657 get_class($e), 658 $e->getMessage(), 659 $e->getTraceAsString())); 660 } 661 662 function printStatus($ok, $message) { 663 $lines = explode("\n", $message); 664 if (count($lines) > 1) { 665 echo '# ' . implode("\n# ", $lines) . "\n"; 666 } 667 $last_line = array_pop($lines); 668 $caller = self::getCallerInfo(); 669 670 printf("%s %d - %s:%d: %s\n", 671 $ok ? 'ok' : 'not ok', 672 self::$test_number++, 673 $caller['file'], 674 $caller['line'], 675 $last_line); 676 } 677 678 function fail($message) { 679 $this->printStatus(false, $message); 680 throw new TestSkipException(); 681 } 682 683 function ok($message) { 684 $this->printStatus(true, $message); 685 } 686 687 688 /** 689 * Returns info about the caller function. 690 * 691 * @return map 692 */ 693 private static final function getCallerInfo() { 694 $caller = array(); 695 696 foreach (array_slice(debug_backtrace(), 1) as $location) { 697 $function = idx($location, 'function'); 698 699 if (idx($location, 'file') == __FILE__) { 700 continue; 701 } 702 $caller = $location; 703 break; 704 } 705 706 return array( 707 'file' => basename(idx($caller, 'file')), 708 'line' => idx($caller, 'line'), 709 ); 710 } 711 712 static function printable($value) { 713 return json_encode($value); 714 } 715 716 function assertEqual($expected, $actual, $message = null) { 717 if ($message === null) { 718 $message = sprintf("Expected %s to equal %s", 719 self::printable($actual), 720 self::printable($expected)); 721 } 722 if ($expected === $actual) { 723 $this->ok($message); 724 } else { 725 $this->fail($message); 726 } 727 } 728 729 function assertTrue($actual, $message = null) { 730 $this->assertEqual(true, $actual, $message); 731 } 732 733 function assertFalse($actual, $message = null) { 734 $this->assertEqual(false, $actual, $message); 735 } 736 737 function assertFailure($message) { 738 return $this->fail($message); 739 } 740 741 function assertSkipped($message) { 742 $this->ok("skip: $message"); 743 throw new TestSkipException(); 744 } 745} 746 747// vim:ts=2:sw=2:et: 748