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