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