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