TestFileReader.php 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. <?php
  2. /**
  3. * This program is free software; you can redistribute it and/or modify
  4. * it under the terms of the GNU General Public License as published by
  5. * the Free Software Foundation; either version 2 of the License, or
  6. * (at your option) any later version.
  7. *
  8. * This program is distributed in the hope that it will be useful,
  9. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. * GNU General Public License for more details.
  12. *
  13. * You should have received a copy of the GNU General Public License along
  14. * with this program; if not, write to the Free Software Foundation, Inc.,
  15. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  16. * http://www.gnu.org/copyleft/gpl.html
  17. *
  18. * @file
  19. * @ingroup Testing
  20. */
  21. class TestFileReader {
  22. private $file;
  23. private $fh;
  24. private $section = null;
  25. /** String|null: current test section being analyzed */
  26. private $sectionData = [];
  27. private $sectionLineNum = [];
  28. private $lineNum = 0;
  29. private $runDisabled;
  30. private $runParsoid;
  31. private $regex;
  32. private $articles = [];
  33. private $requirements = [];
  34. private $tests = [];
  35. public static function read( $file, array $options = [] ) {
  36. $reader = new self( $file, $options );
  37. $reader->execute();
  38. $requirements = [];
  39. foreach ( $reader->requirements as $type => $reqsOfType ) {
  40. foreach ( $reqsOfType as $name => $unused ) {
  41. $requirements[] = [
  42. 'type' => $type,
  43. 'name' => $name
  44. ];
  45. }
  46. }
  47. return [
  48. 'requirements' => $requirements,
  49. 'tests' => $reader->tests,
  50. 'articles' => $reader->articles
  51. ];
  52. }
  53. private function __construct( $file, $options ) {
  54. $this->file = $file;
  55. $this->fh = fopen( $this->file, "rt" );
  56. if ( !$this->fh ) {
  57. throw new MWException( "Couldn't open file '$file'\n" );
  58. }
  59. $options = $options + [
  60. 'runDisabled' => false,
  61. 'runParsoid' => false,
  62. 'regex' => '//',
  63. ];
  64. $this->runDisabled = $options['runDisabled'];
  65. $this->runParsoid = $options['runParsoid'];
  66. $this->regex = $options['regex'];
  67. }
  68. private function addCurrentTest() {
  69. // "input" and "result" are old section names allowed
  70. // for backwards-compatibility.
  71. $input = $this->checkSection( [ 'wikitext', 'input' ], false );
  72. $nonTidySection = $this->checkSection(
  73. [ 'html/php', 'html/*', 'html', 'result' ], false );
  74. // Some tests have "with tidy" and "without tidy" variants
  75. $tidySection = $this->checkSection( [ 'html/php+tidy', 'html+tidy' ], false );
  76. // Remove trailing newline
  77. $data = array_map( 'ParserTestRunner::chomp', $this->sectionData );
  78. // Apply defaults
  79. $data += [
  80. 'options' => '',
  81. 'config' => ''
  82. ];
  83. if ( $input === false ) {
  84. throw new MWException( "Test at {$this->file}:{$this->sectionLineNum['test']} " .
  85. "lacks input section" );
  86. }
  87. if ( preg_match( '/\\bdisabled\\b/i', $data['options'] ) && !$this->runDisabled ) {
  88. // Disabled
  89. return;
  90. }
  91. if ( $tidySection === false && $nonTidySection === false ) {
  92. if ( isset( $data['html/parsoid'] ) || isset( $data['wikitext/edited'] ) ) {
  93. // Parsoid only
  94. return;
  95. } else {
  96. throw new MWException( "Test at {$this->file}:{$this->sectionLineNum['test']} " .
  97. "lacks result section" );
  98. }
  99. }
  100. if ( preg_match( '/\\bparsoid\\b/i', $data['options'] ) && $nonTidySection === 'html'
  101. && !$this->runParsoid
  102. ) {
  103. // A test which normally runs on Parsoid but can optionally be run with MW
  104. return;
  105. }
  106. if ( !preg_match( $this->regex, $data['test'] ) ) {
  107. // Filtered test
  108. return;
  109. }
  110. $commonInfo = [
  111. 'test' => $data['test'],
  112. 'desc' => $data['test'],
  113. 'input' => $data[$input],
  114. 'options' => $data['options'],
  115. 'config' => $data['config'],
  116. 'line' => $this->sectionLineNum['test'],
  117. 'file' => $this->file
  118. ];
  119. if ( $nonTidySection !== false ) {
  120. // Add non-tidy test
  121. $this->tests[] = [
  122. 'result' => $data[$nonTidySection],
  123. 'resultSection' => $nonTidySection
  124. ] + $commonInfo;
  125. if ( $tidySection !== false ) {
  126. // Add tidy subtest
  127. $this->tests[] = [
  128. 'desc' => $data['test'] . ' (with tidy)',
  129. 'result' => $data[$tidySection],
  130. 'resultSection' => $tidySection,
  131. 'options' => $data['options'] . ' tidy',
  132. 'isSubtest' => true,
  133. ] + $commonInfo;
  134. }
  135. } elseif ( $tidySection !== false ) {
  136. // No need to override desc when there is no subtest
  137. $this->tests[] = [
  138. 'result' => $data[$tidySection],
  139. 'resultSection' => $tidySection,
  140. 'options' => $data['options'] . ' tidy'
  141. ] + $commonInfo;
  142. } else {
  143. throw new MWException( "Test at {$this->file}:{$this->sectionLineNum['test']} " .
  144. "lacks result section" );
  145. }
  146. }
  147. private function execute() {
  148. while ( false !== ( $line = fgets( $this->fh ) ) ) {
  149. $this->lineNum++;
  150. $matches = [];
  151. if ( preg_match( '/^!!\s*(\S+)/', $line, $matches ) ) {
  152. $this->section = strtolower( $matches[1] );
  153. if ( $this->section == 'endarticle' ) {
  154. $this->checkSection( 'text' );
  155. $this->checkSection( 'article' );
  156. $this->addArticle(
  157. ParserTestRunner::chomp( $this->sectionData['article'] ),
  158. $this->sectionData['text'], $this->lineNum );
  159. $this->clearSection();
  160. continue;
  161. }
  162. if ( $this->section == 'endhooks' ) {
  163. $this->checkSection( 'hooks' );
  164. foreach ( explode( "\n", $this->sectionData['hooks'] ) as $line ) {
  165. $line = trim( $line );
  166. if ( $line ) {
  167. $this->addRequirement( 'hook', $line );
  168. }
  169. }
  170. $this->clearSection();
  171. continue;
  172. }
  173. if ( $this->section == 'endfunctionhooks' ) {
  174. $this->checkSection( 'functionhooks' );
  175. foreach ( explode( "\n", $this->sectionData['functionhooks'] ) as $line ) {
  176. $line = trim( $line );
  177. if ( $line ) {
  178. $this->addRequirement( 'functionHook', $line );
  179. }
  180. }
  181. $this->clearSection();
  182. continue;
  183. }
  184. if ( $this->section == 'endtransparenthooks' ) {
  185. $this->checkSection( 'transparenthooks' );
  186. foreach ( explode( "\n", $this->sectionData['transparenthooks'] ) as $line ) {
  187. $line = trim( $line );
  188. if ( $line ) {
  189. $this->addRequirement( 'transparentHook', $line );
  190. }
  191. }
  192. $this->clearSection();
  193. continue;
  194. }
  195. if ( $this->section == 'end' ) {
  196. $this->checkSection( 'test' );
  197. $this->addCurrentTest();
  198. $this->clearSection();
  199. continue;
  200. }
  201. if ( isset( $this->sectionData[$this->section] ) ) {
  202. throw new MWException( "duplicate section '$this->section' "
  203. . "at line {$this->lineNum} of $this->file\n" );
  204. }
  205. $this->sectionLineNum[$this->section] = $this->lineNum;
  206. $this->sectionData[$this->section] = '';
  207. continue;
  208. }
  209. if ( $this->section ) {
  210. $this->sectionData[$this->section] .= $line;
  211. }
  212. }
  213. }
  214. /**
  215. * Clear section name and its data
  216. */
  217. private function clearSection() {
  218. $this->sectionLineNum = [];
  219. $this->sectionData = [];
  220. $this->section = null;
  221. }
  222. /**
  223. * Verify the current section data has some value for the given token
  224. * name(s) (first parameter).
  225. * Throw an exception if it is not set, referencing current section
  226. * and adding the current file name and line number
  227. *
  228. * @param string|array $tokens Expected token(s) that should have been
  229. * mentioned before closing this section
  230. * @param bool $fatal True iff an exception should be thrown if
  231. * the section is not found.
  232. * @return bool|string
  233. * @throws MWException
  234. */
  235. private function checkSection( $tokens, $fatal = true ) {
  236. if ( is_null( $this->section ) ) {
  237. throw new MWException( __METHOD__ . " can not verify a null section!\n" );
  238. }
  239. if ( !is_array( $tokens ) ) {
  240. $tokens = [ $tokens ];
  241. }
  242. if ( count( $tokens ) == 0 ) {
  243. throw new MWException( __METHOD__ . " can not verify zero sections!\n" );
  244. }
  245. $data = $this->sectionData;
  246. $tokens = array_filter( $tokens, function ( $token ) use ( $data ) {
  247. return isset( $data[$token] );
  248. } );
  249. if ( count( $tokens ) == 0 ) {
  250. if ( !$fatal ) {
  251. return false;
  252. }
  253. throw new MWException( sprintf(
  254. "'%s' without '%s' at line %s of %s\n",
  255. $this->section,
  256. implode( ',', $tokens ),
  257. $this->lineNum,
  258. $this->file
  259. ) );
  260. }
  261. if ( count( $tokens ) > 1 ) {
  262. throw new MWException( sprintf(
  263. "'%s' with unexpected tokens '%s' at line %s of %s\n",
  264. $this->section,
  265. implode( ',', $tokens ),
  266. $this->lineNum,
  267. $this->file
  268. ) );
  269. }
  270. return array_values( $tokens )[0];
  271. }
  272. private function addArticle( $name, $text, $line ) {
  273. $this->articles[] = [
  274. 'name' => $name,
  275. 'text' => $text,
  276. 'line' => $line,
  277. 'file' => $this->file
  278. ];
  279. }
  280. private function addRequirement( $type, $name ) {
  281. $this->requirements[$type][$name] = true;
  282. }
  283. }