diff --git a/src/parser/xhpast/__tests__/PHPASTParserTestCase.php b/src/parser/xhpast/__tests__/PHPASTParserTestCase.php --- a/src/parser/xhpast/__tests__/PHPASTParserTestCase.php +++ b/src/parser/xhpast/__tests__/PHPASTParserTestCase.php @@ -80,7 +80,6 @@ switch ($type) { case 'pass': - case 'fail-parse': $this->assertEqual(0, $err, pht('Exit code for "%s".', $name)); if (!strlen($expect)) { @@ -89,16 +88,6 @@ } try { - $expect = phutil_json_decode($expect); - } catch (PhutilJSONParserException $ex) { - throw new PhutilProxyException( - pht( - 'Expect data for test "%s" is not valid JSON.', - $name), - $ex); - } - - try { $stdout = phutil_json_decode($stdout); } catch (PhutilJSONParserException $ex) { throw new PhutilProxyException( @@ -108,21 +97,16 @@ $ex); } - $json = new PhutilJSON(); + $stdout_nice = $this->newReadableAST($stdout, $data); - $expect_nice = $json->encodeFormatted($expect); - $stdout_nice = $json->encodeFormatted($stdout); + echo "\n\n\n"; + echo $stdout_nice; + echo "\n\n\n"; - if ($type == 'pass') { - $this->assertEqual( - $expect_nice, - $stdout_nice, - pht('Parser output for "%s".', $name)); - } else { - $this->assertFalse( - ($expect_nice == $stdout_nice), - pht('Expected parser to parse "%s" incorrectly.', $name)); - } + $this->assertEqual( + $expect, + $stdout_nice, + pht('Parser output for "%s".', $name)); break; case 'fail-syntax': $this->assertEqual(1, $err, pht('Exit code for "%s".', $name)); @@ -130,7 +114,141 @@ (bool)preg_match('/syntax error/', $stderr), pht('Expect "syntax error" in stderr or "%s".', $name)); break; + default: + throw new Exception( + pht( + 'Unknown PHPAST parser test type "%s"!', + $type)); + } + } + + /** + * Build a human-readable, stable, relatively diff-able string representing + * an AST (both the node tree itself and the accompanying token stream) for + * use in unit tests. + */ + private function newReadableAST(array $data, $source) { + $tree = new XHPASTTree($data['tree'], $data['stream'], $source); + + $out = array(); + $out[] = $this->newReadableTree($tree); + $out[] = $this->newReadableDivider(); + $out[] = $this->newReadableStream($tree); + $out = implode('', $out); + + return $out; + } + + private function newReadableTree(XHPASTTree $tree) { + $root = $tree->getRootNode(); + + $depth = 0; + $list = $this->newReadableTreeLines($root, $depth); + + return implode('', $list); + } + + private function newReadableDivider() { + return str_repeat('-', 80)."\n"; + } + + private function newReadableStream(XHPASTTree $tree) { + $tokens = $tree->getRawTokenStream(); + + // Identify the longest token name this stream uses so we can format the + // output into two columns. + $max_len = 0; + foreach ($tokens as $token) { + $max_len = max($max_len, strlen($token->getTypeName())); + } + + $out = array(); + + $tokens = $tree->getRawTokenStream(); + foreach ($tokens as $token) { + $value = $token->getValue(); + $vector = $this->getEscapedValueVector($value); + + $vector = array_merge( + array( + str_pad($token->getTypeName(), $max_len), + ' ', + ), + $vector); + + $out[] = $this->wrapVector($vector); } + + $out = implode('', $out); + + return $out; + } + + private function newReadableTreeLines(AASTNode $node, $depth) { + $out = array(); + + try { + $type_name = $node->getTypeName(); + } catch (Exception $ex) { + $type_name = sprintf('', $node->getTypeID()); + } + + $out[] = str_repeat(' ', $depth).'+ '.$type_name."\n"; + + $children = $node->getChildren(); + if ($children) { + foreach ($children as $child) { + foreach ($this->newReadableTreeLines($child, $depth + 1) as $line) { + $out[] = $line; + } + } + } else { + $value = $node->getConcreteString(); + $vector = $this->getEscapedValueVector($value); + $out[] = $this->wrapVector($vector, $depth + 1); + } + + return $out; + } + + private function getEscapedValueVector($value) { + if (!$value) { + return array(''); + } + + $vector = phutil_utf8v_combined($value); + foreach ($vector as $key => $character) { + $vector[$key] = addcslashes($character, "\r\n\t\"\\"); + } + + return $vector; + } + + private function wrapVector(array $vector, $indent = 0, $width = 80) { + $lines = array(); + $prefix = str_repeat(' ', $indent); + + $line = $prefix.'> "'; + $len = strlen($line); + foreach ($vector as $character) { + // We're just wrapping based on bytes. This isn't the most correct + // wrapping for human language, but sufficient and stable for unit + // tests, which will rarely have long sequences of combining or + // multibyte characters. + $charlen = strlen($character); + if ($len + $charlen > ($width - 1)) { + $lines[] = $line."\"\n"; + $line = $prefix.'. "'; + $len = strlen($line); + } + + $line .= $character; + $len += $charlen; + } + + $lines[] = $line."\"\n"; + + return implode('', $lines); } } diff --git a/src/parser/xhpast/__tests__/data/a-self-test.php.test b/src/parser/xhpast/__tests__/data/a-self-test.php.test new file mode 100644 --- /dev/null +++ b/src/parser/xhpast/__tests__/data/a-self-test.php.test @@ -0,0 +1,60 @@ + " "$the_senate" + + n_OPERATOR + > "=" + + n_HEREDOC + > "<< "T_OPEN_TAG "T_WHITESPACE \n\n" +> "T_VARIABLE $the_senate" +> "T_WHITESPACE " +> "= =" +> "T_WHITESPACE " +> "T_HEREDOC << "; ;" +> "T_WHITESPACE \n\n" diff --git a/src/parser/xhpast/__tests__/data/base-fail-parse.php.test b/src/parser/xhpast/__tests__/data/base-fail-parse.php.test deleted file mode 100644 --- a/src/parser/xhpast/__tests__/data/base-fail-parse.php.test +++ /dev/null @@ -1,8 +0,0 @@ -