├── .gitignore ├── .travis.yml ├── PreCommitManager.class.php ├── README ├── checks ├── BasePreCommitCheck.class.php ├── EmptyCommentCheck.class.php ├── NoTabsCheck.class.php └── TicketReferenceCheck.class.php ├── pre-commit.tmpl ├── svn ├── svn_functions.php └── svn_functions.test.php ├── svn_pre_commit_hook.php └── test ├── base ├── PreCommitCheckTest.php ├── PreCommitManagerTest.php └── svn_pre_commit_hookTest.php ├── checks ├── EmptyCommentCheckTest.php ├── NoTabsCheckTest.php └── TicketReferenceCheckTest.php ├── lime └── lime.php └── run_all.php /.gitignore: -------------------------------------------------------------------------------- 1 | .buildpath 2 | .project 3 | .settings 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 5.3 4 | - 5.4 5 | script: "php test/run_all.php" -------------------------------------------------------------------------------- /PreCommitManager.class.php: -------------------------------------------------------------------------------- 1 | repoName = $args[0]; 18 | $this->trxNum = $args[1]; 19 | 20 | // Read potential options 21 | $this->options = array(); 22 | for ($i=2; $i < count($args); $i++){ 23 | if (strpos($args[$i], '--') !== 0){ 24 | throw new Exception("Invalid argument [".$args[$i]."], all options must start by '--'"); 25 | } 26 | if (strpos($args[$i], '=') === false){ 27 | $optName = $args[$i]; 28 | $optValue = true; 29 | } 30 | else { 31 | list($optName, $optValue) = explode('=', $args[$i]); 32 | } 33 | $this->options[substr($optName,2)] = $optValue; 34 | } 35 | 36 | // Reject invalid one 37 | $invalid = array_diff(array_keys($this->options), array('test-mode', 'include', 'exclude')); 38 | if (count($invalid)) { 39 | throw new Exception("Invalid option name ".json_encode($invalid)); 40 | } 41 | 42 | return $this->options; 43 | } 44 | 45 | public function getChecksToProcess(){ 46 | 47 | // Build up the list 48 | if (isset($this->options['include'])){ 49 | $checks = explode(':', $this->options['include']); 50 | } 51 | else { 52 | $checks = array(); 53 | foreach (scandir($this->getCheckDirectory()) as $scriptName) { 54 | if ( substr($scriptName,strlen($scriptName)-15) == 'Check.class.php' && $scriptName != "BasePreCommitCheck.class.php") { 55 | $checks[] = substr($scriptName,0,strlen($scriptName)-15); 56 | } 57 | } 58 | } 59 | 60 | // Remove exculded 61 | if (isset($this->options['exclude'])){ 62 | $exculded = explode(':', $this->options['exclude']); 63 | if (count(array_diff($exculded, $checks)) > 0){ 64 | throw new Exception("Invalid check to exculde: ".json_encode(array_diff($exculded, $checks))); 65 | } 66 | $checks = array_diff($checks, $exculded); 67 | } 68 | 69 | return $checks; 70 | } 71 | 72 | public function getCheckDirectory(){ 73 | return dirname(__FILE__).DIRECTORY_SEPARATOR.'checks'; 74 | } 75 | 76 | 77 | 78 | public function processChecks(){ 79 | 80 | // Include the SVN base functions 81 | $svnScript = 'svn_functions.'.(isset($this->options['test-mode'])?'test.':'').'php'; 82 | require(dirname(__FILE__).DIRECTORY_SEPARATOR.'svn'.DIRECTORY_SEPARATOR.$svnScript); 83 | 84 | // Read the message and the file changed 85 | $mess = svn_get_commit_message($this->repoName, $this->trxNum); 86 | $fileChanges = svn_get_commited_files($this->repoName, $this->trxNum); 87 | 88 | // Run all the script 89 | $this->checksWithError = array(); 90 | foreach ($this->getChecksToProcess() as $checkName) { 91 | try { 92 | require_once $this->getCheckDirectory().DIRECTORY_SEPARATOR.$checkName.'Check.class.php'; 93 | $className = $checkName.'Check'; 94 | $check = new $className($mess); 95 | $check->runCheck($fileChanges); 96 | if ($check->fail()){ 97 | $this->checksWithError[] = $check; 98 | } 99 | } 100 | catch (Exception $e){ 101 | throw new Exception("Error in the check subscript: $checkName\n"); 102 | } 103 | } 104 | } 105 | 106 | public function allCheckPassed(){ 107 | return is_array($this->checksWithError) && count($this->checksWithError) == 0; 108 | } 109 | 110 | public function getErrorMsg(){ 111 | // Generate a human message with errors 112 | $resume = "The following pre commit check fail:\n"; 113 | $detail = strtoupper("\nDetail of the checks errors:\n"); 114 | foreach ($this->checksWithError as $check){ 115 | $resume .= ' * '.$check->getTitle().': '.$check->renderErrorSummary()."\n"; 116 | $detail .= $check->getTitle().":\n".$check->renderErrorDetail()."\n".$check->renderInstructions()."\n"; 117 | } 118 | return "\n\nPRE COMMIT HOOK FAIL:\n".$resume.$detail; 119 | } 120 | 121 | } -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | PHP SVN HOOKS 2 | ============= 3 | 4 | This micro projet allow you to easily write SVN pre_commit hook in PHP. 5 | 6 | Installation 7 | ------------ 8 | 9 | * Just deploy this project to your server. 10 | * Copy the pre_commit.tmpl to the repository of your choice, rename it to pre_commit 11 | * Edit and adapt the pre_commmit script to match the project directory 12 | 13 | Create your own hook 14 | -------------------- 15 | 16 | * Add a new file XXXCheck.class.php in the checks directory 17 | * Extend the class BasePreCommitCheck 18 | * Override the mandatory methods: 19 | * getTitle(); 20 | * renderErrorSummary(); 21 | * Override the methods of your choice, according to what you wanna test: 22 | * checkSvnComment($comment); 23 | * checkFileLine($file, $pos, $content); 24 | * checkFullFile($lines, $filename) 25 | 26 | 27 | Testing 28 | ------- 29 | 30 | As it's can be panful to process a commit each time you wanna test, there is test suite avaliable based on lime. 31 | Just go to the root folder and run 32 | php test/run_all.php 33 | 34 | To run a specific check test, call directly the test file, for exemple: 35 | php test/checks/NoTabCheckTest.php 36 | 37 | To write your own test, just copy and paste and existing test file, and adapt it! 38 | 39 | 40 | How to contribute 41 | ----------------- 42 | 43 | If you want to contribute please fell to fork or to send your comment. I will be happy to complete this repo according to your needs... 44 | -------------------------------------------------------------------------------- /checks/BasePreCommitCheck.class.php: -------------------------------------------------------------------------------- 1 | svnComment = $svnComment; 12 | $this->parseOptions(); 13 | } 14 | 15 | abstract function getTitle(); 16 | 17 | abstract function renderErrorSummary(); 18 | 19 | public function runCheck($svnCommitedFiles){ 20 | 21 | // Check on the comment 22 | $result = $this->checkSvnComment($this->svnComment); 23 | if ($result !== null){ 24 | $this->globalError[] = $result; 25 | } 26 | 27 | // Check on the files 28 | foreach ($svnCommitedFiles as $filename => $lines){ 29 | 30 | //Check the entire content 31 | if($fileResult = $this->checkFullFile($lines, $filename)){ 32 | $this->globalError[] = $fileResult; 33 | } 34 | 35 | //Check line by line 36 | foreach ($lines as $pos => $line){ 37 | $result = $this->checkFileLine($filename, $pos, $line); 38 | if ($result !== null){ 39 | $this->codeError[$filename.':'.($pos+1)] = $result; 40 | } 41 | } 42 | } 43 | } 44 | 45 | public function fail() { 46 | return count($this->globalError) > 0 || count($this->codeError) > 0; 47 | } 48 | 49 | public function checkSvnComment($comment){ 50 | } 51 | 52 | public function checkFileLine($file, $pos, $content){ 53 | } 54 | 55 | public function checkFullFile($lines, $filename){ 56 | } 57 | 58 | 59 | public function renderErrorDetail(){ 60 | $details = implode("\n",$this->globalError); 61 | foreach ($this->codeError as $position => $error){ 62 | $details .= $position . ' ' . $error . "\n"; 63 | } 64 | return $details; 65 | } 66 | 67 | public function renderInstructions(){ 68 | return ""; 69 | } 70 | 71 | public function hasOption($name){ 72 | return isset($this->options[$name]); 73 | } 74 | 75 | public function getOption($name){ 76 | if (!$this->hasOption($name)){ 77 | throw new Exception("Option [$name] does not exist"); 78 | } 79 | return $this->options[$name]; 80 | } 81 | 82 | protected function parseOptions() { 83 | preg_match_all('/\-\-([^\s]+)/', $this->svnComment, $matches); 84 | foreach ($matches[1] as $option) { 85 | $option = explode('=', $option); 86 | $this->options[$option[0]] = isset($option[1]) ? $option[1] : true; 87 | } 88 | } 89 | 90 | /** 91 | * Return extension of a given file 92 | * @param string $filename 93 | * @return string or null 94 | */ 95 | public static function getExtension($filename){ 96 | preg_match("@^.*\.([^\.]*)$@", $filename, $match); 97 | return isset($match[1]) ? $match[1] : null; 98 | } 99 | 100 | } -------------------------------------------------------------------------------- /checks/EmptyCommentCheck.class.php: -------------------------------------------------------------------------------- 1 | codeError) . " tabs found"; 19 | } 20 | 21 | public function checkFileLine($file, $pos, $line){ 22 | if ( $this->hasOption('allow-tabs') ){ 23 | return; 24 | } 25 | if ( ! in_array($this->getExtension($file), $this->extensionsToCheck) ){ 26 | return; 27 | } 28 | if ( ($pos = strpos($line, "\t")) !== false ){ 29 | return "Char $pos is a tab"; 30 | } 31 | } 32 | 33 | public function renderInstructions(){ 34 | return "If you want to force commit tabs, add the parameter --allow-tabs in your comment"; 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /checks/TicketReferenceCheck.class.php: -------------------------------------------------------------------------------- 1 | hasOption('no-ticket') ){ 16 | return; 17 | } 18 | 19 | // 1) find all URL containing # (protocol://server.domain.ch/path/to#) 20 | $match_url = preg_match_all("/\w+:\/\/\S+#\d+/", $comment, $url_matches); 21 | 22 | // 2) find all # patterns (URL patterns of point 1 included) 23 | $match_total = preg_match_all("/#\d+/", $comment, $matches); 24 | 25 | if ($match_total == 0) { 26 | return "Impossible to find any ticket reference in the commit message"; 27 | } else if ($match_total == $match_url) { 28 | return "Impossible to find any ticket reference in the commit message (Note: URL are invalid references, please use # syntax)"; 29 | } 30 | } 31 | 32 | public function renderInstructions(){ 33 | return "If you want to force commit without referring any ticket, add the parameter --no-ticket in your comment"; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /pre-commit.tmpl: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # The svn_pre_commit_hook.php must be call with $1 and $2 mandatory parameters. 4 | # 5 | # After that you can specifie some aditionnal parameters: 6 | # * --inculde=XX:YY:ZZZ To list the checks that must be inculde for that repo 7 | # (without include all checks are process) 8 | # * --exculde=XX:ZZZ To list the checks that must be ignore for that repo 9 | # 10 | 11 | php /{PATH_TO_PHP_SVN_HOOKS_DIR}/svn_pre_commit_hook.php $1 $2 --include=EmptyComment:NoTabs -------------------------------------------------------------------------------- /svn/svn_functions.php: -------------------------------------------------------------------------------- 1 | array('line 1', 'line 2', 'last line of code'), 19 | 'path/file2' => array('first line', 'last line of code') 20 | ); 21 | } -------------------------------------------------------------------------------- /svn_pre_commit_hook.php: -------------------------------------------------------------------------------- 1 | parseArguments($argv); 9 | $manager->processChecks(); 10 | if ($manager->allCheckPassed()) { 11 | echo "All pre commit checks successed"; 12 | exit(0); 13 | } 14 | else { 15 | fwrite($stderr, $manager->getErrorMsg()); 16 | exit(1); 17 | } 18 | } 19 | catch (Exception $e){ 20 | fwrite($stderr, "PRE COMMIT HOOK SYSTEM ERROR, PLEASE CONTACT SERVER ADMIN.\n (".$e->getMessage().")\n"); 21 | exit(1); 22 | } -------------------------------------------------------------------------------- /test/base/PreCommitCheckTest.php: -------------------------------------------------------------------------------- 1 | runCheck(array('file'=>array('line'))); 15 | $t->ok(!$c->fail(),"->runCheck() The base call is not affecting the check result"); 16 | 17 | $c = new PreCommitTest('Comment --option1'); 18 | $t->ok($c->hasOption('option1'),"->hasOption() Is working for existing option"); 19 | $t->ok(!$c->hasOption('option2'),"->hasOption() Is working for non existing option"); 20 | 21 | $c = new PreCommitTest("Comment --option1 \n--option2=toto"); 22 | $t->ok($c->getOption('option1'),"->getOption() Return true for simple option"); 23 | $t->is($c->getOption('option2'), 'toto',"->getOption() Return value for keyVal option"); 24 | 25 | $t->is(PreCommitTest::getExtension('toto.doc'), 'doc', "->getExtension() Return valid extension on basic filename"); 26 | $t->is(PreCommitTest::getExtension('toto.doc.xls'), 'xls', "->getExtension() Return valid extension on multi dot filename"); 27 | $t->is(PreCommitTest::getExtension('toto_doc'), null, "->getExtension() Return null when there is no extension"); 28 | -------------------------------------------------------------------------------- /test/base/PreCommitManagerTest.php: -------------------------------------------------------------------------------- 1 | parseArguments(): Script should fail when the two required args are not pprovide"; 12 | try { $manager->parseArguments(array("repoName")); $t->fail($test); } 13 | catch (Exception $e) { 14 | $t->pass($test); 15 | $t->is($e->getMessage(), "Missing arguments! Usage: script_name.php SVN_REPO SVN_TRX [ --opt]*", $test . ": Appropriate Exception msg sent"); 16 | } 17 | $test = "->parseArguments(): Fail as third arg must be an option stating by --*"; 18 | try { $manager->parseArguments(array("repoName", "trxNum", "invalidArg")); $t->fail($test); } 19 | catch (Exception $e) { 20 | $t->pass($test); 21 | $t->is($e->getMessage(), "Invalid argument [invalidArg], all options must start by '--'", $test . ": Appropriate Exception msg sent"); 22 | } 23 | $test = "->parseArguments(): Fail as only a subset of options are allow"; 24 | try { $manager->parseArguments(array("repoName", "trxNum", "--invalidOpt")); $t->fail($test); } 25 | catch (Exception $e) { 26 | $t->pass($test); 27 | $t->is($e->getMessage(), "Invalid option name [\"invalidOpt\"]", $test . ": Appropriate Exception msg sent"); 28 | } 29 | $options = $manager->parseArguments(array("repoName", "trxNum", "--test-mode", "--include=123")); 30 | $t->is($options, array("test-mode"=>true, "include"=>"123"), "->parseArguments() Valid options are well parsed"); 31 | 32 | 33 | // Test check list generation 34 | $manager->parseArguments(array("repoName", "trxNum")); 35 | $t->is(count($manager->getChecksToProcess()), count(scandir($manager->getCheckDirectory()))-3, "->getChecksToProcess() Return by default all the checks from the check direcorty"); 36 | $manager->parseArguments(array("repoName", "trxNum", "--include=NoTabs")); 37 | $t->is($manager->getChecksToProcess(), array("NoTabs"), "->getChecksToProcess() With --include=XX retunr the test to inculde"); 38 | $manager->parseArguments(array("repoName", "trxNum", "--include=NoTabs:EmptyComment")); 39 | $t->is($manager->getChecksToProcess(), array("NoTabs", "EmptyComment"), "->getChecksToProcess() With --include=XX:YY return the tests to inculde"); 40 | $manager->parseArguments(array("repoName", "trxNum", "--include=NoTabs:EmptyComment", "--exclude=EmptyComment")); 41 | $t->is($manager->getChecksToProcess(), array("NoTabs"), "->getChecksToProcess() With --include=XX:YY, --exclude=YY remove the precedent include"); 42 | $manager->parseArguments(array("repoName", "trxNum", "--exclude=EmptyComment")); 43 | $t->is(count($manager->getChecksToProcess()), count(scandir($manager->getCheckDirectory()))-4, "->getChecksToProcess() With --exclude=YY, remove a test"); 44 | $test = "->getChecksToProcess() exclude invalid tests throw an exception"; 45 | $manager->parseArguments(array("repoName", "trxNum", "--exclude=tata:toto:NoTabs")); 46 | try { $manager->getChecksToProcess(); $t->fail($test); } 47 | catch (Exception $e) { 48 | $t->pass($test); 49 | $t->is($e->getMessage(), "Invalid check to exculde: [\"tata\",\"toto\"]", $test . ": Appropriate Exception msg sent"); 50 | } 51 | 52 | -------------------------------------------------------------------------------- /test/base/svn_pre_commit_hookTest.php: -------------------------------------------------------------------------------- 1 | array('pipe', 'w'), // stdout 7 | 2 => array('pipe', 'w'), // stderr 8 | ); 9 | $process = proc_open($cmd, $descriptorspec, $pipes); 10 | if (!is_resource($process)) { 11 | throw new RuntimeException("Unable to execute the command. [$cmd]"); 12 | } 13 | stream_set_blocking($pipes[1], false); 14 | stream_set_blocking($pipes[2], false); 15 | $output = $error = ''; 16 | foreach ($pipes as $key => $pipe) { 17 | while (!feof($pipe)) { 18 | if (!$line = fread($pipe, 128)){ 19 | continue; 20 | } 21 | if (1 == $key) { 22 | $output .= $line; // stdout 23 | } 24 | else { 25 | $error .= $line; // stderr 26 | } 27 | } 28 | fclose($pipe); 29 | } 30 | $returnCode = proc_close($process); 31 | } 32 | 33 | 34 | 35 | // Init lime 36 | include_once dirname(__FILE__).'/../lime/lime.php'; 37 | $t = new lime_test(8, new lime_output_color()); 38 | $scriptPath = realpath(dirname(__FILE__).'/../../svn_pre_commit_hook.php'); 39 | 40 | 41 | // Test calling the script in an invalid way 42 | execute("php $scriptPath repoName trxNum --toto", $output, $error, $returnCode); 43 | $t->is($returnCode, 1, "Script fail as two arguments are required"); 44 | $t->is($error, "PRE COMMIT HOOK SYSTEM ERROR, PLEASE CONTACT SERVER ADMIN.\n (Invalid option name [\"toto\"])\n", "Valid error message is return"); 45 | 46 | 47 | // First test with a working commit 48 | $cmd = "php $scriptPath repoName trxNum --test-mode --include=EmptyComment"; 49 | execute($cmd, $output, $error, $returnCode); 50 | $t->is($returnCode, 0, "On success, return code is 0"); 51 | $t->is($output, "All pre commit checks successed", "On success a success message is return"); 52 | $t->is($error, "", "On success, no error output on stderr"); 53 | 54 | 55 | // Second test with a fail commit, due to an empty comment 56 | $cmd = "php $scriptPath emptyComment trxNum --test-mode --include=EmptyComment"; 57 | execute($cmd, $output, $error, $returnCode); 58 | $t->is($returnCode, 1, "On error, return code is 1"); 59 | $t->is($output, "", "On error, no echo on the stdout"); 60 | $errorMsg = <<< EOC 61 | 62 | 63 | PRE COMMIT HOOK FAIL: 64 | The following pre commit check fail: 65 | * Reject minimalistic comment: Commit message empty or too short 66 | 67 | DETAIL OF THE CHECKS ERRORS: 68 | Reject minimalistic comment: 69 | Commit message has been rejected (too short). Please provide more details about changes you want to commit. 70 | 71 | 72 | EOC; 73 | $t->is($error, $errorMsg, "A well formated message is generated on stderr"); 74 | -------------------------------------------------------------------------------- /test/checks/EmptyCommentCheckTest.php: -------------------------------------------------------------------------------- 1 | runCheck(array('file1'=>array('line1', 'line2'))); 12 | $t->ok(!$c->fail(),"The check is not failling if there is a good commit msg"); 13 | 14 | $c = new EmptyCommentCheck('toto'); 15 | $c->runCheck(array()); 16 | $t->ok($c->fail(),"The check fails when comment msg is too small"); 17 | $t->is($c->renderErrorSummary(), "Commit message empty or too short", "A valid summary message is return"); 18 | $t->is($c->renderErrorDetail(), "Commit message has been rejected (too short). Please provide more details about changes you want to commit.", "A valid detail message is return"); 19 | 20 | $c = new EmptyCommentCheck("--allow-tabs --no-ticket\n--any-other-option,--unusual-comma-separator"); 21 | $c->runCheck(array()); 22 | $t->ok($c->fail(), "The check fails when comment msg only contains php-svn-hook parameters"); 23 | 24 | $c = new EmptyCommentCheck("Fix #430\n\n--allow-tabs"); 25 | $c->runCheck(array()); 26 | $t->ok(!$c->fail(), "The check is not failing when comment msg is long enough, even when parameters are ignored"); 27 | 28 | $c = new EmptyCommentCheck("\n \n!\n \n"); 29 | $c->runCheck(array()); 30 | $t->ok($c->fail(), "The check fails when comment msg does not contain any word"); 31 | 32 | $c = new EmptyCommentCheck("\n \n\n \n--no-ticket"); 33 | $c->runCheck(array()); 34 | $t->ok($c->fail(), "The check fails when comment msg contains only blank characters and a parameter"); 35 | 36 | -------------------------------------------------------------------------------- /test/checks/NoTabsCheckTest.php: -------------------------------------------------------------------------------- 1 | runCheck(array($phpFilename=>array('line1', 'line2'))); 17 | $t->ok(!$c->fail(),"The check is not failling if there is no tab in the files"); 18 | 19 | // Simple file with tab 20 | $c = new NoTabsCheck(); 21 | $c->runCheck(array($phpFilename=>array("line1", "\tline2", "line3\t"))); 22 | $t->ok($c->fail(),"The check works when there is a tab in the file"); 23 | $t->is($c->renderErrorSummary(), "2 tabs found", "A valid summary message is return"); 24 | $t->is($c->renderErrorDetail(), "$phpFilename:2 Char 0 is a tab\n$phpFilename:3 Char 5 is a tab\n", "A valid detail message is return"); 25 | 26 | // Simple file with tab but options --allow-tabs 27 | $c = new NoTabsCheck('comment --allow-tabs'); 28 | $c->runCheck(array($phpFilename=>array("line1", "\tline2", "line3\t"))); 29 | $t->ok(!$c->fail(),"The check is ommited when there is the option --alow-tabs"); 30 | 31 | // Simple file but with txt extension 32 | $c = new NoTabsCheck(); 33 | $c->runCheck(array($txtFilename=>array("line1", "\tline2", "line3\t"))); 34 | $t->ok(!$c->fail(),"The check is ommited when file extention is not in ".json_encode($c->extensionsToCheck)); 35 | 36 | // Two files containings tabs 37 | $c = new NoTabsCheck(); 38 | $c->runCheck(array( 39 | $phpFilename=>array("line1", "\tline2", "line3\t"), 40 | $phpFilename2=>array("line1", "line2", "\tline3", "line4"), 41 | )); 42 | $t->is($c->renderErrorSummary(), "3 tabs found", "A valid summary message is return when there is tabs in multiple files"); 43 | $t->is($c->renderErrorDetail(), "$phpFilename:2 Char 0 is a tab\n$phpFilename:3 Char 5 is a tab\n$phpFilename2:3 Char 0 is a tab\n", "A valid detail message is return when there is tabs in multiple files"); 44 | 45 | -------------------------------------------------------------------------------- /test/checks/TicketReferenceCheckTest.php: -------------------------------------------------------------------------------- 1 | runCheck(array()); 12 | $t->ok(!$c->fail(), "Valid commit with One Ticket reference"); 13 | 14 | $c = new TicketReferenceCheck("Closes #62373. Commit with Three Ticket Numbers (see also #1 and #387)"); 15 | $c->runCheck(array()); 16 | $t->ok(!$c->fail(), "Valid commit with Three Ticket references"); 17 | 18 | $c = new TicketReferenceCheck("Commit with One Ticket Number, but without # char. Ticket 927"); 19 | $c->runCheck(array()); 20 | $t->ok($c->fail(), "Invalid commit with One Ticket Number, but without # char"); 21 | 22 | $c = new TicketReferenceCheck("Commit without reference to any ticket"); 23 | $c->runCheck(array()); 24 | $t->ok($c->fail(), "Invalid commit because no ticket is referenced."); 25 | 26 | $c = new TicketReferenceCheck("Forced commit without reference to any ticket.\n--no-ticket"); 27 | $c->runCheck(array()); 28 | $t->ok(!$c->fail(), "Check skipped if option --no-ticket is given"); 29 | 30 | $c = new TicketReferenceCheck("see http://www.test.com/cms#1-page"); 31 | $c->runCheck(array()); 32 | $t->ok($c->fail(), "Invalid commit with a # pattern part of an URL"); 33 | 34 | $c = new TicketReferenceCheck("see also http://www.split.me/#146-2"); 35 | $c->runCheck(array()); 36 | $t->ok($c->fail(), "Another Invalid commit with a # pattern part of an URL"); 37 | 38 | $c = new TicketReferenceCheck("Workaround to known issue https://github.com/rails/rails#666\n\nFix PR #4"); 39 | $c->runCheck(array()); 40 | $t->ok(!$c->fail(), "Valid commit with two # patterns, once in an URL, but also in a ticket reference"); 41 | 42 | $c = new TicketReferenceCheck("#1234"); 43 | $c->runCheck(array()); 44 | $t->ok(!$c->fail(), "Very short comment, without spaces, without any keyword"); 45 | 46 | $c = new TicketReferenceCheck("fix #1"); 47 | $c->runCheck(array()); 48 | $t->ok(!$c->fail(), "Single line comment with nothing AFTER # pattern"); 49 | 50 | $c = new TicketReferenceCheck("#9 fixed"); 51 | $c->runCheck(array()); 52 | $t->ok(!$c->fail(), "Single line comment with nothing BEFORE # pattern"); 53 | 54 | $c = new TicketReferenceCheck("Fixed issues:\n- #331\n* #793\nOne more line"); 55 | $c->runCheck(array()); 56 | $t->ok(!$c->fail(), "Multi-line comment for regexp corner cases"); 57 | 58 | $c = new TicketReferenceCheck("--no-tabs\n#178"); 59 | $c->runCheck(array()); 60 | $t->ok(!$c->fail(), "Ticket reference just after a line break"); 61 | 62 | $c = new TicketReferenceCheck("fix#5932"); 63 | $c->runCheck(array()); 64 | $t->ok(!$c->fail(), "Ugly, but valid: Words can be collated to ticket reference without any space"); 65 | 66 | $c = new TicketReferenceCheck("#5932Ι#5821-#453"); 67 | $c->runCheck(array()); 68 | $t->ok(!$c->fail(), "Ugly, but valid: A sequence of Ticket references without any separator, except # prefixes."); 69 | 70 | $c = new TicketReferenceCheck("Feature #93: first prototype"); 71 | $c->runCheck(array()); 72 | $t->ok(!$c->fail(), "Some separators can be collated to the right side, like ':' in 'ref #575: Apply workaround...'"); 73 | 74 | $c = new TicketReferenceCheck("Fix #33, but code refactoring still needed"); 75 | $c->runCheck(array()); 76 | $t->ok(!$c->fail(), "Some separators can be collated to the right side, like ',' in 'Fix #33, but...'"); 77 | 78 | $c = new TicketReferenceCheck("Fix a typo in i18n file, that resolves #59321."); 79 | $c->runCheck(array()); 80 | $t->ok(!$c->fail(), "Some separators can be collated to the right side, like '.' in '... that fixes #5322.'"); 81 | 82 | $c = new TicketReferenceCheck("Redmine commit message style (#343, #2100)"); 83 | $c->runCheck(array()); 84 | $t->ok(!$c->fail(), "Ticket references can be enclosed between parentheses (...)"); 85 | 86 | -------------------------------------------------------------------------------- /test/lime/lime.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | /** 12 | * Unit test library. 13 | * 14 | * @package lime 15 | * @author Fabien Potencier 16 | * @version SVN: $Id: lime.php 29529 2010-05-19 13:41:48Z fabien $ 17 | */ 18 | class lime_test 19 | { 20 | const EPSILON = 0.0000000001; 21 | 22 | protected $test_nb = 0; 23 | protected $output = null; 24 | protected $results = array(); 25 | protected $options = array(); 26 | 27 | static protected $all_results = array(); 28 | 29 | public function __construct($plan = null, $options = array()) 30 | { 31 | // for BC 32 | if (!is_array($options)) 33 | { 34 | $options = array('output' => $options); 35 | } 36 | 37 | $this->options = array_merge(array( 38 | 'force_colors' => false, 39 | 'output' => null, 40 | 'verbose' => false, 41 | 'error_reporting' => false, 42 | ), $options); 43 | 44 | $this->output = $this->options['output'] ? $this->options['output'] : new lime_output($this->options['force_colors']); 45 | 46 | $caller = $this->find_caller(debug_backtrace()); 47 | self::$all_results[] = array( 48 | 'file' => $caller[0], 49 | 'tests' => array(), 50 | 'stats' => array('plan' => $plan, 'total' => 0, 'failed' => array(), 'passed' => array(), 'skipped' => array(), 'errors' => array()), 51 | ); 52 | 53 | $this->results = &self::$all_results[count(self::$all_results) - 1]; 54 | 55 | null !== $plan and $this->output->echoln(sprintf("1..%d", $plan)); 56 | 57 | set_error_handler(array($this, 'handle_error')); 58 | set_exception_handler(array($this, 'handle_exception')); 59 | } 60 | 61 | static public function reset() 62 | { 63 | self::$all_results = array(); 64 | } 65 | 66 | static public function to_array() 67 | { 68 | return self::$all_results; 69 | } 70 | 71 | static public function to_xml($results = null) 72 | { 73 | if (is_null($results)) 74 | { 75 | $results = self::$all_results; 76 | } 77 | 78 | $dom = new DOMDocument('1.0', 'UTF-8'); 79 | $dom->formatOutput = true; 80 | $dom->appendChild($testsuites = $dom->createElement('testsuites')); 81 | 82 | $errors = 0; 83 | $failures = 0; 84 | $errors = 0; 85 | $skipped = 0; 86 | $assertions = 0; 87 | 88 | foreach ($results as $result) 89 | { 90 | $testsuites->appendChild($testsuite = $dom->createElement('testsuite')); 91 | $testsuite->setAttribute('name', basename($result['file'], '.php')); 92 | $testsuite->setAttribute('file', $result['file']); 93 | $testsuite->setAttribute('failures', count($result['stats']['failed'])); 94 | $testsuite->setAttribute('errors', count($result['stats']['errors'])); 95 | $testsuite->setAttribute('skipped', count($result['stats']['skipped'])); 96 | $testsuite->setAttribute('tests', $result['stats']['plan']); 97 | $testsuite->setAttribute('assertions', $result['stats']['plan']); 98 | 99 | $failures += count($result['stats']['failed']); 100 | $errors += count($result['stats']['errors']); 101 | $skipped += count($result['stats']['skipped']); 102 | $assertions += $result['stats']['plan']; 103 | 104 | foreach ($result['tests'] as $test) 105 | { 106 | $testsuite->appendChild($testcase = $dom->createElement('testcase')); 107 | $testcase->setAttribute('name', $test['message']); 108 | $testcase->setAttribute('file', $test['file']); 109 | $testcase->setAttribute('line', $test['line']); 110 | $testcase->setAttribute('assertions', 1); 111 | if (!$test['status']) 112 | { 113 | $testcase->appendChild($failure = $dom->createElement('failure')); 114 | $failure->setAttribute('type', 'lime'); 115 | if (isset($test['error'])) 116 | { 117 | $failure->appendChild($dom->createTextNode($test['error'])); 118 | } 119 | } 120 | } 121 | } 122 | 123 | $testsuites->setAttribute('failures', $failures); 124 | $testsuites->setAttribute('errors', $errors); 125 | $testsuites->setAttribute('tests', $assertions); 126 | $testsuites->setAttribute('assertions', $assertions); 127 | $testsuites->setAttribute('skipped', $skipped); 128 | 129 | return $dom->saveXml(); 130 | } 131 | 132 | public function __destruct() 133 | { 134 | $plan = $this->results['stats']['plan']; 135 | $passed = count($this->results['stats']['passed']); 136 | $failed = count($this->results['stats']['failed']); 137 | $total = $this->results['stats']['total']; 138 | is_null($plan) and $plan = $total and $this->output->echoln(sprintf("1..%d", $plan)); 139 | 140 | if ($total > $plan) 141 | { 142 | $this->output->red_bar(sprintf("# Looks like you planned %d tests but ran %d extra.", $plan, $total - $plan)); 143 | } 144 | elseif ($total < $plan) 145 | { 146 | $this->output->red_bar(sprintf("# Looks like you planned %d tests but only ran %d.", $plan, $total)); 147 | } 148 | 149 | if ($failed) 150 | { 151 | $this->output->red_bar(sprintf("# Looks like you failed %d tests of %d.", $failed, $passed + $failed)); 152 | } 153 | else if ($total == $plan) 154 | { 155 | $this->output->green_bar("# Looks like everything went fine."); 156 | } 157 | 158 | flush(); 159 | } 160 | 161 | /** 162 | * Tests a condition and passes if it is true 163 | * 164 | * @param mixed $exp condition to test 165 | * @param string $message display output message when the test passes 166 | * 167 | * @return boolean 168 | */ 169 | public function ok($exp, $message = '') 170 | { 171 | $this->update_stats(); 172 | 173 | if ($result = (boolean) $exp) 174 | { 175 | $this->results['stats']['passed'][] = $this->test_nb; 176 | } 177 | else 178 | { 179 | $this->results['stats']['failed'][] = $this->test_nb; 180 | } 181 | $this->results['tests'][$this->test_nb]['message'] = $message; 182 | $this->results['tests'][$this->test_nb]['status'] = $result; 183 | $this->output->echoln(sprintf("%s %d%s", $result ? 'ok' : 'not ok', $this->test_nb, $message = $message ? sprintf('%s %s', 0 === strpos($message, '#') ? '' : ' -', $message) : '')); 184 | 185 | if (!$result) 186 | { 187 | $this->output->diag(sprintf(' Failed test (%s at line %d)', str_replace(getcwd(), '.', $this->results['tests'][$this->test_nb]['file']), $this->results['tests'][$this->test_nb]['line'])); 188 | } 189 | 190 | return $result; 191 | } 192 | 193 | /** 194 | * Compares two values and passes if they are equal (==) 195 | * 196 | * @param mixed $exp1 left value 197 | * @param mixed $exp2 right value 198 | * @param string $message display output message when the test passes 199 | * 200 | * @return boolean 201 | */ 202 | public function is($exp1, $exp2, $message = '') 203 | { 204 | if (is_object($exp1) || is_object($exp2)) 205 | { 206 | $value = $exp1 === $exp2; 207 | } 208 | else if (is_float($exp1) && is_float($exp2)) 209 | { 210 | $value = abs($exp1 - $exp2) < self::EPSILON; 211 | } 212 | else 213 | { 214 | $value = $exp1 == $exp2; 215 | } 216 | 217 | if (!$result = $this->ok($value, $message)) 218 | { 219 | $this->set_last_test_errors(array(sprintf(" got: %s", var_export($exp1, true)), sprintf(" expected: %s", var_export($exp2, true)))); 220 | } 221 | 222 | return $result; 223 | } 224 | 225 | /** 226 | * Compares two values and passes if they are not equal 227 | * 228 | * @param mixed $exp1 left value 229 | * @param mixed $exp2 right value 230 | * @param string $message display output message when the test passes 231 | * 232 | * @return boolean 233 | */ 234 | public function isnt($exp1, $exp2, $message = '') 235 | { 236 | if (!$result = $this->ok($exp1 != $exp2, $message)) 237 | { 238 | $this->set_last_test_errors(array(sprintf(" %s", var_export($exp2, true)), ' ne', sprintf(" %s", var_export($exp2, true)))); 239 | } 240 | 241 | return $result; 242 | } 243 | 244 | /** 245 | * Tests a string against a regular expression 246 | * 247 | * @param string $exp value to test 248 | * @param string $regex the pattern to search for, as a string 249 | * @param string $message display output message when the test passes 250 | * 251 | * @return boolean 252 | */ 253 | public function like($exp, $regex, $message = '') 254 | { 255 | if (!$result = $this->ok(preg_match($regex, $exp), $message)) 256 | { 257 | $this->set_last_test_errors(array(sprintf(" '%s'", $exp), sprintf(" doesn't match '%s'", $regex))); 258 | } 259 | 260 | return $result; 261 | } 262 | 263 | /** 264 | * Checks that a string doesn't match a regular expression 265 | * 266 | * @param string $exp value to test 267 | * @param string $regex the pattern to search for, as a string 268 | * @param string $message display output message when the test passes 269 | * 270 | * @return boolean 271 | */ 272 | public function unlike($exp, $regex, $message = '') 273 | { 274 | if (!$result = $this->ok(!preg_match($regex, $exp), $message)) 275 | { 276 | $this->set_last_test_errors(array(sprintf(" '%s'", $exp), sprintf(" matches '%s'", $regex))); 277 | } 278 | 279 | return $result; 280 | } 281 | 282 | /** 283 | * Compares two arguments with an operator 284 | * 285 | * @param mixed $exp1 left value 286 | * @param string $op operator 287 | * @param mixed $exp2 right value 288 | * @param string $message display output message when the test passes 289 | * 290 | * @return boolean 291 | */ 292 | public function cmp_ok($exp1, $op, $exp2, $message = '') 293 | { 294 | $php = sprintf("\$result = \$exp1 $op \$exp2;"); 295 | // under some unknown conditions the sprintf() call causes a segmentation fault 296 | // when placed directly in the eval() call 297 | eval($php); 298 | 299 | if (!$this->ok($result, $message)) 300 | { 301 | $this->set_last_test_errors(array(sprintf(" %s", str_replace("\n", '', var_export($exp1, true))), sprintf(" %s", $op), sprintf(" %s", str_replace("\n", '', var_export($exp2, true))))); 302 | } 303 | 304 | return $result; 305 | } 306 | 307 | /** 308 | * Checks the availability of a method for an object or a class 309 | * 310 | * @param mixed $object an object instance or a class name 311 | * @param string|array $methods one or more method names 312 | * @param string $message display output message when the test passes 313 | * 314 | * @return boolean 315 | */ 316 | public function can_ok($object, $methods, $message = '') 317 | { 318 | $result = true; 319 | $failed_messages = array(); 320 | foreach ((array) $methods as $method) 321 | { 322 | if (!method_exists($object, $method)) 323 | { 324 | $failed_messages[] = sprintf(" method '%s' does not exist", $method); 325 | $result = false; 326 | } 327 | } 328 | 329 | !$this->ok($result, $message); 330 | 331 | !$result and $this->set_last_test_errors($failed_messages); 332 | 333 | return $result; 334 | } 335 | 336 | /** 337 | * Checks the type of an argument 338 | * 339 | * @param mixed $var variable instance 340 | * @param string $class class or type name 341 | * @param string $message display output message when the test passes 342 | * 343 | * @return boolean 344 | */ 345 | public function isa_ok($var, $class, $message = '') 346 | { 347 | $type = is_object($var) ? get_class($var) : gettype($var); 348 | if (!$result = $this->ok($type == $class, $message)) 349 | { 350 | $this->set_last_test_errors(array(sprintf(" variable isn't a '%s' it's a '%s'", $class, $type))); 351 | } 352 | 353 | return $result; 354 | } 355 | 356 | /** 357 | * Checks that two arrays have the same values 358 | * 359 | * @param mixed $exp1 first variable 360 | * @param mixed $exp2 second variable 361 | * @param string $message display output message when the test passes 362 | * 363 | * @return boolean 364 | */ 365 | public function is_deeply($exp1, $exp2, $message = '') 366 | { 367 | if (!$result = $this->ok($this->test_is_deeply($exp1, $exp2), $message)) 368 | { 369 | $this->set_last_test_errors(array(sprintf(" got: %s", str_replace("\n", '', var_export($exp1, true))), sprintf(" expected: %s", str_replace("\n", '', var_export($exp2, true))))); 370 | } 371 | 372 | return $result; 373 | } 374 | 375 | /** 376 | * Always passes--useful for testing exceptions 377 | * 378 | * @param string $message display output message 379 | * 380 | * @return true 381 | */ 382 | public function pass($message = '') 383 | { 384 | return $this->ok(true, $message); 385 | } 386 | 387 | /** 388 | * Always fails--useful for testing exceptions 389 | * 390 | * @param string $message display output message 391 | * 392 | * @return false 393 | */ 394 | public function fail($message = '') 395 | { 396 | return $this->ok(false, $message); 397 | } 398 | 399 | /** 400 | * Outputs a diag message but runs no test 401 | * 402 | * @param string $message display output message 403 | * 404 | * @return void 405 | */ 406 | public function diag($message) 407 | { 408 | $this->output->diag($message); 409 | } 410 | 411 | /** 412 | * Counts as $nb_tests tests--useful for conditional tests 413 | * 414 | * @param string $message display output message 415 | * @param integer $nb_tests number of tests to skip 416 | * 417 | * @return void 418 | */ 419 | public function skip($message = '', $nb_tests = 1) 420 | { 421 | for ($i = 0; $i < $nb_tests; $i++) 422 | { 423 | $this->pass(sprintf("# SKIP%s", $message ? ' '.$message : '')); 424 | $this->results['stats']['skipped'][] = $this->test_nb; 425 | array_pop($this->results['stats']['passed']); 426 | } 427 | } 428 | 429 | /** 430 | * Counts as a test--useful for tests yet to be written 431 | * 432 | * @param string $message display output message 433 | * 434 | * @return void 435 | */ 436 | public function todo($message = '') 437 | { 438 | $this->pass(sprintf("# TODO%s", $message ? ' '.$message : '')); 439 | $this->results['stats']['skipped'][] = $this->test_nb; 440 | array_pop($this->results['stats']['passed']); 441 | } 442 | 443 | /** 444 | * Validates that a file exists and that it is properly included 445 | * 446 | * @param string $file file path 447 | * @param string $message display output message when the test passes 448 | * 449 | * @return boolean 450 | */ 451 | public function include_ok($file, $message = '') 452 | { 453 | if (!$result = $this->ok((@include($file)) == 1, $message)) 454 | { 455 | $this->set_last_test_errors(array(sprintf(" Tried to include '%s'", $file))); 456 | } 457 | 458 | return $result; 459 | } 460 | 461 | private function test_is_deeply($var1, $var2) 462 | { 463 | if (gettype($var1) != gettype($var2)) 464 | { 465 | return false; 466 | } 467 | 468 | if (is_array($var1)) 469 | { 470 | ksort($var1); 471 | ksort($var2); 472 | 473 | $keys1 = array_keys($var1); 474 | $keys2 = array_keys($var2); 475 | if (array_diff($keys1, $keys2) || array_diff($keys2, $keys1)) 476 | { 477 | return false; 478 | } 479 | $is_equal = true; 480 | foreach ($var1 as $key => $value) 481 | { 482 | $is_equal = $this->test_is_deeply($var1[$key], $var2[$key]); 483 | if ($is_equal === false) 484 | { 485 | break; 486 | } 487 | } 488 | 489 | return $is_equal; 490 | } 491 | else 492 | { 493 | return $var1 === $var2; 494 | } 495 | } 496 | 497 | public function comment($message) 498 | { 499 | $this->output->comment($message); 500 | } 501 | 502 | public function info($message) 503 | { 504 | $this->output->info($message); 505 | } 506 | 507 | public function error($message, $file = null, $line = null, array $traces = array()) 508 | { 509 | $this->output->error($message, $file, $line, $traces); 510 | 511 | $this->results['stats']['errors'][] = array( 512 | 'message' => $message, 513 | 'file' => $file, 514 | 'line' => $line, 515 | ); 516 | } 517 | 518 | protected function update_stats() 519 | { 520 | ++$this->test_nb; 521 | ++$this->results['stats']['total']; 522 | 523 | list($this->results['tests'][$this->test_nb]['file'], $this->results['tests'][$this->test_nb]['line']) = $this->find_caller(debug_backtrace()); 524 | } 525 | 526 | protected function set_last_test_errors(array $errors) 527 | { 528 | $this->output->diag($errors); 529 | 530 | $this->results['tests'][$this->test_nb]['error'] = implode("\n", $errors); 531 | } 532 | 533 | protected function find_caller($traces) 534 | { 535 | // find the first call to a method of an object that is an instance of lime_test 536 | $t = array_reverse($traces); 537 | foreach ($t as $trace) 538 | { 539 | if (isset($trace['object']) && $trace['object'] instanceof lime_test) 540 | { 541 | return array($trace['file'], $trace['line']); 542 | } 543 | } 544 | 545 | // return the first call 546 | $last = count($traces) - 1; 547 | return array($traces[$last]['file'], $traces[$last]['line']); 548 | } 549 | 550 | public function handle_error($code, $message, $file, $line, $context) 551 | { 552 | if (!$this->options['error_reporting'] || ($code & error_reporting()) == 0) 553 | { 554 | return false; 555 | } 556 | 557 | switch ($code) 558 | { 559 | case E_WARNING: 560 | $type = 'Warning'; 561 | break; 562 | default: 563 | $type = 'Notice'; 564 | break; 565 | } 566 | 567 | $trace = debug_backtrace(); 568 | array_shift($trace); // remove the handle_error() call from the trace 569 | 570 | $this->error($type.': '.$message, $file, $line, $trace); 571 | } 572 | 573 | public function handle_exception(Exception $exception) 574 | { 575 | $this->error(get_class($exception).': '.$exception->getMessage(), $exception->getFile(), $exception->getLine(), $exception->getTrace()); 576 | 577 | // exception was handled 578 | return true; 579 | } 580 | } 581 | 582 | class lime_output 583 | { 584 | public $colorizer = null; 585 | public $base_dir = null; 586 | 587 | public function __construct($force_colors = false, $base_dir = null) 588 | { 589 | $this->colorizer = new lime_colorizer($force_colors); 590 | $this->base_dir = $base_dir === null ? getcwd() : $base_dir; 591 | } 592 | 593 | public function diag() 594 | { 595 | $messages = func_get_args(); 596 | foreach ($messages as $message) 597 | { 598 | echo $this->colorizer->colorize('# '.join("\n# ", (array) $message), 'COMMENT')."\n"; 599 | } 600 | } 601 | 602 | public function comment($message) 603 | { 604 | echo $this->colorizer->colorize(sprintf('# %s', $message), 'COMMENT')."\n"; 605 | } 606 | 607 | public function info($message) 608 | { 609 | echo $this->colorizer->colorize(sprintf('> %s', $message), 'INFO_BAR')."\n"; 610 | } 611 | 612 | public function error($message, $file = null, $line = null, $traces = array()) 613 | { 614 | if ($file !== null) 615 | { 616 | $message .= sprintf("\n(in %s on line %s)", $file, $line); 617 | } 618 | 619 | // some error messages contain absolute file paths 620 | $message = $this->strip_base_dir($message); 621 | 622 | $space = $this->colorizer->colorize(str_repeat(' ', 71), 'RED_BAR')."\n"; 623 | $message = trim($message); 624 | $message = wordwrap($message, 66, "\n"); 625 | 626 | echo "\n".$space; 627 | foreach (explode("\n", $message) as $message_line) 628 | { 629 | echo $this->colorizer->colorize(str_pad(' '.$message_line, 71, ' '), 'RED_BAR')."\n"; 630 | } 631 | echo $space."\n"; 632 | 633 | if (count($traces) > 0) 634 | { 635 | echo $this->colorizer->colorize('Exception trace:', 'COMMENT')."\n"; 636 | 637 | $this->print_trace(null, $file, $line); 638 | 639 | foreach ($traces as $trace) 640 | { 641 | if (array_key_exists('class', $trace)) 642 | { 643 | $method = sprintf('%s%s%s()', $trace['class'], $trace['type'], $trace['function']); 644 | } 645 | else 646 | { 647 | $method = sprintf('%s()', $trace['function']); 648 | } 649 | 650 | if (array_key_exists('file', $trace)) 651 | { 652 | $this->print_trace($method, $trace['file'], $trace['line']); 653 | } 654 | else 655 | { 656 | $this->print_trace($method); 657 | } 658 | } 659 | 660 | echo "\n"; 661 | } 662 | } 663 | 664 | protected function print_trace($method = null, $file = null, $line = null) 665 | { 666 | if (!is_null($method)) 667 | { 668 | $method .= ' '; 669 | } 670 | 671 | echo ' '.$method.'at '; 672 | 673 | if (!is_null($file) && !is_null($line)) 674 | { 675 | printf("%s:%s\n", $this->colorizer->colorize($this->strip_base_dir($file), 'TRACE'), $this->colorizer->colorize($line, 'TRACE')); 676 | } 677 | else 678 | { 679 | echo "[internal function]\n"; 680 | } 681 | } 682 | 683 | public function echoln($message, $colorizer_parameter = null, $colorize = true) 684 | { 685 | if ($colorize) 686 | { 687 | $message = preg_replace('/(?:^|\.)((?:not ok|dubious|errors) *\d*)\b/e', '$this->colorizer->colorize(\'$1\', \'ERROR\')', $message); 688 | $message = preg_replace('/(?:^|\.)(ok *\d*)\b/e', '$this->colorizer->colorize(\'$1\', \'INFO\')', $message); 689 | $message = preg_replace('/"(.+?)"/e', '$this->colorizer->colorize(\'$1\', \'PARAMETER\')', $message); 690 | $message = preg_replace('/(\->|\:\:)?([a-zA-Z0-9_]+?)\(\)/e', '$this->colorizer->colorize(\'$1$2()\', \'PARAMETER\')', $message); 691 | } 692 | 693 | echo ($colorizer_parameter ? $this->colorizer->colorize($message, $colorizer_parameter) : $message)."\n"; 694 | } 695 | 696 | public function green_bar($message) 697 | { 698 | echo $this->colorizer->colorize($message.str_repeat(' ', 71 - min(71, strlen($message))), 'GREEN_BAR')."\n"; 699 | } 700 | 701 | public function red_bar($message) 702 | { 703 | echo $this->colorizer->colorize($message.str_repeat(' ', 71 - min(71, strlen($message))), 'RED_BAR')."\n"; 704 | } 705 | 706 | protected function strip_base_dir($text) 707 | { 708 | return str_replace(DIRECTORY_SEPARATOR, '/', str_replace(realpath($this->base_dir).DIRECTORY_SEPARATOR, '', $text)); 709 | } 710 | } 711 | 712 | class lime_output_color extends lime_output 713 | { 714 | } 715 | 716 | class lime_colorizer 717 | { 718 | static public $styles = array(); 719 | 720 | protected $colors_supported = false; 721 | 722 | public function __construct($force_colors = false) 723 | { 724 | if ($force_colors) 725 | { 726 | $this->colors_supported = true; 727 | } 728 | else 729 | { 730 | // colors are supported on windows with ansicon or on tty consoles 731 | if (DIRECTORY_SEPARATOR == '\\') 732 | { 733 | $this->colors_supported = false !== getenv('ANSICON'); 734 | } 735 | else 736 | { 737 | $this->colors_supported = function_exists('posix_isatty') && @posix_isatty(STDOUT); 738 | } 739 | } 740 | } 741 | 742 | public static function style($name, $options = array()) 743 | { 744 | self::$styles[$name] = $options; 745 | } 746 | 747 | public function colorize($text = '', $parameters = array()) 748 | { 749 | 750 | if (!$this->colors_supported) 751 | { 752 | return $text; 753 | } 754 | 755 | static $options = array('bold' => 1, 'underscore' => 4, 'blink' => 5, 'reverse' => 7, 'conceal' => 8); 756 | static $foreground = array('black' => 30, 'red' => 31, 'green' => 32, 'yellow' => 33, 'blue' => 34, 'magenta' => 35, 'cyan' => 36, 'white' => 37); 757 | static $background = array('black' => 40, 'red' => 41, 'green' => 42, 'yellow' => 43, 'blue' => 44, 'magenta' => 45, 'cyan' => 46, 'white' => 47); 758 | 759 | !is_array($parameters) && isset(self::$styles[$parameters]) and $parameters = self::$styles[$parameters]; 760 | 761 | $codes = array(); 762 | isset($parameters['fg']) and $codes[] = $foreground[$parameters['fg']]; 763 | isset($parameters['bg']) and $codes[] = $background[$parameters['bg']]; 764 | foreach ($options as $option => $value) 765 | { 766 | isset($parameters[$option]) && $parameters[$option] and $codes[] = $value; 767 | } 768 | 769 | return "\033[".implode(';', $codes).'m'.$text."\033[0m"; 770 | } 771 | } 772 | 773 | lime_colorizer::style('ERROR', array('bg' => 'red', 'fg' => 'white', 'bold' => true)); 774 | lime_colorizer::style('INFO', array('fg' => 'green', 'bold' => true)); 775 | lime_colorizer::style('TRACE', array('fg' => 'green', 'bold' => true)); 776 | lime_colorizer::style('PARAMETER', array('fg' => 'cyan')); 777 | lime_colorizer::style('COMMENT', array('fg' => 'yellow')); 778 | 779 | lime_colorizer::style('GREEN_BAR', array('fg' => 'white', 'bg' => 'green', 'bold' => true)); 780 | lime_colorizer::style('RED_BAR', array('fg' => 'white', 'bg' => 'red', 'bold' => true)); 781 | lime_colorizer::style('INFO_BAR', array('fg' => 'cyan', 'bold' => true)); 782 | 783 | class lime_harness extends lime_registration 784 | { 785 | public $options = array(); 786 | public $php_cli = null; 787 | public $stats = array(); 788 | public $output = null; 789 | 790 | public function __construct($options = array()) 791 | { 792 | // for BC 793 | if (!is_array($options)) 794 | { 795 | $options = array('output' => $options); 796 | } 797 | 798 | $this->options = array_merge(array( 799 | 'php_cli' => null, 800 | 'force_colors' => false, 801 | 'output' => null, 802 | 'verbose' => false, 803 | ), $options); 804 | 805 | $this->php_cli = $this->find_php_cli($this->options['php_cli']); 806 | $this->output = $this->options['output'] ? $this->options['output'] : new lime_output($this->options['force_colors']); 807 | } 808 | 809 | protected function find_php_cli($php_cli = null) 810 | { 811 | if (is_null($php_cli)) 812 | { 813 | if (getenv('PHP_PATH')) 814 | { 815 | $php_cli = getenv('PHP_PATH'); 816 | 817 | if (!is_executable($php_cli)) 818 | { 819 | throw new Exception('The defined PHP_PATH environment variable is not a valid PHP executable.'); 820 | } 821 | } 822 | else 823 | { 824 | $php_cli = PHP_BINDIR.DIRECTORY_SEPARATOR.'php'; 825 | } 826 | } 827 | 828 | if (is_executable($php_cli)) 829 | { 830 | return $php_cli; 831 | } 832 | 833 | $path = getenv('PATH') ? getenv('PATH') : getenv('Path'); 834 | $exe_suffixes = DIRECTORY_SEPARATOR == '\\' ? (getenv('PATHEXT') ? explode(PATH_SEPARATOR, getenv('PATHEXT')) : array('.exe', '.bat', '.cmd', '.com')) : array(''); 835 | foreach (array('php5', 'php') as $php_cli) 836 | { 837 | foreach ($exe_suffixes as $suffix) 838 | { 839 | foreach (explode(PATH_SEPARATOR, $path) as $dir) 840 | { 841 | $file = $dir.DIRECTORY_SEPARATOR.$php_cli.$suffix; 842 | if (is_executable($file)) 843 | { 844 | return $file; 845 | } 846 | } 847 | } 848 | } 849 | 850 | throw new Exception("Unable to find PHP executable."); 851 | } 852 | 853 | public function to_array() 854 | { 855 | $results = array(); 856 | foreach ($this->stats['files'] as $file => $stat) 857 | { 858 | $results = array_merge($results, $stat['output']); 859 | } 860 | 861 | return $results; 862 | } 863 | 864 | public function to_xml() 865 | { 866 | return lime_test::to_xml($this->to_array()); 867 | } 868 | 869 | public function run() 870 | { 871 | if (!count($this->files)) 872 | { 873 | throw new Exception('You must register some test files before running them!'); 874 | } 875 | 876 | // sort the files to be able to predict the order 877 | sort($this->files); 878 | 879 | $this->stats = array( 880 | 'files' => array(), 881 | 'failed_files' => array(), 882 | 'failed_tests' => 0, 883 | 'total' => 0, 884 | ); 885 | 886 | foreach ($this->files as $file) 887 | { 888 | $this->stats['files'][$file] = array(); 889 | $stats = &$this->stats['files'][$file]; 890 | 891 | $relative_file = $this->get_relative_file($file); 892 | 893 | $test_file = tempnam(sys_get_temp_dir(), 'lime'); 894 | $result_file = tempnam(sys_get_temp_dir(), 'lime'); 895 | file_put_contents($test_file, <<&1', escapeshellarg($this->php_cli), escapeshellarg($test_file)), $return); 909 | ob_end_clean(); 910 | unlink($test_file); 911 | 912 | $output = file_get_contents($result_file); 913 | $stats['output'] = $output ? unserialize($output) : ''; 914 | if (!$stats['output']) 915 | { 916 | $stats['output'] = array(array('file' => $file, 'tests' => array(), 'stats' => array('plan' => 1, 'total' => 1, 'failed' => array(0), 'passed' => array(), 'skipped' => array(), 'errors' => array()))); 917 | } 918 | unlink($result_file); 919 | 920 | $file_stats = &$stats['output'][0]['stats']; 921 | 922 | $delta = 0; 923 | if ($return > 0) 924 | { 925 | $stats['status'] = $file_stats['errors'] ? 'errors' : 'dubious'; 926 | $stats['status_code'] = $return; 927 | } 928 | else 929 | { 930 | $this->stats['total'] += $file_stats['total']; 931 | 932 | if (!$file_stats['plan']) 933 | { 934 | $file_stats['plan'] = $file_stats['total']; 935 | } 936 | 937 | $delta = $file_stats['plan'] - $file_stats['total']; 938 | if (0 != $delta) 939 | { 940 | $stats['status'] = $file_stats['errors'] ? 'errors' : 'dubious'; 941 | $stats['status_code'] = 255; 942 | } 943 | else 944 | { 945 | $stats['status'] = $file_stats['failed'] ? 'not ok' : ($file_stats['errors'] ? 'errors' : 'ok'); 946 | $stats['status_code'] = 0; 947 | } 948 | } 949 | 950 | $this->output->echoln(sprintf('%s%s%s', substr($relative_file, -min(67, strlen($relative_file))), str_repeat('.', 70 - min(67, strlen($relative_file))), $stats['status'])); 951 | 952 | if ('dubious' == $stats['status']) 953 | { 954 | $this->output->echoln(sprintf(' Test returned status %s', $stats['status_code'])); 955 | } 956 | 957 | if ('ok' != $stats['status']) 958 | { 959 | $this->stats['failed_files'][] = $file; 960 | } 961 | 962 | if ($delta > 0) 963 | { 964 | $this->output->echoln(sprintf(' Looks like you planned %d tests but only ran %d.', $file_stats['plan'], $file_stats['total'])); 965 | 966 | $this->stats['failed_tests'] += $delta; 967 | $this->stats['total'] += $delta; 968 | } 969 | else if ($delta < 0) 970 | { 971 | $this->output->echoln(sprintf(' Looks like you planned %s test but ran %s extra.', $file_stats['plan'], $file_stats['total'] - $file_stats['plan'])); 972 | } 973 | 974 | if (false !== $file_stats && $file_stats['failed']) 975 | { 976 | $this->stats['failed_tests'] += count($file_stats['failed']); 977 | 978 | $this->output->echoln(sprintf(" Failed tests: %s", implode(', ', $file_stats['failed']))); 979 | } 980 | 981 | if (false !== $file_stats && $file_stats['errors']) 982 | { 983 | $this->output->echoln(' Errors:'); 984 | 985 | $error_count = count($file_stats['errors']); 986 | for ($i = 0; $i < 3 && $i < $error_count; ++$i) 987 | { 988 | $this->output->echoln(' - ' . $file_stats['errors'][$i]['message'], null, false); 989 | } 990 | if ($error_count > 3) 991 | { 992 | $this->output->echoln(sprintf(' ... and %s more', $error_count-3)); 993 | } 994 | } 995 | } 996 | 997 | if (count($this->stats['failed_files'])) 998 | { 999 | $format = "%-30s %4s %5s %5s %5s %s"; 1000 | $this->output->echoln(sprintf($format, 'Failed Test', 'Stat', 'Total', 'Fail', 'Errors', 'List of Failed')); 1001 | $this->output->echoln("--------------------------------------------------------------------------"); 1002 | foreach ($this->stats['files'] as $file => $stat) 1003 | { 1004 | if (!in_array($file, $this->stats['failed_files'])) 1005 | { 1006 | continue; 1007 | } 1008 | $relative_file = $this->get_relative_file($file); 1009 | 1010 | if (isset($stat['output'][0])) 1011 | { 1012 | $this->output->echoln(sprintf($format, substr($relative_file, -min(30, strlen($relative_file))), $stat['status_code'], count($stat['output'][0]['stats']['failed']) + count($stat['output'][0]['stats']['passed']), count($stat['output'][0]['stats']['failed']), count($stat['output'][0]['stats']['errors']), implode(' ', $stat['output'][0]['stats']['failed']))); 1013 | } 1014 | else 1015 | { 1016 | $this->output->echoln(sprintf($format, substr($relative_file, -min(30, strlen($relative_file))), $stat['status_code'], '', '', '')); 1017 | } 1018 | } 1019 | 1020 | $this->output->red_bar(sprintf('Failed %d/%d test scripts, %.2f%% okay. %d/%d subtests failed, %.2f%% okay.', 1021 | $nb_failed_files = count($this->stats['failed_files']), 1022 | $nb_files = count($this->files), 1023 | ($nb_files - $nb_failed_files) * 100 / $nb_files, 1024 | $nb_failed_tests = $this->stats['failed_tests'], 1025 | $nb_tests = $this->stats['total'], 1026 | $nb_tests > 0 ? ($nb_tests - $nb_failed_tests) * 100 / $nb_tests : 0 1027 | )); 1028 | 1029 | if ($this->options['verbose']) 1030 | { 1031 | foreach ($this->to_array() as $testsuite) 1032 | { 1033 | $first = true; 1034 | foreach ($testsuite['stats']['failed'] as $testcase) 1035 | { 1036 | if (!isset($testsuite['tests'][$testcase]['file'])) 1037 | { 1038 | continue; 1039 | } 1040 | 1041 | if ($first) 1042 | { 1043 | $this->output->echoln(''); 1044 | $this->output->error($this->get_relative_file($testsuite['file']).$this->extension); 1045 | $first = false; 1046 | } 1047 | 1048 | $this->output->comment(sprintf(' at %s line %s', $this->get_relative_file($testsuite['tests'][$testcase]['file']).$this->extension, $testsuite['tests'][$testcase]['line'])); 1049 | $this->output->info(' '.$testsuite['tests'][$testcase]['message']); 1050 | $this->output->echoln($testsuite['tests'][$testcase]['error'], null, false); 1051 | } 1052 | } 1053 | } 1054 | } 1055 | else 1056 | { 1057 | $this->output->green_bar(' All tests successful.'); 1058 | $this->output->green_bar(sprintf(' Files=%d, Tests=%d', count($this->files), $this->stats['total'])); 1059 | } 1060 | 1061 | return $this->stats['failed_files'] ? false : true; 1062 | } 1063 | 1064 | public function get_failed_files() 1065 | { 1066 | return isset($this->stats['failed_files']) ? $this->stats['failed_files'] : array(); 1067 | } 1068 | } 1069 | 1070 | class lime_coverage extends lime_registration 1071 | { 1072 | public $files = array(); 1073 | public $extension = '.php'; 1074 | public $base_dir = ''; 1075 | public $harness = null; 1076 | public $verbose = false; 1077 | protected $coverage = array(); 1078 | 1079 | public function __construct($harness) 1080 | { 1081 | $this->harness = $harness; 1082 | 1083 | if (!function_exists('xdebug_start_code_coverage')) 1084 | { 1085 | throw new Exception('You must install and enable xdebug before using lime coverage.'); 1086 | } 1087 | 1088 | if (!ini_get('xdebug.extended_info')) 1089 | { 1090 | throw new Exception('You must set xdebug.extended_info to 1 in your php.ini to use lime coverage.'); 1091 | } 1092 | } 1093 | 1094 | public function run() 1095 | { 1096 | if (!count($this->harness->files)) 1097 | { 1098 | throw new Exception('You must register some test files before running coverage!'); 1099 | } 1100 | 1101 | if (!count($this->files)) 1102 | { 1103 | throw new Exception('You must register some files to cover!'); 1104 | } 1105 | 1106 | $this->coverage = array(); 1107 | 1108 | $this->process($this->harness->files); 1109 | 1110 | $this->output($this->files); 1111 | } 1112 | 1113 | public function process($files) 1114 | { 1115 | if (!is_array($files)) 1116 | { 1117 | $files = array($files); 1118 | } 1119 | 1120 | $tmp_file = sys_get_temp_dir().DIRECTORY_SEPARATOR.'test.php'; 1121 | foreach ($files as $file) 1122 | { 1123 | $tmp = <<'.serialize(xdebug_get_code_coverage()).''; 1128 | EOF; 1129 | file_put_contents($tmp_file, $tmp); 1130 | ob_start(); 1131 | // see http://trac.symfony-project.org/ticket/5437 for the explanation on the weird "cd" thing 1132 | passthru(sprintf('cd & %s %s 2>&1', escapeshellarg($this->harness->php_cli), escapeshellarg($tmp_file)), $return); 1133 | $retval = ob_get_clean(); 1134 | 1135 | if (0 != $return) // test exited without success 1136 | { 1137 | // something may have gone wrong, we should warn the user so they know 1138 | // it's a bug in their code and not symfony's 1139 | 1140 | $this->harness->output->echoln(sprintf('Warning: %s returned status %d, results may be inaccurate', $file, $return), 'ERROR'); 1141 | } 1142 | 1143 | if (false === $cov = @unserialize(substr($retval, strpos($retval, '') + 9, strpos($retval, '') - 9))) 1144 | { 1145 | if (0 == $return) 1146 | { 1147 | // failed to serialize, but PHP said it should of worked. 1148 | // something is seriously wrong, so abort with exception 1149 | throw new Exception(sprintf('Unable to unserialize coverage for file "%s"', $file)); 1150 | } 1151 | else 1152 | { 1153 | // failed to serialize, but PHP warned us that this might have happened. 1154 | // so we should ignore and move on 1155 | continue; // continue foreach loop through $this->harness->files 1156 | } 1157 | } 1158 | 1159 | foreach ($cov as $file => $lines) 1160 | { 1161 | if (!isset($this->coverage[$file])) 1162 | { 1163 | $this->coverage[$file] = $lines; 1164 | continue; 1165 | } 1166 | 1167 | foreach ($lines as $line => $flag) 1168 | { 1169 | if ($flag == 1) 1170 | { 1171 | $this->coverage[$file][$line] = 1; 1172 | } 1173 | } 1174 | } 1175 | } 1176 | 1177 | if (file_exists($tmp_file)) 1178 | { 1179 | unlink($tmp_file); 1180 | } 1181 | } 1182 | 1183 | public function output($files) 1184 | { 1185 | ksort($this->coverage); 1186 | $total_php_lines = 0; 1187 | $total_covered_lines = 0; 1188 | foreach ($files as $file) 1189 | { 1190 | $file = realpath($file); 1191 | $is_covered = isset($this->coverage[$file]); 1192 | $cov = isset($this->coverage[$file]) ? $this->coverage[$file] : array(); 1193 | $covered_lines = array(); 1194 | $missing_lines = array(); 1195 | 1196 | foreach ($cov as $line => $flag) 1197 | { 1198 | switch ($flag) 1199 | { 1200 | case 1: 1201 | $covered_lines[] = $line; 1202 | break; 1203 | case -1: 1204 | $missing_lines[] = $line; 1205 | break; 1206 | } 1207 | } 1208 | 1209 | $total_lines = count($covered_lines) + count($missing_lines); 1210 | if (!$total_lines) 1211 | { 1212 | // probably means that the file is not covered at all! 1213 | $total_lines = count($this->get_php_lines(file_get_contents($file))); 1214 | } 1215 | 1216 | $output = $this->harness->output; 1217 | $percent = $total_lines ? count($covered_lines) * 100 / $total_lines : 0; 1218 | 1219 | $total_php_lines += $total_lines; 1220 | $total_covered_lines += count($covered_lines); 1221 | 1222 | $relative_file = $this->get_relative_file($file); 1223 | $output->echoln(sprintf("%-70s %3.0f%%", substr($relative_file, -min(70, strlen($relative_file))), $percent), $percent == 100 ? 'INFO' : ($percent > 90 ? 'PARAMETER' : ($percent < 20 ? 'ERROR' : ''))); 1224 | if ($this->verbose && $is_covered && $percent != 100) 1225 | { 1226 | $output->comment(sprintf("missing: %s", $this->format_range($missing_lines))); 1227 | } 1228 | } 1229 | 1230 | $output->echoln(sprintf("TOTAL COVERAGE: %3.0f%%", $total_php_lines ? $total_covered_lines * 100 / $total_php_lines : 0)); 1231 | } 1232 | 1233 | public static function get_php_lines($content) 1234 | { 1235 | if (is_readable($content)) 1236 | { 1237 | $content = file_get_contents($content); 1238 | } 1239 | 1240 | $tokens = token_get_all($content); 1241 | $php_lines = array(); 1242 | $current_line = 1; 1243 | $in_class = false; 1244 | $in_function = false; 1245 | $in_function_declaration = false; 1246 | $end_of_current_expr = true; 1247 | $open_braces = 0; 1248 | foreach ($tokens as $token) 1249 | { 1250 | if (is_string($token)) 1251 | { 1252 | switch ($token) 1253 | { 1254 | case '=': 1255 | if (false === $in_class || (false !== $in_function && !$in_function_declaration)) 1256 | { 1257 | $php_lines[$current_line] = true; 1258 | } 1259 | break; 1260 | case '{': 1261 | ++$open_braces; 1262 | $in_function_declaration = false; 1263 | break; 1264 | case ';': 1265 | $in_function_declaration = false; 1266 | $end_of_current_expr = true; 1267 | break; 1268 | case '}': 1269 | $end_of_current_expr = true; 1270 | --$open_braces; 1271 | if ($open_braces == $in_class) 1272 | { 1273 | $in_class = false; 1274 | } 1275 | if ($open_braces == $in_function) 1276 | { 1277 | $in_function = false; 1278 | } 1279 | break; 1280 | } 1281 | 1282 | continue; 1283 | } 1284 | 1285 | list($id, $text) = $token; 1286 | 1287 | switch ($id) 1288 | { 1289 | case T_CURLY_OPEN: 1290 | case T_DOLLAR_OPEN_CURLY_BRACES: 1291 | ++$open_braces; 1292 | break; 1293 | case T_WHITESPACE: 1294 | case T_OPEN_TAG: 1295 | case T_CLOSE_TAG: 1296 | $end_of_current_expr = true; 1297 | $current_line += count(explode("\n", $text)) - 1; 1298 | break; 1299 | case T_COMMENT: 1300 | case T_DOC_COMMENT: 1301 | $current_line += count(explode("\n", $text)) - 1; 1302 | break; 1303 | case T_CLASS: 1304 | $in_class = $open_braces; 1305 | break; 1306 | case T_FUNCTION: 1307 | $in_function = $open_braces; 1308 | $in_function_declaration = true; 1309 | break; 1310 | case T_AND_EQUAL: 1311 | case T_BREAK: 1312 | case T_CASE: 1313 | case T_CATCH: 1314 | case T_CLONE: 1315 | case T_CONCAT_EQUAL: 1316 | case T_CONTINUE: 1317 | case T_DEC: 1318 | case T_DECLARE: 1319 | case T_DEFAULT: 1320 | case T_DIV_EQUAL: 1321 | case T_DO: 1322 | case T_ECHO: 1323 | case T_ELSEIF: 1324 | case T_EMPTY: 1325 | case T_ENDDECLARE: 1326 | case T_ENDFOR: 1327 | case T_ENDFOREACH: 1328 | case T_ENDIF: 1329 | case T_ENDSWITCH: 1330 | case T_ENDWHILE: 1331 | case T_EVAL: 1332 | case T_EXIT: 1333 | case T_FOR: 1334 | case T_FOREACH: 1335 | case T_GLOBAL: 1336 | case T_IF: 1337 | case T_INC: 1338 | case T_INCLUDE: 1339 | case T_INCLUDE_ONCE: 1340 | case T_INSTANCEOF: 1341 | case T_ISSET: 1342 | case T_IS_EQUAL: 1343 | case T_IS_GREATER_OR_EQUAL: 1344 | case T_IS_IDENTICAL: 1345 | case T_IS_NOT_EQUAL: 1346 | case T_IS_NOT_IDENTICAL: 1347 | case T_IS_SMALLER_OR_EQUAL: 1348 | case T_LIST: 1349 | case T_LOGICAL_AND: 1350 | case T_LOGICAL_OR: 1351 | case T_LOGICAL_XOR: 1352 | case T_MINUS_EQUAL: 1353 | case T_MOD_EQUAL: 1354 | case T_MUL_EQUAL: 1355 | case T_NEW: 1356 | case T_OBJECT_OPERATOR: 1357 | case T_OR_EQUAL: 1358 | case T_PLUS_EQUAL: 1359 | case T_PRINT: 1360 | case T_REQUIRE: 1361 | case T_REQUIRE_ONCE: 1362 | case T_RETURN: 1363 | case T_SL: 1364 | case T_SL_EQUAL: 1365 | case T_SR: 1366 | case T_SR_EQUAL: 1367 | case T_SWITCH: 1368 | case T_THROW: 1369 | case T_TRY: 1370 | case T_UNSET: 1371 | case T_UNSET_CAST: 1372 | case T_USE: 1373 | case T_WHILE: 1374 | case T_XOR_EQUAL: 1375 | $php_lines[$current_line] = true; 1376 | $end_of_current_expr = false; 1377 | break; 1378 | default: 1379 | if (false === $end_of_current_expr) 1380 | { 1381 | $php_lines[$current_line] = true; 1382 | } 1383 | } 1384 | } 1385 | 1386 | return $php_lines; 1387 | } 1388 | 1389 | public function compute($content, $cov) 1390 | { 1391 | $php_lines = self::get_php_lines($content); 1392 | 1393 | // we remove from $cov non php lines 1394 | foreach (array_diff_key($cov, $php_lines) as $line => $tmp) 1395 | { 1396 | unset($cov[$line]); 1397 | } 1398 | 1399 | return array($cov, $php_lines); 1400 | } 1401 | 1402 | public function format_range($lines) 1403 | { 1404 | sort($lines); 1405 | $formatted = ''; 1406 | $first = -1; 1407 | $last = -1; 1408 | foreach ($lines as $line) 1409 | { 1410 | if ($last + 1 != $line) 1411 | { 1412 | if ($first != -1) 1413 | { 1414 | $formatted .= $first == $last ? "$first " : "[$first - $last] "; 1415 | } 1416 | $first = $line; 1417 | $last = $line; 1418 | } 1419 | else 1420 | { 1421 | $last = $line; 1422 | } 1423 | } 1424 | if ($first != -1) 1425 | { 1426 | $formatted .= $first == $last ? "$first " : "[$first - $last] "; 1427 | } 1428 | 1429 | return $formatted; 1430 | } 1431 | } 1432 | 1433 | class lime_registration 1434 | { 1435 | public $files = array(); 1436 | public $extension = '.php'; 1437 | public $base_dir = ''; 1438 | 1439 | public function register($files_or_directories) 1440 | { 1441 | foreach ((array) $files_or_directories as $f_or_d) 1442 | { 1443 | if (is_file($f_or_d)) 1444 | { 1445 | $this->files[] = realpath($f_or_d); 1446 | } 1447 | elseif (is_dir($f_or_d)) 1448 | { 1449 | $this->register_dir($f_or_d); 1450 | } 1451 | else 1452 | { 1453 | throw new Exception(sprintf('The file or directory "%s" does not exist.', $f_or_d)); 1454 | } 1455 | } 1456 | } 1457 | 1458 | public function register_glob($glob) 1459 | { 1460 | if ($dirs = glob($glob)) 1461 | { 1462 | foreach ($dirs as $file) 1463 | { 1464 | $this->files[] = realpath($file); 1465 | } 1466 | } 1467 | } 1468 | 1469 | public function register_dir($directory) 1470 | { 1471 | if (!is_dir($directory)) 1472 | { 1473 | throw new Exception(sprintf('The directory "%s" does not exist.', $directory)); 1474 | } 1475 | 1476 | $files = array(); 1477 | 1478 | $current_dir = opendir($directory); 1479 | while ($entry = readdir($current_dir)) 1480 | { 1481 | if ($entry == '.' || $entry == '..') continue; 1482 | 1483 | if (is_dir($entry)) 1484 | { 1485 | $this->register_dir($entry); 1486 | } 1487 | elseif (preg_match('#'.$this->extension.'$#', $entry)) 1488 | { 1489 | $files[] = realpath($directory.DIRECTORY_SEPARATOR.$entry); 1490 | } 1491 | } 1492 | 1493 | $this->files = array_merge($this->files, $files); 1494 | } 1495 | 1496 | protected function get_relative_file($file) 1497 | { 1498 | return str_replace(DIRECTORY_SEPARATOR, '/', str_replace(array(realpath($this->base_dir).DIRECTORY_SEPARATOR, $this->extension), '', $file)); 1499 | } 1500 | } 1501 | -------------------------------------------------------------------------------- /test/run_all.php: -------------------------------------------------------------------------------- 1 | register_dir(dirname(__FILE__).'/base'); 7 | $testSuite->register_dir(dirname(__FILE__).'/checks'); 8 | $testSuite->run(); --------------------------------------------------------------------------------