1<?php 2 3/* 4 * This file is part of Twig. 5 * 6 * (c) Fabien Potencier 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Twig\Test; 13 14use PHPUnit\Framework\TestCase; 15use Twig\Environment; 16use Twig\Error\Error; 17use Twig\Extension\ExtensionInterface; 18use Twig\Loader\ArrayLoader; 19use Twig\Loader\SourceContextLoaderInterface; 20use Twig\RuntimeLoader\RuntimeLoaderInterface; 21use Twig\Source; 22use Twig\TwigFilter; 23use Twig\TwigFunction; 24use Twig\TwigTest; 25 26/** 27 * Integration test helper. 28 * 29 * @author Fabien Potencier <fabien@symfony.com> 30 * @author Karma Dordrak <drak@zikula.org> 31 */ 32abstract class IntegrationTestCase extends TestCase 33{ 34 /** 35 * @return string 36 */ 37 abstract protected function getFixturesDir(); 38 39 /** 40 * @return RuntimeLoaderInterface[] 41 */ 42 protected function getRuntimeLoaders() 43 { 44 return []; 45 } 46 47 /** 48 * @return ExtensionInterface[] 49 */ 50 protected function getExtensions() 51 { 52 return []; 53 } 54 55 /** 56 * @return TwigFilter[] 57 */ 58 protected function getTwigFilters() 59 { 60 return []; 61 } 62 63 /** 64 * @return TwigFunction[] 65 */ 66 protected function getTwigFunctions() 67 { 68 return []; 69 } 70 71 /** 72 * @return TwigTest[] 73 */ 74 protected function getTwigTests() 75 { 76 return []; 77 } 78 79 /** 80 * @dataProvider getTests 81 */ 82 public function testIntegration($file, $message, $condition, $templates, $exception, $outputs) 83 { 84 $this->doIntegrationTest($file, $message, $condition, $templates, $exception, $outputs); 85 } 86 87 /** 88 * @dataProvider getLegacyTests 89 * @group legacy 90 */ 91 public function testLegacyIntegration($file, $message, $condition, $templates, $exception, $outputs) 92 { 93 $this->doIntegrationTest($file, $message, $condition, $templates, $exception, $outputs); 94 } 95 96 public function getTests($name, $legacyTests = false) 97 { 98 $fixturesDir = realpath($this->getFixturesDir()); 99 $tests = []; 100 101 foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($fixturesDir), \RecursiveIteratorIterator::LEAVES_ONLY) as $file) { 102 if (!preg_match('/\.test$/', $file)) { 103 continue; 104 } 105 106 if ($legacyTests xor false !== strpos($file->getRealpath(), '.legacy.test')) { 107 continue; 108 } 109 110 $test = file_get_contents($file->getRealpath()); 111 112 if (preg_match('/--TEST--\s*(.*?)\s*(?:--CONDITION--\s*(.*))?\s*((?:--TEMPLATE(?:\(.*?\))?--(?:.*?))+)\s*(?:--DATA--\s*(.*))?\s*--EXCEPTION--\s*(.*)/sx', $test, $match)) { 113 $message = $match[1]; 114 $condition = $match[2]; 115 $templates = self::parseTemplates($match[3]); 116 $exception = $match[5]; 117 $outputs = [[null, $match[4], null, '']]; 118 } elseif (preg_match('/--TEST--\s*(.*?)\s*(?:--CONDITION--\s*(.*))?\s*((?:--TEMPLATE(?:\(.*?\))?--(?:.*?))+)--DATA--.*?--EXPECT--.*/s', $test, $match)) { 119 $message = $match[1]; 120 $condition = $match[2]; 121 $templates = self::parseTemplates($match[3]); 122 $exception = false; 123 preg_match_all('/--DATA--(.*?)(?:--CONFIG--(.*?))?--EXPECT--(.*?)(?=\-\-DATA\-\-|$)/s', $test, $outputs, PREG_SET_ORDER); 124 } else { 125 throw new \InvalidArgumentException(sprintf('Test "%s" is not valid.', str_replace($fixturesDir.'/', '', $file))); 126 } 127 128 $tests[] = [str_replace($fixturesDir.'/', '', $file), $message, $condition, $templates, $exception, $outputs]; 129 } 130 131 if ($legacyTests && empty($tests)) { 132 // add a dummy test to avoid a PHPUnit message 133 return [['not', '-', '', [], '', []]]; 134 } 135 136 return $tests; 137 } 138 139 public function getLegacyTests() 140 { 141 return $this->getTests('testLegacyIntegration', true); 142 } 143 144 protected function doIntegrationTest($file, $message, $condition, $templates, $exception, $outputs) 145 { 146 if (!$outputs) { 147 $this->markTestSkipped('no tests to run'); 148 } 149 150 if ($condition) { 151 eval('$ret = '.$condition.';'); 152 if (!$ret) { 153 $this->markTestSkipped($condition); 154 } 155 } 156 157 $loader = new ArrayLoader($templates); 158 159 foreach ($outputs as $i => $match) { 160 $config = array_merge([ 161 'cache' => false, 162 'strict_variables' => true, 163 ], $match[2] ? eval($match[2].';') : []); 164 $twig = new Environment($loader, $config); 165 $twig->addGlobal('global', 'global'); 166 foreach ($this->getRuntimeLoaders() as $runtimeLoader) { 167 $twig->addRuntimeLoader($runtimeLoader); 168 } 169 170 foreach ($this->getExtensions() as $extension) { 171 $twig->addExtension($extension); 172 } 173 174 foreach ($this->getTwigFilters() as $filter) { 175 $twig->addFilter($filter); 176 } 177 178 foreach ($this->getTwigTests() as $test) { 179 $twig->addTest($test); 180 } 181 182 foreach ($this->getTwigFunctions() as $function) { 183 $twig->addFunction($function); 184 } 185 186 $p = new \ReflectionProperty($twig, 'templateClassPrefix'); 187 $p->setAccessible(true); 188 $p->setValue($twig, '__TwigTemplate_'.hash('sha256', uniqid(mt_rand(), true), false).'_'); 189 190 try { 191 $template = $twig->load('index.twig'); 192 } catch (\Exception $e) { 193 if (false !== $exception) { 194 $message = $e->getMessage(); 195 $this->assertSame(trim($exception), trim(sprintf('%s: %s', \get_class($e), $message))); 196 $last = substr($message, \strlen($message) - 1); 197 $this->assertTrue('.' === $last || '?' === $last, $message, 'Exception message must end with a dot or a question mark.'); 198 199 return; 200 } 201 202 throw new Error(sprintf('%s: %s', \get_class($e), $e->getMessage()), -1, null, $e); 203 } 204 205 try { 206 $output = trim($template->render(eval($match[1].';')), "\n "); 207 } catch (\Exception $e) { 208 if (false !== $exception) { 209 $this->assertSame(trim($exception), trim(sprintf('%s: %s', \get_class($e), $e->getMessage()))); 210 211 return; 212 } 213 214 $e = new Error(sprintf('%s: %s', \get_class($e), $e->getMessage()), -1, null, $e); 215 216 $output = trim(sprintf('%s: %s', \get_class($e), $e->getMessage())); 217 } 218 219 if (false !== $exception) { 220 list($class) = explode(':', $exception); 221 $constraintClass = class_exists('PHPUnit\Framework\Constraint\Exception') ? 'PHPUnit\Framework\Constraint\Exception' : 'PHPUnit_Framework_Constraint_Exception'; 222 $this->assertThat(null, new $constraintClass($class)); 223 } 224 225 $expected = trim($match[3], "\n "); 226 227 if ($expected !== $output) { 228 printf("Compiled templates that failed on case %d:\n", $i + 1); 229 230 foreach (array_keys($templates) as $name) { 231 echo "Template: $name\n"; 232 $loader = $twig->getLoader(); 233 if (!$loader instanceof SourceContextLoaderInterface) { 234 $source = new Source($loader->getSource($name), $name); 235 } else { 236 $source = $loader->getSourceContext($name); 237 } 238 echo $twig->compile($twig->parse($twig->tokenize($source))); 239 } 240 } 241 $this->assertEquals($expected, $output, $message.' (in '.$file.')'); 242 } 243 } 244 245 protected static function parseTemplates($test) 246 { 247 $templates = []; 248 preg_match_all('/--TEMPLATE(?:\((.*?)\))?--(.*?)(?=\-\-TEMPLATE|$)/s', $test, $matches, PREG_SET_ORDER); 249 foreach ($matches as $match) { 250 $templates[($match[1] ? $match[1] : 'index.twig')] = $match[2]; 251 } 252 253 return $templates; 254 } 255} 256 257class_alias('Twig\Test\IntegrationTestCase', 'Twig_Test_IntegrationTestCase'); 258