Testing_DocTest
[ class tree: Testing_DocTest ] [ index: Testing_DocTest ] [ all elements ]

Source for file Default.php

Documentation is available at Default.php

  1. <?php
  2.  
  3. /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
  4.  
  5. /**
  6.  * This file is part of the PEAR Testing_DocTest package.
  7.  *
  8.  * PHP version 5
  9.  *
  10.  * LICENSE: This source file is subject to the MIT license that is available
  11.  * through the world-wide-web at the following URI:
  12.  * http://opensource.org/licenses/mit-license.php
  13.  *
  14.  * @category  Testing
  15.  * @package   Testing_DocTest
  16.  * @author    David JEAN LOUIS <izimobil@gmail.com>
  17.  * @copyright 2008 David JEAN LOUIS
  18.  * @license   http://opensource.org/licenses/mit-license.php MIT License
  19.  * @version   CVS: $Id$
  20.  * @link      http://pear.php.net/package/Testing_DocTest
  21.  * @since     File available since release 0.1.0
  22.  * @filesource
  23.  */
  24.  
  25. /**
  26.  * Required file.
  27.  */
  28. require_once 'Testing/DocTest/ParserInterface.php';
  29. require_once 'Testing/DocTest/TestCase.php';
  30.  
  31. /**
  32.  * DocTest Parser default class.
  33.  * Important note: this class will be refactored soon so do not rely on it yet
  34.  * if you want to subclass or customize Testing_DocTest.
  35.  *
  36.  * @category  Testing
  37.  * @package   Testing_DocTest
  38.  * @author    David JEAN LOUIS <izimobil@gmail.com>
  39.  * @copyright 2008 David JEAN LOUIS
  40.  * @license   http://opensource.org/licenses/mit-license.php MIT License
  41.  * @version   Release: 0.3.0
  42.  * @link      http://pear.php.net/package/Testing_DocTest
  43.  * @since     Class available since release 0.1.0
  44.  */
  45. class Testing_DocTest_Parser_Default implements Testing_DocTest_ParserInterface
  46. {
  47.     // doctest syntax prefix {{{
  48.  
  49.     /**
  50.      * Doctest syntax prefix default is a standard php inline comment: '//'
  51.      */
  52.     const SYNTAX_PREFIX = '//';
  53.  
  54.     // }}}
  55.     // Keywords constants {{{
  56.  
  57.     /**
  58.      * Keyword for the name of the doctest
  59.      */
  60.     const KW_DOCTEST_NAME = 'doctest';
  61.  
  62.     /**
  63.      * Keyword for the doctest flags
  64.      */
  65.     const KW_DOCTEST_FLAGS = 'flags';
  66.  
  67.     /**
  68.      * Keyword for the skip condition
  69.      */
  70.     const KW_DOCTEST_SKIP_IF = 'skip-if';
  71.  
  72.     /**
  73.      * Keyword for the ini settings
  74.      */
  75.     const KW_DOCTEST_INI_SET = 'ini-set';
  76.  
  77.     /**
  78.      * Keyword for the doctest expected result
  79.      */
  80.     const KW_DOCTEST_EXPECTS = 'expects';
  81.  
  82.     /**
  83.      * Keyword for the doctest expected file
  84.      */
  85.     const KW_DOCTEST_EXPECTS_FILE = 'expects-file';
  86.  
  87.     /**
  88.      * Keyword for the clean part
  89.      */
  90.     const KW_DOCTEST_CLEAN = 'clean';
  91.  
  92.     // }}}
  93.     // State constants {{{
  94.  
  95.     /**
  96.      * State after parsing a doctest line.
  97.      */
  98.     const STATE_DOCTEST = 1;
  99.  
  100.     /**
  101.      * State after parsing flags.
  102.      */
  103.     const STATE_FLAGS = 2;
  104.  
  105.     /**
  106.      * State after parsing a skip condition line.
  107.      */
  108.     const STATE_SKIP_IF = 3;
  109.  
  110.     /**
  111.      * State after parsing a ini-set line.
  112.      */
  113.     const STATE_INI_SET = 4;
  114.  
  115.     /**
  116.      * State after parsing expects line.
  117.      */
  118.     const STATE_EXPECTS = 5;
  119.  
  120.     /**
  121.      * State after parsing expects-file line.
  122.      */
  123.     const STATE_EXPECTS_FILE = 6;
  124.  
  125.     /**
  126.      * State after parsing code line.
  127.      */
  128.     const STATE_CODE = 7;
  129.  
  130.     /**
  131.      * State after parsing clean line.
  132.      */
  133.     const STATE_CLEAN = 8;
  134.  
  135.     // }}}
  136.     // Properties {{{
  137.  
  138.     /**
  139.      * Current state of the parser, null or one of the STATE_*  constants.
  140.      *
  141.      * @var int $_state 
  142.      * @access private
  143.      */
  144.     private $_state = null;
  145.  
  146.     /**
  147.      * Testing_DocTest_TestCase instance.
  148.      *
  149.      * @var object $_testCase 
  150.      * @access private
  151.      */
  152.     private $_testCase = null;
  153.  
  154.     // }}}
  155.     // parse() {{{
  156.  
  157.     /**
  158.      * Parse the files passed and return an array of type:
  159.      *
  160.      * <code>
  161.      * array(
  162.      *     'file1' => array($testCase1, $testCase2),
  163.      *     'file2' => array($testCase1, $testCase2),
  164.      * )
  165.      * </code>
  166.      *
  167.      * @param array $files an array of file pathes
  168.      *
  169.      * @access public
  170.      * @return array 
  171.      */
  172.     public function parse(array $files)
  173.     {
  174.         $ret = array();
  175.         $kw  = preg_quote(self::KW_DOCTEST_NAME'/')    '|'
  176.              . preg_quote(self::KW_DOCTEST_FLAGS'/')   '|'
  177.              . preg_quote(self::KW_DOCTEST_SKIP_IF'/''|'
  178.              . preg_quote(self::KW_DOCTEST_INI_SET'/''|'
  179.              . preg_quote(self::KW_DOCTEST_CLEAN'/')   '|'
  180.              . preg_quote(self::KW_DOCTEST_EXPECTS'/''|'
  181.              . preg_quote(self::KW_DOCTEST_EXPECTS_FILE'/');
  182.         foreach ($files as $file{
  183.             $testCaseArray $this->_parseFile($file);
  184.             foreach ($testCaseArray as $testCaseData{
  185.                 // split raw code into lines
  186.                 $docblocs $this->_extractCodeBlocs($testCaseData['docComment']);
  187.                 if (!isset($ret[$file])) {
  188.                     $ret[$file= array();
  189.                 }
  190.                 foreach ($docblocs as $docbloc{
  191.                     $this->_testCase        = new Testing_DocTest_TestCase();
  192.                     $this->_testCase->file  = $file;
  193.                     $this->_testCase->level = $testCaseData['level'];
  194.                     $this->_testCase->name  = $testCaseData['name'];
  195.                     // split string into an array of lines
  196.                     $lines preg_split('/(\n|\r\n)/'$docbloc);
  197.                     foreach ($lines as $i=>$l{
  198.                         // remove spaces and * at the beginning
  199.                         $l preg_replace('/^\s*\*\s?/'''$l);
  200.                         $p preg_quote(self::SYNTAX_PREFIX'/');
  201.                         if (preg_match("/^\s*$p\s?($kw):\s*(.*)$/"$l$m)) {
  202.                             switch ($m[1]{
  203.                             case self::KW_DOCTEST_NAME:
  204.                                 $this->_handleDoctestLine($m[2]);
  205.                                 break;
  206.                             case self::KW_DOCTEST_FLAGS:
  207.                                 $this->_handleFlagsLine($m[2]);
  208.                                 break;
  209.                             case self::KW_DOCTEST_SKIP_IF:
  210.                                 $this->_handleFlagsLine($m[2]);
  211.                                 break;
  212.                             case self::KW_DOCTEST_INI_SET:
  213.                                 $this->_handleIniSetLine($m[2]);
  214.                                 break;
  215.                             case self::KW_DOCTEST_EXPECTS:
  216.                                 $this->_handleExpectsLine($m[2]);
  217.                                 break;
  218.                             case self::KW_DOCTEST_EXPECTS_FILE:
  219.                                 $this->_handleExpectsFileLine($m[2]);
  220.                                 break;
  221.                             case self::KW_DOCTEST_CLEAN:
  222.                                 $this->_handleCleanLine($m[2]);
  223.                             }
  224.                         else if (preg_match('/^\s*'.$p.'\s?(.*)$/'$l$m)) {
  225.                             $this->_handleLineContinuation($m[1]);
  226.                         else {
  227.                             if (trim($l!= ''{
  228.                                 $this->_handleCodeLine($l);
  229.                             }
  230.                         }
  231.                     }
  232.                 
  233.                     // trim last eol
  234.                     $this->_testCase->expectedValue =
  235.                         substr($this->_testCase->expectedValue0-1);
  236.                     // reset state
  237.                     $this->_state = null;
  238.                     // append the test case
  239.                     $ret[$file][$this->_testCase;
  240.                 }
  241.             }
  242.         }
  243.         return $ret;
  244.     }
  245.  
  246.     // }}}
  247.     // _parseFile() {{{
  248.  
  249.     /**
  250.      * Parse the file $file and return an array of Testing_DocTest_TestCase
  251.      * instances.
  252.      *
  253.      * @param string $file path to the file to parse.
  254.      *
  255.      * @access private
  256.      * @return array 
  257.      */
  258.     private function _parseFile($file)
  259.     {
  260.         $return = array();
  261.         $tokens $this->_tokenize($file);
  262.         if (false === $tokens{
  263.             // return an empty array
  264.             return $return;
  265.         }
  266.         $curlyLevel = -1;
  267.         $curlyOpen  = -1;
  268.         $className  = null;
  269.         $inClass    = false;
  270.         while (false !== ($item each($tokens))) {
  271.             // memoize curly level in order to detect if we are inside a class
  272.             if (is_string($item['value'])) {
  273.                 if ($item['value'== '{'{
  274.                     $curlyLevel++;
  275.                 else if ($item['value'== '}' && --$curlyLevel == $curlyOpen{
  276.                     // curly is the close curly of current class
  277.                     $inClass = false;
  278.                 }
  279.                 continue;
  280.             }
  281.             list($id$token$line$item['value'];
  282.             // skip all tokens but doc comments
  283.             if ($id !== T_DOC_COMMENT{
  284.                 continue;
  285.             }
  286.             // find next token
  287.             $ids  = array(T_CLASST_FUNCTIONT_DOC_COMMENT);
  288.             $next $this->_findNextToken($ids$tokens);
  289.             if (false === $next && !empty($return)) {
  290.                 break;
  291.             }
  292.             // build Testing_DocTest_TestCase instance
  293.             $ret               = array();
  294.             $ret['docComment'$token;
  295.             $ret['file']       $file;
  296.             if (false === $next || T_DOC_COMMENT === $next[0]{
  297.                 $ret['name']  'test';
  298.                 $ret['level''file level';
  299.             else {
  300.                 $nToken $this->_findNextToken(T_STRING$tokens);
  301.                 if (false === $nToken{
  302.                     continue;
  303.                 }
  304.                 if ($next[0=== T_CLASS{
  305.                     $inClass      = true;
  306.                     $curlyOpen    $curlyLevel;
  307.                     $ret['name']  $nToken[1];
  308.                     $className    $nToken[1];
  309.                     $ret['level''class';
  310.                 else if ($inClass{
  311.                     $ret['name']  $className '::' $nToken[1];
  312.                     $ret['level''method';
  313.                 else {
  314.                     $ret['name']  $nToken[1];
  315.                     $ret['level''function';
  316.                 }
  317.             }
  318.             $return[$ret;
  319.         }
  320.         return $return;
  321.     }
  322.  
  323.     // }}}
  324.     // _tokenize() {{{
  325.  
  326.     /**
  327.      * Tokenize the file $file into an array of tokens using the builtin php
  328.      * tokenizer extension. Before tokenizing the method check that the file
  329.      * contains at least a doctest.
  330.      *
  331.      * @param string $file the file to parse.
  332.      *
  333.      * @access private
  334.      * @return array array of tokens
  335.      */
  336.     private function _tokenize($file)
  337.     
  338.         $data file_get_contents($file);
  339.         // speed improvement, don't bother tokenizing file if it does not 
  340.         // contain any doctest
  341.         if (false === strstr($dataself::KW_DOCTEST_EXPECTS)) {
  342.             return array();
  343.         }
  344.         return token_get_all($data);
  345.     }
  346.  
  347.     // }}}
  348.     // _findNextToken() {{{
  349.  
  350.     /**
  351.      * Find the next token matching the id $id and return it or return false if
  352.      * no matching token is found.
  353.      *
  354.      * @param mixed $id      id or array of ids the token must match
  355.      * @param array &$tokens tokens array passed by reference
  356.      *
  357.      * @access private
  358.      * @return array array of tokens
  359.      */
  360.     private function _findNextToken($id&$tokens)
  361.     
  362.         $next current($tokens);
  363.         while ($next !== false{
  364.             if (!is_string($next)) {
  365.                 if (is_int($id&& $next[0=== $id{
  366.                     return $next;
  367.                 }
  368.                 if (is_array($id&& in_array($next[0]$id)) {
  369.                     return $next;
  370.                 }
  371.             }
  372.             // move to next token
  373.             $next next($tokens);
  374.         }
  375.         return false;
  376.     }
  377.  
  378.     // }}}
  379.     // _extractCodeBlocs() {{{
  380.  
  381.     /**
  382.      * Extract all <code></code> blocs in the given raw docstring.
  383.      *
  384.      * @param string $docstring raw docstring
  385.      *
  386.      * @access private
  387.      * @return array an array of code blocs strings.
  388.      */
  389.     private function _extractCodeBlocs($docstring)
  390.     {
  391.         $ret = array();
  392.         // extract <code></code> blocks, we use preg_match_all because there 
  393.         // could be more than one code block by docstring
  394.         $rx '/<code>[\s\*]*(<[\?\%](php)?)?\s*' 
  395.             . '(.*?)\s*([\?\%]>)?[\s\*]*<\/code>/si';
  396.         preg_match_all($rx$docstring$tokens);
  397.         if (isset($tokens[3]&& is_array($tokens[3])) {
  398.             foreach ($tokens[3as $i => $token{
  399.                 if (!$this->_hasDocTest($token)) {
  400.                     // not a doctest
  401.                     continue;
  402.                 }
  403.                 $ret[$token;
  404.             }
  405.         }
  406.         return $ret;
  407.     }
  408.  
  409.     // }}}
  410.     // _hasDocTest() {{{
  411.  
  412.     /**
  413.      * Return true if the string data provided contains a doctest.
  414.      *
  415.      * @param string $data string data
  416.      *
  417.      * @access private
  418.      * @return boolean 
  419.      */
  420.     private function _hasDocTest($data)
  421.     {
  422.         $p preg_quote(self::SYNTAX_PREFIX'/');
  423.         $k preg_quote(self::KW_DOCTEST_EXPECTS'/');
  424.         return preg_match("/$p\s?$k/m"$data);
  425.     }
  426.  
  427.     // }}}
  428.     // _handleDoctestLine() {{{
  429.  
  430.     /**
  431.      * Parse the doctest line provided.
  432.      *
  433.      * @param string $line the line of code to parse
  434.      *
  435.      * @access private
  436.      * @return void 
  437.      * @throws Testing_DocTest_Exception
  438.      */
  439.     private function _handleDoctestLine($line)
  440.     {
  441.         $states = array(nullself::STATE_FLAGSself::STATE_DOCTEST,
  442.             self::STATE_SKIP_IFself::STATE_INI_SET);
  443.         if (!in_array($this->_state$states)) {
  444.             throw new Testing_DocTest_Exception("Unexpected doctest line: $line");
  445.         }
  446.         $this->_testCase->altname .= $line;
  447.         $this->_state              = self::STATE_DOCTEST;
  448.     }
  449.  
  450.     // }}}
  451.     // _handleFlagsLine() {{{
  452.  
  453.     /**
  454.      * Parse the flag line provided.
  455.      *
  456.      * @param string $line The flag line to parse
  457.      *
  458.      * @access private
  459.      * @return void 
  460.      * @throws Testing_DocTest_Exception
  461.      */
  462.     private function _handleFlagsLine($line)
  463.     {
  464.         $states = array(nullself::STATE_FLAGSself::STATE_DOCTEST,
  465.             self::STATE_SKIP_IFself::STATE_INI_SET);
  466.         if (!in_array($this->_state$states)) {
  467.             throw new Testing_DocTest_Exception("Unexpected flags line: $line");
  468.         }
  469.         $flags explode(','$line);
  470.         foreach ($flags as $flag{
  471.             $const 'Testing_DocTest::FLAG_' strtoupper(trim($flag));
  472.             if (defined($const)) {
  473.                 $this->_testCase->flags |= constant($const);
  474.             }
  475.         }
  476.         $this->_state = self::STATE_FLAGS;
  477.     }
  478.  
  479.     // }}}
  480.     // _handleExpectsLine() {{{
  481.  
  482.     /**
  483.      * Parse the expects line provided.
  484.      *
  485.      * @param string $line the expects line to parse
  486.      *
  487.      * @access private
  488.      * @return void 
  489.      * @throws Testing_DocTest_Exception
  490.      */
  491.     private function _handleExpectsLine($line)
  492.     {
  493.         $states = array(self::STATE_CODEself::STATE_EXPECTS);
  494.         if (!in_array($this->_state$states)) {
  495.             throw new Exception("unexpected expects line: $line");
  496.         }
  497.         $this->_testCase->expectedValue .= $line;
  498.         // handle line continuation
  499.         if (substr(trim($line)-1!== '\\'{
  500.             $this->_testCase->expectedValue .= "\n";
  501.         else {
  502.             $this->_testCase->expectedValue = 
  503.                 trim($this->_testCase->expectedValue'\\');
  504.         }
  505.         $this->_state = self::STATE_EXPECTS;
  506.     }
  507.  
  508.     // }}}
  509.     // _handleExpectsFileLine() {{{
  510.  
  511.     /**
  512.      * Parse the expects-file line provided.
  513.      *
  514.      * @param string $line the expects-file line to parse
  515.      *
  516.      * @access private
  517.      * @return void 
  518.      * @throws Testing_DocTest_Exception
  519.      */
  520.     private function _handleExpectsFileLine($line)
  521.     {
  522.         $states = array(self::STATE_CODEself::STATE_EXPECTS_FILE);
  523.         if (!in_array($this->_state$states)) {
  524.             throw new Exception("unexpected expects-file line: $line");
  525.         }
  526.         $f realpath(trim($line));
  527.         if (false === ($contents @file_get_contents($f))) {
  528.             throw new Testing_DocTest_Exception("Unable to read expects file $f");
  529.         }
  530.         $this->_testCase->expectedValue = $contents;
  531.         $this->_state                   = self::STATE_EXPECTS_FILE;
  532.     }
  533.  
  534.     // }}}
  535.     // _handleCodeLine() {{{
  536.  
  537.     /**
  538.      * Parse the code line provided.
  539.      *
  540.      * @param string $line the code line to parse
  541.      *
  542.      * @access private
  543.      * @return void 
  544.      * @throws Testing_DocTest_Exception
  545.      */
  546.     private function _handleCodeLine($line)
  547.     {
  548.         $states = array(self::STATE_EXPECTSself::STATE_EXPECTS_FILE);
  549.         if (in_array($this->_state$states)) {
  550.             throw new Testing_DocTest_Exception("Unexpected code line: $line");
  551.         }
  552.         $this->_testCase->code .= rtrim($line"\n";
  553.         $this->_state           = self::STATE_CODE;
  554.     }
  555.  
  556.     // }}}
  557.     // _handleSkipIfLine() {{{
  558.  
  559.     /**
  560.      * Parse the skip-if line provided.
  561.      *
  562.      * @param string $line the skip-if line to parse
  563.      *
  564.      * @access private
  565.      * @return void 
  566.      * @throws Testing_DocTest_Exception
  567.      */
  568.     private function _handleSkipIfLine($line)
  569.     {
  570.         $states = array(nullself::STATE_FLAGSself::STATE_DOCTEST,
  571.             self::STATE_SKIP_IFself::STATE_INI_SET);
  572.         if (!in_array($this->_state$states)) {
  573.             throw new Testing_DocTest_Exception("Unexpected skip-if line: $line");
  574.         }
  575.         $this->_testCase->skipIfCode .= rtrim($line"\n";
  576.         $this->_state                 = self::STATE_SKIP_IF;
  577.     }
  578.  
  579.     // }}}
  580.     // _handleIniSetLine() {{{
  581.  
  582.     /**
  583.      * Parse the ini-set line provided.
  584.      *
  585.      * @param string $line the ini-set line to parse
  586.      *
  587.      * @access private
  588.      * @return void 
  589.      * @throws Testing_DocTest_Exception
  590.      */
  591.     private function _handleIniSetLine($line)
  592.     {
  593.         $states = array(nullself::STATE_FLAGSself::STATE_DOCTEST,
  594.             self::STATE_SKIP_IFself::STATE_INI_SET);
  595.         if (!in_array($this->_state$states)) {
  596.             throw new Testing_DocTest_Exception("Unexpected ini-set line: $line");
  597.         }
  598.         $a explode('='trim($line));
  599.         if (count($a!= 2{
  600.             throw new Testing_DocTest_Exception("Malformed ini-set line: $line");
  601.         }
  602.         $this->_testCase->iniSettings[$a[0]] $a[1];
  603.         $this->_state                        = self::STATE_INI_SET;
  604.     }
  605.  
  606.     // }}}
  607.     // _handleCleanLine() {{{
  608.  
  609.     /**
  610.      * Parse the clean line provided.
  611.      *
  612.      * @param string $line the clean line to parse
  613.      *
  614.      * @access private
  615.      * @return void 
  616.      * @throws Testing_DocTest_Exception
  617.      */
  618.     private function _handleCleanLine($line)
  619.     {
  620.         $states = array(self::STATE_EXPECTSself::STATE_EXPECTS_FILE,
  621.             self::STATE_CLEAN);
  622.         if (!in_array($this->_state$states)) {
  623.             throw new Testing_DocTest_Exception("Unexpected clean line: $line");
  624.         }
  625.         $this->_testCase->cleanCode .= rtrim($line"\n";
  626.         $this->_state                = self::STATE_CLEAN;
  627.     }
  628.  
  629.     // }}}
  630.     // _handleLineContinuation() {{{
  631.  
  632.     /**
  633.      * Parse a line continuation.
  634.      *
  635.      * @param string $line the line to parse
  636.      *
  637.      * @access private
  638.      * @return void 
  639.      */
  640.     private function _handleLineContinuation($line)
  641.     {
  642.         switch ($this->_state{
  643.         case self::STATE_EXPECTS:
  644.             $this->_handleExpectsLine($line);
  645.             break;
  646.         case self::STATE_FLAGS:
  647.             $this->_handleFlagsLine($line);
  648.             break;
  649.         case self::STATE_DOCTEST:
  650.             $this->_handleDoctestLine($line);
  651.             break;
  652.         case self::STATE_SKIP_IF:
  653.             $this->_handleSkipIfLine($line);
  654.             break;
  655.         case self::STATE_INI_SET:
  656.             $this->_handleIniSetLine($line);
  657.             break;
  658.         case self::STATE_CLEAN:
  659.             $this->_handleCleanLine($line);
  660.         }
  661.     }
  662.  
  663.     // }}}
  664. }

Documentation generated on Mon, 11 Mar 2019 15:18:32 -0400 by phpDocumentor 1.4.4. PEAR Logo Copyright © PHP Group 2004.