diff --git a/.gitignore b/.gitignore index 53eeb426..096c30cb 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,6 @@ # This is an OS X build artifact. /support/xhpast/xhpast.dSYM + +# Generated shell completion rulesets. +/support/shell/rules/ diff --git a/resources/shell/bash-completion b/resources/shell/bash-completion deleted file mode 100644 index 0ba9ef48..00000000 --- a/resources/shell/bash-completion +++ /dev/null @@ -1,26 +0,0 @@ -if [[ -n ${ZSH_VERSION-} ]]; then - autoload -U +X bashcompinit && bashcompinit -fi - -_arc () -{ - CUR="${COMP_WORDS[COMP_CWORD]}" - COMPREPLY=() - OPTS=$(echo | arc shell-complete --current ${COMP_CWORD} -- ${COMP_WORDS[@]}) - - if [ $? -ne 0 ]; then - return $? - fi - - if [ "$OPTS" = "FILE" ]; then - COMPREPLY=( $(compgen -f -- ${CUR}) ) - return 0 - fi - - if [ "$OPTS" = "ARGUMENT" ]; then - return 0 - fi - - COMPREPLY=( $(compgen -W "${OPTS}" -- ${CUR}) ) -} -complete -F _arc -o filenames arc diff --git a/scripts/__init_script__.php b/scripts/__init_script__.php index f6cdcc31..98de4ef9 100644 --- a/scripts/__init_script__.php +++ b/scripts/__init_script__.php @@ -1,3 +1,3 @@ 99): + display_deaths = 99 + + status.addstr('%s ' % display_deaths, curses.A_BOLD) status.addch(curses.ACS_LTEE) if Block.killed == Block.total: diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 2f9a3798..fe1e4178 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -232,7 +232,6 @@ phutil_register_library_map(array( 'ArcanistJSHintLinter' => 'lint/linter/ArcanistJSHintLinter.php', 'ArcanistJSHintLinterTestCase' => 'lint/linter/__tests__/ArcanistJSHintLinterTestCase.php', 'ArcanistJSONLintLinter' => 'lint/linter/ArcanistJSONLintLinter.php', - 'ArcanistJSONLintLinterTestCase' => 'lint/linter/__tests__/ArcanistJSONLintLinterTestCase.php', 'ArcanistJSONLintRenderer' => 'lint/renderer/ArcanistJSONLintRenderer.php', 'ArcanistJSONLinter' => 'lint/linter/ArcanistJSONLinter.php', 'ArcanistJSONLinterTestCase' => 'lint/linter/__tests__/ArcanistJSONLinterTestCase.php', @@ -1130,7 +1129,6 @@ phutil_register_library_map(array( 'ArcanistJSHintLinter' => 'ArcanistExternalLinter', 'ArcanistJSHintLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistJSONLintLinter' => 'ArcanistExternalLinter', - 'ArcanistJSONLintLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistJSONLintRenderer' => 'ArcanistLintRenderer', 'ArcanistJSONLinter' => 'ArcanistLinter', 'ArcanistJSONLinterTestCase' => 'ArcanistLinterTestCase', diff --git a/src/channel/__tests__/PhutilPHPObjectProtocolChannelTestCase.php b/src/channel/__tests__/PhutilPHPObjectProtocolChannelTestCase.php index c9373918..29999d53 100644 --- a/src/channel/__tests__/PhutilPHPObjectProtocolChannelTestCase.php +++ b/src/channel/__tests__/PhutilPHPObjectProtocolChannelTestCase.php @@ -45,7 +45,8 @@ final class PhutilPHPObjectProtocolChannelTestCase extends PhutilTestCase { } 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); diff --git a/src/error/__tests__/PhutilOpaqueEnvelopeTestCase.php b/src/error/__tests__/PhutilOpaqueEnvelopeTestCase.php index 2f6c06b2..ba416ce6 100644 --- a/src/error/__tests__/PhutilOpaqueEnvelopeTestCase.php +++ b/src/error/__tests__/PhutilOpaqueEnvelopeTestCase.php @@ -8,9 +8,13 @@ final class PhutilOpaqueEnvelopeTestCase extends PhutilTestCase { // the diff itself, and thus this source code. Since we look for the secret // in traces later on, split it apart here so that invocation via // "arc diff" doesn't create a false test failure. - $secret = 'hunter'.'2'; + // Also split apart this "signpost" value which we are not going to put in + // an envelope. We expect to be able to find it in the argument lists in + // stack traces, and don't want a false positive. + $signpost = 'shaman'.'3'; + $envelope = new PhutilOpaqueEnvelope($secret); $this->assertFalse(strpos(var_export($envelope, true), $secret)); @@ -24,23 +28,34 @@ final class PhutilOpaqueEnvelopeTestCase extends PhutilTestCase { $this->assertFalse(strpos($dump, $secret)); try { - $this->throwTrace($envelope); + $this->throwTrace($envelope, $signpost); } catch (Exception $ex) { $trace = $ex->getTrace(); - $this->assertFalse(strpos(print_r($trace, true), $secret)); + + // NOTE: The entire trace may be very large and contain complex + // recursive datastructures. Look at only the last few frames: we expect + // to see the signpost value but not the secret. + $trace = array_slice($trace, 0, 2); + $trace = print_r($trace, true); + + $this->assertTrue(strpos($trace, $signpost) !== false); + $this->assertFalse(strpos($trace, $secret)); } - $backtrace = $this->getBacktrace($envelope); + $backtrace = $this->getBacktrace($envelope, $signpost); + $backtrace = array_slice($backtrace, 0, 2); + + $this->assertTrue(strpos($trace, $signpost) !== false); $this->assertFalse(strpos(print_r($backtrace, true), $secret)); $this->assertEqual($secret, $envelope->openEnvelope()); } - private function throwTrace($v) { + private function throwTrace($v, $w) { throw new Exception('!'); } - private function getBacktrace($v) { + private function getBacktrace($v, $w) { return debug_backtrace(); } diff --git a/src/filesystem/Filesystem.php b/src/filesystem/Filesystem.php index a88ed12a..475ae0c1 100644 --- a/src/filesystem/Filesystem.php +++ b/src/filesystem/Filesystem.php @@ -488,9 +488,8 @@ final class Filesystem extends Phobject { pht( '%s requires the PHP OpenSSL extension to be installed and enabled '. 'to access an entropy source. On Windows, this extension is usually '. - 'installed but not enabled by default. Enable it in your "s".', - __METHOD__.'()', - 'php.ini')); + 'installed but not enabled by default. Enable it in your "php.ini".', + __METHOD__.'()')); } throw new Exception( @@ -950,8 +949,23 @@ final class Filesystem extends Phobject { // 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); @@ -1104,6 +1118,18 @@ final class Filesystem extends Phobject { 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 )------------------------------------------------------------- */ diff --git a/src/filesystem/__tests__/FileFinderTestCase.php b/src/filesystem/__tests__/FileFinderTestCase.php index f7214bd1..d0a7459f 100644 --- a/src/filesystem/__tests__/FileFinderTestCase.php +++ b/src/filesystem/__tests__/FileFinderTestCase.php @@ -125,6 +125,16 @@ final class FileFinderTestCase extends PhutilTestCase { } 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. @@ -211,6 +221,7 @@ final class FileFinderTestCase extends PhutilTestCase { 'php', 'shell', ); + foreach ($modes as $mode) { $actual = id(clone $finder) ->setForceMode($mode) diff --git a/src/filesystem/__tests__/FilesystemTestCase.php b/src/filesystem/__tests__/FilesystemTestCase.php index b04de456..a3aff733 100644 --- a/src/filesystem/__tests__/FilesystemTestCase.php +++ b/src/filesystem/__tests__/FilesystemTestCase.php @@ -127,6 +127,12 @@ final class FilesystemTestCase extends PhutilTestCase { 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)); diff --git a/src/filesystem/__tests__/PhutilFileLockTestCase.php b/src/filesystem/__tests__/PhutilFileLockTestCase.php index 9dcc2c30..6d91205f 100644 --- a/src/filesystem/__tests__/PhutilFileLockTestCase.php +++ b/src/filesystem/__tests__/PhutilFileLockTestCase.php @@ -170,16 +170,20 @@ final class PhutilFileLockTestCase extends PhutilTestCase { 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/test/lock-file.php'; + private function buildLockFuture(/* ... */) { + $argv = func_get_args(); + $bin = $this->getSupportExecutable('lock'); - $flags = (array)$flags; + 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 -f %R -- %Ls %R', $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..90d40737 100644 --- a/src/filesystem/linesofalarge/__tests__/LinesOfALargeExecFutureTestCase.php +++ b/src/filesystem/linesofalarge/__tests__/LinesOfALargeExecFutureTestCase.php @@ -43,7 +43,9 @@ final class LinesOfALargeExecFutureTestCase extends PhutilTestCase { } 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(); 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 @@ -3,8 +3,10 @@ final class FutureIteratorTestCase extends PhutilTestCase { public function testAddingFuture() { - $future1 = new ExecFuture('cat'); - $future2 = new ExecFuture('cat'); + $bin = $this->getSupportExecutable('cat'); + + $future1 = new ExecFuture('php -f %R', $bin); + $future2 = new ExecFuture('php -f %R', $bin); $iterator = new FutureIterator(array($future1)); $iterator->limit(2); diff --git a/src/future/exec/ExecFuture.php b/src/future/exec/ExecFuture.php index 5e0f0062..2eb41ebd 100644 --- a/src/future/exec/ExecFuture.php +++ b/src/future/exec/ExecFuture.php @@ -42,7 +42,6 @@ final class ExecFuture extends PhutilExecutableFuture { private $profilerCallID; private $killedByTimeout; - private $useWindowsFileStreams = false; private $windowsStdoutTempFile = null; private $windowsStderrTempFile = null; @@ -181,21 +180,6 @@ final class ExecFuture extends PhutilExecutableFuture { } - /** - * Set whether to use non-blocking streams on Windows. - * - * @param bool Whether to use non-blocking streams. - * @return this - * @task config - */ - public function setUseWindowsFileStreams($use_streams) { - if (phutil_is_windows()) { - $this->useWindowsFileStreams = $use_streams; - } - return $this; - } - - /* -( Interacting With Commands )------------------------------------------ */ @@ -587,6 +571,7 @@ final class ExecFuture extends PhutilExecutableFuture { // classes are always available. if (!$this->pipes) { + $is_windows = phutil_is_windows(); // NOTE: See note above about Phage. if (class_exists('PhutilServiceProfiler')) { @@ -610,18 +595,6 @@ final class ExecFuture extends PhutilExecutableFuture { $pipes = array(); - if (phutil_is_windows()) { - // See T4395. proc_open under Windows uses "cmd /C [cmd]", which will - // strip the first and last quote when there aren't exactly two quotes - // (and some other conditions as well). This results in a command that - // looks like `command" "path to my file" "something something` which is - // clearly wrong. By surrounding the command string with quotes we can - // be sure this process is harmless. - if (strpos($unmasked_command, '"') !== false) { - $unmasked_command = '"'.$unmasked_command.'"'; - } - } - if ($this->hasEnv()) { $env = $this->getEnv(); } else { @@ -638,21 +611,31 @@ final class ExecFuture extends PhutilExecutableFuture { } $spec = self::$descriptorSpec; - if ($this->useWindowsFileStreams) { - $this->windowsStdoutTempFile = new TempFile(); - $this->windowsStderrTempFile = new TempFile(); + if ($is_windows) { + $stdout_file = new TempFile(); + $stderr_file = new TempFile(); + + $stdout_handle = fopen($stdout_file, 'wb'); + if (!$stdout_handle) { + throw new Exception( + pht( + 'Unable to open stdout temporary file ("%s") for writing.', + $stdout_file)); + } + + $stderr_handle = fopen($stderr_file, 'wb'); + if (!$stderr_handle) { + throw new Exception( + pht( + 'Unable to open stderr temporary file ("%s") for writing.', + $stderr_file)); + } $spec = array( - 0 => self::$descriptorSpec[0], // stdin - 1 => fopen($this->windowsStdoutTempFile, 'wb'), // stdout - 2 => fopen($this->windowsStderrTempFile, 'wb'), // stderr + 0 => self::$descriptorSpec[0], + 1 => $stdout_handle, + 2 => $stderr_handle, ); - - if (!$spec[1] || !$spec[2]) { - throw new Exception(pht( - 'Unable to create temporary files for '. - 'Windows stdout / stderr streams')); - } } $proc = @proc_open( @@ -660,23 +643,10 @@ final class ExecFuture extends PhutilExecutableFuture { $spec, $pipes, $cwd, - $env); - - if ($this->useWindowsFileStreams) { - fclose($spec[1]); - fclose($spec[2]); - $pipes = array( - 0 => head($pipes), // stdin - 1 => fopen($this->windowsStdoutTempFile, 'rb'), // stdout - 2 => fopen($this->windowsStderrTempFile, 'rb'), // stderr - ); - - if (!$pipes[1] || !$pipes[2]) { - throw new Exception(pht( - 'Unable to open temporary files for '. - 'reading Windows stdout / stderr streams')); - } - } + $env, + array( + 'bypass_shell' => true, + )); if ($trap) { $err = $trap->getErrorsAsString(); @@ -685,12 +655,56 @@ final class ExecFuture extends PhutilExecutableFuture { $err = error_get_last(); } + if ($is_windows) { + fclose($stdout_handle); + fclose($stderr_handle); + } + if (!is_resource($proc)) { - throw new Exception( + // When you run an invalid command on a Linux system, the "proc_open()" + // works and then the process (really a "/bin/sh -c ...") exits after + // it fails to resolve the command. + + // When you run an invalid command on a Windows system, we bypass the + // shell and the "proc_open()" itself fails. Throw a "CommandException" + // here for consistency with the Linux behavior in this common failure + // case. + + throw new CommandException( pht( - 'Failed to `%s`: %s', - 'proc_open()', - $err)); + 'Call to "proc_open()" to open a subprocess failed: %s', + $err), + $this->command, + 1, + '', + ''); + } + + if ($is_windows) { + $stdout_handle = fopen($stdout_file, 'rb'); + if (!$stdout_handle) { + throw new Exception( + pht( + 'Unable to open stdout temporary file ("%s") for reading.', + $stdout_file)); + } + + $stderr_handle = fopen($stderr_file, 'rb'); + if (!$stderr_handle) { + throw new Exception( + pht( + 'Unable to open stderr temporary file ("%s") for reading.', + $stderr_file)); + } + + $pipes = array( + 0 => $pipes[0], + 1 => $stdout_handle, + 2 => $stderr_handle, + ); + + $this->windowsStdoutTempFile = $stdout_file; + $this->windowsStderrTempFile = $stderr_file; } $this->pipes = $pipes; @@ -698,11 +712,11 @@ final class ExecFuture extends PhutilExecutableFuture { list($stdin, $stdout, $stderr) = $pipes; - if (!phutil_is_windows()) { + if (!$is_windows) { // On Windows, we redirect process standard output and standard error - // through temporary files, and then use stream_select to determine - // if there's more data to read. + // through temporary files. Files don't block, so we don't need to make + // these streams nonblocking. if ((!stream_set_blocking($stdout, false)) || (!stream_set_blocking($stderr, false)) || @@ -780,11 +794,6 @@ final class ExecFuture extends PhutilExecutableFuture { } if ($is_done) { - if ($this->useWindowsFileStreams) { - fclose($stdout); - fclose($stderr); - } - // If the subprocess got nuked with `kill -9`, we get a -1 exitcode. // Upgrade this to a slightly more informative value by examining the // terminating signal code. @@ -864,7 +873,10 @@ final class ExecFuture extends PhutilExecutableFuture { @proc_close($this->proc); $this->proc = null; } - $this->stdin = null; + $this->stdin = null; + + unset($this->windowsStdoutTempFile); + unset($this->windowsStderrTempFile); if ($this->profilerCallID !== null) { $profiler = PhutilServiceProfiler::getInstance(); 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 @@ -6,15 +6,27 @@ final class ExecFutureTestCase extends PhutilTestCase { // NOTE: This is mostly testing that we don't hang while doing an empty // write. - list($stdout) = id(new ExecFuture('cat'))->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) @@ -30,14 +42,14 @@ final class ExecFutureTestCase extends PhutilTestCase { // 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(); @@ -49,7 +61,7 @@ final class ExecFutureTestCase extends PhutilTestCase { // 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. @@ -66,7 +78,7 @@ final class ExecFutureTestCase extends PhutilTestCase { 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); @@ -76,7 +88,7 @@ final class ExecFutureTestCase extends PhutilTestCase { // 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); @@ -86,7 +98,7 @@ final class ExecFutureTestCase extends PhutilTestCase { 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) { @@ -100,8 +112,9 @@ final class ExecFutureTestCase extends PhutilTestCase { 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(); @@ -114,7 +127,7 @@ final class ExecFutureTestCase extends PhutilTestCase { $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 { @@ -131,7 +144,7 @@ final class ExecFutureTestCase extends PhutilTestCase { // 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. 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 @@ -8,7 +8,9 @@ final class ExecPassthruTestCase extends PhutilTestCase { // the terminal, which is undesirable). This makes crafting effective unit // tests a fairly involved process. - $exec = new PhutilExecPassthru('exit'); + $bin = $this->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..3ba41302 100644 --- a/src/future/oauth/__tests__/PhutilOAuth1FutureTestCase.php +++ b/src/future/oauth/__tests__/PhutilOAuth1FutureTestCase.php @@ -63,6 +63,10 @@ final class PhutilOAuth1FutureTestCase extends PhutilTestCase { } 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 diff --git a/src/internationalization/ArcanistUSEnglishTranslation.php b/src/internationalization/ArcanistUSEnglishTranslation.php index f8d01ee9..bfbc40c5 100644 --- a/src/internationalization/ArcanistUSEnglishTranslation.php +++ b/src/internationalization/ArcanistUSEnglishTranslation.php @@ -81,6 +81,11 @@ final class ArcanistUSEnglishTranslation extends PhutilTranslation { 'This commit will be landed:', 'These commits will be landed:', ), + + 'Updated %s librarie(s).' => array( + 'Updated library.', + 'Updated %s libraries.', + ), ); } diff --git a/src/lint/linter/__tests__/ArcanistJSONLintLinterTestCase.php b/src/lint/linter/__tests__/ArcanistJSONLintLinterTestCase.php deleted file mode 100644 index a81935e5..00000000 --- a/src/lint/linter/__tests__/ArcanistJSONLintLinterTestCase.php +++ /dev/null @@ -1,10 +0,0 @@ -executeTestsInDirectory(dirname(__FILE__).'/jsonlint/'); - } - -} diff --git a/src/lint/linter/__tests__/ArcanistJSONLinterTestCase.php b/src/lint/linter/__tests__/ArcanistJSONLinterTestCase.php index 5f8c12d4..a221d906 100644 --- a/src/lint/linter/__tests__/ArcanistJSONLinterTestCase.php +++ b/src/lint/linter/__tests__/ArcanistJSONLinterTestCase.php @@ -3,7 +3,7 @@ final class ArcanistJSONLinterTestCase extends ArcanistLinterTestCase { public function testLinter() { - $this->executeTestsInDirectory(dirname(__FILE__).'/jsonlint/'); + $this->executeTestsInDirectory(dirname(__FILE__).'/json/'); } } diff --git a/src/lint/linter/__tests__/jsonlint/1.lint-test b/src/lint/linter/__tests__/json/1.lint-test similarity index 100% rename from src/lint/linter/__tests__/jsonlint/1.lint-test rename to src/lint/linter/__tests__/json/1.lint-test diff --git a/src/lint/linter/__tests__/jsonlint/10.lint-test b/src/lint/linter/__tests__/json/10.lint-test similarity index 100% rename from src/lint/linter/__tests__/jsonlint/10.lint-test rename to src/lint/linter/__tests__/json/10.lint-test diff --git a/src/lint/linter/__tests__/jsonlint/11.lint-test b/src/lint/linter/__tests__/json/11.lint-test similarity index 100% rename from src/lint/linter/__tests__/jsonlint/11.lint-test rename to src/lint/linter/__tests__/json/11.lint-test diff --git a/src/lint/linter/__tests__/jsonlint/12.lint-test b/src/lint/linter/__tests__/json/12.lint-test similarity index 100% rename from src/lint/linter/__tests__/jsonlint/12.lint-test rename to src/lint/linter/__tests__/json/12.lint-test diff --git a/src/lint/linter/__tests__/jsonlint/13.lint-test b/src/lint/linter/__tests__/json/13.lint-test similarity index 100% rename from src/lint/linter/__tests__/jsonlint/13.lint-test rename to src/lint/linter/__tests__/json/13.lint-test diff --git a/src/lint/linter/__tests__/jsonlint/14.lint-test b/src/lint/linter/__tests__/json/14.lint-test similarity index 100% rename from src/lint/linter/__tests__/jsonlint/14.lint-test rename to src/lint/linter/__tests__/json/14.lint-test diff --git a/src/lint/linter/__tests__/jsonlint/15.lint-test b/src/lint/linter/__tests__/json/15.lint-test similarity index 100% rename from src/lint/linter/__tests__/jsonlint/15.lint-test rename to src/lint/linter/__tests__/json/15.lint-test diff --git a/src/lint/linter/__tests__/jsonlint/16.lint-test b/src/lint/linter/__tests__/json/16.lint-test similarity index 100% rename from src/lint/linter/__tests__/jsonlint/16.lint-test rename to src/lint/linter/__tests__/json/16.lint-test diff --git a/src/lint/linter/__tests__/jsonlint/17.lint-test b/src/lint/linter/__tests__/json/17.lint-test similarity index 100% rename from src/lint/linter/__tests__/jsonlint/17.lint-test rename to src/lint/linter/__tests__/json/17.lint-test diff --git a/src/lint/linter/__tests__/jsonlint/19.lint-test b/src/lint/linter/__tests__/json/19.lint-test similarity index 100% rename from src/lint/linter/__tests__/jsonlint/19.lint-test rename to src/lint/linter/__tests__/json/19.lint-test diff --git a/src/lint/linter/__tests__/jsonlint/2.lint-test b/src/lint/linter/__tests__/json/2.lint-test similarity index 100% rename from src/lint/linter/__tests__/jsonlint/2.lint-test rename to src/lint/linter/__tests__/json/2.lint-test diff --git a/src/lint/linter/__tests__/jsonlint/20.lint-test b/src/lint/linter/__tests__/json/20.lint-test similarity index 100% rename from src/lint/linter/__tests__/jsonlint/20.lint-test rename to src/lint/linter/__tests__/json/20.lint-test diff --git a/src/lint/linter/__tests__/jsonlint/21.lint-test b/src/lint/linter/__tests__/json/21.lint-test similarity index 100% rename from src/lint/linter/__tests__/jsonlint/21.lint-test rename to src/lint/linter/__tests__/json/21.lint-test diff --git a/src/lint/linter/__tests__/jsonlint/22.lint-test b/src/lint/linter/__tests__/json/22.lint-test similarity index 100% rename from src/lint/linter/__tests__/jsonlint/22.lint-test rename to src/lint/linter/__tests__/json/22.lint-test diff --git a/src/lint/linter/__tests__/jsonlint/23.lint-test b/src/lint/linter/__tests__/json/23.lint-test similarity index 100% rename from src/lint/linter/__tests__/jsonlint/23.lint-test rename to src/lint/linter/__tests__/json/23.lint-test diff --git a/src/lint/linter/__tests__/jsonlint/24.lint-test b/src/lint/linter/__tests__/json/24.lint-test similarity index 100% rename from src/lint/linter/__tests__/jsonlint/24.lint-test rename to src/lint/linter/__tests__/json/24.lint-test diff --git a/src/lint/linter/__tests__/jsonlint/25.lint-test b/src/lint/linter/__tests__/json/25.lint-test similarity index 100% rename from src/lint/linter/__tests__/jsonlint/25.lint-test rename to src/lint/linter/__tests__/json/25.lint-test diff --git a/src/lint/linter/__tests__/jsonlint/26.lint-test b/src/lint/linter/__tests__/json/26.lint-test similarity index 100% rename from src/lint/linter/__tests__/jsonlint/26.lint-test rename to src/lint/linter/__tests__/json/26.lint-test diff --git a/src/lint/linter/__tests__/jsonlint/27.lint-test b/src/lint/linter/__tests__/json/27.lint-test similarity index 100% rename from src/lint/linter/__tests__/jsonlint/27.lint-test rename to src/lint/linter/__tests__/json/27.lint-test diff --git a/src/lint/linter/__tests__/jsonlint/28.lint-test b/src/lint/linter/__tests__/json/28.lint-test similarity index 100% rename from src/lint/linter/__tests__/jsonlint/28.lint-test rename to src/lint/linter/__tests__/json/28.lint-test diff --git a/src/lint/linter/__tests__/jsonlint/29.lint-test b/src/lint/linter/__tests__/json/29.lint-test similarity index 100% rename from src/lint/linter/__tests__/jsonlint/29.lint-test rename to src/lint/linter/__tests__/json/29.lint-test diff --git a/src/lint/linter/__tests__/jsonlint/3.lint-test b/src/lint/linter/__tests__/json/3.lint-test similarity index 100% rename from src/lint/linter/__tests__/jsonlint/3.lint-test rename to src/lint/linter/__tests__/json/3.lint-test diff --git a/src/lint/linter/__tests__/jsonlint/30.lint-test b/src/lint/linter/__tests__/json/30.lint-test similarity index 100% rename from src/lint/linter/__tests__/jsonlint/30.lint-test rename to src/lint/linter/__tests__/json/30.lint-test diff --git a/src/lint/linter/__tests__/jsonlint/31.lint-test b/src/lint/linter/__tests__/json/31.lint-test similarity index 100% rename from src/lint/linter/__tests__/jsonlint/31.lint-test rename to src/lint/linter/__tests__/json/31.lint-test diff --git a/src/lint/linter/__tests__/jsonlint/32.lint-test b/src/lint/linter/__tests__/json/32.lint-test similarity index 100% rename from src/lint/linter/__tests__/jsonlint/32.lint-test rename to src/lint/linter/__tests__/json/32.lint-test diff --git a/src/lint/linter/__tests__/jsonlint/33.lint-test b/src/lint/linter/__tests__/json/33.lint-test similarity index 100% rename from src/lint/linter/__tests__/jsonlint/33.lint-test rename to src/lint/linter/__tests__/json/33.lint-test diff --git a/src/lint/linter/__tests__/jsonlint/34.lint-test b/src/lint/linter/__tests__/json/34.lint-test similarity index 100% rename from src/lint/linter/__tests__/jsonlint/34.lint-test rename to src/lint/linter/__tests__/json/34.lint-test diff --git a/src/lint/linter/__tests__/jsonlint/4.lint-test b/src/lint/linter/__tests__/json/4.lint-test similarity index 100% rename from src/lint/linter/__tests__/jsonlint/4.lint-test rename to src/lint/linter/__tests__/json/4.lint-test diff --git a/src/lint/linter/__tests__/jsonlint/5.lint-test b/src/lint/linter/__tests__/json/5.lint-test similarity index 100% rename from src/lint/linter/__tests__/jsonlint/5.lint-test rename to src/lint/linter/__tests__/json/5.lint-test diff --git a/src/lint/linter/__tests__/jsonlint/6.lint-test b/src/lint/linter/__tests__/json/6.lint-test similarity index 100% rename from src/lint/linter/__tests__/jsonlint/6.lint-test rename to src/lint/linter/__tests__/json/6.lint-test diff --git a/src/lint/linter/__tests__/jsonlint/7.lint-test b/src/lint/linter/__tests__/json/7.lint-test similarity index 100% rename from src/lint/linter/__tests__/jsonlint/7.lint-test rename to src/lint/linter/__tests__/json/7.lint-test diff --git a/src/lint/linter/__tests__/jsonlint/8.lint-test b/src/lint/linter/__tests__/json/8.lint-test similarity index 100% rename from src/lint/linter/__tests__/jsonlint/8.lint-test rename to src/lint/linter/__tests__/json/8.lint-test diff --git a/src/lint/linter/__tests__/jsonlint/9.lint-test b/src/lint/linter/__tests__/json/9.lint-test similarity index 100% rename from src/lint/linter/__tests__/jsonlint/9.lint-test rename to src/lint/linter/__tests__/json/9.lint-test 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 @@ -29,6 +29,8 @@ abstract class ArcanistXHPASTLinterRuleTestCase * @return ArcanistXHPASTLinterRule */ protected function getLinterRule() { + $this->assertExecutable('xhpast'); + $class = get_class($this); $matches = null; diff --git a/src/moduleutils/PhutilLibraryMapBuilder.php b/src/moduleutils/PhutilLibraryMapBuilder.php index 410ff8fc..c036ed4c 100644 --- a/src/moduleutils/PhutilLibraryMapBuilder.php +++ b/src/moduleutils/PhutilLibraryMapBuilder.php @@ -183,7 +183,7 @@ final class PhutilLibraryMapBuilder extends Phobject { * Load the library symbol cache, if it exists and is readable and valid. * * @return dict Map of content hashes to cache of output from - * `phutil_symbols.php`. + * `extract-symbols.php`. * * @task symbol */ @@ -256,7 +256,7 @@ final class PhutilLibraryMapBuilder extends Phobject { } /** - * Build a future which returns a `phutil_symbols.php` analysis of a source + * Build a future which returns a `extract-symbols.php` analysis of a source * file. * * @param string Relative path to the source file to analyze. @@ -442,7 +442,7 @@ EOPHP; $symbol_cache = $this->loadSymbolCache(); // If the XHPAST binary is not up-to-date, build it now. Otherwise, - // `phutil_symbols.php` will attempt to build the binary and will fail + // `extract-symbols.php` will attempt to build the binary and will fail // miserably because it will be trying to build the same file multiple // times in parallel. if (!PhutilXHPASTBinary::isAvailable()) { 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 @@ -107,9 +107,16 @@ final class PhutilEditorConfig extends Phobject { $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; @@ -163,12 +170,11 @@ final class PhutilEditorConfig extends Phobject { * 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)) { @@ -187,7 +193,7 @@ final class PhutilEditorConfig extends Phobject { if ($found_root) { break; } - } while ($path != $root && Filesystem::isDescendant($path, $root)); + } return $configs; } diff --git a/src/parser/PhutilJSONParser.php b/src/parser/PhutilJSONParser.php index d3b5aef5..3b06f4b2 100644 --- a/src/parser/PhutilJSONParser.php +++ b/src/parser/PhutilJSONParser.php @@ -16,8 +16,8 @@ final class PhutilJSONParser extends Phobject { } public function parse($json) { - $jsonlint_root = phutil_get_library_root('arcanist'); - $jsonlint_root = $jsonlint_root.'/../externals/jsonlint'; + $arcanist_root = phutil_get_library_root('arcanist'); + $jsonlint_root = $arcanist_root.'/../externals/jsonlint'; require_once $jsonlint_root.'/src/Seld/JsonLint/JsonParser.php'; require_once $jsonlint_root.'/src/Seld/JsonLint/Lexer.php'; 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 @@ -3,6 +3,10 @@ final class PhageAgentTestCase extends PhutilTestCase { public function testPhagePHPAgent() { + if (phutil_is_windows()) { + $this->assertSkipped(pht('Phage does not target Windows.')); + } + return $this->runBootloaderTests(new PhagePHPAgentBootloader()); } diff --git a/src/symbols/PhutilClassMapQuery.php b/src/symbols/PhutilClassMapQuery.php index 738b14c5..4b947a61 100644 --- a/src/symbols/PhutilClassMapQuery.php +++ b/src/symbols/PhutilClassMapQuery.php @@ -45,6 +45,7 @@ final class PhutilClassMapQuery extends Phobject { private $filterNull = false; private $uniqueMethod; private $sortMethod; + private $continueOnFailure; // NOTE: If you add more configurable properties here, make sure that // cache key construction in getCacheKey() is updated properly. @@ -162,6 +163,10 @@ final class PhutilClassMapQuery extends Phobject { return $this; } + public function setContinueOnFailure($continue) { + $this->continueOnFailure = $continue; + return $this; + } /* -( Executing the Query )------------------------------------------------ */ @@ -236,6 +241,7 @@ final class PhutilClassMapQuery extends Phobject { $objects = id(new PhutilSymbolLoader()) ->setAncestorClass($ancestor) + ->setContinueOnFailure($this->continueOnFailure) ->loadObjects(); // Apply the "expand" mechanism, if it is configured. diff --git a/src/symbols/PhutilSymbolLoader.php b/src/symbols/PhutilSymbolLoader.php index cdf23369..32c7a042 100644 --- a/src/symbols/PhutilSymbolLoader.php +++ b/src/symbols/PhutilSymbolLoader.php @@ -49,6 +49,7 @@ final class PhutilSymbolLoader { private $pathPrefix; private $suppressLoad; + private $continueOnFailure; /** @@ -148,6 +149,10 @@ final class PhutilSymbolLoader { return $this; } + public function setContinueOnFailure($continue) { + $this->continueOnFailure = $continue; + return $this; + } /* -( Load )--------------------------------------------------------------- */ @@ -250,19 +255,55 @@ final class PhutilSymbolLoader { } if (!$this->suppressLoad) { + // Loading a class may trigger the autoloader to load more classes + // (usually, the parent class), so we need to keep track of whether we + // are currently loading in "continue on failure" mode. Otherwise, we'll + // fail anyway if we fail to load a parent class. + + // The driving use case for the "continue on failure" mode is to let + // "arc liberate" run so it can rebuild the library map, even if you have + // made changes to Workflow or Config classes which it must load before + // it can operate. If we don't let it continue on failure, it is very + // difficult to remove or move Workflows. + + static $continue_depth = 0; + if ($this->continueOnFailure) { + $continue_depth++; + } + $caught = null; - foreach ($symbols as $symbol) { + foreach ($symbols as $key => $symbol) { try { $this->loadSymbol($symbol); } catch (Exception $ex) { + // If we failed to load this symbol, remove it from the results. + // Otherwise, we may fatal below when trying to reflect it. + unset($symbols[$key]); + $caught = $ex; } } + + $should_continue = ($continue_depth > 0); + + if ($this->continueOnFailure) { + $continue_depth--; + } + if ($caught) { // NOTE: We try to load everything even if we fail to load something, // primarily to make it possible to remove functions from a libphutil // library without breaking library startup. - throw $caught; + if ($should_continue) { + // We may not have `pht()` yet. + fprintf( + STDERR, + "%s: %s\n", + 'IGNORING CLASS LOAD FAILURE', + $caught->getMessage()); + } else { + throw $caught; + } } } @@ -386,11 +427,11 @@ final class PhutilSymbolLoader { $load_failed = null; if ($is_function) { if (!function_exists($name)) { - $load_failed = 'function'; + $load_failed = pht('function'); } } else { if (!class_exists($name, false) && !interface_exists($name, false)) { - $load_failed = 'class/interface'; + $load_failed = pht('class or interface'); } } @@ -400,13 +441,14 @@ final class PhutilSymbolLoader { $name, $load_failed, pht( - 'The symbol map for library "%s" (at "%s") claims this symbol '. - '(of type "%s") is defined in "%s", but loading that source file '. - 'did not cause the symbol to become defined.', + "The symbol map for library '%s' (at '%s') claims this %s is ". + "defined in '%s', but loading that source file did not cause the ". + "%s to become defined.", $lib_name, $lib_path, $load_failed, - $where)); + $where, + $load_failed)); } } diff --git a/src/unit/engine/phutil/PhutilTestCase.php b/src/unit/engine/phutil/PhutilTestCase.php index f16d9f2b..fddb0681 100644 --- a/src/unit/engine/phutil/PhutilTestCase.php +++ b/src/unit/engine/phutil/PhutilTestCase.php @@ -20,6 +20,7 @@ abstract class PhutilTestCase extends Phobject { private $paths; private $renderer; + private static $executables = array(); /* -( Making Test Assertions )--------------------------------------------- */ @@ -748,4 +749,37 @@ abstract class PhutilTestCase extends Phobject { 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/utils/PhutilExecutionEnvironment.php b/src/utils/PhutilExecutionEnvironment.php index 58281899..f9e2b1ba 100644 --- a/src/utils/PhutilExecutionEnvironment.php +++ b/src/utils/PhutilExecutionEnvironment.php @@ -13,4 +13,34 @@ final class PhutilExecutionEnvironment extends Phobject { return php_uname('r'); } + /** + * If the PHP configuration setting "variables_order" does not include "E", + * the `$_ENV` superglobal is not populated with the containing environment. + * For details, see T12071. + * + * This can be fixed by adding "E" to the configuration, but we can also + * repair it ourselves by re-executing a subprocess with the configuration + * option defined to include "E". This is clumsy, but saves users from + * needing to go find and edit their PHP files. + * + * @return void + */ + public static function repairMissingVariablesOrder() { + $variables_order = ini_get('variables_order'); + $variables_order = strtoupper($variables_order); + + if (strpos($variables_order, 'E') !== false) { + // The "variables_order" option already has "E", so we don't need to + // repair $_ENV. + return; + } + + list($env) = execx( + 'php -d variables_order=E -r %s', + 'echo json_encode($_ENV);'); + $env = phutil_json_decode($env); + + $_ENV = $_ENV + $env; + } + } diff --git a/src/utils/__tests__/PhutilUTF8TestCase.php b/src/utils/__tests__/PhutilUTF8TestCase.php index 84c35cb3..7e85f89c 100644 --- a/src/utils/__tests__/PhutilUTF8TestCase.php +++ b/src/utils/__tests__/PhutilUTF8TestCase.php @@ -61,6 +61,13 @@ final class PhutilUTF8TestCase extends PhutilTestCase { ); foreach ($map as $input => $expect) { + if ($input !== $expect) { + $this->assertEqual( + false, + phutil_is_utf8_slowly($input), + pht('Slowly reject overlong form of: %s', $input)); + } + $actual = phutil_utf8ize($input); $this->assertEqual( $expect, @@ -77,6 +84,13 @@ final class PhutilUTF8TestCase extends PhutilTestCase { ); foreach ($map as $input => $expect) { + if ($input !== $expect) { + $this->assertEqual( + false, + phutil_is_utf8_slowly($input), + pht('Slowly reject surrogate: %s', $input)); + } + $actual = phutil_utf8ize($input); $this->assertEqual( $expect, diff --git a/src/utils/__tests__/PhutilUtilsTestCase.php b/src/utils/__tests__/PhutilUtilsTestCase.php index 530babcb..e1bade52 100644 --- a/src/utils/__tests__/PhutilUtilsTestCase.php +++ b/src/utils/__tests__/PhutilUtilsTestCase.php @@ -570,6 +570,7 @@ final class PhutilUtilsTestCase extends PhutilTestCase { } catch (Exception $ex) { $caught = $ex; } + $this->assertTrue($caught instanceof PhutilJSONParserException); } } @@ -965,5 +966,4 @@ final class PhutilUtilsTestCase extends PhutilTestCase { } } - } diff --git a/src/utils/utf8.php b/src/utils/utf8.php index 6f8af083..7aff7f76 100644 --- a/src/utils/utf8.php +++ b/src/utils/utf8.php @@ -149,6 +149,34 @@ function phutil_is_utf8_slowly($string, $only_bmp = false) { continue; } return false; + } else if ($chr == 0xED) { + // See T11525. Some sequences in this block are surrogate codepoints + // that are reserved for use in UTF16. We should reject them. + $codepoint = ($chr & 0x0F) << 12; + ++$ii; + if ($ii >= $len) { + return false; + } + $chr = ord($string[$ii]); + $codepoint += ($chr & 0x3F) << 6; + if ($chr >= 0x80 && $chr <= 0xBF) { + ++$ii; + if ($ii >= $len) { + return false; + } + $chr = ord($string[$ii]); + $codepoint += ($chr & 0x3F); + + if ($codepoint >= 0xD800 && $codepoint <= 0xDFFF) { + // Reject these surrogate codepoints. + return false; + } + + if ($chr >= 0x80 && $chr <= 0xBF) { + continue; + } + } + return false; } else if ($chr > 0xE0 && $chr <= 0xEF) { ++$ii; if ($ii >= $len) { diff --git a/src/utils/utils.php b/src/utils/utils.php index 30a3afc8..495749c5 100644 --- a/src/utils/utils.php +++ b/src/utils/utils.php @@ -1065,8 +1065,8 @@ function phutil_fwrite_nonblocking_stream($stream, $bytes) { // the stream, write to it again if PHP claims that it's writable, and // consider the pipe broken if the write fails. - // (Signals received signals during the "fwrite()" do not appear to affect - // anything, see D20083.) + // (Signals received during the "fwrite()" do not appear to affect anything, + // see D20083.) $read = array(); $write = array($stream); diff --git a/src/xsprintf/PhutilTerminalString.php b/src/xsprintf/PhutilTerminalString.php index 8d99b093..1d42f288 100644 --- a/src/xsprintf/PhutilTerminalString.php +++ b/src/xsprintf/PhutilTerminalString.php @@ -70,6 +70,13 @@ final class PhutilTerminalString extends Phobject { $value = preg_replace('/\r(?!\n)/', '', $value); } + // See T13209. If we print certain invalid unicode byte sequences to the + // terminal under "cmd.exe", the entire string is silently dropped. Avoid + // printing invalid sequences. + if (phutil_is_windows()) { + $value = phutil_utf8ize($value); + } + return $value; } } 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 @@ -39,25 +39,33 @@ final class PhutilCsprintfTestCase extends PhutilTestCase { } 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; } @@ -65,7 +73,10 @@ final class PhutilCsprintfTestCase extends PhutilTestCase { // "%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')); diff --git a/scripts/init/init-script.php b/support/init/init-script.php similarity index 93% rename from scripts/init/init-script.php rename to support/init/init-script.php index 3609a178..77ed7d03 100644 --- a/scripts/init/init-script.php +++ b/support/init/init-script.php @@ -1,12 +1,6 @@ installHandler('phutil.winch', $handler); } -__phutil_init_script__(); +__arcanist_init_script__(); diff --git a/support/lib/extract-symbols.php b/support/lib/extract-symbols.php index 370d5ba8..7b9378b1 100755 --- a/support/lib/extract-symbols.php +++ b/support/lib/extract-symbols.php @@ -6,7 +6,7 @@ $builtins = phutil_symbols_get_builtins(); $root = dirname(dirname(dirname(__FILE__))); -require_once $root.'/scripts/init/init-script.php'; +require_once $root.'/support/init/init-script.php'; $args = new PhutilArgumentParser($argv); $args->setTagline(pht('identify symbols in a PHP source file')); diff --git a/support/lib/rebuild-map.php b/support/lib/rebuild-map.php index 533f7806..66c1c8be 100755 --- a/support/lib/rebuild-map.php +++ b/support/lib/rebuild-map.php @@ -2,7 +2,7 @@ setTagline(pht('rebuild the library map file')); diff --git a/support/shell/hooks/bash-completion.sh b/support/shell/hooks/bash-completion.sh new file mode 100644 index 00000000..edf34f7e --- /dev/null +++ b/support/shell/hooks/bash-completion.sh @@ -0,0 +1,9 @@ +SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" > /dev/null && pwd )" + +# Try to generate the shell completion rules if they do not yet exist. +if [ ! -f "${SCRIPTDIR}/bash-rules.sh" ]; then + arc shell-complete --generate >/dev/null 2>/dev/null +fi; + +# Source the shell completion rules. +source "${SCRIPTDIR}/../rules/bash-rules.sh" diff --git a/support/shell/rules/.keep b/support/shell/rules/.keep new file mode 100644 index 00000000..e69de29b diff --git a/support/shell/templates/bash-template.sh b/support/shell/templates/bash-template.sh new file mode 100644 index 00000000..e2e8047e --- /dev/null +++ b/support/shell/templates/bash-template.sh @@ -0,0 +1,22 @@ +_arcanist_complete_{{{BIN}}} () +{ + COMPREPLY=() + + RESULT=$(echo | {{{BIN}}} shell-complete \ + --current ${COMP_CWORD} \ + -- \ + "${COMP_WORDS[@]}" \ + 2>/dev/null) + + if [ $? -ne 0 ]; then + return $? + fi + + if [ "$RESULT" == "" ]; then + RESULT=$( compgen -A file -- ${COMP_WORDS[COMP_CWORD]} ) + fi + + local IFS=$'\n' + COMPREPLY=( $RESULT ) +} +complete -F _arcanist_complete_{{{BIN}}} -o filenames {{{BIN}}} diff --git a/support/unit/cat.php b/support/unit/cat.php new file mode 100755 index 00000000..4b5ef5c1 --- /dev/null +++ b/support/unit/cat.php @@ -0,0 +1,4 @@ +#!/usr/bin/env php + $arg) { + $args[$key] = addcslashes($arg, "\\\n"); +} +$args = implode("\n", $args); +echo $args; diff --git a/support/unit/exit.php b/support/unit/exit.php new file mode 100755 index 00000000..9983bd17 --- /dev/null +++ b/support/unit/exit.php @@ -0,0 +1,4 @@ +#!/usr/bin/env php +setTagline(pht('acquire and hold a lockfile')); diff --git a/support/unit/sleep.php b/support/unit/sleep.php new file mode 100755 index 00000000..fbdf4144 --- /dev/null +++ b/support/unit/sleep.php @@ -0,0 +1,20 @@ +\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)); +} diff --git a/support/xhpast/build-xhpast.php b/support/xhpast/build-xhpast.php index 4f13142a..f57ee047 100755 --- a/support/xhpast/build-xhpast.php +++ b/support/xhpast/build-xhpast.php @@ -2,7 +2,7 @@