diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..8d8746e --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,29 @@ + + + + + + + + + + + + + src + + + + + + + tests + + + diff --git a/tests/ApplicationTest.php b/tests/ApplicationTest.php new file mode 100644 index 0000000..9ded86a --- /dev/null +++ b/tests/ApplicationTest.php @@ -0,0 +1,462 @@ + + * @license MIT + */ + +namespace Dana\Test\Twigc; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Output\BufferedOutput; + +use Dana\Twigc\Application; + +/** + * Tests for twigc. + * + * This doesn't feel very 'DRY'. Would like to improve it somehow. + */ +class ApplicationTest extends TestCase { + protected $output; + protected $app; + protected $template; + protected $tempDir; + protected $tempFiles = []; + + /** + * Set up before tests. + * + * @return void + */ + protected function setUp() { + $this->output = new BufferedOutput(); + $this->app = new Application(); + $this->template = $this->makeFile('default', 'testEnv: {{ testEnv }}'); + } + + /** + * Tear down after tests. + * + * @return void + */ + protected function tearDown() { + $this->output->fetch(); + + // This is slow, but i'm too lazy to handle recursive deletion properly + if ( ! empty($this->tempFiles) ) { + $dir = escapeshellarg($this->tempDir); + exec("rm -rf ${dir}/?* 2> /dev/null"); + $this->tempFiles = []; + } + } + + /** + * Create a temporary file, and optionally populate it with data. + * + * @param string $name + * The temporary file name/suffix. If the name contains an internal slash, + * all leading directories are created. + * + * @param string|null $content + * (optional) Any data to populate the file with. + * + * @return string The name of the created file. + */ + protected function makeFile(string $name, string $data = null): string { + // Create our temp directory if we don't already have it + if ( ! $this->tempDir ) { + $rand = base64_encode(random_bytes(32)); + $rand = substr(str_replace(['/', '+', '='], '', $rand), 0, 10); + $this->tempDir = sys_get_temp_dir(); + $this->tempDir .= "/Dana.Test.Twigc.ApplicationTest.${rand}"; + + if ( ! is_dir($this->tempDir) ) { + mkdir($this->tempDir, 0700); + } + } + + $dir = $this->tempDir; + $name = trim($name, '/.'); + + if ( strlen($name) === 0 ) { + throw \RuntimeException('Expected file name'); + } + + if ( strpos($name, '/') !== false ) { + $dir .= '/' . dirname($name); + + if ( ! is_dir($dir) ) { + mkdir($dir, 0700, true); + } + + $name = basename($name); + } + + $file = "${dir}/${name}"; + $this->tempFiles[] = $file; + + if ( $data !== null ) { + if ( file_put_contents($file, $data, \LOCK_EX) === false ) { + throw \RuntimeException("Write failed: ${file}"); + } + } elseif ( touch($file) === false ) { + throw \RuntimeException("Write failed: ${file}"); + } + + return $file; + } + + /** + * Run the application and return common test data. + * + * @param $args + * (optional) Zero or more arguments to pass to the application. This should + * NOT include argv[0]. + * + * @return array + * An array containing the application return status as an integer, the raw + * output as a string, and the output as an array of lines. + */ + protected function runApp(...$args) { + $argv = ['', '-e', 'none']; + + if ( is_array($args[0]) ) { + $argv = array_merge($argv, $args[0]); + } else { + $argv = array_merge($argv, $args); + } + + $argv = array_filter($argv, function ($v) { + return $v !== null; + }); + + $ret = $this->app->run($this->output, $argv); + $buffer = $this->output->fetch(); + $lines = explode("\n", rtrim($buffer, "\r\n")); + + return [$ret, $buffer, $lines]; + } + + /** + * Provide data for testEscape(). + * + * All tests assume the following input data (value is literal): + * + * testEnv="" + * + * All tests assume the following template: + * + * testEnv: {{ testEnv }} + * + * @return array[] + */ + public function provideTestEscape() { + return [ + // Escape method: none + ['f', 'testEnv: ""'], + ['false', 'testEnv: ""'], + ['n', 'testEnv: ""'], + ['no', 'testEnv: ""'], + ['none', 'testEnv: ""'], + ['never', 'testEnv: ""'], + + // Escape method: html + ['always', 'testEnv: "<foo$bar>"'], + ['t', 'testEnv: "<foo$bar>"'], + ['true', 'testEnv: "<foo$bar>"'], + ['y', 'testEnv: "<foo$bar>"'], + ['yes', 'testEnv: "<foo$bar>"'], + ['html', 'testEnv: "<foo$bar>"'], + + // Escape method: css + ['css', 'testEnv: \\22 \\3C foo\\24 bar\\3E \\22'], + + // Escape method: html_attr + ['html_attr', 'testEnv: "<foo$bar>"'], + + // Escape method: js + ['js', 'testEnv: \\x22\\x3Cfoo\\x24bar\\x3E\\x22'], + + // Escape method: json + ['json', 'testEnv: "\"\""'], + + // Escape method: sh + ['sh', 'testEnv: "\"\""'], + + // Escape method: url + ['url', 'testEnv: %22%3Cfoo%24bar%3E%22'], + ]; + } + + /** + * Test `-h` / `--help` function. + * + * @return void + */ + public function testHelp() { + foreach ( ['-h', '--help'] as $opt ) { + list($ret, $buffer, $lines) = $this->runApp($opt); + + $this->assertSame(0, $ret); + $this->assertGreaterThan(3, count($lines)); + $this->assertContains('--help', $buffer); + $this->assertContains('--version', $buffer); + } + } + + /** + * Test `-V` / `--version` function. + * + * @return void + */ + public function testVersion() { + foreach ( ['-V', '--version'] as $opt ) { + list($ret, $buffer, $lines) = $this->runApp($opt); + + $this->assertSame(0, $ret); + $this->assertSame(1, count($lines)); + $this->assertContains(' version ', $lines[0]); + } + } + + /** + * Test `--credits` function. + * + * @return void + */ + public function testCredits() { + list($ret, $buffer, $lines) = $this->runApp('--credits'); + + $this->assertSame(0, $ret); + $this->assertGreaterThan(1, count($lines)); + $this->assertContains('licence', $lines[0]); + } + + /** + * Test `-d` / `--dir` function, as well as default include-directory + * functionality. + * + * @return void + */ + public function testDir() { + $templateSame = $this->makeFile('dir1/a.twig', '{% include "b.twig" %}'); + $includeSame = $this->makeFile('dir1/b.twig', 'included: {{ testEnv }}'); + + $templateDiff = $this->makeFile('dir2/a.twig', '{% include "b.twig" %}'); + $includeDiff = $this->makeFile('dir3/b.twig', 'included: {{ testEnv }}'); + + // $includeSame's directory should be searched by default + list($ret, $buffer, $lines) = $this->runApp( + '-p', + 'testEnv=abc123', + $templateSame + ); + $this->assertSame(0, $ret); + + // $includeDiff's directory should have to be specified manually + list($ret, $buffer, $lines) = $this->runApp( + '-p', + 'testEnv=abc123', + $templateDiff + ); + $this->assertNotSame(0, $ret); + + // Now we confirm that that works + foreach ( ['-d', '--dir'] as $opt ) { + list($ret, $buffer, $lines) = $this->runApp( + $opt, + dirname($includeDiff), + '-p', + 'testEnv=abc123', + $templateDiff + ); + + $this->assertSame(0, $ret); + $this->assertSame(1, count($lines)); + $this->assertContains('included: abc123', $lines[0]); + } + } + + /** + * Test `-E` / `--env` function. + * + * @return void + */ + public function testEnv() { + foreach ( ['-E', '--env'] as $opt ) { + // This option can't be set at run time; we'll just test what we have + if ( strpos(ini_get('variables_order'), 'E') === false ) { + list($ret, $buffer, $lines) = $this->runApp($opt, $this->template); + + $this->assertGreaterThan(0, $ret); + $this->assertContains('variables_order', $buffer); + return; + } + + $_ENV['testEnv'] = 'abc123'; + list($ret, $buffer, $lines) = $this->runApp($opt, $this->template); + + $this->assertSame(0, $ret); + $this->assertSame(1, count($lines)); + $this->assertContains('testEnv: abc123', $lines[0]); + } + } + + /** + * Test `-j` / `--json` function (dictionary string). + * + * @return void + */ + public function testJsonDict() { + foreach ( ['-j', '--json'] as $opt ) { + list($ret, $buffer, $lines) = $this->runApp( + $opt, + '{"testEnv": "abc123"}', + $this->template + ); + + $this->assertSame(0, $ret); + $this->assertSame(1, count($lines)); + $this->assertContains('testEnv: abc123', $lines[0]); + } + } + + /** + * Test `-j` / `--json` function (file). + * + * @return void + */ + public function testJsonFile() { + $jsonFile = $this->makeFile('json', '{"testEnv": "abc123"}'); + + foreach ( ['-j', '--json'] as $opt ) { + list($ret, $buffer, $lines) = $this->runApp( + $opt, + $jsonFile, + $this->template + ); + + $this->assertSame(0, $ret); + $this->assertSame(1, count($lines)); + $this->assertContains('testEnv: abc123', $lines[0]); + } + } + + /** + * Test `-p` / `--pair` function. + * + * @return void + */ + public function testPair() { + foreach ( ['-p', '--pair'] as $opt ) { + list($ret, $buffer, $lines) = $this->runApp( + $opt, + 'testEnv=abc123', + $this->template + ); + + $this->assertSame(0, $ret); + $this->assertSame(1, count($lines)); + $this->assertContains('testEnv: abc123', $lines[0]); + } + } + + /** + * Test `--query` function. + * + * @return void + */ + public function testQuery() { + list($ret, $buffer, $lines) = $this->runApp( + '--query', + '?testEnv=abc123&testEnv2=x&testEnv3=y', + $this->template + ); + + $this->assertSame(0, $ret); + $this->assertSame(1, count($lines)); + $this->assertContains('testEnv: abc123', $lines[0]); + } + + /** + * Test input-data precedence. + * + * Input precedence should be as follows (ascending): + * + * env -> query -> json -> pair + * + * @return void + */ + public function testInputDataPrecedence() { + list($ret, $buffer, $lines) = $this->runApp( + '--pair', + 'testEnv=aaa', + '--query', + '?testEnv=bbb', + '--json', + '{ "testEnv": "ccc" }', + $this->template + ); + + $this->assertSame(0, $ret); + $this->assertSame(1, count($lines)); + $this->assertContains('testEnv: aaa', $lines[0]); + } + + /** + * Test handling of undefined variable WITHOUT `-s` / `--strict`. + * + * @return void + */ + public function testUndefinedNoStrict() { + list($ret, $buffer, $lines) = $this->runApp($this->template); + + $this->assertSame(0, $ret); + $this->assertSame(1, count($lines)); + $this->assertContains('testEnv:', $lines[0]); + $this->assertNotContains('abc123', $lines[0]); + } + + /** + * Test handling of undefined variable WITHOUT `-s` / `--strict`. + * + * @return void + */ + public function testUndefinedStrict() { + foreach ( ['-s', '--strict'] as $opt ) { + list($ret, $buffer, $lines) = $this->runApp($opt, $this->template); + + $this->assertSame(1, $ret); + $this->assertContains('testEnv', $lines[0]); + $this->assertNotContains('abc123', $lines[0]); + } + } + + /** + * Test various escape methods with `-e` / `--escape`. + * + * @param string $method The method to test. + * @param string $expected The expected output. + * + * @dataProvider provideTestEscape + * + * @return void + */ + public function testEcape(string $method, string $expected) { + foreach ( ['-e', '--escape'] as $opt ) { + list($ret, $buffer, $lines) = $this->runApp( + $opt, + $method, + '-p', + 'testEnv=""', + $this->template + ); + + $this->assertSame(0, $ret); + $this->assertContains($expected, $lines[0] ?? ''); + } + } +}