diff --git a/src/__tests__/PhutilLibraryTestCase.php b/src/__tests__/PhutilLibraryTestCase.php index 5eebe100..d29d622c 100644 --- a/src/__tests__/PhutilLibraryTestCase.php +++ b/src/__tests__/PhutilLibraryTestCase.php @@ -1,195 +1,197 @@ assertSkipped('TOOLSETS: Many workflows are missing methods.'); id(new PhutilSymbolLoader()) ->setLibrary($this->getLibraryName()) ->selectAndLoadSymbols(); $this->assertTrue(true); } /** * This is more of an acceptance test case instead of a unit test. It verifies * that all the library map is up-to-date. */ public function testLibraryMap() { + $this->assertExecutable('xhpast'); + $root = $this->getLibraryRoot(); $library = phutil_get_library_name_for_root($root); $new_library_map = id(new PhutilLibraryMapBuilder($root)) ->buildMap(); $bootloader = PhutilBootloader::getInstance(); $old_library_map = $bootloader->getLibraryMapWithoutExtensions($library); unset($old_library_map[PhutilLibraryMapBuilder::LIBRARY_MAP_VERSION_KEY]); $identical = ($new_library_map === $old_library_map); if (!$identical) { $differences = $this->getMapDifferences( $old_library_map, $new_library_map); sort($differences); } else { $differences = array(); } $this->assertTrue( $identical, pht( "The library map is out of date. Rebuild it with `%s`.\n". "These entries differ: %s.", 'arc liberate', implode(', ', $differences))); } private function getMapDifferences($old, $new) { $changed = array(); $all = $old + $new; foreach ($all as $key => $value) { $old_exists = array_key_exists($key, $old); $new_exists = array_key_exists($key, $new); // One map has it and the other does not, so mark it as changed. if ($old_exists != $new_exists) { $changed[] = $key; continue; } $oldv = idx($old, $key); $newv = idx($new, $key); if ($oldv === $newv) { continue; } if (is_array($oldv) && is_array($newv)) { $child_changed = $this->getMapDifferences($oldv, $newv); foreach ($child_changed as $child) { $changed[] = $key.'.'.$child; } } else { $changed[] = $key; } } return $changed; } /** * This is more of an acceptance test case instead of a unit test. It verifies * that methods in subclasses have the same visibility as the method in the * parent class. */ public function testMethodVisibility() { $this->assertSkipped('TOOLSETS: Many workflows currently have failures.'); $symbols = id(new PhutilSymbolLoader()) ->setLibrary($this->getLibraryName()) ->selectSymbolsWithoutLoading(); $classes = array(); foreach ($symbols as $symbol) { if ($symbol['type'] == 'class') { $classes[$symbol['name']] = new ReflectionClass($symbol['name']); } } $failures = array(); foreach ($classes as $class_name => $class) { $parents = array(); $parent = $class; while ($parent = $parent->getParentClass()) { $parents[] = $parent; } $interfaces = $class->getInterfaces(); foreach ($class->getMethods() as $method) { $method_name = $method->getName(); foreach (array_merge($parents, $interfaces) as $extends) { if ($extends->hasMethod($method_name)) { $xmethod = $extends->getMethod($method_name); if (!$this->compareVisibility($xmethod, $method)) { $failures[] = pht( 'Class "%s" implements method "%s" with the wrong visibility. '. 'The method has visibility "%s", but it is defined in parent '. '"%s" with visibility "%s". In Phabricator, a method which '. 'overrides another must always have the same visibility.', $class_name, $method_name, $this->getVisibility($method), $extends->getName(), $this->getVisibility($xmethod)); } // We found a declaration somewhere, so stop looking. break; } } } } $this->assertTrue( empty($failures), "\n\n".implode("\n\n", $failures)); } /** * Get the name of the library currently being tested. */ protected function getLibraryName() { return phutil_get_library_name_for_root($this->getLibraryRoot()); } /** * Get the root directory for the library currently being tested. */ protected function getLibraryRoot() { $caller = id(new ReflectionClass($this))->getFileName(); return phutil_get_library_root_for_path($caller); } private function compareVisibility( ReflectionMethod $parent_method, ReflectionMethod $method) { static $bitmask; if ($bitmask === null) { $bitmask = ReflectionMethod::IS_PUBLIC; $bitmask += ReflectionMethod::IS_PROTECTED; $bitmask += ReflectionMethod::IS_PRIVATE; } $parent_modifiers = $parent_method->getModifiers(); $modifiers = $method->getModifiers(); return !(($parent_modifiers ^ $modifiers) & $bitmask); } private function getVisibility(ReflectionMethod $method) { if ($method->isPrivate()) { return 'private'; } else if ($method->isProtected()) { return 'protected'; } else { return 'public'; } } } diff --git a/src/channel/__tests__/PhutilPHPObjectProtocolChannelTestCase.php b/src/channel/__tests__/PhutilPHPObjectProtocolChannelTestCase.php index c9373918..15f56099 100644 --- a/src/channel/__tests__/PhutilPHPObjectProtocolChannelTestCase.php +++ b/src/channel/__tests__/PhutilPHPObjectProtocolChannelTestCase.php @@ -1,66 +1,68 @@ mt_rand(), ); $xp->write($object); $xp->flush(); $result = $yp->waitForMessage(); $this->assertTrue( (array)$object === (array)$result, pht('Values are identical.')); $this->assertFalse( $object === $result, pht('Objects are not the same.')); } public function testCloseSocketWriteChannel() { list($x, $y) = PhutilSocketChannel::newChannelPair(); $xp = new PhutilPHPObjectProtocolChannel($x); $yp = new PhutilPHPObjectProtocolChannel($y); $yp->closeWriteChannel(); $yp->update(); // NOTE: This test is more broad than the implementation needs to be. A // better test would be to verify that this throws an exception: // // $xp->waitForMessage(); // // However, if the test breaks, that method will hang forever instead of // returning, which would be hard to diagnose. Since the current // implementation shuts down the entire channel, just test for that. $this->assertFalse($xp->update(), pht('Expected channel to close.')); } public function testCloseExecWriteChannel() { - $future = new ExecFuture('cat'); + $bin = $this->getSupportExecutable('cat'); + + $future = new ExecFuture('php -f %R', $bin); // If this test breaks, we want to explode, not hang forever. $future->setTimeout(5); $exec_channel = new PhutilExecChannel($future); $exec_channel->write('quack'); $exec_channel->closeWriteChannel(); // If `closeWriteChannel()` did what it is supposed to, this will just // echo "quack" and exit with no error code. If the channel did not close, // this will time out after 5 seconds and throw. $future->resolvex(); $this->assertTrue(true); } } diff --git a/src/filesystem/Filesystem.php b/src/filesystem/Filesystem.php index cdcb22a7..2ead4014 100644 --- a/src/filesystem/Filesystem.php +++ b/src/filesystem/Filesystem.php @@ -1,1174 +1,1189 @@ > 3]; } return $result; } /** * Identify the MIME type of a file. This returns only the MIME type (like * text/plain), not the encoding (like charset=utf-8). * * @param string Path to the file to examine. * @param string Optional default mime type to return if the file's mime * type can not be identified. * @return string File mime type. * * @task file * * @phutil-external-symbol function mime_content_type * @phutil-external-symbol function finfo_open * @phutil-external-symbol function finfo_file */ public static function getMimeType( $path, $default = 'application/octet-stream') { $path = self::resolvePath($path); self::assertExists($path); self::assertIsFile($path); self::assertReadable($path); $mime_type = null; // Fileinfo is the best approach since it doesn't rely on `file`, but // it isn't builtin for older versions of PHP. if (function_exists('finfo_open')) { $finfo = finfo_open(FILEINFO_MIME); if ($finfo) { $result = finfo_file($finfo, $path); if ($result !== false) { $mime_type = $result; } } } // If we failed Fileinfo, try `file`. This works well but not all systems // have the binary. if ($mime_type === null) { list($err, $stdout) = exec_manual( 'file --brief --mime %s', $path); if (!$err) { $mime_type = trim($stdout); } } // If we didn't get anywhere, try the deprecated mime_content_type() // function. if ($mime_type === null) { if (function_exists('mime_content_type')) { $result = mime_content_type($path); if ($result !== false) { $mime_type = $result; } } } // If we come back with an encoding, strip it off. if (strpos($mime_type, ';') !== false) { list($type, $encoding) = explode(';', $mime_type, 2); $mime_type = $type; } if ($mime_type === null) { $mime_type = $default; } return $mime_type; } /* -( Directories )-------------------------------------------------------- */ /** * Create a directory in a manner similar to mkdir(), but throw detailed * exceptions on failure. * * @param string Path to directory. The parent directory must exist and * be writable. * @param int Permission umask. Note that umask is in octal, so you * should specify it as, e.g., `0777', not `777'. * @param boolean Recursively create directories. Default to false. * @return string Path to the created directory. * * @task directory */ public static function createDirectory( $path, $umask = 0755, $recursive = false) { $path = self::resolvePath($path); if (is_dir($path)) { if ($umask) { self::changePermissions($path, $umask); } return $path; } $dir = dirname($path); if ($recursive && !file_exists($dir)) { // Note: We could do this with the recursive third parameter of mkdir(), // but then we loose the helpful FilesystemExceptions we normally get. self::createDirectory($dir, $umask, true); } self::assertIsDirectory($dir); self::assertExists($dir); self::assertWritable($dir); self::assertNotExists($path); if (!mkdir($path, $umask)) { throw new FilesystemException( $path, pht("Failed to create directory '%s'.", $path)); } // Need to change permissions explicitly because mkdir does something // slightly different. mkdir(2) man page: // 'The parameter mode specifies the permissions to use. It is modified by // the process's umask in the usual way: the permissions of the created // directory are (mode & ~umask & 0777)."' if ($umask) { self::changePermissions($path, $umask); } return $path; } /** * Create a temporary directory and return the path to it. You are * responsible for removing it (e.g., with Filesystem::remove()) * when you are done with it. * * @param string Optional directory prefix. * @param int Permissions to create the directory with. By default, * these permissions are very restrictive (0700). * @param string Optional root directory. If not provided, the system * temporary directory (often "/tmp") will be used. * @return string Path to newly created temporary directory. * * @task directory */ public static function createTemporaryDirectory( $prefix = '', $umask = 0700, $root_directory = null) { $prefix = preg_replace('/[^A-Z0-9._-]+/i', '', $prefix); if ($root_directory !== null) { $tmp = $root_directory; self::assertExists($tmp); self::assertIsDirectory($tmp); self::assertWritable($tmp); } else { $tmp = sys_get_temp_dir(); if (!$tmp) { throw new FilesystemException( $tmp, pht('Unable to determine system temporary directory.')); } } $base = $tmp.DIRECTORY_SEPARATOR.$prefix; $tries = 3; do { $dir = $base.substr(base_convert(md5(mt_rand()), 16, 36), 0, 16); try { self::createDirectory($dir, $umask); break; } catch (FilesystemException $ex) { // Ignore. } } while (--$tries); if (!$tries) { $df = disk_free_space($tmp); if ($df !== false && $df < 1024 * 1024) { throw new FilesystemException( $dir, pht('Failed to create a temporary directory: the disk is full.')); } throw new FilesystemException( $dir, pht("Failed to create a temporary directory in '%s'.", $tmp)); } return $dir; } /** * List files in a directory. * * @param string Path, absolute or relative to PWD. * @param bool If false, exclude files beginning with a ".". * * @return array List of files and directories in the specified * directory, excluding `.' and `..'. * * @task directory */ public static function listDirectory($path, $include_hidden = true) { $path = self::resolvePath($path); self::assertExists($path); self::assertIsDirectory($path); self::assertReadable($path); $list = @scandir($path); if ($list === false) { throw new FilesystemException( $path, pht("Unable to list contents of directory '%s'.", $path)); } foreach ($list as $k => $v) { if ($v == '.' || $v == '..' || (!$include_hidden && $v[0] == '.')) { unset($list[$k]); } } return array_values($list); } /** * Return all directories between a path and the specified root directory * (defaulting to "/"). Iterating over them walks from the path to the root. * * @param string Path, absolute or relative to PWD. * @param string The root directory. * @return list List of parent paths, including the provided path. * @task directory */ public static function walkToRoot($path, $root = null) { $path = self::resolvePath($path); if (is_link($path)) { $path = realpath($path); } // NOTE: On Windows, paths start like "C:\", so "/" does not contain // every other path. We could possibly special case "/" to have the same // meaning on Windows that it does on Linux, but just special case the // common case for now. See PHI817. if ($root !== null) { $root = self::resolvePath($root); if (is_link($root)) { $root = realpath($root); } // NOTE: We don't use `isDescendant()` here because we don't want to // reject paths which don't exist on disk. $root_list = new FileList(array($root)); if (!$root_list->contains($path)) { return array(); } } else { if (phutil_is_windows()) { $root = null; } else { $root = '/'; } } $walk = array(); $parts = explode(DIRECTORY_SEPARATOR, $path); foreach ($parts as $k => $part) { if (!strlen($part)) { unset($parts[$k]); } } while (true) { if (phutil_is_windows()) { $next = implode(DIRECTORY_SEPARATOR, $parts); } else { $next = DIRECTORY_SEPARATOR.implode(DIRECTORY_SEPARATOR, $parts); } $walk[] = $next; if ($next == $root) { break; } if (!$parts) { break; } array_pop($parts); } return $walk; } /* -( Paths )-------------------------------------------------------------- */ /** * Checks if a path is specified as an absolute path. * * @param string * @return bool */ public static function isAbsolutePath($path) { if (phutil_is_windows()) { return (bool)preg_match('/^[A-Za-z]+:/', $path); } else { return !strncmp($path, DIRECTORY_SEPARATOR, 1); } } /** * Canonicalize a path by resolving it relative to some directory (by * default PWD), following parent symlinks and removing artifacts. If the * path is itself a symlink it is left unresolved. * * @param string Path, absolute or relative to PWD. * @return string Canonical, absolute path. * * @task path */ public static function resolvePath($path, $relative_to = null) { $is_absolute = self::isAbsolutePath($path); if (!$is_absolute) { if (!$relative_to) { $relative_to = getcwd(); } $path = $relative_to.DIRECTORY_SEPARATOR.$path; } if (is_link($path)) { $parent_realpath = realpath(dirname($path)); if ($parent_realpath !== false) { return $parent_realpath.DIRECTORY_SEPARATOR.basename($path); } } $realpath = realpath($path); if ($realpath !== false) { return $realpath; } // This won't work if the file doesn't exist or is on an unreadable mount // or something crazy like that. Try to resolve a parent so we at least // cover the nonexistent file case. - $parts = explode(DIRECTORY_SEPARATOR, trim($path, DIRECTORY_SEPARATOR)); - while (end($parts) !== false) { + + // We're also normalizing path separators to whatever is normal for the + // environment. + + if (phutil_is_windows()) { + $parts = trim($path, '/\\'); + $parts = preg_split('([/\\\\])', $parts); + + // Normalize the directory separators in the path. If we find a parent + // below, we'll overwrite this with a better resolved path. + $path = str_replace('/', '\\', $path); + } else { + $parts = trim($path, '/'); + $parts = explode('/', $parts); + } + + while ($parts) { array_pop($parts); if (phutil_is_windows()) { $attempt = implode(DIRECTORY_SEPARATOR, $parts); } else { $attempt = DIRECTORY_SEPARATOR.implode(DIRECTORY_SEPARATOR, $parts); } $realpath = realpath($attempt); if ($realpath !== false) { $path = $realpath.substr($path, strlen($attempt)); break; } } return $path; } /** * Test whether a path is descendant from some root path after resolving all * symlinks and removing artifacts. Both paths must exists for the relation * to obtain. A path is always a descendant of itself as long as it exists. * * @param string Child path, absolute or relative to PWD. * @param string Root path, absolute or relative to PWD. * @return bool True if resolved child path is in fact a descendant of * resolved root path and both exist. * @task path */ public static function isDescendant($path, $root) { try { self::assertExists($path); self::assertExists($root); } catch (FilesystemException $e) { return false; } $fs = new FileList(array($root)); return $fs->contains($path); } /** * Convert a canonical path to its most human-readable format. It is * guaranteed that you can use resolvePath() to restore a path to its * canonical format. * * @param string Path, absolute or relative to PWD. * @param string Optionally, working directory to make files readable * relative to. * @return string Human-readable path. * * @task path */ public static function readablePath($path, $pwd = null) { if ($pwd === null) { $pwd = getcwd(); } foreach (array($pwd, self::resolvePath($pwd)) as $parent) { $parent = rtrim($parent, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR; $len = strlen($parent); if (!strncmp($parent, $path, $len)) { $path = substr($path, $len); return $path; } } return $path; } /** * Determine whether or not a path exists in the filesystem. This differs from * file_exists() in that it returns true for symlinks. This method does not * attempt to resolve paths before testing them. * * @param string Test for the existence of this path. * @return bool True if the path exists in the filesystem. * @task path */ public static function pathExists($path) { return file_exists($path) || is_link($path); } /** * Determine if an executable binary (like `git` or `svn`) exists within * the configured `$PATH`. * * @param string Binary name, like `'git'` or `'svn'`. * @return bool True if the binary exists and is executable. * @task exec */ public static function binaryExists($binary) { return self::resolveBinary($binary) !== null; } /** * Locates the full path that an executable binary (like `git` or `svn`) is at * the configured `$PATH`. * * @param string Binary name, like `'git'` or `'svn'`. * @return string The full binary path if it is present, or null. * @task exec */ public static function resolveBinary($binary) { if (phutil_is_windows()) { list($err, $stdout) = exec_manual('where %s', $binary); $stdout = phutil_split_lines($stdout); // If `where %s` could not find anything, check for relative binary if ($err) { $path = self::resolvePath($binary); if (self::pathExists($path)) { return $path; } return null; } $stdout = head($stdout); } else { list($err, $stdout) = exec_manual('which %s', $binary); } return $err === 0 ? trim($stdout) : null; } /** * Determine if two paths are equivalent by resolving symlinks. This is * different from resolving both paths and comparing them because * resolvePath() only resolves symlinks in parent directories, not the * path itself. * * @param string First path to test for equivalence. * @param string Second path to test for equivalence. * @return bool True if both paths are equivalent, i.e. reference the same * entity in the filesystem. * @task path */ public static function pathsAreEquivalent($u, $v) { $u = self::resolvePath($u); $v = self::resolvePath($v); $real_u = realpath($u); $real_v = realpath($v); if ($real_u) { $u = $real_u; } if ($real_v) { $v = $real_v; } return ($u == $v); } public static function concatenatePaths(array $components) { $components = implode($components, DIRECTORY_SEPARATOR); // Replace any extra sequences of directory separators with a single // separator, so we don't end up with "path//to///thing.c". $components = preg_replace( '('.preg_quote(DIRECTORY_SEPARATOR).'{2,})', DIRECTORY_SEPARATOR, $components); return $components; } /* -( Assert )------------------------------------------------------------- */ /** * Assert that something (e.g., a file, directory, or symlink) exists at a * specified location. * * @param string Assert that this path exists. * @return void * * @task assert */ public static function assertExists($path) { if (!self::pathExists($path)) { throw new FilesystemException( $path, pht("File system entity '%s' does not exist.", $path)); } } /** * Assert that nothing exists at a specified location. * * @param string Assert that this path does not exist. * @return void * * @task assert */ public static function assertNotExists($path) { if (file_exists($path) || is_link($path)) { throw new FilesystemException( $path, pht("Path '%s' already exists!", $path)); } } /** * Assert that a path represents a file, strictly (i.e., not a directory). * * @param string Assert that this path is a file. * @return void * * @task assert */ public static function assertIsFile($path) { if (!is_file($path)) { throw new FilesystemException( $path, pht("Requested path '%s' is not a file.", $path)); } } /** * Assert that a path represents a directory, strictly (i.e., not a file). * * @param string Assert that this path is a directory. * @return void * * @task assert */ public static function assertIsDirectory($path) { if (!is_dir($path)) { throw new FilesystemException( $path, pht("Requested path '%s' is not a directory.", $path)); } } /** * Assert that a file or directory exists and is writable. * * @param string Assert that this path is writable. * @return void * * @task assert */ public static function assertWritable($path) { if (!is_writable($path)) { throw new FilesystemException( $path, pht("Requested path '%s' is not writable.", $path)); } } /** * Assert that a file or directory exists and is readable. * * @param string Assert that this path is readable. * @return void * * @task assert */ public static function assertReadable($path) { if (!is_readable($path)) { throw new FilesystemException( $path, pht("Path '%s' is not readable.", $path)); } } } diff --git a/src/filesystem/__tests__/FileFinderTestCase.php b/src/filesystem/__tests__/FileFinderTestCase.php index f7214bd1..d06b428b 100644 --- a/src/filesystem/__tests__/FileFinderTestCase.php +++ b/src/filesystem/__tests__/FileFinderTestCase.php @@ -1,232 +1,246 @@ excludePath('./exclude') ->excludePath('subdir.txt'); } public function testFinderWithChecksums() { $this->assertFinder( pht('Basic Checksums'), $this->newFinder() ->setGenerateChecksums(true) ->withType('f') ->withPath('*') ->withSuffix('txt'), array( '.hidden.txt' => 'b6cfc9ce9afe12b258ee1c19c235aa27', 'file.txt' => '725130ba6441eadb4e5d807898e0beae', 'include_dir.txt/anotherfile.txt' => '91e5c1ad76ff229c6456ac92e74e1d9f', 'include_dir.txt/subdir.txt/alsoinclude.txt' => '91e5c1ad76ff229c6456ac92e74e1d9f', 'test.txt' => 'aea46212fa8b8d0e0e6aa34a15c9e2f5', )); } public function testFinderWithoutChecksums() { $this->assertFinder( pht('Basic No Checksums'), $this->newFinder() ->withType('f') ->withPath('*') ->withSuffix('txt'), array( '.hidden.txt', 'file.txt', 'include_dir.txt/anotherfile.txt', 'include_dir.txt/subdir.txt/alsoinclude.txt', 'test.txt', )); } public function testFinderWithFilesAndDirectories() { $this->assertFinder( pht('With Files And Directories'), $this->newFinder() ->setGenerateChecksums(true) ->withPath('*') ->withSuffix('txt'), array( '.hidden.txt' => 'b6cfc9ce9afe12b258ee1c19c235aa27', 'file.txt' => '725130ba6441eadb4e5d807898e0beae', 'include_dir.txt' => null, 'include_dir.txt/anotherfile.txt' => '91e5c1ad76ff229c6456ac92e74e1d9f', 'include_dir.txt/subdir.txt' => null, 'include_dir.txt/subdir.txt/alsoinclude.txt' => '91e5c1ad76ff229c6456ac92e74e1d9f', 'test.txt' => 'aea46212fa8b8d0e0e6aa34a15c9e2f5', )); } public function testFinderWithDirectories() { $this->assertFinder( pht('Just Directories'), $this->newFinder() ->withType('d'), array( 'include_dir.txt', 'include_dir.txt/subdir.txt', )); } public function testFinderWithPath() { $this->assertFinder( pht('With Path'), $this->newFinder() ->setGenerateChecksums(true) ->withType('f') ->withPath('*/include_dir.txt/subdir.txt/alsoinclude.txt') ->withSuffix('txt'), array( 'include_dir.txt/subdir.txt/alsoinclude.txt' => '91e5c1ad76ff229c6456ac92e74e1d9f', )); } public function testFinderWithNames() { $this->assertFinder( pht('With Names'), $this->newFinder() ->withType('f') ->withPath('*') ->withName('test'), array( 'include_dir.txt/subdir.txt/test', 'include_dir.txt/test', 'test', )); } public function testFinderWithNameAndSuffix() { $this->assertFinder( pht('With Name and Suffix'), $this->newFinder() ->withType('f') ->withName('alsoinclude.txt') ->withSuffix('txt'), array( 'include_dir.txt/subdir.txt/alsoinclude.txt', )); } public function testFinderWithGlobMagic() { + if (phutil_is_windows()) { + // We can't write files with "\" since this is the path separator. + // We can't write files with "*" since Windows rejects them. + // This doesn't leave us too many interesting paths to test, so just + // skip this test case under Windows. + $this->assertSkipped( + pht( + 'Windows can not write files with sufficiently absurd names.')); + } + // Fill a temporary directory with all this magic garbage so we don't have // to check a bunch of files with backslashes in their names into version // control. $tmp_dir = Filesystem::createTemporaryDirectory(); $crazy_magic = array( 'backslash\\.\\*', 'star-*.*', 'star-*.txt', 'star.t*t', 'star.tesseract', ); foreach ($crazy_magic as $sketchy_path) { Filesystem::writeFile($tmp_dir.'/'.$sketchy_path, '.'); } $this->assertFinder( pht('Glob Magic, Literal .t*t'), $this->newFinder($tmp_dir) ->withType('f') ->withSuffix('t*t'), array( 'star.t*t', )); $this->assertFinder( pht('Glob Magic, .tesseract'), $this->newFinder($tmp_dir) ->withType('f') ->withSuffix('tesseract'), array( 'star.tesseract', )); $this->assertFinder( pht('Glob Magic, Name'), $this->newFinder($tmp_dir) ->withType('f') ->withName('star-*'), array()); $this->assertFinder( pht('Glob Magic, Name + Suffix'), $this->newFinder($tmp_dir) ->withType('f') ->withName('star-*.*'), array( 'star-*.*', )); $this->assertFinder( pht('Glob Magic, Backslash Suffix'), $this->newFinder($tmp_dir) ->withType('f') ->withSuffix('\\*'), array( 'backslash\\.\\*', )); $this->assertFinder( pht('Glob Magic, With Globs'), $this->newFinder($tmp_dir) ->withType('f') ->withNameGlob('star-*'), array( 'star-*.*', 'star-*.txt', )); $this->assertFinder( pht('Glob Magic, With Globs + Suffix'), $this->newFinder($tmp_dir) ->withType('f') ->withNameGlob('star-*') ->withSuffix('txt'), array( 'star-*.txt', )); } private function assertFinder($label, FileFinder $finder, $expect) { $modes = array( 'php', - 'shell', ); + + if (!phutil_is_windows()) { + $modes[] = 'shell'; + } + foreach ($modes as $mode) { $actual = id(clone $finder) ->setForceMode($mode) ->find(); if ($finder->getGenerateChecksums()) { ksort($actual); } else { sort($actual); } $this->assertEqual( $expect, $actual, pht('Test Case "%s" in Mode "%s"', $label, $mode)); } } } diff --git a/src/filesystem/__tests__/FilesystemTestCase.php b/src/filesystem/__tests__/FilesystemTestCase.php index c6b136da..10dc1f69 100644 --- a/src/filesystem/__tests__/FilesystemTestCase.php +++ b/src/filesystem/__tests__/FilesystemTestCase.php @@ -1,178 +1,173 @@ assertEqual( true, Filesystem::binaryExists($exists)); // We don't expect to find this binary on any system. $this->assertEqual( false, Filesystem::binaryExists('halting-problem-decider')); } public function testResolveBinary() { // Test to make sure resolveBinary() returns the full path to the `which` // and `where` binaries. if (phutil_is_windows()) { $binary = 'where'; } else { $binary = 'which'; } $path = Filesystem::resolveBinary($binary); $this->assertFalse(null === $path); $this->assertTrue(file_exists($path)); $this->assertFalse(is_dir($path)); $this->assertEqual(null, Filesystem::resolveBinary('halting-problem-decider')); } public function testWriteUniqueFile() { $tmp = new TempFile(); $dir = dirname($tmp); // Writing an empty file should work. $f = Filesystem::writeUniqueFile($dir, ''); $this->assertEqual('', Filesystem::readFile($f)); // File name should be unique. $g = Filesystem::writeUniqueFile($dir, 'quack'); $this->assertTrue($f != $g); } public function testReadRandomBytes() { $number_of_bytes = 1024; $data = Filesystem::readRandomBytes($number_of_bytes); $this->assertTrue(strlen($data) == $number_of_bytes); $data1 = Filesystem::readRandomBytes(128); $data2 = Filesystem::readRandomBytes(128); $this->assertFalse($data1 == $data2); $caught = null; try { Filesystem::readRandomBytes(0); } catch (Exception $ex) { $caught = $ex; } $this->assertTrue($caught instanceof Exception); } public function testWalkToRoot() { $test_cases = array( array( dirname(__FILE__).'/data/include_dir.txt/subdir.txt/test', dirname(__FILE__), array( dirname(__FILE__).'/data/include_dir.txt/subdir.txt/test', dirname(__FILE__).'/data/include_dir.txt/subdir.txt', dirname(__FILE__).'/data/include_dir.txt', dirname(__FILE__).'/data', dirname(__FILE__), ), ), array( dirname(__FILE__).'/data/include_dir.txt/subdir.txt', dirname(__FILE__), array( dirname(__FILE__).'/data/include_dir.txt/subdir.txt', dirname(__FILE__).'/data/include_dir.txt', dirname(__FILE__).'/data', dirname(__FILE__), ), ), 'root and path are identical' => array( dirname(__FILE__), dirname(__FILE__), array( dirname(__FILE__), ), ), 'root is not an ancestor of path' => array( dirname(__FILE__), dirname(__FILE__).'/data/include_dir.txt/subdir.txt', array(), ), - 'fictional paths work' => array( - '/x/y/z', - '/', - array( - '/x/y/z', - '/x/y', - '/x', - '/', - ), - ), - ); foreach ($test_cases as $test_case) { list($path, $root, $expected) = $test_case; + // On Windows, paths will have backslashes rather than forward slashes. + // Normalize our expectations to the path format for the environment. + foreach ($expected as $key => $epath) { + $expected[$key] = str_replace('/', DIRECTORY_SEPARATOR, $epath); + } + $this->assertEqual( $expected, Filesystem::walkToRoot($path, $root)); } } public function testisDescendant() { $test_cases = array( array( __FILE__, dirname(__FILE__), true, ), array( dirname(__FILE__), dirname(dirname(__FILE__)), true, ), array( dirname(__FILE__), phutil_get_library_root_for_path(__FILE__), true, ), array( dirname(dirname(__FILE__)), dirname(__FILE__), false, ), array( dirname(__FILE__).'/quack', dirname(__FILE__), false, ), ); foreach ($test_cases as $test_case) { list($path, $root, $expected) = $test_case; $this->assertEqual( $expected, Filesystem::isDescendant($path, $root), sprintf( 'Filesystem::isDescendant(%s, %s)', phutil_var_export($path), phutil_var_export($root))); } } } diff --git a/src/filesystem/__tests__/PhutilDeferredLogTestCase.php b/src/filesystem/__tests__/PhutilDeferredLogTestCase.php index b9a9761e..359ac633 100644 --- a/src/filesystem/__tests__/PhutilDeferredLogTestCase.php +++ b/src/filesystem/__tests__/PhutilDeferredLogTestCase.php @@ -1,169 +1,172 @@ checkLog( "derp\n", 'derp', array()); $this->checkLog( "[20 Aug 1984] alincoln\n", '[%T] %u', array( 'T' => '20 Aug 1984', 'u' => 'alincoln', )); $this->checkLog( "%%%%%\n", '%%%%%%%%%%', array( '%' => '%', )); $this->checkLog( "\\000\\001\\002\n", '%a%b%c', array( 'a' => chr(0), 'b' => chr(1), 'c' => chr(2), )); $this->checkLog( "Download: 100%\n", 'Download: %C', array( 'C' => '100%', )); $this->checkLog( "- bee -\n", '%a %b %c', array( 'b' => 'bee', )); $this->checkLog( "\\\\\n", '%b', array( 'b' => '\\', )); $this->checkLog( "a\t\\t\n", "%a\t%b", array( 'a' => 'a', 'b' => "\t", )); $this->checkLog( "\1ab\n", "\1a%a", array( 'a' => 'b', )); $this->checkLog( "a % xb\n", '%a %% x%b', array( 'a' => 'a', 'b' => 'b', )); } public function testLogWriteFailure() { $caught = null; try { if (phutil_is_hiphop_runtime()) { // In HipHop exceptions thrown in destructors are not normally // catchable, so call __destruct() explicitly. $log = new PhutilDeferredLog('/derp/derp/derp/derp/derp', 'derp'); $log->__destruct(); } else { new PhutilDeferredLog('/derp/derp/derp/derp/derp', 'derp'); } } catch (Exception $ex) { $caught = $ex; } $this->assertTrue($caught instanceof Exception); } public function testManyWriters() { - $root = phutil_get_library_root('arcanist').'/../'; - $bin = $root.'support/unit/deferred_log.php'; + $bin = $this->getSupportExecutable('log'); $n_writers = 3; $n_lines = 8; $tmp = new TempFile(); $futures = array(); for ($ii = 0; $ii < $n_writers; $ii++) { - $futures[] = new ExecFuture('%s %d %s', $bin, $n_lines, (string)$tmp); + $futures[] = new ExecFuture( + 'php -f %R -- %d %s', + $bin, + $n_lines, + (string)$tmp); } id(new FutureIterator($futures)) ->resolveAll(); $this->assertEqual( str_repeat("abcdefghijklmnopqrstuvwxyz\n", $n_writers * $n_lines), Filesystem::readFile($tmp)); } public function testNoWrite() { $tmp = new TempFile(); $log = new PhutilDeferredLog($tmp, 'xyz'); $log->setFile(null); unset($log); $this->assertEqual('', Filesystem::readFile($tmp), pht('No Write')); } public function testDoubleWrite() { $tmp = new TempFile(); $log = new PhutilDeferredLog($tmp, 'xyz'); $log->write(); $log->write(); unset($log); $this->assertEqual( "xyz\n", Filesystem::readFile($tmp), pht('Double Write')); } public function testSetAfterWrite() { $tmp1 = new TempFile(); $tmp2 = new TempFile(); $log = new PhutilDeferredLog($tmp1, 'xyz'); $log->write(); $caught = null; try { $log->setFile($tmp2); } catch (Exception $ex) { $caught = $ex; } $this->assertTrue($caught instanceof Exception, pht('Set After Write')); } private function checkLog($expect, $format, $data) { $tmp = new TempFile(); $log = new PhutilDeferredLog($tmp, $format); $log->setData($data); unset($log); $this->assertEqual($expect, Filesystem::readFile($tmp), $format); } } diff --git a/src/filesystem/__tests__/PhutilFileLockTestCase.php b/src/filesystem/__tests__/PhutilFileLockTestCase.php index 5f889887..6d91205f 100644 --- a/src/filesystem/__tests__/PhutilFileLockTestCase.php +++ b/src/filesystem/__tests__/PhutilFileLockTestCase.php @@ -1,184 +1,190 @@ assertTrue($this->lockTest($file)); $this->assertTrue($this->lockTest($file)); } public function testLockHolding() { // When a process is holding a lock, other processes should be unable // to acquire it. $file = new TempFile(); $hold = $this->holdLock($file); $this->assertFalse($this->lockTest($file)); $hold->resolveKill(); $this->assertTrue($this->lockTest($file)); } public function testInProcessLocking() { // Other processes should be unable to lock a file if we hold the lock. $file = new TempFile(); $lock = PhutilFileLock::newForPath($file); $lock->lock(); $this->assertFalse($this->lockTest($file)); $lock->unlock(); $this->assertTrue($this->lockTest($file)); } public function testInProcessHolding() { // We should be unable to lock a file if another process is holding the // lock. $file = new TempFile(); $lock = PhutilFileLock::newForPath($file); $hold = $this->holdLock($file); $caught = null; try { $lock->lock(); } catch (PhutilLockException $ex) { $caught = $ex; } $this->assertTrue($caught instanceof PhutilLockException); $hold->resolveKill(); $this->assertTrue($this->lockTest($file)); $lock->lock(); $lock->unlock(); } public function testRelock() { // Trying to lock a file twice should throw an exception. $file = new TempFile(); $lock = PhutilFileLock::newForPath($file); $lock->lock(); $caught = null; try { $lock->lock(); } catch (Exception $ex) { $caught = $ex; } $this->assertTrue($caught instanceof Exception); } public function testExcessiveUnlock() { // Trying to unlock a file twice should throw an exception. $file = new TempFile(); $lock = PhutilFileLock::newForPath($file); $lock->lock(); $lock->unlock(); $caught = null; try { $lock->unlock(); } catch (Exception $ex) { $caught = $ex; } $this->assertTrue($caught instanceof Exception); } public function testUnlockAll() { // unlockAll() should release all locks. $file = new TempFile(); $lock = PhutilFileLock::newForPath($file); $lock->lock(); $this->assertFalse($this->lockTest($file)); PhutilFileLock::unlockAll(); $this->assertTrue($this->lockTest($file)); // Calling this again shouldn't do anything bad. PhutilFileLock::unlockAll(); $this->assertTrue($this->lockTest($file)); $lock->lock(); $lock->unlock(); } public function testIsLocked() { // isLocked() should report lock status accurately. $file = new TempFile(); $lock = PhutilFileLock::newForPath($file); $this->assertFalse($lock->isLocked()); $lock->lock(); $this->assertTrue($lock->isLocked()); $lock->unlock(); $this->assertFalse($lock->isLocked()); } private function lockTest($file) { list($err) = $this->buildLockFuture('--test', $file)->resolve(); return ($err == 0); } private function holdLock($file) { $future = $this->buildLockFuture('--hold', $file); // We can't return until we're sure the subprocess has had time to acquire // the lock. Since actually testing for the lock would be kind of silly // and guarantee that we loop forever if the locking primitive broke, // watch stdout for a *claim* that it has acquired the lock instead. // Make sure we don't loop forever, no matter how bad things get. $future->setTimeout(30); $buf = ''; while (!$future->isReady()) { list($stdout) = $future->read(); $buf .= $stdout; if (strpos($buf, 'LOCK ACQUIRED') !== false) { return $future; } } throw new Exception(pht('Unable to hold lock in external process!')); } - private function buildLockFuture($flags, $file) { - $root = dirname(phutil_get_library_root('arcanist')); - $bin = $root.'/support/unit/lock.php'; + private function buildLockFuture(/* ... */) { + $argv = func_get_args(); + $bin = $this->getSupportExecutable('lock'); + + if (phutil_is_windows()) { + $future = new ExecFuture('php -f %R -- %Ls', $bin, $argv); + } else { + // NOTE: Use `exec` so this passes on Ubuntu, where the default `dash` + // shell will eat any kills we send during the tests. + $future = new ExecFuture('exec php -f %R -- %Ls', $bin, $argv); + } - // NOTE: Use `exec` so this passes on Ubuntu, where the default `dash` shell - // will eat any kills we send during the tests. - $future = new ExecFuture('exec php %s %C %s', $bin, $flags, $file); $future->start(); + return $future; } } diff --git a/src/filesystem/linesofalarge/__tests__/LinesOfALargeExecFutureTestCase.php b/src/filesystem/linesofalarge/__tests__/LinesOfALargeExecFutureTestCase.php index fc552637..2df6bb60 100644 --- a/src/filesystem/linesofalarge/__tests__/LinesOfALargeExecFutureTestCase.php +++ b/src/filesystem/linesofalarge/__tests__/LinesOfALargeExecFutureTestCase.php @@ -1,62 +1,64 @@ writeAndRead( "cat\ndog\nbird\n", array( 'cat', 'dog', 'bird', )); } public function testExecLargeFile() { $line = pht('The quick brown fox jumps over the lazy dog.'); $n = 100; $this->writeAndRead( str_repeat($line."\n", $n), array_fill(0, $n, $line)); } public function testExecLongLine() { $line = str_repeat('x', 64 * 1024); $this->writeAndRead($line, array($line)); } public function testExecException() { $caught = null; try { $future = new ExecFuture('does-not-exist.exe.sh'); foreach (new LinesOfALargeExecFuture($future) as $line) { // ignore } } catch (Exception $ex) { $caught = $ex; } $this->assertTrue($caught instanceof CommandException); } private function writeAndRead($write, $read) { - $future = new ExecFuture('cat'); + $bin = $this->getSupportExecutable('cat'); + + $future = new ExecFuture('php -f %R', $bin); $future->write($write); $lines = array(); foreach (new LinesOfALargeExecFuture($future) as $line) { $lines[] = $line; } $this->assertEqual( $read, $lines, pht('Write: %s', id(new PhutilUTF8StringTruncator()) ->setMaximumGlyphs(32) ->truncateString($write))); } } diff --git a/src/future/__tests__/FutureIteratorTestCase.php b/src/future/__tests__/FutureIteratorTestCase.php index a310ee23..6590583c 100644 --- a/src/future/__tests__/FutureIteratorTestCase.php +++ b/src/future/__tests__/FutureIteratorTestCase.php @@ -1,23 +1,25 @@ getSupportExecutable('cat'); + + $future1 = new ExecFuture('php -f %R', $bin); + $future2 = new ExecFuture('php -f %R', $bin); $iterator = new FutureIterator(array($future1)); $iterator->limit(2); $results = array(); foreach ($iterator as $future) { if ($future === $future1) { $iterator->addFuture($future2); } $results[] = $future->resolve(); } $this->assertEqual(2, count($results)); } } diff --git a/src/future/exec/__tests__/ExecFutureTestCase.php b/src/future/exec/__tests__/ExecFutureTestCase.php index 2ce0ad0c..25c5e46a 100644 --- a/src/future/exec/__tests__/ExecFutureTestCase.php +++ b/src/future/exec/__tests__/ExecFutureTestCase.php @@ -1,155 +1,168 @@ write('')->resolvex(); + list($stdout) = $this->newCat() + ->write('') + ->resolvex(); $this->assertEqual('', $stdout); } + private function newCat() { + $bin = $this->getSupportExecutable('cat'); + return new ExecFuture('php -f %R', $bin); + } + + private function newSleep($duration) { + $bin = $this->getSupportExecutable('sleep'); + return new ExecFuture('php -f %R -- %s', $bin, $duration); + } + public function testKeepPipe() { // NOTE: This is mostly testing the semantics of $keep_pipe in write(). - list($stdout) = id(new ExecFuture('cat')) + list($stdout) = $this->newCat() ->write('', true) ->start() ->write('x', true) ->write('y', true) ->write('z', false) ->resolvex(); $this->assertEqual('xyz', $stdout); } public function testLargeBuffer() { // NOTE: This is mostly a coverage test to hit branches where we're still // flushing a buffer. $data = str_repeat('x', 1024 * 1024 * 4); - list($stdout) = id(new ExecFuture('cat'))->write($data)->resolvex(); + list($stdout) = $this->newCat()->write($data)->resolvex(); $this->assertEqual($data, $stdout); } public function testBufferLimit() { $data = str_repeat('x', 1024 * 1024); - list($stdout) = id(new ExecFuture('cat')) + list($stdout) = $this->newCat() ->setStdoutSizeLimit(1024) ->write($data) ->resolvex(); $this->assertEqual(substr($data, 0, 1024), $stdout); } public function testResolveTimeoutTestShouldRunLessThan1Sec() { // NOTE: This tests interactions between the resolve() timeout and the // ExecFuture timeout, which are similar but not identical. - $future = id(new ExecFuture('sleep 32000'))->start(); + $future = $this->newSleep(32000)->start(); $future->setTimeout(32000); // We expect this to return in 0.01s. $result = $future->resolve(0.01); $this->assertEqual($result, null); // We expect this to now force the time out / kill immediately. If we don't // do this, we'll hang when exiting until our subprocess exits (32000 // seconds!) $future->setTimeout(0.01); $future->resolve(); } public function testTerminateWithoutStart() { // We never start this future, but it should be fine to kill a future from // any state. - $future = new ExecFuture('sleep 1'); + $future = $this->newSleep(1); $future->resolveKill(); $this->assertTrue(true); } public function testTimeoutTestShouldRunLessThan1Sec() { // NOTE: This is partly testing that we choose appropriate select wait // times; this test should run for significantly less than 1 second. - $future = new ExecFuture('sleep 32000'); + $future = $this->newSleep(32000); list($err) = $future->setTimeout(0.01)->resolve(); $this->assertTrue($err > 0); $this->assertTrue($future->getWasKilledByTimeout()); } public function testMultipleTimeoutsTestShouldRunLessThan1Sec() { $futures = array(); for ($ii = 0; $ii < 4; $ii++) { - $futures[] = id(new ExecFuture('sleep 32000'))->setTimeout(0.01); + $futures[] = $this->newSleep(32000)->setTimeout(0.01); } foreach (new FutureIterator($futures) as $future) { list($err) = $future->resolve(); $this->assertTrue($err > 0); $this->assertTrue($future->getWasKilledByTimeout()); } } public function testMultipleResolves() { // It should be safe to call resolve(), resolvex(), resolveKill(), etc., // as many times as you want on the same process. + $bin = $this->getSupportExecutable('echo'); - $future = new ExecFuture('echo quack'); + $future = new ExecFuture('php -f %R -- quack', $bin); $future->resolve(); $future->resolvex(); list($err) = $future->resolveKill(); $this->assertEqual(0, $err); } public function testReadBuffering() { $str_len_8 = 'abcdefgh'; $str_len_4 = 'abcd'; // This is a write/read with no read buffer. - $future = new ExecFuture('cat'); + $future = $this->newCat(); $future->write($str_len_8); do { $future->isReady(); list($read) = $future->read(); if (strlen($read)) { break; } } while (true); // We expect to get the entire string back in the read. $this->assertEqual($str_len_8, $read); $future->resolve(); // This is a write/read with a read buffer. - $future = new ExecFuture('cat'); + $future = $this->newCat(); $future->write($str_len_8); // Set the read buffer size. $future->setReadBufferSize(4); do { $future->isReady(); list($read) = $future->read(); if (strlen($read)) { break; } } while (true); // We expect to get the entire string back in the read. $this->assertEqual($str_len_4, $read); // Remove the limit so we can resolve the future. $future->setReadBufferSize(null); $future->resolve(); } } diff --git a/src/future/exec/__tests__/ExecPassthruTestCase.php b/src/future/exec/__tests__/ExecPassthruTestCase.php index f7795fc4..cba1b768 100644 --- a/src/future/exec/__tests__/ExecPassthruTestCase.php +++ b/src/future/exec/__tests__/ExecPassthruTestCase.php @@ -1,16 +1,18 @@ getSupportExecutable('exit'); + + $exec = new PhutilExecPassthru('php -f %R', $bin); $err = $exec->execute(); $this->assertEqual(0, $err); } } diff --git a/src/future/oauth/__tests__/PhutilOAuth1FutureTestCase.php b/src/future/oauth/__tests__/PhutilOAuth1FutureTestCase.php index 73e66375..8c86984d 100644 --- a/src/future/oauth/__tests__/PhutilOAuth1FutureTestCase.php +++ b/src/future/oauth/__tests__/PhutilOAuth1FutureTestCase.php @@ -1,159 +1,164 @@ setTimestamp(1191242090) ->setNonce('hsu94j3884jdopsl') ->setConsumerKey('dpf43f3p2l4k3l03') ->setConsumerSecret(new PhutilOpaqueEnvelope('kd94hf93k423kf44')) ->setSignatureMethod('PLAINTEXT'); $this->assertEqual('kd94hf93k423kf44&', $future->getSignature()); $uri = 'http://photos.example.net/photos'; $data = array( 'file' => 'vacation.jpg', 'size' => 'original', ); $future = id(new PhutilOAuth1Future($uri, $data)) ->setMethod('GET') ->setTimestamp(1191242096) ->setNonce('kllo9940pd9333jh') ->setConsumerKey('dpf43f3p2l4k3l03') ->setConsumerSecret(new PhutilOpaqueEnvelope('kd94hf93k423kf44')) ->setSignatureMethod('HMAC-SHA1') ->setToken('nnch734d00sl2jdk') ->setTokenSecret('pfkkdhi9sl3r4s00'); $this->assertEqual('tR3+Ty81lMeYAr/Fid0kMTYa/WM=', $future->getSignature()); } public function testOAuth1SigningWithTwitterExamples() { // NOTE: This example is from Twitter. // https://dev.twitter.com/docs/auth/creating-signature $uri = 'https://api.twitter.com/1/statuses/update.json?'. 'include_entities=true'; $data = array( 'status' => 'Hello Ladies + Gentlemen, a signed OAuth request!', ); $future = id(new PhutilOAuth1Future($uri, $data)) ->setMethod('POST') ->setConsumerKey('xvz1evFS4wEEPTGEFPHBog') ->setConsumerSecret( new PhutilOpaqueEnvelope('kAcSOqF21Fu85e7zjz7ZN2U4ZRhfV3WpwPAoE3Z7kBw')) ->setNonce('kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg') ->setSignatureMethod('HMAC-SHA1') ->setTimestamp(1318622958) ->setToken('370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb') ->setTokenSecret('LswwdoUaIvS8ltyTt5jkRh4J50vUPVVHtR2YPi5kE'); $this->assertEqual('tnnArxj06cWHq44gCs1OSKk/jLY=', $future->getSignature()); } public function testOAuth1SigningWithJIRAExamples() { + if (!function_exists('openssl_pkey_get_private')) { + $this->assertSkipped( + pht('Required "openssl" extension is not installed.')); + } // NOTE: This is an emprically example against JIRA v6.0.6, in that the // code seems to work when actually authing. It primarily serves as a check // of the RSA-SHA1 signature method. $public_key = <<setConsumerKey('quackquack') ->setPrivateKey(new PhutilOpaqueEnvelope($private_key)) ->setTimestamp('1375984131') ->setNonce('iamaduck') ->setSignatureMethod('RSA-SHA1'); // The actual signature is 684 bytes and begins "QwigfVxpOm0AKoWJkFRwbyseso // VJobhiXpyY0J79Kzki+vwlT4Xz2Tr4vlwDLsra5gJbfdeme4qJ2rE..." $this->assertEqual( '5e63e65237e2b8078426996d5ef1a706', md5($future->getSignature())); } + } diff --git a/src/lint/linter/__tests__/ArcanistXHPASTLinterTestCase.php b/src/lint/linter/__tests__/ArcanistXHPASTLinterTestCase.php index bf6a3b63..b3338fd0 100644 --- a/src/lint/linter/__tests__/ArcanistXHPASTLinterTestCase.php +++ b/src/lint/linter/__tests__/ArcanistXHPASTLinterTestCase.php @@ -1,9 +1,11 @@ assertExecutable('xhpast'); + $this->executeTestsInDirectory(dirname(__FILE__).'/xhpast/'); } } diff --git a/src/lint/linter/xhpast/rules/__tests__/ArcanistXHPASTLinterRuleTestCase.php b/src/lint/linter/xhpast/rules/__tests__/ArcanistXHPASTLinterRuleTestCase.php index faf349bf..da3590c4 100644 --- a/src/lint/linter/xhpast/rules/__tests__/ArcanistXHPASTLinterRuleTestCase.php +++ b/src/lint/linter/xhpast/rules/__tests__/ArcanistXHPASTLinterRuleTestCase.php @@ -1,43 +1,45 @@ getLinterRule(); $rules = array( $syntax_rule, $test_rule, ); return id(new ArcanistXHPASTLinter()) ->setRules($rules); } /** * Returns an instance of the linter rule being tested. * * @return ArcanistXHPASTLinterRule */ protected function getLinterRule() { + $this->assertExecutable('xhpast'); + $class = get_class($this); $matches = null; if (!preg_match('/^(\w+XHPASTLinterRule)TestCase$/', $class, $matches) || !is_subclass_of($matches[1], 'ArcanistXHPASTLinterRule')) { throw new Exception(pht('Unable to infer linter rule class name.')); } return newv($matches[1], array()); } } diff --git a/src/markup/engine/__tests__/PhutilRemarkupEngineTestCase.php b/src/markup/engine/__tests__/PhutilRemarkupEngineTestCase.php index 74932d9a..9c7b274c 100644 --- a/src/markup/engine/__tests__/PhutilRemarkupEngineTestCase.php +++ b/src/markup/engine/__tests__/PhutilRemarkupEngineTestCase.php @@ -1,121 +1,127 @@ markupText($root.$file); } } private function markupText($markup_file) { $contents = Filesystem::readFile($markup_file); $file = basename($markup_file); $parts = explode("\n~~~~~~~~~~\n", $contents); $this->assertEqual(3, count($parts), $markup_file); list($input_remarkup, $expected_output, $expected_text) = $parts; $engine = $this->buildNewTestEngine(); switch ($file) { case 'raw-escape.txt': // NOTE: Here, we want to test PhutilRemarkupEscapeRemarkupRule and // PhutilRemarkupBlockStorage, which are triggered by "\1". In the // test, "~" is used as a placeholder for "\1" since it's hard to type // "\1". $input_remarkup = str_replace('~', "\1", $input_remarkup); $expected_output = str_replace('~', "\1", $expected_output); $expected_text = str_replace('~', "\1", $expected_text); break; case 'toc.txt': $engine->setConfig('header.generate-toc', true); break; case 'link-same-window.txt': $engine->setConfig('uri.same-window', true); break; case 'link-square.txt': $engine->setConfig('uri.base', 'http://www.example.com/'); $engine->setConfig('uri.here', 'http://www.example.com/page/'); break; + case 'quoted-code-block.txt': + // These tests depend on the syntax highlighting provided by "xhpast", + // so the output will differ if we're falling back to a different + // syntax highlighter. + $this->assertExecutable('xhpast'); + break; } $actual_output = (string)$engine->markupText($input_remarkup); switch ($file) { case 'toc.txt': $table_of_contents = PhutilRemarkupHeaderBlockRule::renderTableOfContents($engine); $actual_output = $table_of_contents."\n\n".$actual_output; break; } $this->assertEqual( $expected_output, $actual_output, pht("Failed to markup HTML in file '%s'.", $file)); $engine->setMode(PhutilRemarkupEngine::MODE_TEXT); $actual_output = (string)$engine->markupText($input_remarkup); $this->assertEqual( $expected_text, $actual_output, pht("Failed to markup text in file '%s'.", $file)); } private function buildNewTestEngine() { $engine = new PhutilRemarkupEngine(); $engine->setConfig( 'uri.allowed-protocols', array( 'http' => true, 'mailto' => true, 'tel' => true, )); $rules = array(); $rules[] = new PhutilRemarkupEscapeRemarkupRule(); $rules[] = new PhutilRemarkupMonospaceRule(); $rules[] = new PhutilRemarkupDocumentLinkRule(); $rules[] = new PhutilRemarkupHyperlinkRule(); $rules[] = new PhutilRemarkupBoldRule(); $rules[] = new PhutilRemarkupItalicRule(); $rules[] = new PhutilRemarkupDelRule(); $rules[] = new PhutilRemarkupUnderlineRule(); $rules[] = new PhutilRemarkupHighlightRule(); $blocks = array(); $blocks[] = new PhutilRemarkupQuotesBlockRule(); $blocks[] = new PhutilRemarkupReplyBlockRule(); $blocks[] = new PhutilRemarkupHeaderBlockRule(); $blocks[] = new PhutilRemarkupHorizontalRuleBlockRule(); $blocks[] = new PhutilRemarkupCodeBlockRule(); $blocks[] = new PhutilRemarkupLiteralBlockRule(); $blocks[] = new PhutilRemarkupNoteBlockRule(); $blocks[] = new PhutilRemarkupTableBlockRule(); $blocks[] = new PhutilRemarkupSimpleTableBlockRule(); $blocks[] = new PhutilRemarkupDefaultBlockRule(); $blocks[] = new PhutilRemarkupListBlockRule(); $blocks[] = new PhutilRemarkupInterpreterBlockRule(); foreach ($blocks as $block) { if (!($block instanceof PhutilRemarkupCodeBlockRule)) { $block->setMarkupRules($rules); } } $engine->setBlockRules($blocks); return $engine; } } diff --git a/src/markup/syntax/highlighter/__tests__/PhutilXHPASTSyntaxHighlighterTestCase.php b/src/markup/syntax/highlighter/__tests__/PhutilXHPASTSyntaxHighlighterTestCase.php index 2ec9be17..a67fafb1 100644 --- a/src/markup/syntax/highlighter/__tests__/PhutilXHPASTSyntaxHighlighterTestCase.php +++ b/src/markup/syntax/highlighter/__tests__/PhutilXHPASTSyntaxHighlighterTestCase.php @@ -1,39 +1,41 @@ getHighlightFuture($source); return $future->resolve(); } private function read($file) { $path = dirname(__FILE__).'/xhpast/'.$file; return Filesystem::readFile($path); } public function testBuiltinClassnames() { + $this->assertExecutable('xhpast'); + $this->assertEqual( $this->read('builtin-classname.expect'), (string)$this->highlight($this->read('builtin-classname.source')), pht('Builtin classnames should not be marked as linkable symbols.')); $this->assertEqual( rtrim($this->read('trailing-comment.expect')), (string)$this->highlight($this->read('trailing-comment.source')), pht('Trailing comments should not be dropped.')); $this->assertEqual( $this->read('multiline-token.expect'), (string)$this->highlight($this->read('multiline-token.source')), pht('Multi-line tokens should be split across lines.')); $this->assertEqual( $this->read('leading-whitespace.expect'), (string)$this->highlight($this->read('leading-whitespace.source')), pht('Snippets with leading whitespace should be preserved.')); $this->assertEqual( $this->read('no-leading-whitespace.expect'), (string)$this->highlight($this->read('no-leading-whitespace.source')), pht('Snippets with no leading whitespace should be preserved.')); } } diff --git a/src/parser/PhutilEditorConfig.php b/src/parser/PhutilEditorConfig.php index a03d2bf6..d2bf93a3 100644 --- a/src/parser/PhutilEditorConfig.php +++ b/src/parser/PhutilEditorConfig.php @@ -1,195 +1,201 @@ array( 'latin1', 'utf-8', 'utf-8-bom', 'utf-16be', 'utf-16le', ), self::END_OF_LINE => array('lf', 'cr', 'crlf'), self::INDENT_SIZE => 'int|string', self::INDENT_STYLE => array('space', 'tab'), self::FINAL_NEWLINE => 'bool', self::LINE_LENGTH => 'int', self::TAB_WIDTH => 'int', self::TRAILING_WHITESPACE => 'bool', ); private $root; /** * Constructor. * * @param string The root directory. */ public function __construct($root) { $this->root = $root; } /** * Get the specified EditorConfig property for the specified path. * * @param string * @param string * @return wild */ public function getProperty($path, $key) { if (!idx(self::$knownProperties, $key)) { throw new InvalidArgumentException(pht('Invalid EditorConfig property.')); } $props = $this->getProperties($path); switch ($key) { case self::INDENT_SIZE: if (idx($props, self::INDENT_SIZE) === null && idx($props, self::INDENT_STYLE) === 'tab') { return 'tab'; } else if (idx($props, self::INDENT_SIZE) === 'tab' && idx($props, self::TAB_WIDTH) === null) { return idx($props, self::TAB_WIDTH); } break; case self::TAB_WIDTH: if (idx($props, self::TAB_WIDTH) === null && idx($props, self::INDENT_SIZE) !== null && idx($props, self::INDENT_SIZE) !== 'tab') { return idx($props, self::INDENT_SIZE); } break; } return idx($props, $key); } /** * Get the EditorConfig properties for the specified path. * * Returns a map containing all of the EditorConfig properties which apply * to the specified path. The following rules are applied when processing * EditorConfig files: * * - If a glob does not contain `/`, it can match a path in any subdirectory. * - If the first character of a glob is `/`, it will only match files in the * same directory as the `.editorconfig` file. * - Properties and values are case-insensitive. * - Unknown properties will be silently ignored. * - Values are not validated against the specification (this may change in * the future). * - Invalid glob patterns will be silently ignored. * * @param string * @return map */ public function getProperties($path) { $configs = $this->getEditorConfigs($path); $matches = array(); + // Normalize directory separators to "/". The ".editorconfig" standard + // uses only "/" as a directory separator, not "\". + $path = str_replace(DIRECTORY_SEPARATOR, '/', $path); + foreach ($configs as $config) { list($path_prefix, $editorconfig) = $config; + // Normalize path separators, as above. + $path_prefix = str_replace(DIRECTORY_SEPARATOR, '/', $path_prefix); + foreach ($editorconfig as $glob => $properties) { if (!$glob) { continue; } if (strpos($glob, '/') === false) { $glob = '**/'.$glob; } else if (strncmp($glob, '/', 0)) { $glob = substr($glob, 1); } $glob = $path_prefix.'/'.$glob; try { if (!phutil_fnmatch($glob, $path)) { continue; } } catch (Exception $ex) { // Invalid glob pattern... ignore it. continue; } foreach ($properties as $property => $value) { $property = strtolower($property); if (!idx(self::$knownProperties, $property)) { // Unknown property... ignore it. continue; } if (is_string($value)) { $value = strtolower($value); } if ($value === '') { $value = null; } $matches[$property] = $value; } } } return $matches; } /** * Returns the EditorConfig files which affect the specified path. * * Find and parse all `.editorconfig` files between the specified path and * the root directory. The results are returned in the same order that they * should be matched. * * return list> */ private function getEditorConfigs($path) { - $configs = array(); - $found_root = false; - $root = $this->root; + $configs = array(); - do { - $path = dirname($path); + $found_root = false; + $paths = Filesystem::walkToRoot($path, $this->root); + foreach ($paths as $path) { $file = $path.'/.editorconfig'; if (!Filesystem::pathExists($file)) { continue; } $contents = Filesystem::readFile($file); $config = phutil_ini_decode($contents); if (idx($config, 'root') === true) { $found_root = true; } unset($config['root']); array_unshift($configs, array($path, $config)); if ($found_root) { break; } - } while ($path != $root && Filesystem::isDescendant($path, $root)); + } return $configs; } } diff --git a/src/parser/__tests__/ArcanistBundleTestCase.php b/src/parser/__tests__/ArcanistBundleTestCase.php index 4ddf32a3..7fdd3bf4 100644 --- a/src/parser/__tests__/ArcanistBundleTestCase.php +++ b/src/parser/__tests__/ArcanistBundleTestCase.php @@ -1,987 +1,989 @@ getResourcePath($name)); } private function getResourcePath($name) { return dirname(__FILE__).'/bundle/'.$name; } private function loadDiff($old, $new) { + $this->assertExecutable('diff'); + list($err, $stdout) = exec_manual( 'diff --unified=65535 --label %s --label %s -- %s %s', 'file 9999-99-99', 'file 9999-99-99', $this->getResourcePath($old), $this->getResourcePath($new)); $this->assertEqual( 1, $err, pht( "Expect `%s` to find changes between '%s' and '%s'.", 'diff', $old, $new)); return $stdout; } private function loadOneChangeBundle($old, $new) { $diff = $this->loadDiff($old, $new); return ArcanistBundle::newFromDiff($diff); } public function testTabEncoding() { // See T8768. Test that we add semantic trailing tab literals to diffs // touching files with spaces in them. This is a pain to encode using the // support toolset here so just do it manually. // Note that the "b/X Y.txt" line has a trailing tab literal. $diff = <<getChanges(); $this->assertEqual(1, count($changes)); // The path should parse as "X Y.txt" despite the trailing tab. $change = head($changes); $this->assertEqual('X Y.txt', $change->getCurrentPath()); // The tab should be restored when the diff is output again. $this->assertEqual($diff, $bundle->toGitPatch()); } /** * Unarchive a saved git repository and apply each commit as though via * "arc patch", verifying that the resulting tree hash is identical to the * tree hash produced by the real commit. */ public function testGitRepository() { if (phutil_is_windows()) { $this->assertSkipped(pht('This test is not supported under Windows.')); } $archive = dirname(__FILE__).'/bundle.git.tgz'; $fixture = PhutilDirectoryFixture::newFromArchive($archive); $old_dir = getcwd(); chdir($fixture->getPath()); $caught = null; try { $this->runGitRepositoryTests($fixture); } catch (Exception $ex) { $caught = $ex; } chdir($old_dir); if ($caught) { throw $ex; } } private function runGitRepositoryTests(PhutilDirectoryFixture $fixture) { $patches = dirname(__FILE__).'/patches/'; list($commits) = execx( 'git log --format=%s', '%H %T %s'); $commits = explode("\n", trim($commits)); // The very first commit doesn't have a meaningful parent, so don't examine // it. array_pop($commits); foreach ($commits as $commit) { list($commit_hash, $tree_hash, $subject) = explode(' ', $commit, 3); execx('git reset --hard %s --', $commit_hash); $fixture_path = $fixture->getPath(); $working_copy = ArcanistWorkingCopy::newFromWorkingDirectory( $fixture_path); $repository_api = $working_copy->newRepositoryAPI(); $repository_api->setBaseCommitArgumentRules('arc:this'); $diff = $repository_api->getFullGitDiff( $repository_api->getBaseCommit(), $repository_api->getHeadCommit()); $parser = new ArcanistDiffParser(); $parser->setRepositoryAPI($repository_api); $changes = $parser->parseDiff($diff); $this->makeChangeAssertions($commit_hash, $changes); $bundle = ArcanistBundle::newFromChanges($changes); execx('git reset --hard %s^ --', $commit_hash); $patch = $bundle->toGitPatch(); $expect_path = $patches.'/'.$commit_hash.'.gitpatch'; $expect = null; if (Filesystem::pathExists($expect_path)) { $expect = Filesystem::readFile($expect_path); } if ($patch === $expect) { $this->assertEqual($expect, $patch); } else { Filesystem::writeFile($expect_path.'.real', $patch); throw new Exception( pht( "Expected patch and actual patch for %s differ. ". "Wrote actual patch to '%s.real'.", $commit_hash, $expect_path)); } try { id(new ExecFuture('git apply --index --reject')) ->write($patch) ->resolvex(); } catch (CommandException $ex) { $temp = new TempFile(substr($commit_hash, 0, 8).'.patch'); $temp->setPreserveFile(true); Filesystem::writeFile($temp, $patch); PhutilConsole::getConsole()->writeErr( "%s\n", pht("Wrote failing patch to '%s'.", $temp)); throw $ex; } // If these aren't configured, Git complains even if we pass --author. $git_name = 'unit-test'; $git_email = 'unit-test@phabricator.com'; execx( 'git -c user.name=%s -c user.email=%s commit -m %s', $git_name, $git_email, $subject); list($result_hash) = execx('git log -n1 --format=%s', '%T'); $result_hash = trim($result_hash); $this->assertEqual( $tree_hash, $result_hash, pht('Commit %s: %s', $commit_hash, $subject)); } } private function makeChangeAssertions($commit, array $raw_changes) { $changes = array(); // Verify that there are no duplicate changes, and rekey the changes on // affected path because we don't care about the order in which the // changes appear. foreach ($raw_changes as $change) { $this->assertTrue( empty($changes[$change->getCurrentPath()]), 'Unique Path: '.$change->getCurrentPath()); $changes[$change->getCurrentPath()] = $change; } switch ($commit) { case '1830a13adf764b55743f7edc6066451898d8ffa4': // "Mark koan2 as +x and edit it." $this->assertEqual(1, count($changes)); $c = $changes['koan2']; $this->assertEqual( ArcanistDiffChangeType::TYPE_CHANGE, $c->getType()); $this->assertEqual( '100644', idx($c->getOldProperties(), 'unix:filemode')); $this->assertEqual( '100755', idx($c->getNewProperties(), 'unix:filemode')); break; case '8ecc728bcc9b482a9a91527ea471b04fc1a025cf': // "Move 'text' to 'executable' and mark it +x." $this->assertEqual(2, count($changes)); $c = $changes['executable']; $this->assertEqual( ArcanistDiffChangeType::TYPE_MOVE_HERE, $c->getType()); $this->assertEqual( '100644', idx($c->getOldProperties(), 'unix:filemode')); $this->assertEqual( '100755', idx($c->getNewProperties(), 'unix:filemode')); break; case '39c8e7dd3914edff087a6214f0cd996ad08e5b3d': // "Mark koan as +x." // Primarily a test against a recusive synthetic hunk construction bug. $this->assertEqual(1, count($changes)); $c = $changes['koan']; $this->assertEqual( ArcanistDiffChangeType::TYPE_CHANGE, $c->getType()); $this->assertEqual( '100644', idx($c->getOldProperties(), 'unix:filemode')); $this->assertEqual( '100755', idx($c->getNewProperties(), 'unix:filemode')); break; case 'c573c25d1a767d270fed504cd993e78aba936338': // "Copy a koan over text, editing the original koan." // Git doesn't really do anything meaningful with this. $this->assertEqual(2, count($changes)); $c = $changes['koan']; $this->assertEqual( ArcanistDiffChangeType::TYPE_CHANGE, $c->getType()); $c = $changes['text']; $this->assertEqual( ArcanistDiffChangeType::TYPE_CHANGE, $c->getType()); break; case 'd26628e588cf7d16368845b121c6ac6c781e81d0': // "Copy a koan, modifying both the source and destination." $this->assertEqual(2, count($changes)); $c = $changes['koan']; $this->assertEqual( ArcanistDiffChangeType::TYPE_COPY_AWAY, $c->getType()); $c = $changes['koan2']; $this->assertEqual( ArcanistDiffChangeType::TYPE_COPY_HERE, $c->getType()); break; case 'b0c9663ecda5f666f62dad245a3a7549aac5e636': // "Remove a koan copy." $this->assertEqual(1, count($changes)); $c = $changes['koan2']; $this->assertEqual( ArcanistDiffChangeType::TYPE_DELETE, $c->getType()); break; case 'b6ecdb3b4801f3028d88ba49940a558360847dbf': // "Copy a koan and edit the destination." // Git does not detect this as a copy without --find-copies-harder. $this->assertEqual(1, count($changes)); $c = $changes['koan2']; $this->assertEqual( ArcanistDiffChangeType::TYPE_ADD, $c->getType()); break; case '30d23787e1ecd254c884afbe37afa612f61e3904': // "Move and edit a koan." $this->assertEqual(2, count($changes)); $c = $changes['koan2']; $this->assertEqual( ArcanistDiffChangeType::TYPE_MOVE_AWAY, $c->getType()); $c = $changes['koan']; $this->assertEqual( ArcanistDiffChangeType::TYPE_MOVE_HERE, $c->getType()); break; case 'c0ba9bfe3695f95c3f558bc5797eeba421d32483': // "Remove two koans." $this->assertEqual(2, count($changes)); $c = $changes['koan3']; $this->assertEqual( ArcanistDiffChangeType::TYPE_DELETE, $c->getType()); $c = $changes['koan4']; $this->assertEqual( ArcanistDiffChangeType::TYPE_DELETE, $c->getType()); break; case '2658fd01d5355abe5d4c7ead3a0e7b4b3449fe77': // "Multicopy a koan." $this->assertEqual(3, count($changes)); $c = $changes['koan']; $this->assertEqual( ArcanistDiffChangeType::TYPE_MULTICOPY, $c->getType()); $c = $changes['koan3']; $this->assertEqual( ArcanistDiffChangeType::TYPE_COPY_HERE, $c->getType()); $c = $changes['koan4']; $this->assertEqual( ArcanistDiffChangeType::TYPE_MOVE_HERE, $c->getType()); break; case '1c5fe4e2243bb19d6b3bf15896177b13768e6eb6': // "Copy a koan." // Git does not detect this as a copy without --find-copies-harder. $this->assertEqual(1, count($changes)); $c = $changes['koan']; $this->assertEqual( ArcanistDiffChangeType::TYPE_ADD, $c->getType()); break; case '6d9eb65a2c2b56dee64d72f59554c1cca748dd34': // "Move a koan." $this->assertEqual(2, count($changes)); $c = $changes['koan']; $this->assertEqual( ArcanistDiffChangeType::TYPE_MOVE_AWAY, $c->getType()); $c = $changes['koan2']; $this->assertEqual( ArcanistDiffChangeType::TYPE_MOVE_HERE, $c->getType()); break; case '141452e2a775ee86409e8779dd2eda767b4fe8ab': // "Add a koan." $this->assertEqual(1, count($changes)); $c = $changes['koan']; $this->assertEqual( ArcanistDiffChangeType::TYPE_ADD, $c->getType()); break; case '5dec8bf28557f078d1987c4e8cfb53d08310f522': // "Copy an image, and replace the original." // `image_2.png` is copied to `image.png` and then replaced. $this->assertEqual(2, count($changes)); $c = $changes['image.png']; $this->assertEqual( ArcanistDiffChangeType::TYPE_COPY_HERE, $c->getType()); $this->assertEqual( ArcanistDiffChangeType::FILE_BINARY, $c->getFileType()); $this->assertEqual( null, $c->getOriginalFileData()); $this->assertEqual( '8645053452b2cc2f955ef3944ac0831a', md5($c->getCurrentFileData())); $c = $changes['image_2.png']; $this->assertEqual( ArcanistDiffChangeType::TYPE_COPY_AWAY, $c->getType()); $this->assertEqual( ArcanistDiffChangeType::FILE_BINARY, $c->getFileType()); $this->assertEqual( '8645053452b2cc2f955ef3944ac0831a', md5($c->getOriginalFileData())); $this->assertEqual( 'c9ec1b952480da09b393ba672d9b13da', md5($c->getCurrentFileData())); break; case 'fb28468d25a5fdd063aca4ca559454c998a0af51': // "Multicopy image." // `image.png` is copied to `image_2.png` and `image_3.png` and then // deleted. Git detects this as a move and an add. $this->assertEqual(3, count($changes)); $c = $changes['image.png']; $this->assertEqual( ArcanistDiffChangeType::TYPE_MULTICOPY, $c->getType()); $this->assertEqual( ArcanistDiffChangeType::FILE_BINARY, $c->getFileType()); $this->assertEqual( '8645053452b2cc2f955ef3944ac0831a', md5($c->getOriginalFileData())); $this->assertEqual( null, $c->getCurrentFileData()); $c = $changes['image_2.png']; $this->assertEqual( ArcanistDiffChangeType::TYPE_COPY_HERE, $c->getType()); $this->assertEqual( ArcanistDiffChangeType::FILE_BINARY, $c->getFileType()); $this->assertEqual( null, $c->getOriginalFileData()); $this->assertEqual( '8645053452b2cc2f955ef3944ac0831a', md5($c->getCurrentFileData())); $c = $changes['image_3.png']; $this->assertEqual( ArcanistDiffChangeType::TYPE_MOVE_HERE, $c->getType()); $this->assertEqual( ArcanistDiffChangeType::FILE_BINARY, $c->getFileType()); $this->assertEqual( null, $c->getOriginalFileData()); $this->assertEqual( '8645053452b2cc2f955ef3944ac0831a', md5($c->getCurrentFileData())); break; case 'df340e88d8aba12e8f2b8827f01f0cd9f35eb758': // "Remove binary image." // `image_2.png` is deleted. $this->assertEqual(1, count($changes)); $c = $changes['image_2.png']; $this->assertEqual( ArcanistDiffChangeType::TYPE_DELETE, $c->getType()); $this->assertEqual( ArcanistDiffChangeType::FILE_BINARY, $c->getFileType()); $this->assertEqual( '8645053452b2cc2f955ef3944ac0831a', md5($c->getOriginalFileData())); $this->assertEqual( null, $c->getCurrentFileData()); break; case '3f5c6d735e64c25a04f83be48ef184b25b5282f0': // "Copy binary image." // `image_2.png` is copied to `image.png`. Git does not detect this as // a copy without --find-copies-harder. $this->assertEqual(1, count($changes)); $c = $changes['image.png']; $this->assertEqual( ArcanistDiffChangeType::TYPE_ADD, $c->getType()); $this->assertEqual( ArcanistDiffChangeType::FILE_BINARY, $c->getFileType()); $this->assertEqual( null, $c->getOriginalFileData()); $this->assertEqual( '8645053452b2cc2f955ef3944ac0831a', md5($c->getCurrentFileData())); break; case 'b454edb3bb29890ee5b3af5ef66ce6a24d15d882': // "Move binary image." // `image.png` is moved to `image_2.png`. $this->assertEqual(2, count($changes)); $c = $changes['image.png']; $this->assertEqual( ArcanistDiffChangeType::TYPE_MOVE_AWAY, $c->getType()); $this->assertEqual( ArcanistDiffChangeType::FILE_BINARY, $c->getFileType()); $this->assertEqual( '8645053452b2cc2f955ef3944ac0831a', md5($c->getOriginalFileData())); $this->assertEqual( null, $c->getCurrentFileData()); $c = $changes['image_2.png']; $this->assertEqual( ArcanistDiffChangeType::TYPE_MOVE_HERE, $c->getType()); $this->assertEqual( ArcanistDiffChangeType::FILE_BINARY, $c->getFileType()); $this->assertEqual( null, $c->getOriginalFileData()); $this->assertEqual( '8645053452b2cc2f955ef3944ac0831a', md5($c->getCurrentFileData())); break; case '5de5f3dfda1b7db2eb054e57699f05aaf1f4483e': // "Add a binary image." // `image.png` is added. $c = $changes['image.png']; $this->assertEqual( ArcanistDiffChangeType::TYPE_ADD, $c->getType()); $this->assertEqual( ArcanistDiffChangeType::FILE_BINARY, $c->getFileType()); $this->assertEqual( null, $c->getOriginalFileData()); $this->assertEqual( '8645053452b2cc2f955ef3944ac0831a', md5($c->getCurrentFileData())); break; case '176a4c2c3fd88b2d598ce41a55d9c3958be9fd2d': // "Convert \r\n newlines to \n newlines." case 'a73b28e139296d23ade768f2346038318b331f94': // "Add text with \r\n newlines." case '337ccec314075a2bdb4a912ef467d35d04a713e4': // "Convert \n newlines to \r\n newlines."; case '6d5e64a4a7a6a036c53b1d087184cb2c70099f2c': // "Remove tabs." case '49395994a1a8a06287e40a3b318be4349e8e0288': // "Add tabs." case 'a5a53c424f3c2a7e85f6aee35e834c8ec5b3dbe3': // "Add trailing newline." case 'd53dc614090c6c7d6d023e170877d7f611f18f5a': // "Remove trailing newline." case 'f19fb9fa1385c01b53bdb6d8842dd154e47151ec': // "Edit a text file." $this->assertEqual(1, count($changes)); $c = $changes['text']; $this->assertEqual( ArcanistDiffChangeType::TYPE_CHANGE, $c->getType()); $this->assertEqual( ArcanistDiffChangeType::FILE_TEXT, $c->getFileType()); break; case '228d7be4840313ed805c25c15bba0f7b188af3e6': // "Add a text file." // This commit is never reached because we skip the 0th commit junk. $this->assertTrue(true, pht('This is never reached.')); break; default: throw new Exception( pht('Commit %s has no change assertions!', $commit)); } } public function testTrailingContext() { // Diffs need to generate without extra trailing context, or 'patch' will // choke on them. $this->assertEqual( $this->loadResource('trailing-context.diff'), $this->loadOneChangeBundle( 'trailing-context.old', 'trailing-context.new')->toUnifiedDiff()); } public function testDisjointHunks() { // Diffs need to generate without overlapping hunks. $this->assertEqual( $this->loadResource('disjoint-hunks.diff'), $this->loadOneChangeBundle( 'disjoint-hunks.old', 'disjoint-hunks.new')->toUnifiedDiff()); } public function testMergeHunks() { // Hunks should merge if represented by sufficiently few unchanged // lines. $this->assertEqual( $this->loadResource('merge-hunks.diff'), $this->loadOneChangeBundle( 'merge-hunks.old', 'merge-hunks.new')->toUnifiedDiff()); // Hunks should not merge if they are separated by too many unchanged // lines. $this->assertEqual( $this->loadResource('no-merge-hunks.diff'), $this->loadOneChangeBundle( 'no-merge-hunks.old', 'no-merge-hunks.new')->toUnifiedDiff()); } public function testNonlocalTrailingNewline() { // Diffs without changes near the end of the file should not generate a // bogus, change-free hunk if the file has no trailing newline. $this->assertEqual( $this->loadResource('trailing-newline.diff'), $this->loadOneChangeBundle( 'trailing-newline.old', 'trailing-newline.new')->toUnifiedDiff()); } public function testEncodeBase85() { $data = ''; for ($ii = 0; $ii <= 255; $ii++) { $data .= chr($ii); } for ($ii = 255; $ii >= 0; $ii--) { $data .= chr($ii); } $expect = Filesystem::readFile(dirname(__FILE__).'/base85/expect1.txt'); $this->assertBase85($expect, $data, pht('Byte Sequences')); // This is just a large block of random binary data, it has no special // significance. $data = "\x56\x4c\xb3\x63\xe5\x4a\x9f\x03\xa3\x4c\xdd\x5d\x85\x86\x10". "\x30\x3f\xc1\x28\x51\xd8\xb2\x1a\xc3\x79\x15\x85\x31\x66\xf9". "\x8e\xe1\x20\x8f\x12\xa1\x94\x0e\xbf\xb6\x9c\xb5\xc0\x15\x43". "\x3d\xad\xed\x00\x3c\x16\xfa\x76\x2f\xed\x99\x3a\x78\x3e\xd1". "\x91\xf8\xb0\xca\xb9\x29\xfe\xd4\x0f\x16\x70\x19\xad\xd9\x42". "\x15\xb4\x8f\xd6\x8f\x80\x62\xe9\x48\x77\x9f\x38\x6d\x3f\xd6". "\x0e\x40\x68\x68\x93\xae\x75\x6d\x7f\x75\x9c\x80\x69\x94\x22". "\x87\xb6\xc0\x62\x6b\xab\x49\xb8\x91\xe9\x96\xbf\x04\xc2\x50". "\x30\xae\xea\xc1\x70\x8e\x91\xd0\xb6\xec\x56\x14\x78\xd5\x8a". "\x8c\x52\xd1\x3c\xde\x65\x21\xec\x93\xab\xcf\x7e\xf5\xfd\x6d". "\x2d\x69\xb9\x2e\xa3\x42\x7b\x4d\xa5\xfb\x28\x6d\x74\xa3\x7b". "\x3a\xc5\x34\x7c\x63\xa9\xf9\x8e\x34\x14\x42\xb0\xf1\x0e\xe2". "\xd0\xd2\x04\x81\xff\x62\xd5\xd9\x46\x3b\x36\x88\x8a\x93\x55". "\x02\x2c\xff\x9f\x48\xd6\x7a\xcb\xbf\x6a\x33\xaa\x6b\x08\x4c". "\x96\x98\x89\x53\x56\xb4\xb3\x9b\x06\xb1\xa0\x13\x69\xfa\x6a". "\xa8\x0d\x6a\xda\xb2\x6f\x62\x0b\xa8\xf6\x59\x29\x46\x7d\x04". "\x44\xeb\x90\x6f\xd7\xc7\xb6\xca\xc5\xeb\xde\x10\x9b\xbd\xf2". "\x66\x8e\xd0\x0b\xda\x8c\xeb\x90\x73\x73\x33\xe7\x6f\x26\x57". "\x4e\xfc\x95\xe0\xfc\x62\x93\xa7\x28\xe6\x0c\x46\x73\xdd\x01". "\xce\x43\x9b\x4e\x16\x74\x5b\x36\x92\x5a\x66\x4c\xe3\x9e\x90". "\x2d\x9a\x1a\x3d\x69\x39\x67\x04\xd6\xf8\x5f\x45\xee\xbb\xd4". "\x63\xcf\x8c\x9b\x31\x69\x98\x1a\x98\x57\x4b\xa9\x49\xf6\x1b". "\x76\x28\xd7\xe3\x8f\x63\x95\x5b\x06\xe2\xa8\x66\x60\xf9\x49". "\x4e\x40\x53\x32\x9b\x74\x36\xc0\x56\xf4\x33\xec\x83\xd2\x2c". "\x69\x60\x55\x11\x3b\x4f\xd6\x0a\xf6\x04\x38\x75\xb6\xc2\x82". "\x4d\xfa\x83\x56\xba\x35\x42\xc3\xcb\xdc\x28\xf4\x69\x48\xa9". "\xe0\x51\x41\x79\x66\xfe\x61\xd1\xf2\x9f\x7b\xde\xc4\x3e\x8f". "\x8f\xb6\x9c\x0a\x74\xf8\x71\x03\x37\x37\x30\x8d\x2a\x6a\xc9". "\x51\xa1\xe2\x34\xe5\x42\xdb\x4f\x61\x4e\x16\xfc\x23\x72\x12". "\x46\x53\x12\x82\x3e\x44\x63\x23\x82\xaa\xab\x7e\x8d\x70\x66". "\xf1\x94\x86\x02\xc5\x3e\x9c\x79\x17\x1e\x9f\x13\x89\x3d\x25". "\x45\xc9\x3b\x1e\xa0\x1a\x03\x20\x1c\x81\x6b\xfc\xb5\xc9\xe2". "\xda\xb1\x87\x34\xa0\xb2\x72\x36\x68\x12\x05\x53\x7c\x68\x6b". "\x1e\x2a\x56\x2a\x7e\x7f\xd0\x9c\x13\xa9\xb2\x4c\xe6\x8a\x65". "\xd7\x67\xad\xf3\xf3\x2b\x9c\xe8\x10\x07\x8a\xe2\x20\x67\xe4". "\x51\x47\xc1\x22\x91\x05\x22\x39\x1a\xef\x54\xd2\x8a\x88\x55". "\x3f\x83\xba\x73\xd4\x95\xc7\xb8\xa2\xfd\x4d\x4e\x5d\xff\xdd". "\xaf\x1a\xc2\x7e\xb5\xfa\x86\x5f\x93\x38\x5d\xca\x9a\x5a\x7e". "\xb7\x47\xd5\x5c\x6b\xf3\x32\x03\x11\x44\xe9\x49\x12\x40\x82". "\x67\x7d\x2a\x5a\x61\x81\xbd\x24\xaa\xd7\x7c\xc9\xcf\xaf\xb0". "\x3e\xb0\x43\xcd\xce\x21\xe4\x1b\x5a\xd6\x40\xf5\x0e\x23\xef". "\x70\xf4\xc6\xd2\xd7\x36\xd7\x20\xda\x8d\x39\x46\xea\xfc\x78". "\x55\xa2\x02\xd6\x77\x21\xc8\x97\x1e\xdf\x45\xde\x93\xa7\x74". "\xd8\x59\x10\x24\x8a\xe8\xcd\xe9\x00\xb5\x4e\xe6\x49\xb0\xde". "\x14\x1a\x5d\xdd\x38\x47\xb0\xc7\x1e\xec\x7c\x76\xc9\x21\x3c". "\x3a\x85\x4f\x71\x97\xed\x4a\x94\x2c\x51\x48\x9c\x43\x90\x70". "\xe9\x0e\x84\x55\xd2\xa4\x48\xfa\xfd\x54\x12\x11\xb9\x32\xfc". "\x1d\x66\xe7\x42\xe3\x5e\x65\xf4\x3d\xea\x1a\x53\xe3\x7b\x4b". "\xee\xdb\x74\xce\x30\xd3\x04\xcb\xda\xa4\xdd\xad\x98\x3a\x76". "\xe8\xba\x1b\x03\x53\xed\x46\x5d\xef\xd4\x34\xc2\x8d\xef\xae". "\x51\x35\x0f\x4d\x40\xaa\x3a\xdb\x50\x1a\xbe\x5f\x8b\xb8\x24". "\x40\x19\x8f\x8a\x6b\x44\x4f\x9b\xe0\xf4\x9c\x4b\xc4\x23\x37". "\xf0\xb3\xe1\x58\x9d\x0e\xd9\xa9\xf7\x3e\x86\x43\x9b\x5b\x90". "\x3c\xc0\x20\xa0\xc5\x86\x4f\xc6\xcb\xb5\xcb\xd4\x88\xc6\x72". "\x57\xa7\x57\x2c\x34\x26\x91\x44\x15\xa8\xf4\x88\xca\x74\x56". "\x9e\x12\x6c\xdf\x52\xef\xc0\xb4\x5c\x16\xe8\xaa\xf7\xb6\xf3". "\x7c\xda\xcd\x42\xf9\x1c\x40\x88\x44\x68\x4f\x1b\x5a\x7b\x8f". "\xc3\x47\x48\xd3\xf3\xe5\xf5\x66\x35\x48\xbe\x64\xdf\xfe\x35". "\xf1\xc3\xe4\xa8\xfc\x86\xfb\x69\x20\xc9\xf4\x16\x96\xc1\x7a". "\x51\x14\x77\xa4\x6e\x13\xe8\x59\x35\x24\xf1\xe5\xfe\xe9\x98". "\x0d\xd1\xe8\xce\x9c\x7f\xf8\x3b\x79\x39\x3a\x1d\xa3\x77\xef". "\x4f\x4b\x59\x73\x03\xb3\xfe\xae\x70\x2a\x3a\xf0\x79\x9d\x7e". "\x9b\xaa\xb1\x18\xf9\x43\x69\xf3\x55\x46\xad\x38\xa2\xf1\xcb". "\xce\x37\xa9\x88\x20\x38\xea\x19\x29\x95\x8c\x75\x06\x9d\x1d". "\x9e\xf2\xb7\x64\x98\x21\x36\x90\x92\xf8\xb8\x89\x1e\x5c\x5d". "\x09\x3b\x52\xc5\x6a\x87\x7e\x46\xca\x8c\xdf\xe7\xca\xa9\x7b". "\x11\x63\x0f\x9e\x42\x9a\x3e\xe0\x8b\x80\x9e\x91\x76\x88\x9a". "\xa1\xe2\x96\xae\xfb\x18\x39\xdc\x92\x99\x34\xfd\x98\x20\xa8". "\x89\x61\x2c\x26\xe0\xb8\x83\xa7\xe7\x50\x42\x8f\xfc\x36\x66". "\x6b\x25\xc5\x6d\xb4\x31\xe1\x4d\x0f\x2e\xf8\x44\xe2\xb6\x6a". "\x6d\xfe\x83\x9e\x2c\x07\x2f\x15\x41\xf3\xe7\xa6\x18\x2b\x84". "\x7e\xeb\x43\xcc\xbb\xdb\xa9\x54\x5c\xbc\x59\x6a\xdc\x26\x2a". "\xf4\x59\xa7\x75\xa4\xac\xed\x73\x8f\x16\x43\x0d\x97\x10\x2c". "\x70\xef\x9e\xb2\xc9\xdf\xe6\xa7\x9b\x08\x79\xa3\xf7\x99\xf5". "\x59\xe4\xd5\x89\x10\xe5\xc9\xf7\xe7\x29\x72\x06\xc6\x54\xc3". "\xcd\xd0\xff\x69\xf8\xdf\x19\xf2\x66\x1c\x69\x40\xbc\x97\xf1". "\x49\x5e\x78\x62\x52\x46\x7f\xcf\x44\x50\x8b\x5f\xe7\xa8\xeb". "\xd5\x84\x24\x81\xc0\x2c\x65\xf7\x95\xbd\xf2\x8e\x43\xfb\x6a". "\x49\x3c\x6a\xe5\x2a\x39\xf0\xfa\x89\x59\x5f\x39\x75\xb4\x6f". "\x04\xf1\xe0\x2c\xcd\x77\x34\xec\x6b\x45\x16\xe3\x18\x24\x05". "\xb9\x68\xc1\x4e\x71\x4b\xff\x88\x18\xea\x0d\x56\x49\x55\xdf". "\xe5\xb0\x59\xdb\x74\x9e\x0b\x38\x03\x9f\x10\x6f\xd9\x34\x07". "\x44\x29\x08\xb1\xd4\x77\xc6\x84\x0d\xbb\xb5\xd5\x09\x05\x19". "\x01\x62\x29\x45\x52\x1d\xc6\x4f\x25\x78\x7e\xbc\xae\x07\xb3". "\xd4\xe0\x19\x91\x03\xd6\x8d\x2f\x00\xc9\xb2\x66\x3b\x4e\x3d". "\x75\xf7\x23\x9a\x3e\xa4\xd5\x7f\x75\x47\xd0\xbc\xc3\xc8\x2a". "\xdc\x85\x09\x6c\x0c\x90\x38\xd8\xef\xcf\xf4\x7a\x1b\xc7\x76". "\xe0\xdb\x81\xa8\x1b\x2b\x8d\xd4\x36\x90\x76\xde\x8a\x90\xc8". "\x5b\x05\x00\xeb\xb3\x20\xce\x6e\x5c\xb9\x35\x3d\x95\x3a\x79". "\x4a\x60\xeb\x23\x11\xfb\x90\x2d\xf6\xb7\x05\x4a\x43\x41\x79". "\x51\xaa\xe6\x90\x0a\x71\x87\x80\xbe\xb0\x89\x0f\xd3\x84\x19". "\xce\x6c\xf9\xbb\x1b\x15\x4d\x0f\x33\x65\xf7\x9e\x3a\xd9\x8c". "\x02\x43\xcf\xdf\xb2\x60\xc1\x4c\xe9\xa5\x3c\xaf\xfa\x41\x2d". "\xb9\x1f\x45\x32\xcb\x39\x2f\x94\xae\x44\x6d\x69\xc1\xc9\x57". "\x8c\xe5\xf4\xa4\x3a\xb6\x70\x61\xf9\xbb\x41\xdc\x78\xf0\xf7". "\xbf\xa8\x8e\xe3\x77\x51\xce\x25\x2f\xdf\x27\x6b\x07\x30\x9f". "\xce\xdb\x59\x58\xaa\xb2\x2e\xdc\x90\x92\x82\x55\xfe\x25\x36". "\x49\x7f\x6d\x2d\x39\x51\xef\x3d\xc8\xa3\x87\x0b\xe7\xf2\xac". "\x90\xa0\x1d\xd8\xc7\xea\x93\x53\x3b\x21\x84\x2e\x52\x6c\xfb". "\x4f\x31\xda\xd1\xea\x45\x3e\xdc\xeb\x52\x81\x8c\x2b\xf4\x2a". "\xbc\x01\xc4\xe7\x68\x36\x9c\xd5\x2d\xc1\x61\xcb\x9a\x5f\x18". "\x00\x6a\xc8\x9a\x4e\xfd\x31\x5b\xce\x90\x4e\x45\xff\x7f\xea". "\xb2\x26\xad\xc1\x3a\x21\xa9\xe8\x7c\x14\xae\x81\x1e\xbe\xa3". "\x6d\xda\x92\x1b\xeb\xf2\x69\x76\x3e\xf1\x2b\xf7\x1a\x45\xd5". "\xb3\x81\xb1\xbe\x80\x7f\x24\xba\x0e\xd5\x68\x34\x3f\x1a\x29". "\x15\x0e\xc2\x26\x62\x0c\xaa\xa9\x20\x4c\x61\x65\x49\x07\xbe". "\x69\xf4\xc9\xec\x2f\x1c\xfa\x59\x2e\x72\xc0\x17\xc5\x4c\xfa". "\xba\x2f\x64\xab\xa9\xb4\xcb\xdc\xcb\x25\x5f\xcf\x0c\x87\xcc". "\xf0\x36\x2b\xce\x81\x5a\x22\x85\xa0\x50\x50\x97\x8e\xda\x36". "\x80\x74\xb5\x1e\x02\x3f\xd7\xc8\x29\x11\xeb\x1d\x3d\x74\x9f". "\x26\x1a\xa4\x3d\xf9\x0e\xf0\x2d\x5c\xa9\x43\xbf\x51\x6c\x8d". "\xe6\x78\xe0\x67\x57\xf0\xc8\x0e\x97\x9c\x57\x23\x30\xac\x63". "\xdf\x46\x98\xa4\xaf\x4e\xa7\xe5\xac\x31\xbd\xeb\x6a\xa0\xb0". "\xe4\x94\x7e\x51\xf6\x89\x81\x3e\xab\x4f\x64\xb7\xc5\x51\x71". "\xcd\x74\x02\xa9\x02\x99\x5c\xab\x0e\x14\x47\x3b\x04\xc1\x9b". "\x59\x1a\x93\x92\x4c\x71\x20\x5f\x6e\xd3\xf3\xa7\x47\x1b\x39". "\x3e\x73\x69\xe2\xec\xcb\x52\xb3\x5c\x7a\x95\x25\x3f\x16\x98". "\x60\xa8\xa2\x5d\xc4\x5a\x67\xe4\x11\x06\x06\xf9\x7a\xb4\x14". "\xe0\xbc\x7b\x13\x1d\x0f\xf2\xca\x0b\xd4\xaa\x71\x35\x3e\xd6". "\x2e\x2e\x5d\x7b\x15\xc9\x23\x1a\xa9\x24\x31\x48\xd4\xcf\x4a". "\xf4\x32\x17\x9b\x1d\x4b\xfe\x49\x69\xd6\xc0\x8f\xb9\xdb\x72". "\x52\x2c\xe8\xf3\xc4\xfc\x46\xf5\xb8\x1b\x05\x06\xcf\xcc\x23". "\x34\xbf\x25\x6a\xea\x3c\xc7\x64\xd4\xd5\xb3\x67\xed\x24\x27". "\xd3\x67\xc1\xbd\x9f\x7b\x7d\x19\x04\x5c\xd1\x96\x7e\xa5\xc7". "\xbb\xb2\x84\x68\x98\x38\x11\x90\xfb\x62\x15\xfd\xe6\xb7\x24". "\x77\xb2\x78\xc7\x73\x91\xc9\x60\x1d\x91\x6d\x04\x2b\x41\xe9". "\xc9\xfa\xe4\x98\x54\x83\x9a\x6e\x76\x8c\x21\xf9\x91\x38\x1f". "\xdc\xfe\x13\x09\x30\xd7\x53\x63\x62\xba\xe3\x2c\x70\xd5\xfc". "\x78\x35\x36\x79\x5d\xb6\x0e\x35\x3d\x46\x87\xfb\xf5\x64\x1f". "\x3e\xfd\x2f\x1c\xbb\xed\x95\x2d\xd6\x63\xdc\xa7\x6a\x39\x8f". "\xbd\xcb\x79\x95\xe9\x45\xbf\xe4\x3e\x05\x55\x00\xdb\x33\x28". "\x3a\x6c\xe2\x35\xbb\xac\x70\x52\x2b\xac\x4e\x11\x44\x58\x16". "\x21\xb4\xae\x0d\x6a\xb9\xdc\x85\x5d\x90\x11\x26\x85\xdb\xc3". "\xf0\x38\x6f\x8a\xff\x12\xf0\xc9\x9e\xf0\xfc\xae\x94\x11\x4d". "\xce\x96\x29\x09\x6c\xf4\x2a\x6c\xda\x1e\x4c\x4a\xa2\x96\x5a". "\xef\xc6\x38\x5c\x60\xa2\x28\x13\x58\x73\x96\xde\x59\x2a\x57". "\x64\x6c\x14\x94\x8a\x2e\x8e\x21\x3f\xa2\x43\xde\xf6\x2d\x23". "\x74\x5c\xbd\x7a\x10\xdb\x17\xa8\x93\xd0\x74\x86\x9d\x33\x07". "\x48\xee\xac\x18\x6d\x64\x61\x7b\x61\x2b\xa4\xa2\xab\x99\x59". "\xbe\x19\xd7\x19\x41\x1e\x61\x87\xad\x40\x5b\x69\x8c\x32\xf5". "\xb6\x49\xbe\x1f\xad\xd8\x0f\x3e\xd9\x62\xac\x3a\x76\xde\x32". "\xa3\xb2\x41\x95\xad\x17\x23\xab\xa1\x37\x9c\xab\x73\x79\x70". "\xd6\x66\x0d\x6e\x4d\x8b\xa0\xac\xe3\x44\x1e\x0a\xee\xf0\x74". "\x64\xd8\x44\xd1\x6c\xa6\xd5\x36\x2e\xd9\x55\x6e\x90\x63\xb7". "\xf7\x8e\xc6\x28\xa3\x40\x00\x60\x9a\x3c\xfe\xff\x03\x30\x11". "\x18\x92\x2f\x5b\x23\xe1\x4e\x99\xe4\x82\xc9\x51\xe2\x15\x6a". "\x76\x5c\x67\xae\xa3\xa2\x9c\x85\x51\xe0\x44\x89\x63\xa5\x71". "\x99\xbc\x2d\x9c\xab\x9a\xfb\x20\x37\x58\xd6\x2d\x8b\x7d\x42". "\x13\x35\x44\x4c\x11\x97\x66\x27\x17\xac\x44\xe8\x6a\x03\x78". "\xa2\x88\xc6\x36\x71\x5a\x5a\x5a\x72\xa3\xe9\x72\x0c\x91\x31". "\xfc\xae\x7b\xa0\x75\x21\x0a\xc1\x4b\x95\xcb\xe3\xc2\xee\x03". "\x0f\xb8\xb2\x51\xc7\xc8\x9c\x8d\x6d\x3a\xe7\x4e\x2c\xaa\xeb". "\x5e\x49\x93\xe0\x8f\xa1\x54\x93\xe7\x7c\x5d\x31\xc7\x05\x00". "\x28\x14\x57\x47\xb3\x05\x2d\x17\x92\x28\x45\xee\x85\x3a\x59". "\xb6\xa6\x04\xc0\x5c\x07\x1f\xe6\x5b\x36\x53\x62\x82\x64\xd5". "\xb6\xf2\xf5\x67\x19\x11\xee\xd2\x70\xc5\x14\x63\xc1\x75\xe1". "\x24\xe5\x01\x59\x52\x7c\x88\x17\xb4\xe0\x15\xe9\x12\x05\xcd". "\x88\x7a\xd5\xea\x45\xc3\xbb\x65\xd4\xdd\x0d\xde\x36\x94\x98". "\x0d\x2c\xfb\x3c\x2f\x69\xd0\x28\xe2\x85\xd9\x27\xf3\x7a\xad". "\x50\x68\x96\x54\x5e\xeb\xbc\x2a\x74\xde\xf3\x4e\x8b\x27\x0a". "\xcf\x4c\x60\x40\xe8\xc5\x72\xab\x8c\xfd\xe9\xab\xff\x51\xe5". "\xd6\xea\x9e\x34\x73\xe1\xe6\xf8\x5b\xb1\x10\xf0\xf9\x2d\x23". "\x0e\xfe\xe5\xf4\x8d\xb6\x6d\x37\x14\xed\x54\x97\x92\x5c\x68". "\x40\x88\xf1\x43\x29\xef\x5e\x96\x77\xa2\xe8\x3c\xae\x7f\xb1". "\x99\x17\xa7\x0c\x6f\xe2\x43\x32\x9b\x14\x43\xf2\x15\x6b\x13". "\x10\x68\x56\x0b\xaa\x06\x2e\xc0\xf8\xde\x9e\x54\x9d\xba\xff". "\x76\x26\x6d\x5e\x9e\x88\x3a\x2b\x9b\x20\x43\xb9\x1a\x0e\x58". "\x65\xec\xdb\x9e\x97\xb8\xfb\x03\x6c\xb0\x7f\xa2\xf1\xf4\x27". "\x24\x21\x47\x51\x21\x40\x45\x28\x71\xf7\xa1\x6b\xbe\x0e\xc8". "\x3f\x9b\xda\x62\x9d\x73\xf7\x5f\x70\x6c\xba\x1e\xeb\x16\x5c". "\x2e\x44\x0a\x22\x02\x6c\xbe\xb9\x69\x93\xfd\xa5\x33\x26\x64". "\x24\x6c\xc2\x3d\x2f\xf3\xd1\x97\xde\x60\x43\x1c\x0d\x1b\x94". "\xb3\x48\x45\x7c\xd5\xd0\x71\x4d\xad\xbf\xa4\x0a\x22\x27\x04". "\x38\x84\x19\x66\x63\xf0\xf3\xfc\xb0\xf3\x1d\xea\xba\xb9\xe4". "\xe5\x80\xed\xe3\xf1\x78\x24\xc3\x25\x27\x71\x81\xc2\xec\x54". "\xed\xcc\x63\xf7\x39\xcd\x83\xdf\x32\x88\xc0\x3b\xd4\x62\xb8". "\xea\x34\xd8\xcf\xbc\x3a\x89\x38\x64\x60\x44\xde\xb6\x76\x59". "\xb1\x95\x6a\x26\x08\xf0\xf4\x71\x25\x8b\xf8\x81\xdd\x0d\x2f". "\x8c\xe2\x70\xc2\x96\xc2\xd8\x9b\xe4\x3f\xec\x8b\xfd\xbd\xc9". "\x36\x33\xb7\xbc\x59\x37\x19\x09\x30\x5e\xef\x67\xae\x67\x48". "\x72\x0b\xf4\x2a\x82\xff\xcb\xd7\xd9\x9d\x6d\x7c\xa6\x20\x42". "\x50\x2b\x0a\x2f\x45\x99\x5b\x76\x6d\x99\x39\xa9\xb6\x32\x06". "\x11\xf8\x19\xd1\x3f\xc0\xd6\x1f\x67\xfa\xd5\xae\x7a\x71\x8c". "\xbc\x3d\xb4\x5f\x5c\x81\x7c\xa1\x39\x70\x0a\x17\x24\xb7\x22". "\x86\x50\xd8\x1f\xc8\x6c\x59\x9a\xdc\xf0\x71\x01\xda\xd8\x53". "\x98\x1c\x73\x36\xf1\x09\x86\xc9\xa7\x26\x25\xc0\x03\x3e\x13". "\x4e\x29\xeb\xf0\x8d\xe3\x38\x03\x54\xee\x37\xfb\x51\x2e\xb4". "\xf6\x12\x1f\xb2\x8c\x66\x75\x00\x30\x5b\xef\x59\xf9\x63\xa9". "\x74\x07\x91\xe4\x9c\xb7\xc9\x89\xd9\xa9\x51\x93\xcb\xb1\xa7". "\x64\x08\x79\x8f\xb4\x6d\x09\xd7\xc5\xbf\x0a\xdb\x50\xe0\x1c". "\x83\xca\xf8\xcf\xa7\x81\xbb\x0b\xe6\xcf\x1b\x0e\x0a\xe0\xcd". "\x68\xe2\xde\xc4\x2d\xba\x55\xc7\xc7\x1e\x6c\x5e\xca\x9b\x20". "\x75\x96\x94\x92\x84\xec\xf5\x22\x25\x78\x67\xcd\xbe\x01\xfe". "\x53\xa5\xcc\x6a\x40\x33\x83\xa4\x7a\x44\x93\x0b\xf9\x4c\xb2". "\x95\xb6\x7e\x4b\xa4\xc8\x86\xfe\x8a\xf1\x77\x40\x56\x13\xc1". "\x31\x2c\x8c\x4a\xa8\x89\x61\x0c\x39\x33\x78\x8c\xd5\x50\x3b". "\x89\xc3\xd3\x80\x1c\xa7\xb6\x36\xc2\x00\x8d\x0a\x7f\xcc\xd3". "\x20\x74\x60\x70\x36\x7d\xda\xdc\xc4\x49\x04\xf0\xe6\x6c\xd1". "\xbe\xcb\xfb\xf1\xa2\xd6\xd4\xe4\x97\x3f\x35\x09\x5b\xda\x06". "\x6b\x6d\x86\x53\x23\x0c\x26\x51\x2a\x15\xaa\xe2\x73\xfb\xc7". "\x41\x54\xdc\x5d\x99\x0b\x0a\x1e\xd4\xdb\x70\xa3\x8e\xfd\x5b". "\xf0\xa8\x3e\x9b\xff\x57\x98\xbc\xd9\x2a\x56\xd3\x19\xf9\x0b". "\xd9\x67\x0f\x10\x9c\x23\xe5\x6b\x12\xc6\xb6\x4b\xd1\x0c\xe9". "\x45\x36\xdf\x54\x6f\xcc\xfe\xb5\xcc\xb9\xfe\xde\xc8\xb5\xc9". "\x04\x59\x61\x75\x1e\x72\x37\x54\xfd\xc6\xc3\x7e\x74\xae\x55". "\x31\x6a\xbc\x8a\xd8\x45\x91\xe2\x8d\x20\x97\x71\xe7\x55\xd6". "\x8a\xb8\x82\x2a\x27\x4f\xdc\x53\x89\x28\xf7\x3a\xfe\x07\xef". "\x60\xb2\x32\x7c\xbc\x13\xc4\x3d\xda\xd7\xfb\xb8\x61\x7d\x69". "\xae\x0e\x9a\x71\xd6\x00\x26\x97\xff\xdb\xe6\xbe\x45\x7a\xb5". "\x00\x31\xfd\x70\xcc\xd7\x34\x88\xe4\x05\x61\xf5\x72\x1d\x14". "\xf0\x7e\x90\xdb\x0e\xc7\xda\xd4\xf3\x99\xd4\x60\xd9\xa7\xc8". "\x5b\x33\x34\xb5\x23\x74\x2c\x5f\x6b\x56\x95\x9c\x1b\x2a\xac". "\xf9\xfe\x46\xc3\xf1\x9b\x24\x7e\x4b\xca\x25\x58\x41\x10\x63". "\xe8\xe7\x68\xda\xcc\xb6\x4d\x5b\x8f\xc9\xa9\x31\xeb\x5c\x2a". "\xcf\x9d\x89\xd5\x51\x93\x80\x30\xf4\xc9\x2c\x8c\xb8\x8c\x62". "\xd6\x33\xbd\x95\x9f\xfa\x19\xf2\x48\x28\x09\x73\xc9\x53\x61". "\x94\x3a\x62\x68\x6c\xc6\xd6\x0a\xb4\xae\x27\x96\xfb\x29\xd7". "\x46\x67\x11\x7a\xe8\x3a\x9a\x3f\xf4\x9a\x75\xed\x24\x67\x45". "\x79\xdc\x8b\x19\xf2\xef\x57\xaa\xc7\x84\xff\x9d\x2d\xc3\xa8". "\x85\x54\xb7\x9d\xe1\xd6\x2b\xe9\x31\x9d\x6c\xb8\x4e\x76\x50". "\x80\x44\x46\x8f\x5e\x7e\x20\xaa\xa0\x8a\x36\x6b\xef\xd1\x75". "\xf8\x3f\x20\xdd\x09\x73\xbf\xa5\xf7\xb4\x87\xb2\x44\xc0\x0f". "\x10\xc0\x95\x2e\x8a\x42\xfa\xc3\x49\x17\xb9\xb5\x1a\xc3\x80". "\x93\x0c\xd8\xe3\xcd\xa4\x38\x61\x7a\x22\x73\x8e\x32\x8f\x55". "\x9c\x91\x08\xd9\x65\xa9\x02\x28\xc6\x59\xc8\x51\x32\x20\x48". "\xea\x2c\xae\x0e\xa6\x35\x5b\xe2\x63\xf9\xf2\x9d\x5f\xe3\x45". "\xdc\x41\xba\xfb\x40\xcc\x8d\xde\x6c\x3d\x50\x97\x9d\x83\xa0". "\xda\x41\x61\xba\xaf\xf8\x74\xd2\x21\x7b\x09\xcc\x83\xe1\x08". "\x01\x04\x42\xce\xcb\xec\x1d\x6b\xb7\x6f\x0f\x4b\xd4\x53\x90". "\x55\x3b\xcf\x9f\x93\xb8\xad\xce\x5f\x13\x83\xb3\x89\x6f\x5a". "\x1b\xa4\xf5\x95\x4b\xb4\x22\x22\x1d\x35\xaa\xfa\xc7\x14\x8c". "\xcd\x50\x66\x14\x47\xff\x67\xb2\xf8\x12\x09\xb3\x8a\xe5\x7d". "\xb8\xc9\xe4\x89\xf7\xa4\xb5\x70\xfa\x2d\xeb\x95\x89\xec\xbb". "\x49\x59\xd2\xc1\x6d\x0e\x06\xe4\x5e\xd5\x13\x13\x0d\x72\x6e". "\xf0\x6d\xa9\xd5\xe7\x54\x68\x35\xcd\xd0\xd5\xa6\xe5\xb2\xe4". "\xb1\x19\xe4\xf1\xe3\x8a\x56\x4c\x3b\x3d\xb8\x03\xfe\x22\x2f". "\xc6\xdc\x88\x7b\xca\x5c\xc6\xdd\x17\x34\x08\x22\xf0\x17\x61". "\x0e\x60\x9c\xb4\x27\x57\x30\x6e\xb8\x4f\xdd\x25\x7b\xef\x9e". "\x8e\x88\x6b\xd8\x10\x23\xc2\x44\x53\x73\x64\x8f\x40\x22\xe1". "\xe8\xa2\xb0\x3f\x8a\x07\x66\xcd\x64\x4f\x9c\x1e\x89\x76\x04". "\x6d\xab\xc2\xbb\x16\x85\x80\x01\xa5\xb1\xe2\x12\x04\x2e\x39". "\x87\x8c\xee\xbc\xfb\x07\x6d\x03\x4c\x3a\xa5\x7b\x95\xd9\xd7". "\xd6\xee\x2b\xe9\xcb\xe6\xec\xa8\x84\x6a\x42\xf9\xb2\x25\xc8". "\xf3\x6a\xaa\x34\x3b\xd9\x72\xd9\x70\x81\x3b\xd4\x5e\x66\x97". "\x1b\xe6\x2b\x88\x71\x82\xa3\x8a\x98\xb0\x16\xd9\xbb\x97\x8b". "\x57\x79\x41\x56\x6e\xc2\x8f\xdf\xfa\x5b\xc7\x68\x5b\xb8\x09". "\x41\x31\x7c\x19\xe1\x95\x2e\x05\x4c\xac\x38\x81\xda\xb3\x8b". "\x3e\x1c\x79\x9a\x31\xac\x3e\x3d\x6d\xab\xf3\x5a\x5e\xc7\x6e". "\x8e\x39\xcd\x7b\x6f\x62\xee\xb9\x73\xdd\x82\x42\x6f\x09\xe4". "\xc3\xae\x92\xe8\x18\x99\xa0\x5e\xa2\x12\xf4\xe2\xe0\xe6\x95". "\x58\x3a\x45\xad\xfe\x23\x79\x5f\x82\xce\x95\x88\x73\xeb\x46". "\xc8\x00\xac\xc3\x2a\xdc\x7e\xab\x9b\xf8\xbb\x46\x5c\xa8\x46". "\xbc\xfd\x99\xae\x4c\xa7\x77\xeb\x7c\x58\xbf\xbb\x52\x68\x62". "\x3d\x0b\x79\x64\x38\x65\xa7\xcb\x7b\xe9\xb2\x33\xb5\x59\x52". "\x7b\x17\xb4\x02\x2b\x07\x0d\x3a\x11\x57\x92\xa5\x22\x2b\xbc". "\xe6\x97\x05\x12\x05\xe7\x91\xe3\xfa\xae\x15\xbe\x20\xe5\x5c". "\x71\x24\x80\x85\xc9\x66\xc1\x53\x5c\x8f\x08\xd4\x52\xe1\x10". "\xb6\xd6\x20\x08\x01\x79\x33\x9f\x1b\xbd\xa0\xab\x7c\xb1\xd9". "\xdc\xca\x44\x22\x49\xb7\xb7\x3d\x84\xac\x92\xf4\xfa\x0a\xc9". "\xc5\xb2\x42\x2b\x9a\x63\xbb\x8a\x82\x04\x2f\xf7\xe9\x30\x05". "\x67\x32\xd1\x41\x1a\x69\x6e\xb9\xf8\x5f\x6d\xb7\xe5\x4e\x85". "\x21\xfa\x16\x8a\x44\xfd\xf6\xd9\xa2\x5f\x68\x2b\xf3\xe2\x3c". "\x8a\x69\xd2\xc1\x38\xed\x83\xef\x0d\x53\x86\x93\x32\x23\xc6". "\x14\x0c\xb0\xb6\x6e\x77\xa4\x20\x0f\xb1\x6e\xe2\xce\xca\x6f". "\x93\x1c\x3a\x8f\xd0\xd2\x5a\x6e\x30\xd6\x8e\x5f\x4b\xa5\xef". "\xa9\x62\xeb\x28\xa0\x5e\x3f\xc1\xbc\x0a\x68\xab\xd7\xfa\xa2". "\xb7\x8f\x12\xb0\x99\xbc\x93\x20\xb8\x95\x8d\xca\xc7\xa7\xd9". "\x2e\x19\xac\x06\xb9\x4e\x56\x8e\x74\xef\x2a\x04\xd8\x75\x04". "\x38\x2a\xc7\xa0\xa4\x89\xf3\xa4\x8a\xd4\x2c\x2c\x58\x6f\x00". "\x03\x23\xb8\xaf\x02\x48\x7d\x50\x46\x6f\x5a\x08\x41\xe3\x56". "\x6d\xcb\xe2\x4f\xea\x8e\xab\x74\xcd\xf9\xef\xcf\xf9\x1e\xf1". "\xf8\xb9\x6c\xaa\x3b\x37\xd1\x21\x42\x67\xec\xd6\x44\x55\x33". "\xe8\x1d\xa4\x18\xf3\x73\x82\xb4\x50\x59\xc2\x34\x36\x05\xeb"; $expect = Filesystem::readFile(dirname(__FILE__).'/base85/expect2.txt'); $this->assertBase85($expect, $data, pht('Random Data')); } private function assertBase85($expect, $data, $label) { $modes = array( '32bit', ); // If this is a 64-bit machine, we can also test 64-bit mode. $has_64bit = (PHP_INT_SIZE >= 8); if ($has_64bit) { $modes[] = '64bit'; } foreach ($modes as $mode) { $this->assertEqual( $expect, ArcanistBundle::newBase85Data($data, "\n", $mode), pht('base85/%s: %s', $mode, $label)); } } } diff --git a/src/parser/xhpast/api/__tests__/XHPASTNodeTestCase.php b/src/parser/xhpast/api/__tests__/XHPASTNodeTestCase.php index 8e4ecbb1..4047d739 100644 --- a/src/parser/xhpast/api/__tests__/XHPASTNodeTestCase.php +++ b/src/parser/xhpast/api/__tests__/XHPASTNodeTestCase.php @@ -1,101 +1,107 @@ assertExecutable('xhpast'); + $this->assertStringVariables(array(), '""'); $this->assertStringVariables(array(2 => 'abc'), '"$abc"'); $this->assertStringVariables(array(), '"\$abc"'); $this->assertStringVariables(array(2 => 'a'), '"$a[1]"'); $this->assertStringVariables(array(3 => 'a'), '"{$a[1]}"'); $this->assertStringVariables(array(2 => 'a', 5 => 'a'), '"$a $a"'); $this->assertStringVariables(array(), "''"); $this->assertStringVariables(array(), "'\$a'"); $this->assertStringVariables(array(), "<<assertStringVariables(array(8 => 'a'), "<<assertStringVariables(array(), "<<<'EOT'\n\$a\nEOT"); } private function assertStringVariables($expected, $string) { + $this->assertExecutable('xhpast'); + $statement = XHPASTTree::newStatementFromString($string); $this->assertEqual( $expected, $statement->getChildByIndex(0)->getStringVariables(), $string); } public function testGetNamespace() { + $this->assertExecutable('xhpast'); + $dir = dirname(__FILE__).'/namespace/'; $files = id(new FileFinder($dir)) ->withType('f') ->withSuffix('php.test') ->find(); foreach ($files as $file) { list($tree, $expect) = $this->readTestData($dir.'/'.$file); $root = $tree->getRootNode(); $classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION'); foreach ($classes as $class) { $id = (string)$class->getID(); if (idx($expect, $id, false) === false) { throw new Exception( pht( 'No expected value for node %d in file "%s".', $class->getID(), $file)); } $this->assertEqual( $expect[$id], $class->getNamespace()); } } } /** * Reads and parses test data from a specified file. * * This method reads and parses test data from a file. The file is expected * to have the following structure * * ``` * The first element of the pair is the * `XHPASTTree` contained within the test file. * The second element of the pair is the * "expect" data. */ private function readTestData($file) { $contents = Filesystem::readFile($file); $contents = preg_split('/^~{10}$/m', $contents); if (count($contents) < 2) { throw new Exception( pht( "Expected '%s' separating test case and results.", '~~~~~~~~~~')); } list($data, $expect) = $contents; $tree = XHPASTTree::newFromData($data); $expect = phutil_json_decode($expect); return array($tree, $expect); } } diff --git a/src/parser/xhpast/api/__tests__/XHPASTTreeTestCase.php b/src/parser/xhpast/api/__tests__/XHPASTTreeTestCase.php index dfea2c14..c7cacd72 100644 --- a/src/parser/xhpast/api/__tests__/XHPASTTreeTestCase.php +++ b/src/parser/xhpast/api/__tests__/XHPASTTreeTestCase.php @@ -1,140 +1,142 @@ assertExecutable('xhpast'); + $this->assertEval(1, '1'); $this->assertEval('a', '"a"'); $this->assertEval(-1.1, '-1.1'); $this->assertEval( array('foo', 'bar', -1, +2, -3.4, +4.3, 1e10, 1e-5, -2.3e7), "array('foo', 'bar', -1, +2, -3.4, +4.3, 1e10, 1e-5, -2.3e7)"); $this->assertEval( array(), 'array()'); $this->assertEval( array(42 => 7, 'a' => 5, 1, 2, 3, 4, 1 => 'goo'), "array(42 => 7, 'a' => 5, 1, 2, 3, 4, 1 => 'goo')"); $this->assertEval( array('a' => 'a', 'b' => array(1, 2, array(3))), "array('a' => 'a', 'b' => array(1, 2, array(3)))"); $this->assertEval( array(true, false, null), 'array(true, false, null)'); // Duplicate keys $this->assertEval( array(0 => '1', 0 => '2'), "array(0 => '1', 0 => '2')"); $this->assertEval('simple string', "'simple string'"); $this->assertEval('42', "'42'"); $this->assertEval('binary string', "b'binary string'"); $this->assertEval(3.1415926, '3.1415926'); $this->assertEval(42, '42'); $this->assertEval( array(2147483648, 2147483647, -2147483648, -2147483647), 'array(2147483648, 2147483647, -2147483648, -2147483647)'); $this->assertEval(INF, 'INF'); $this->assertEval(-INF, '-INF'); $this->assertEval(0x1b, '0x1b'); $this->assertEval(0X0A, '0X0A'); // Octal $this->assertEval(010, '010'); // TODO: this passes on < PHP 7 for some reason but fails on PHP 7 correctly //$this->assertEval(080, '080'); // Invalid! // Leading 0, but float, not octal. $this->assertEval(0.11e1, '0.11e1'); $this->assertEval(0e1, '0e1'); $this->assertEval(0, '0'); // Static evaluation treats '$' as a literal dollar glyph. $this->assertEval('$asdf', '"$asdf"'); $this->assertEval( '\a\b\c\d\e\f\g\h\i\j\k\l\m\n\o\p\q\r\s\t\u\v\w\x\y\z'. '\1\2\3\4\5\6\7\8\9\0'. '\!\@\#\$\%\^\&\*\(\)'. '\`\~\\\|\[\]\{\}\<\>\,\.\/\?\:\;\-\_\=\+', "'\\a\\b\\c\\d\\e\\f\\g\\h\\i\\j\\k\\l\\m\\n\\o\\p\\q". "\\r\\s\\t\\u\\v\\w\\x\\y\\z". "\\1\\2\\3\\4\\5\\6\\7\\8\\9\\0". "\\!\\@\\#\\$\\%\\^\\&\\*\\(\\)". "\\`\\~\\\\\\|\\[\\]\\{\\}\\<\\>\\,\\.\\/\\?\\:\\;\\-\\_\\=\\+". "'"); // After PHP 5.4.0, "\e" means "escape", not "backslash e". We implement the // newer rules, but if we're running in an older version of PHP we can not // express them with "\e". $this->assertEval(chr(27), '"\\e"'); $this->assertEval( "\a\b\c\d\x1B\f\g\h\i\j\k\l\m\n\o\p\q\r\s\t\u\v\w\x\y\z". "\1\2\3\4\5\6\7\8\9\0". "\!\@\#\$\%\^\&\*\(\)". "\`\~\\\|\[\]\{\}\<\>\,\.\/\?\:\;\-\_\=\+", '"\\a\\b\\c\\d\\e\\f\\g\\h\\i\\j\\k\\l\\m\\n\\o\\p\\q'. '\\r\\s\\t\\u\\v\\w\\x\\y\\z'. '\\1\\2\\3\\4\\5\\6\\7\\8\\9\\0'. '\\!\\@\\#\\$\\%\\^\\&\\*\\(\\)'. '\\`\\~\\\\\\|\\[\\]\\{\\}\\<\\>\\,\\.\\/\\?\\:\\;\\-\\_\\=\\+"'); $this->assertEval( '\' "', "'\\' \"'"); $this->assertEval( '\\ \\\\ ', '\'\\\\ \\\\\\\\ \''); $this->assertEval( '\ \\ ', "'\\ \\\\ '"); $this->assertEval( '\x92', '\'\x92\''); $this->assertEval( "\x92", '"\x92"'); $this->assertEval( "\x", '"\x"'); $this->assertEval( "\x1", '"\x1"'); $this->assertEval( "\x000 !", '"\x000 !"'); $this->assertEval( "\x0", '"\x0"'); $this->assertEval( "\xg", '"\xg"'); } private function assertEval($value, $string) { $this->assertEqual( $value, XHPASTTree::newStatementFromString($string)->evalStatic(), $string); } } diff --git a/src/parser/xhpast/bin/PhutilXHPASTBinary.php b/src/parser/xhpast/bin/PhutilXHPASTBinary.php index 9a8274c3..3fed4430 100644 --- a/src/parser/xhpast/bin/PhutilXHPASTBinary.php +++ b/src/parser/xhpast/bin/PhutilXHPASTBinary.php @@ -1,134 +1,134 @@ write($data); return $future; } /** * Returns the path to the XHPAST binary. * * @return string */ public static function getPath() { if (phutil_is_windows()) { return dirname(__FILE__).'\\xhpast.exe'; } return dirname(__FILE__).'/xhpast'; } /** * Returns the XHPAST version. * * @return string */ public static function getVersion() { if (self::$version === null) { $bin = self::getPath(); if (Filesystem::pathExists($bin)) { list($err, $stdout) = exec_manual('%s --version', $bin); if (!$err) { self::$version = trim($stdout); } } } return self::$version; } /** * Checks if XHPAST is built and up-to-date. * * @return bool */ public static function isAvailable() { return self::getVersion() == self::EXPECTED_VERSION; } } diff --git a/src/phage/__tests__/PhageAgentTestCase.php b/src/phage/__tests__/PhageAgentTestCase.php index 4973081a..1e0c43b6 100644 --- a/src/phage/__tests__/PhageAgentTestCase.php +++ b/src/phage/__tests__/PhageAgentTestCase.php @@ -1,49 +1,53 @@ assertSkipped(pht('Phage does not target Windows.')); + } + return $this->runBootloaderTests(new PhagePHPAgentBootloader()); } private function runBootloaderTests(PhageAgentBootloader $boot) { $name = get_class($boot); $exec = new ExecFuture('%C', $boot->getBootCommand()); $exec->write($boot->getBootSequence(), $keep_open = true); $exec_channel = new PhutilExecChannel($exec); $agent = new PhutilJSONProtocolChannel($exec_channel); $agent->write( array( 'type' => 'EXEC', 'key' => 1, 'command' => 'echo phage', 'timeout' => null, )); $this->agentExpect( $agent, array( 'type' => 'RSLV', 'key' => 1, 'err' => 0, 'stdout' => "phage\n", 'stderr' => '', 'timeout' => false, ), pht("'%s' for %s", 'echo phage', $name)); $agent->write( array( 'type' => 'EXIT', )); } private function agentExpect(PhutilChannel $agent, $expect, $what) { $message = $agent->waitForMessage(); $this->assertEqual($expect, $message, $what); } } diff --git a/src/repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php b/src/repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php index da0347bf..0c130a52 100644 --- a/src/repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php +++ b/src/repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php @@ -1,199 +1,195 @@ parseState('git_basic.git.tgz'); - $this->parseState('git_submodules_dirty.git.tgz'); - $this->parseState('git_submodules_staged.git.tgz'); - $this->parseState('git_spaces.git.tgz'); - } else { - $this->assertSkipped(pht('Git is not installed')); - } + $this->assertExecutable('git'); + + $this->parseState('git_basic.git.tgz'); + $this->parseState('git_submodules_dirty.git.tgz'); + $this->parseState('git_submodules_staged.git.tgz'); + $this->parseState('git_spaces.git.tgz'); } public function testHgStateParsing() { - if (Filesystem::binaryExists('hg')) { - $this->parseState('hg_basic.hg.tgz'); - } else { - $this->assertSkipped(pht('Mercurial is not installed')); - } + $this->assertExecutable('hg'); + + $this->parseState('hg_basic.hg.tgz'); } public function testSvnStateParsing() { - if (Filesystem::binaryExists('svn')) { - $this->parseState('svn_basic.svn.tgz'); - } else { - $this->assertSkipped(pht('Subversion is not installed')); - } + $this->assertExecutable('svn'); + + $this->parseState('svn_basic.svn.tgz'); } private function parseState($test) { + $this->assertExecutable('tar'); + $dir = dirname(__FILE__).'/state/'; $fixture = PhutilDirectoryFixture::newFromArchive($dir.'/'.$test); $fixture_path = $fixture->getPath(); $working_copy = ArcanistWorkingCopy::newFromWorkingDirectory($fixture_path); $api = $working_copy->newRepositoryAPI(); $api->setBaseCommitArgumentRules('arc:this'); if ($api instanceof ArcanistSubversionAPI) { // Upgrade the repository so that the test will still pass if the local // `svn` is newer than the `svn` which created the repository. // NOTE: Some versions of Subversion (1.7.x?) exit with an error code on // a no-op upgrade, although newer versions do not. We just ignore the // error here; if it's because of an actual problem we'll hit an error // shortly anyway. $api->execManualLocal('upgrade'); } $this->assertCorrectState($test, $api); } private function assertCorrectState($test, ArcanistRepositoryAPI $api) { if ($api instanceof ArcanistGitAPI) { $version = $api->getGitVersion(); if (version_compare($version, '2.11.0', '<')) { // Behavior differs slightly on older versions of git; rather than code // both variants, skip the tests in the presence of such a git. $this->assertSkipped(pht('Behavior differs slightly on git < 2.11.0')); return; } } $f_mod = ArcanistRepositoryAPI::FLAG_MODIFIED; $f_add = ArcanistRepositoryAPI::FLAG_ADDED; $f_del = ArcanistRepositoryAPI::FLAG_DELETED; $f_unt = ArcanistRepositoryAPI::FLAG_UNTRACKED; $f_con = ArcanistRepositoryAPI::FLAG_CONFLICT; $f_mis = ArcanistRepositoryAPI::FLAG_MISSING; $f_uns = ArcanistRepositoryAPI::FLAG_UNSTAGED; $f_unc = ArcanistRepositoryAPI::FLAG_UNCOMMITTED; $f_ext = ArcanistRepositoryAPI::FLAG_EXTERNALS; $f_obs = ArcanistRepositoryAPI::FLAG_OBSTRUCTED; $f_inc = ArcanistRepositoryAPI::FLAG_INCOMPLETE; switch ($test) { case 'svn_basic.svn.tgz': $expect_uncommitted = array( 'ADDED' => $f_add, 'COPIED_TO' => $f_add, 'DELETED' => $f_del, 'MODIFIED' => $f_mod, 'MOVED' => $f_del, 'MOVED_TO' => $f_add, 'PROPCHANGE' => $f_mod, 'UNTRACKED' => $f_unt, ); $this->assertEqual($expect_uncommitted, $api->getUncommittedStatus()); $expect_range = array(); $this->assertEqual($expect_range, $api->getCommitRangeStatus()); $expect_working = array( 'ADDED' => $f_add, 'COPIED_TO' => $f_add, 'DELETED' => $f_del, 'MODIFIED' => $f_mod, 'MOVED' => $f_del, 'MOVED_TO' => $f_add, 'PROPCHANGE' => $f_mod, 'UNTRACKED' => $f_unt, ); $this->assertEqual($expect_working, $api->getWorkingCopyStatus()); break; case 'git_basic.git.tgz': $expect_uncommitted = array( 'UNCOMMITTED' => $f_add | $f_unc, 'UNSTAGED' => $f_mod | $f_uns | $f_unc, 'UNTRACKED' => $f_unt, ); $this->assertEqual($expect_uncommitted, $api->getUncommittedStatus()); $expect_range = array( 'ADDED' => $f_add, 'DELETED' => $f_del, 'MODIFIED' => $f_mod, 'UNSTAGED' => $f_add, ); $this->assertEqual($expect_range, $api->getCommitRangeStatus()); $expect_working = array( 'ADDED' => $f_add, 'DELETED' => $f_del, 'MODIFIED' => $f_mod, 'UNCOMMITTED' => $f_add | $f_unc, 'UNSTAGED' => $f_add | $f_mod | $f_uns | $f_unc, 'UNTRACKED' => $f_unt, ); $this->assertEqual($expect_working, $api->getWorkingCopyStatus()); break; case 'git_submodules_dirty.git.tgz': $expect_uncommitted = array( '.gitmodules' => $f_mod | $f_uns | $f_unc, 'added/' => $f_unt, 'deleted' => $f_del | $f_uns | $f_unc, 'modified-commit' => $f_mod | $f_uns | $f_unc, 'modified-commit-dirty' => $f_ext | $f_mod | $f_uns | $f_unc, 'modified-dirty' => $f_ext | $f_mod | $f_uns | $f_unc, ); $this->assertEqual($expect_uncommitted, $api->getUncommittedStatus()); break; case 'git_submodules_staged.git.tgz': $expect_uncommitted = array( '.gitmodules' => $f_mod | $f_unc, 'added' => $f_add | $f_unc, 'deleted' => $f_del | $f_unc, 'modified-commit' => $f_mod | $f_unc, 'modified-commit-dirty' => $f_ext | $f_mod | $f_uns | $f_unc, 'modified-dirty' => $f_ext | $f_mod | $f_uns | $f_unc, ); $this->assertEqual($expect_uncommitted, $api->getUncommittedStatus()); break; case 'git_spaces.git.tgz': $expect_working = array( 'SPACES ADDED' => $f_add, 'SPACES DELETED' => $f_del, 'SPACES MODIFIED' => $f_mod, 'SPACES UNCOMMITTED' => $f_add | $f_unc, 'SPACES UNSTAGED' => $f_add | $f_mod | $f_uns | $f_unc, 'SPACES UNTRACKED' => $f_unt, ); $this->assertEqual($expect_working, $api->getWorkingCopyStatus()); break; case 'hg_basic.hg.tgz': $expect_uncommitted = array( 'UNCOMMITTED' => $f_mod | $f_unc, 'UNTRACKED' => $f_unt, ); $this->assertEqual($expect_uncommitted, $api->getUncommittedStatus()); $expect_range = array( 'ADDED' => $f_add, 'DELETED' => $f_del, 'MODIFIED' => $f_mod, 'UNCOMMITTED' => $f_add, ); $this->assertEqual($expect_range, $api->getCommitRangeStatus()); $expect_working = array( 'ADDED' => $f_add, 'DELETED' => $f_del, 'MODIFIED' => $f_mod, 'UNCOMMITTED' => $f_add | $f_mod | $f_unc, 'UNTRACKED' => $f_unt, ); $this->assertEqual($expect_working, $api->getWorkingCopyStatus()); break; default: throw new Exception( pht("No test cases for working copy '%s'!", $test)); } } } diff --git a/src/unit/engine/phutil/PhutilTestCase.php b/src/unit/engine/phutil/PhutilTestCase.php index 48279ac9..0df40bfc 100644 --- a/src/unit/engine/phutil/PhutilTestCase.php +++ b/src/unit/engine/phutil/PhutilTestCase.php @@ -1,754 +1,798 @@ assertions++; return; } $this->failAssertionWithExpectedValue('false', $result, $message); } /** * Assert that a value is `true`, strictly. The test fails if it is not. * * @param wild The empirically derived value, generated by executing the * test. * @param string A human-readable description of what these values represent, * and particularly of what a discrepancy means. * * @return void * @task assert */ final protected function assertTrue($result, $message = null) { if ($result === true) { $this->assertions++; return; } $this->failAssertionWithExpectedValue('true', $result, $message); } /** * Assert that two values are equal, strictly. The test fails if they are not. * * NOTE: This method uses PHP's strict equality test operator (`===`) to * compare values. This means values and types must be equal, key order must * be identical in arrays, and objects must be referentially identical. * * @param wild The theoretically expected value, generated by careful * reasoning about the properties of the system. * @param wild The empirically derived value, generated by executing the * test. * @param string A human-readable description of what these values represent, * and particularly of what a discrepancy means. * * @return void * @task assert */ final protected function assertEqual($expect, $result, $message = null) { if ($expect === $result) { $this->assertions++; return; } $expect = PhutilReadableSerializer::printableValue($expect); $result = PhutilReadableSerializer::printableValue($result); $caller = self::getCallerInfo(); $file = $caller['file']; $line = $caller['line']; if ($message !== null) { $output = pht( 'Assertion failed, expected values to be equal (at %s:%d): %s', $file, $line, $message); } else { $output = pht( 'Assertion failed, expected values to be equal (at %s:%d).', $file, $line); } $output .= "\n"; + static $have_diff; + if ($have_diff === null) { + $have_diff = Filesystem::binaryExists('diff'); + } + if (strpos($expect, "\n") === false && strpos($result, "\n") === false) { $output .= pht("Expected: %s\n Actual: %s", $expect, $result); - } else { + } else if ($have_diff) { $output .= pht( "Expected vs Actual Output Diff\n%s", ArcanistDiffUtils::renderDifferences( $expect, $result, $lines = 0xFFFF)); + } else { + // On systems without `diff`, including Windows, just show the raw + // values instead of using `diff` to compare them. + $output .= "EXPECTED\n{$expect}\n\nACTUAL\n{$result}\n"; } $this->failTest($output); throw new PhutilTestTerminatedException($output); } /** * Assert an unconditional failure. This is just a convenience method that * better indicates intent than using dummy values with assertEqual(). This * causes test failure. * * @param string Human-readable description of the reason for test failure. * @return void * @task assert */ final protected function assertFailure($message) { $this->failTest($message); throw new PhutilTestTerminatedException($message); } /** * End this test by asserting that the test should be skipped for some * reason. * * @param string Reason for skipping this test. * @return void * @task assert */ final protected function assertSkipped($message) { $this->skipTest($message); throw new PhutilTestSkippedException($message); } /* -( Exception Handling )------------------------------------------------- */ /** * This simplest way to assert exceptions are thrown. * * @param exception The expected exception. * @param callable The thing which throws the exception. * * @return void * @task exceptions */ final protected function assertException( $expected_exception_class, $callable) { $this->tryTestCases( array('assertException' => array()), array(false), $callable, $expected_exception_class); } /** * Straightforward method for writing unit tests which check if some block of * code throws an exception. For example, this allows you to test the * exception behavior of ##is_a_fruit()## on various inputs: * * public function testFruit() { * $this->tryTestCases( * array( * 'apple is a fruit' => new Apple(), * 'rock is not a fruit' => new Rock(), * ), * array( * true, * false, * ), * array($this, 'tryIsAFruit'), * 'NotAFruitException'); * } * * protected function tryIsAFruit($input) { * is_a_fruit($input); * } * * @param map Map of test case labels to test case inputs. * @param list List of expected results, true to indicate that the case * is expected to succeed and false to indicate that the case * is expected to throw. * @param callable Callback to invoke for each test case. * @param string Optional exception class to catch, defaults to * 'Exception'. * @return void * @task exceptions */ final protected function tryTestCases( array $inputs, array $expect, $callable, $exception_class = 'Exception') { if (count($inputs) !== count($expect)) { $this->assertFailure( pht('Input and expectations must have the same number of values.')); } $labels = array_keys($inputs); $inputs = array_values($inputs); $expecting = array_values($expect); foreach ($inputs as $idx => $input) { $expect = $expecting[$idx]; $label = $labels[$idx]; $caught = null; try { call_user_func($callable, $input); } catch (Exception $ex) { if ($ex instanceof PhutilTestTerminatedException) { throw $ex; } if (!($ex instanceof $exception_class)) { throw $ex; } $caught = $ex; } $actual = !($caught instanceof Exception); if ($expect === $actual) { if ($expect) { $message = pht("Test case '%s' did not throw, as expected.", $label); } else { $message = pht("Test case '%s' threw, as expected.", $label); } } else { if ($expect) { $message = pht( "Test case '%s' was expected to succeed, but it ". "raised an exception of class %s with message: %s", $label, get_class($ex), $ex->getMessage()); } else { $message = pht( "Test case '%s' was expected to raise an ". "exception, but it did not throw anything.", $label); } } $this->assertEqual($expect, $actual, $message); } } /** * Convenience wrapper around @{method:tryTestCases} for cases where your * inputs are scalar. For example: * * public function testFruit() { * $this->tryTestCaseMap( * array( * 'apple' => true, * 'rock' => false, * ), * array($this, 'tryIsAFruit'), * 'NotAFruitException'); * } * * protected function tryIsAFruit($input) { * is_a_fruit($input); * } * * For cases where your inputs are not scalar, use @{method:tryTestCases}. * * @param map Map of scalar test inputs to expected success (true * expects success, false expects an exception). * @param callable Callback to invoke for each test case. * @param string Optional exception class to catch, defaults to * 'Exception'. * @return void * @task exceptions */ final protected function tryTestCaseMap( array $map, $callable, $exception_class = 'Exception') { return $this->tryTestCases( array_fuse(array_keys($map)), array_values($map), $callable, $exception_class); } /* -( Hooks for Setup and Teardown )--------------------------------------- */ /** * This hook is invoked once, before any tests in this class are run. It * gives you an opportunity to perform setup steps for the entire class. * * @return void * @task hook */ protected function willRunTests() { return; } /** * This hook is invoked once, after any tests in this class are run. It gives * you an opportunity to perform teardown steps for the entire class. * * @return void * @task hook */ protected function didRunTests() { return; } /** * This hook is invoked once per test, before the test method is invoked. * * @param string Method name of the test which will be invoked. * @return void * @task hook */ protected function willRunOneTest($test_method_name) { return; } /** * This hook is invoked once per test, after the test method is invoked. * * @param string Method name of the test which was invoked. * @return void * @task hook */ protected function didRunOneTest($test_method_name) { return; } /** * This hook is invoked once, before any test cases execute. It gives you * an opportunity to perform setup steps for the entire suite of test cases. * * @param list List of test cases to be run. * @return void * @task hook */ public function willRunTestCases(array $test_cases) { return; } /** * This hook is invoked once, after all test cases execute. * * @param list List of test cases that ran. * @return void * @task hook */ public function didRunTestCases(array $test_cases) { return; } /* -( Internals )---------------------------------------------------------- */ /** * Construct a new test case. This method is ##final##, use willRunTests() to * provide test-wide setup logic. * * @task internal */ final public function __construct() {} /** * Mark the currently-running test as a failure. * * @param string Human-readable description of problems. * @return void * * @task internal */ final private function failTest($reason) { $this->resultTest(ArcanistUnitTestResult::RESULT_FAIL, $reason); } /** * This was a triumph. I'm making a note here: HUGE SUCCESS. * * @param string Human-readable overstatement of satisfaction. * @return void * * @task internal */ final private function passTest($reason) { $this->resultTest(ArcanistUnitTestResult::RESULT_PASS, $reason); } /** * Mark the current running test as skipped. * * @param string Description for why this test was skipped. * @return void * @task internal */ final private function skipTest($reason) { $this->resultTest(ArcanistUnitTestResult::RESULT_SKIP, $reason); } final private function resultTest($test_result, $reason) { $coverage = $this->endCoverage(); $result = new ArcanistUnitTestResult(); $result->setCoverage($coverage); $result->setNamespace(get_class($this)); $result->setName($this->runningTest); $result->setLink($this->getLink($this->runningTest)); $result->setResult($test_result); $result->setDuration(microtime(true) - $this->testStartTime); $result->setUserData($reason); $this->results[] = $result; if ($this->renderer) { echo $this->renderer->renderUnitResult($result); } } /** * Execute the tests in this test case. You should not call this directly; * use @{class:PhutilUnitTestEngine} to orchestrate test execution. * * @return void * @task internal */ final public function run() { $this->results = array(); $reflection = new ReflectionClass($this); $methods = $reflection->getMethods(); // Try to ensure that poorly-written tests which depend on execution order // (and are thus not properly isolated) will fail. shuffle($methods); $this->willRunTests(); foreach ($methods as $method) { $name = $method->getName(); if (preg_match('/^test/', $name)) { $this->runningTest = $name; $this->assertions = 0; $this->testStartTime = microtime(true); try { $this->willRunOneTest($name); $this->beginCoverage(); $exceptions = array(); try { call_user_func_array( array($this, $name), array()); $this->passTest( pht( '%s assertion(s) passed.', new PhutilNumber($this->assertions))); } catch (Exception $ex) { $exceptions['Execution'] = $ex; } try { $this->didRunOneTest($name); } catch (Exception $ex) { $exceptions['Shutdown'] = $ex; } if ($exceptions) { if (count($exceptions) == 1) { throw head($exceptions); } else { throw new PhutilAggregateException( pht('Multiple exceptions were raised during test execution.'), $exceptions); } } if (!$this->assertions) { $this->failTest( pht( 'This test case made no assertions. Test cases must make at '. 'least one assertion.')); } } catch (PhutilTestTerminatedException $ex) { // Continue with the next test. } catch (PhutilTestSkippedException $ex) { // Continue with the next test. } catch (Exception $ex) { $ex_class = get_class($ex); $ex_message = $ex->getMessage(); $ex_trace = $ex->getTraceAsString(); $message = sprintf( "%s (%s): %s\n%s", pht('EXCEPTION'), $ex_class, $ex_message, $ex_trace); $this->failTest($message); } } } $this->didRunTests(); return $this->results; } final public function setEnableCoverage($enable_coverage) { $this->enableCoverage = $enable_coverage; return $this; } /** * @phutil-external-symbol function xdebug_start_code_coverage */ final private function beginCoverage() { if (!$this->enableCoverage) { return; } $this->assertCoverageAvailable(); xdebug_start_code_coverage(XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE); } /** * @phutil-external-symbol function xdebug_get_code_coverage * @phutil-external-symbol function xdebug_stop_code_coverage */ final private function endCoverage() { if (!$this->enableCoverage) { return; } $result = xdebug_get_code_coverage(); xdebug_stop_code_coverage($cleanup = false); $coverage = array(); foreach ($result as $file => $report) { $project_root = $this->getProjectRoot(); if (strncmp($file, $project_root, strlen($project_root))) { continue; } $max = max(array_keys($report)); $str = ''; for ($ii = 1; $ii <= $max; $ii++) { $c = null; if (isset($report[$ii])) { $c = $report[$ii]; } if ($c === -1) { $str .= 'U'; // Un-covered. } else if ($c === -2) { // TODO: This indicates "unreachable", but it flags the closing braces // of functions which end in "return", which is super ridiculous. Just // ignore it for now. // // See http://bugs.xdebug.org/view.php?id=1041 $str .= 'N'; // Not executable. } else if ($c === 1) { $str .= 'C'; // Covered. } else { $str .= 'N'; // Not executable. } } $coverage[substr($file, strlen($project_root) + 1)] = $str; } // Only keep coverage information for files modified by the change. In // the case of --everything, we won't have paths, so just return all the // coverage data. if ($this->paths) { $coverage = array_select_keys($coverage, $this->paths); } return $coverage; } final private function assertCoverageAvailable() { if (!function_exists('xdebug_start_code_coverage')) { throw new Exception( pht("You've enabled code coverage but XDebug is not installed.")); } } final public function getWorkingCopy() { return $this->workingCopy; } final public function setWorkingCopy( ArcanistWorkingCopyIdentity $working_copy) { $this->workingCopy = $working_copy; return $this; } final public function getProjectRoot() { $working_copy = $this->getWorkingCopy(); if (!$working_copy) { throw new PhutilInvalidStateException('setWorkingCopy'); } return $working_copy->getProjectRoot(); } final public function setPaths(array $paths) { $this->paths = $paths; return $this; } final protected function getLink($method) { // TOOLSETS: Restore this. return null; $base_uri = $this ->getWorkingCopy() ->getProjectConfig('phabricator.uri'); $uri = id(new PhutilURI($base_uri)) ->setPath("/diffusion/symbol/{$method}/") ->setQueryParam('context', get_class($this)) ->setQueryParam('jump', 'true') ->setQueryParam('lang', 'php'); return (string)$uri; } final public function setRenderer(ArcanistUnitRenderer $renderer) { $this->renderer = $renderer; return $this; } /** * Returns info about the caller function. * * @return map */ final private static function getCallerInfo() { $callee = array(); $caller = array(); $seen = false; foreach (array_slice(debug_backtrace(), 1) as $location) { $function = idx($location, 'function'); if (!$seen && preg_match('/^assert[A-Z]/', $function)) { $seen = true; $caller = $location; } else if ($seen && !preg_match('/^assert[A-Z]/', $function)) { $callee = $location; break; } } return array( 'file' => basename(idx($caller, 'file')), 'line' => idx($caller, 'line'), 'function' => idx($callee, 'function'), 'class' => idx($callee, 'class'), 'object' => idx($caller, 'object'), 'type' => idx($callee, 'type'), 'args' => idx($caller, 'args'), ); } /** * Fail an assertion which checks that some result is equal to a specific * value, like 'true' or 'false'. This prints a readable error message and * fails the current test. * * This method throws and does not return. * * @param string Human readable description of the expected value. * @param string The actual value. * @param string|null Optional assertion message. * @return void * @task internal */ private function failAssertionWithExpectedValue( $expect_description, $actual_result, $message) { $caller = self::getCallerInfo(); $file = $caller['file']; $line = $caller['line']; if ($message !== null) { $description = pht( "Assertion failed, expected '%s' (at %s:%d): %s", $expect_description, $file, $line, $message); } else { $description = pht( "Assertion failed, expected '%s' (at %s:%d).", $expect_description, $file, $line); } $actual_result = PhutilReadableSerializer::printableValue($actual_result); $header = pht('ACTUAL VALUE'); $output = $description."\n\n".$header."\n".$actual_result; $this->failTest($output); throw new PhutilTestTerminatedException($output); } + final protected function assertExecutable($binary) { + if (!isset(self::$executables[$binary])) { + switch ($binary) { + case 'xhpast': + $ok = true; + if (!PhutilXHPASTBinary::isAvailable()) { + try { + PhutilXHPASTBinary::build(); + } catch (Exception $ex) { + $ok = false; + } + } + break; + default: + $ok = Filesystem::binaryExists($binary); + break; + } + + self::$executables[$binary] = $ok; + } + + if (!self::$executables[$binary]) { + $this->assertSkipped( + pht('Required executable "%s" is not available.', $binary)); + } + } + + final protected function getSupportExecutable($executable) { + $root = dirname(phutil_get_library_root('arcanist')); + return $root.'/support/unit/'.$executable.'.php'; + } + + } diff --git a/src/xsprintf/__tests__/PhutilCsprintfTestCase.php b/src/xsprintf/__tests__/PhutilCsprintfTestCase.php index 05a26c72..b7cbc18b 100644 --- a/src/xsprintf/__tests__/PhutilCsprintfTestCase.php +++ b/src/xsprintf/__tests__/PhutilCsprintfTestCase.php @@ -1,96 +1,107 @@ true, // For arguments which have any characters which are not safe in some // context, %R should apply standard escaping. 'a b' => false, 'http://domain.com/path/' => true, 'svn+ssh://domain.com/path/' => true, '`rm -rf`' => false, '$VALUE' => false, ); foreach ($inputs as $input => $expect_same) { $actual = (string)csprintf('%R', $input); if ($expect_same) { $this->assertEqual($input, $actual); } else { $this->assertFalse($input === $actual); } } } public function testPowershell() { $cmd = csprintf('%s', "\n"); $cmd->setEscapingMode(PhutilCommandString::MODE_POWERSHELL); $this->assertEqual( '"`n"', (string)$cmd); } public function testNoPowershell() { - if (!phutil_is_windows()) { - $cmd = csprintf('%s', '#'); - $cmd->setEscapingMode(PhutilCommandString::MODE_DEFAULT); - - $this->assertEqual( - '\'#\'', - (string)$cmd); + if (phutil_is_windows()) { + // TOOLSETS: Restructure this. We must skip because tests fail if they + // do not make any assertions. + $this->assertSkipped( + pht( + 'This test can not currently run under Windows.')); } + + $cmd = csprintf('%s', '#'); + $cmd->setEscapingMode(PhutilCommandString::MODE_DEFAULT); + + $this->assertEqual( + '\'#\'', + (string)$cmd); } public function testPasswords() { + $bin = $this->getSupportExecutable('echo'); + // Normal "%s" doesn't do anything special. - $command = csprintf('echo %s', 'hunter2trustno1'); + $command = csprintf('php -f %R -- %s', $bin, 'hunter2trustno1'); $this->assertTrue(strpos($command, 'hunter2trustno1') !== false); // "%P" takes a PhutilOpaqueEnvelope. $caught = null; try { - csprintf('echo %P', 'hunter2trustno1'); + csprintf('php -f %R -- %P', $bin, 'hunter2trustno1'); } catch (Exception $ex) { $caught = $ex; } $this->assertTrue($caught instanceof InvalidArgumentException); // "%P" masks the provided value. - $command = csprintf('echo %P', new PhutilOpaqueEnvelope('hunter2trustno1')); + $command = csprintf( + 'php -f %R -- %P', + $bin, + new PhutilOpaqueEnvelope('hunter2trustno1')); $this->assertFalse(strpos($command, 'hunter2trustno1')); // Executing the command works as expected. list($out) = execx('%C', $command); $this->assertTrue(strpos($out, 'hunter2trustno1') !== false); } public function testEscapingIsRobust() { if (phutil_is_windows()) { $this->assertSkipped(pht("This test doesn't work on Windows.")); } // Escaping should be robust even when used to escape commands which take // other commands. list($out) = execx( 'sh -c %s', csprintf( 'sh -c %s', csprintf( 'sh -c %s', csprintf( 'echo %P', new PhutilOpaqueEnvelope('!@#$%^&*()'))))); $this->assertTrue(strpos($out, '!@#$%^&*()') !== false); } } diff --git a/support/unit/cat.php b/support/unit/cat.php new file mode 100755 index 00000000..9ce8cac5 --- /dev/null +++ b/support/unit/cat.php @@ -0,0 +1 @@ + $arg) { + $args[$key] = addcslashes($arg, "\\\n"); +} +$args = implode($args, "\n"); +echo $args; diff --git a/support/unit/exit.php b/support/unit/exit.php new file mode 100755 index 00000000..69610125 --- /dev/null +++ b/support/unit/exit.php @@ -0,0 +1 @@ +\n"; + exit(1); +} + +// NOTE: Sleep for the requested duration even if our actual sleep() call is +// interrupted by a signal. + +$then = microtime(true) + (double)$argv[1]; +while (true) { + $now = microtime(true); + if ($now >= $then) { + break; + } + + $sleep = max(1, ($then - $now)); + usleep((int)($sleep * 1000000)); +}