PageRenderTime 55ms CodeModel.GetById 16ms RepoModel.GetById 1ms app.codeStats 0ms

/sapphire/dev/TestViewer.php

https://github.com/benbruscella/vpcounselling.com
PHP | 253 lines | 201 code | 25 blank | 27 comment | 44 complexity | 6e4d15654e12c5e62272846ce7289dc6 MD5 | raw file
  1. <?php
  2. /**
  3. * Allows human reading of a test in a format suitable for agile documentation
  4. * @package sapphire
  5. * @subpackage testing
  6. */
  7. class TestViewer extends Controller {
  8. /**
  9. * Define a simple finite state machine.
  10. * Top keys are the state names. 'start' is the first state, and 'die' is the error state.
  11. * Inner keys are token names/codes. The values are either a string, new state, or an array(new state, handler method).
  12. * The handler method will be passed the PHP token as an argument, and is expected to populate a property of the object.
  13. */
  14. static $fsm = array(
  15. 'start' => array(
  16. T_CLASS => array('className','createClass'),
  17. ),
  18. 'className' => array(
  19. T_STRING => array('classSpec', 'setClassName'),
  20. ),
  21. 'classSpec' => array(
  22. '{' => 'classBody',
  23. ),
  24. 'classBody' => array(
  25. T_FUNCTION => array('methodName','createMethod'),
  26. '}' => array('start', 'completeClass'),
  27. ),
  28. 'methodName' => array(
  29. T_STRING => array('methodSpec', 'setMethodName'),
  30. ),
  31. 'methodSpec' => array(
  32. '{' => 'methodBody',
  33. ),
  34. 'methodBody' => array(
  35. '{' => array('!push','appendMethodContent'),
  36. '}' => array(
  37. 'hasstack' => array('!pop', 'appendMethodContent'),
  38. 'nostack' => array('classBody', 'completeMethod'),
  39. ),
  40. T_VARIABLE => array('variable', 'potentialMethodCall'),
  41. T_COMMENT => array('', 'appendMethodComment'),
  42. '*' => array('', 'appendMethodContent'),
  43. ),
  44. 'variable' => array(
  45. T_OBJECT_OPERATOR => array('variableArrow', 'potentialMethodCall'),
  46. '*' => array('methodBody', 'appendMethodContent'),
  47. ),
  48. 'variableArrow' => array(
  49. T_STRING => array('methodOrProperty', 'potentialMethodCall'),
  50. T_WHITESPACE => array('', 'potentialMethodCall'),
  51. '*' => array('methodBody', 'appendMethodContent'),
  52. ),
  53. 'methodOrProperty' => array(
  54. '(' => array('methodCall', 'potentialMethodCall'),
  55. T_WHITESPACE => array('', 'potentialMethodCall'),
  56. '*' => array('methodBody', 'appendMethodContent'),
  57. ),
  58. 'methodCall' => array(
  59. '(' => array('!push/nestedInMethodCall', 'potentialMethodCall'),
  60. ')' => array('methodBody', 'completeMethodCall'),
  61. '*' => array('', 'potentialMethodCall'),
  62. ),
  63. 'nestedInMethodCall' => array(
  64. '(' => array('!push', 'potentialMethodCall'),
  65. ')' => array('!pop', 'potentialMethodCall'),
  66. '*' => array('', 'potentialMethodCall'),
  67. ),
  68. );
  69. function init() {
  70. parent::init();
  71. $canAccess = (Director::isDev() || Director::is_cli() || Permission::check("ADMIN"));
  72. if(!$canAccess) return Security::permissionFailure($this);
  73. }
  74. function createClass($token) {
  75. $this->currentClass = array();
  76. }
  77. function setClassName($token) {
  78. $this->currentClass['name'] = $token[1];
  79. }
  80. function completeClass($token) {
  81. $this->classes[] = $this->currentClass;
  82. }
  83. function createMethod($token) {
  84. $this->currentMethod = array();
  85. $this->currentMethod['content'] = "<pre>";
  86. }
  87. function setMethodName($token) {
  88. $this->currentMethod['name'] = $token[1];
  89. }
  90. function appendMethodComment($token) {
  91. if(substr($token[1],0,2) == '/*') {
  92. $comment = preg_replace('/^\/\*/','',$token[1]);
  93. $comment = preg_replace('/\*\/$/','',$comment);
  94. $comment = preg_replace('/\n[\t ]*\* */m',"\n",$comment);
  95. $this->closeOffMethodContentPre();
  96. $this->currentMethod['content'] .= "<p>$comment</p><pre>";
  97. } else {
  98. $this->currentMethod['content'] .= $this->renderToken($token);
  99. }
  100. }
  101. function appendMethodContent($token) {
  102. if($this->potentialMethodCall) {
  103. $this->currentMethod['content'] .= $this->potentialMethodCall;
  104. $this->potentialMethodCall = "";
  105. }
  106. $this->currentMethod['content'] .= $this->renderToken($token);
  107. }
  108. function completeMethod($token) {
  109. $this->closeOffMethodContentPre();
  110. $this->currentMethod['content'] = str_replace("\n\t\t","\n",$this->currentMethod['content']);
  111. $this->currentClass['methods'][] = $this->currentMethod;
  112. }
  113. protected $potentialMethodCall = "";
  114. function potentialMethodCall($token) {
  115. $this->potentialMethodCall .= $this->renderToken($token);
  116. }
  117. function completeMethodCall($token) {
  118. $this->potentialMethodCall .= $this->renderToken($token);
  119. if(strpos($this->potentialMethodCall, '-&gt;</span><span class="T_STRING">assert') !== false) {
  120. $this->currentMethod['content'] .= "<strong>" . $this->potentialMethodCall . "</strong>";
  121. } else {
  122. $this->currentMethod['content'] .= $this->potentialMethodCall;
  123. }
  124. $this->potentialMethodCall = "";
  125. }
  126. /**
  127. * Finish the "pre" block in method content.
  128. * Will remove whitespace and empty "pre" blocks
  129. */
  130. function closeOffMethodContentPre() {
  131. $this->currentMethod['content'] = trim($this->currentMethod['content']);
  132. if(substr($this->currentMethod['content'],-5) == '<pre>') $this->currentMethod['content'] = substr($this->currentMethod['content'], 0,-5);
  133. else $this->currentMethod['content'] .= '</pre>';
  134. }
  135. /**
  136. * Render the given token as HTML
  137. */
  138. function renderToken($token) {
  139. $tokenContent = htmlentities(is_array($token) ? $token[1] : $token);
  140. $tokenName = is_array($token) ? token_name($token[0]) : 'T_PUNCTUATION';
  141. switch($tokenName) {
  142. case "T_WHITESPACE":
  143. return $tokenContent;
  144. default:
  145. return "<span class=\"$tokenName\">$tokenContent</span>";
  146. }
  147. }
  148. protected $classes = array();
  149. protected $currentMethod, $currentClass;
  150. function Content() {
  151. $className = $this->urlParams['ID'];
  152. if($className && ClassInfo::exists($className)) {
  153. return $this->testAnalysis(getClassFile($className));
  154. } else {
  155. $result = "<h1>View any of the following test classes</h1>";
  156. $classes = ClassInfo::subclassesFor('SapphireTest');
  157. ksort($classes);
  158. foreach($classes as $className) {
  159. if($className == 'SapphireTest') continue;
  160. $result .= "<li><a href=\"TestViewer/show/$className\">$className</a></li>";
  161. }
  162. return $result;
  163. }
  164. }
  165. function testAnalysis($file) {
  166. $content = file_get_contents($file);
  167. $tokens = token_get_all($content);
  168. // Execute a finite-state-machine with a built-in state stack
  169. // This FSM+stack gives us enough expressive power for simple PHP parsing
  170. $state = "start";
  171. $stateStack = array();
  172. //echo "<li>state $state";
  173. foreach($tokens as $token) {
  174. // Get token name - some tokens are arrays, some arent'
  175. if(is_array($token)) $tokenName = $token[0]; else $tokenName = $token;
  176. //echo "<li>token '$tokenName'";
  177. // Find the rule for that token in the current state
  178. if(isset(self::$fsm[$state][$tokenName])) $rule = self::$fsm[$state][$tokenName];
  179. else if(isset(self::$fsm[$state]['*'])) $rule = self::$fsm[$state]['*'];
  180. else $rule = null;
  181. // Check to see if we have specified multiple rules depending on whether the stack is populated
  182. if(is_array($rule) && array_keys($rule) == array('hasstack', 'nostack')) {
  183. if($stateStack) $rule = $rule['hasstack'];
  184. else $rule = $rule = $rule['nostack'];
  185. }
  186. if(is_array($rule)) {
  187. list($destState, $methodName) = $rule;
  188. $this->$methodName($token);
  189. } else if($rule) {
  190. $destState = $rule;
  191. } else {
  192. $destState = null;
  193. }
  194. //echo "<li>->state $destState";
  195. if(preg_match('/!(push|pop)(\/[a-zA-Z0-9]+)?/', $destState, $parts)) {
  196. $action = $parts[1];
  197. $argument = isset($parts[2]) ? substr($parts[2],1) : null;
  198. $destState = null;
  199. switch($action) {
  200. case "push":
  201. $stateStack[] = $state;
  202. if($argument) $destState = $argument;
  203. break;
  204. case "pop":
  205. if($stateStack) $destState = array_pop($stateStack);
  206. else if($argument) $destState = $argument;
  207. else user_error("State transition '!pop' was attempted with an empty state-stack and no default option specified.", E_USER_ERROR);
  208. }
  209. }
  210. if($destState) $state = $destState;
  211. if(!isset(self::$fsm[$state])) user_error("Transition to unrecognised state '$state'", E_USER_ERROR);
  212. }
  213. $subclasses = ClassInfo::subclassesFor('SapphireTest');
  214. foreach($this->classes as $classDef) {
  215. if(in_array($classDef['name'], $subclasses)) {
  216. echo "<h1>$classDef[name]</h1>";
  217. if($classDef['methods']) foreach($classDef['methods'] as $method) {
  218. if(substr($method['name'],0,4) == 'test') {
  219. //$title = ucfirst(strtolower(preg_replace('/([a-z])([A-Z])/', '$1 $2', substr($method['name'], 4))));
  220. $title = $method['name'];
  221. echo "<h2>$title</h2>";
  222. echo $method['content'];
  223. }
  224. }
  225. }
  226. }
  227. }
  228. }