Version: current

      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
     70     function init() {
     71         parent::init();
     72
     73         $canAccess = (Director::isDev() || Director::is_cli() || Permission::check("ADMIN"));
     74         if(!$canAccess) return Security::permissionFailure($this);
     75     }
     76
     77     function createClass($token) {
     78         $this->currentClass = array();
     79     }
     80     function setClassName($token) {
     81         $this->currentClass['name'] = $token[1];
     82     }
     83     function completeClass($token) {
     84         $this->classes[] = $this->currentClass;
     85     }
     86
     87     function createMethod($token) {
     88         $this->currentMethod = array();
     89         $this->currentMethod['content'] = "<pre>";
     90     }
     91     function setMethodName($token) {
     92         $this->currentMethod['name'] = $token[1];
     93     }
     94     function appendMethodComment($token) {
     95         if(substr($token[1],0,2) == '/*') {
     96             $comment = preg_replace('/^\/\*/','',$token[1]);
     97             $comment = preg_replace('/\*\/$/','',$comment);
     98             $comment = preg_replace('/\n[\t ]*\* */m',"\n",$comment);
     99
    100             $this->closeOffMethodContentPre();
    101             $this->currentMethod['content'] .= "<p>$comment</p><pre>";
    102         } else {
    103             $this->currentMethod['content'] .= $this->renderToken($token);
    104         }
    105
    106     }
    107     function appendMethodContent($token) {
    108         if($this->potentialMethodCall) {
    109             $this->currentMethod['content'] .= $this->potentialMethodCall;
    110             $this->potentialMethodCall = "";
    111         }
    112         $this->currentMethod['content'] .= $this->renderToken($token);
    113     }
    114     function completeMethod($token) {
    115         $this->closeOffMethodContentPre();
    116         $this->currentMethod['content'] = str_replace("\n\t\t","\n",$this->currentMethod['content']);
    117         $this->currentClass['methods'][] = $this->currentMethod;
    118     }
    119
    120     protected $potentialMethodCall = "";
    121     function potentialMethodCall($token) {
    122         $this->potentialMethodCall .= $this->renderToken($token);
    123     }
    124     function completeMethodCall($token) {
    125         $this->potentialMethodCall .= $this->renderToken($token);
    126         if(strpos($this->potentialMethodCall, '-&gt;</span><span class="T_STRING">assert') !== false) {
    127             $this->currentMethod['content'] .= "<strong>" . $this->potentialMethodCall . "</strong>";
    128         } else {
    129             $this->currentMethod['content'] .= $this->potentialMethodCall;
    130         }
    131         $this->potentialMethodCall = "";
    132     }
    133
    134     /**
    135      * Finish the "pre" block in method content.
    136      * Will remove whitespace and empty "pre" blocks
    137      */
    138     function closeOffMethodContentPre() {
    139         $this->currentMethod['content'] = trim($this->currentMethod['content']);
    140         if(substr($this->currentMethod['content'],-5) == '<pre>') $this->currentMethod['content'] = substr($this->currentMethod['content'], 0,-5);
    141         else $this->currentMethod['content'] .= '</pre>';
    142     }
    143
    144     /**
    145      * Render the given token as HTML
    146      */
    147     function renderToken($token) {
    148         $tokenContent = htmlentities(is_array($token) ? $token[1] : $token);
    149         $tokenName = is_array($token) ? token_name($token[0]) : 'T_PUNCTUATION';
    150
    151         switch($tokenName) {
    152             case "T_WHITESPACE":
    153                 return $tokenContent;
    154             default:
    155                 return "<span class=\"$tokenName\">$tokenContent</span>";
    156         }
    157     }
    158
    159     protected $classes = array();
    160     protected $currentMethod, $currentClass;
    161
    162     function Content() {
    163         $className = $this->urlParams['ID'];
    164         if($className && ClassInfo::exists($className)) {
    165             return $this->testAnalysis(getClassFile($className));
    166         } else {
    167             $result = "<h1>View any of the following test classes</h1>";
    168             $classes = ClassInfo::subclassesFor('SapphireTest');
    169             ksort($classes);
    170             foreach($classes as $className) {
    171                 if($className == 'SapphireTest') continue;
    172                 $result .= "<li><a href=\"TestViewer/show/$className\">$className</a></li>";
    173             }
    174             return $result;
    175         }
    176     }
    177
    178     function testAnalysis($file) {
    179         $content = file_get_contents($file);
    180         $tokens = token_get_all($content);
    181
    182         // Execute a finite-state-machine with a built-in state stack
    183         // This FSM+stack gives us enough expressive power for simple PHP parsing
    184         $state = "start";
    185         $stateStack = array();
    186
    187         //echo "<li>state $state";
    188         foreach($tokens as $token) {
    189             // Get token name - some tokens are arrays, some arent'
    190             if(is_array($token)) $tokenName = $token[0]; else $tokenName = $token;
    191             //echo "<li>token '$tokenName'";
    192
    193             // Find the rule for that token in the current state
    194             if(isset(self::$fsm[$state][$tokenName])) $rule = self::$fsm[$state][$tokenName];
    195             else if(isset(self::$fsm[$state]['*'])) $rule = self::$fsm[$state]['*'];
    196             else $rule = null;
    197
    198             // Check to see if we have specified multiple rules depending on whether the stack is populated
    199             if(is_array($rule) && array_keys($rule) == array('hasstack', 'nostack')) {
    200                 if($stateStack) $rule = $rule['hasstack'];
    201                 else $rule = $rule = $rule['nostack'];
    202             }
    203
    204             if(is_array($rule)) {
    205                 list($destState, $methodName) = $rule;
    206                 $this->$methodName($token);
    207             } else if($rule) {
    208                 $destState = $rule;
    209             } else {
    210                 $destState = null;
    211             }
    212             //echo "<li>->state $destState";
    213
    214             if(preg_match('/!(push|pop)(\/[a-zA-Z0-9]+)?/', $destState, $parts)) {
    215                 $action = $parts[1];
    216                 $argument = isset($parts[2]) ? substr($parts[2],1) : null;
    217                 $destState = null;
    218
    219                 switch($action) {
    220                     case "push":
    221                         $stateStack[] = $state;
    222                         if($argument) $destState = $argument;
    223                         break;
    224
    225                     case "pop":
    226                         if($stateStack) $destState = array_pop($stateStack);
    227                         else if($argument) $destState = $argument;
    228                         else user_error("State transition '!pop' was attempted with an empty state-stack and no default option specified.", E_USER_ERROR);
    229                 }
    230             }
    231
    232             if($destState) $state = $destState;
    233             if(!isset(self::$fsm[$state])) user_error("Transition to unrecognised state '$state'", E_USER_ERROR);
    234         }
    235
    236         $subclasses = ClassInfo::subclassesFor('SapphireTest');
    237         foreach($this->classes as $classDef) {
    238             if(in_array($classDef['name'], $subclasses)) {
    239                 echo "<h1>$classDef[name]</h1>";
    240                 if($classDef['methods']) foreach($classDef['methods'] as $method) {
    241                     if(substr($method['name'],0,4) == 'test') {
    242                         //$title = ucfirst(strtolower(preg_replace('/([a-z])([A-Z])/', '$1 $2', substr($method['name'], 4))));
    243                         $title = $method['name'];
    244
    245                         echo "<h2>$title</h2>";
    246                         echo $method['content'];
    247                     }
    248                 }
    249             }
    250
    251         }
    252     }
    253 }

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -