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

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