TestFileEditor.php 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. <?php
  2. class TestFileEditor {
  3. private $lines;
  4. private $numLines;
  5. private $deletions;
  6. private $changes;
  7. private $pos;
  8. private $warningCallback;
  9. private $result;
  10. public static function edit( $text, array $deletions, array $changes, $warningCallback = null ) {
  11. $editor = new self( $text, $deletions, $changes, $warningCallback );
  12. $editor->execute();
  13. return $editor->result;
  14. }
  15. private function __construct( $text, array $deletions, array $changes, $warningCallback ) {
  16. $this->lines = explode( "\n", $text );
  17. $this->numLines = count( $this->lines );
  18. $this->deletions = array_flip( $deletions );
  19. $this->changes = $changes;
  20. $this->pos = 0;
  21. $this->warningCallback = $warningCallback;
  22. $this->result = '';
  23. }
  24. private function execute() {
  25. while ( $this->pos < $this->numLines ) {
  26. $line = $this->lines[$this->pos];
  27. switch ( $this->getHeading( $line ) ) {
  28. case 'test':
  29. $this->parseTest();
  30. break;
  31. case 'hooks':
  32. case 'functionhooks':
  33. case 'transparenthooks':
  34. $this->parseHooks();
  35. break;
  36. default:
  37. if ( $this->pos < $this->numLines - 1 ) {
  38. $line .= "\n";
  39. }
  40. $this->emitComment( $line );
  41. $this->pos++;
  42. }
  43. }
  44. foreach ( $this->deletions as $deletion => $unused ) {
  45. $this->warning( "Could not find test \"$deletion\" to delete it" );
  46. }
  47. foreach ( $this->changes as $test => $sectionChanges ) {
  48. foreach ( $sectionChanges as $section => $change ) {
  49. $this->warning( "Could not find section \"$section\" in test \"$test\" " .
  50. "to {$change['op']} it" );
  51. }
  52. }
  53. }
  54. private function warning( $text ) {
  55. $cb = $this->warningCallback;
  56. if ( $cb ) {
  57. $cb( $text );
  58. }
  59. }
  60. private function getHeading( $line ) {
  61. if ( preg_match( '/^!!\s*(\S+)/', $line, $m ) ) {
  62. return $m[1];
  63. } else {
  64. return false;
  65. }
  66. }
  67. private function parseTest() {
  68. $test = [];
  69. $line = $this->lines[$this->pos++];
  70. $heading = $this->getHeading( $line );
  71. $section = [
  72. 'name' => $heading,
  73. 'headingLine' => $line,
  74. 'contents' => ''
  75. ];
  76. while ( $this->pos < $this->numLines ) {
  77. $line = $this->lines[$this->pos++];
  78. $nextHeading = $this->getHeading( $line );
  79. if ( $nextHeading === 'end' ) {
  80. $test[] = $section;
  81. // Add trailing line breaks to the "end" section, to allow for neat deletions
  82. $trail = '';
  83. for ( $i = 0; $i < $this->numLines - $this->pos - 1; $i++ ) {
  84. if ( $this->lines[$this->pos + $i] === '' ) {
  85. $trail .= "\n";
  86. } else {
  87. break;
  88. }
  89. }
  90. $this->pos += strlen( $trail );
  91. $test[] = [
  92. 'name' => 'end',
  93. 'headingLine' => $line,
  94. 'contents' => $trail
  95. ];
  96. $this->emitTest( $test );
  97. return;
  98. } elseif ( $nextHeading !== false ) {
  99. $test[] = $section;
  100. $heading = $nextHeading;
  101. $section = [
  102. 'name' => $heading,
  103. 'headingLine' => $line,
  104. 'contents' => ''
  105. ];
  106. } else {
  107. $section['contents'] .= "$line\n";
  108. }
  109. }
  110. throw new Exception( 'Unexpected end of file' );
  111. }
  112. private function parseHooks() {
  113. $line = $this->lines[$this->pos++];
  114. $heading = $this->getHeading( $line );
  115. $expectedEnd = 'end' . $heading;
  116. $contents = "$line\n";
  117. do {
  118. $line = $this->lines[$this->pos++];
  119. $nextHeading = $this->getHeading( $line );
  120. $contents .= "$line\n";
  121. } while ( $this->pos < $this->numLines && $nextHeading !== $expectedEnd );
  122. if ( $nextHeading !== $expectedEnd ) {
  123. throw new Exception( 'Unexpected end of file' );
  124. }
  125. $this->emitHooks( $heading, $contents );
  126. }
  127. protected function emitComment( $contents ) {
  128. $this->result .= $contents;
  129. }
  130. protected function emitTest( $test ) {
  131. $testName = false;
  132. foreach ( $test as $section ) {
  133. if ( $section['name'] === 'test' ) {
  134. $testName = rtrim( $section['contents'], "\n" );
  135. }
  136. }
  137. if ( isset( $this->deletions[$testName] ) ) {
  138. // Acknowledge deletion
  139. unset( $this->deletions[$testName] );
  140. return;
  141. }
  142. if ( isset( $this->changes[$testName] ) ) {
  143. $changes =& $this->changes[$testName];
  144. foreach ( $test as $i => $section ) {
  145. $sectionName = $section['name'];
  146. if ( isset( $changes[$sectionName] ) ) {
  147. $change = $changes[$sectionName];
  148. switch ( $change['op'] ) {
  149. case 'rename':
  150. $test[$i]['name'] = $change['value'];
  151. $test[$i]['headingLine'] = "!! {$change['value']}";
  152. break;
  153. case 'update':
  154. $test[$i]['contents'] = $change['value'];
  155. break;
  156. case 'delete':
  157. $test[$i]['deleted'] = true;
  158. break;
  159. default:
  160. throw new Exception( "Unknown op: ${change['op']}" );
  161. }
  162. // Acknowledge
  163. // Note that we use the old section name for the rename op
  164. unset( $changes[$sectionName] );
  165. }
  166. }
  167. }
  168. foreach ( $test as $section ) {
  169. if ( isset( $section['deleted'] ) ) {
  170. continue;
  171. }
  172. $this->result .= $section['headingLine'] . "\n";
  173. $this->result .= $section['contents'];
  174. }
  175. }
  176. protected function emitHooks( $heading, $contents ) {
  177. $this->result .= $contents;
  178. }
  179. }