Runner.php

<?php

namespace Tlf\Tester;

class Runner extends \Tlf\Cli {

    public $server_port = null;

    public function backward_compatability(){

        if (isset($this->args['dir.test'])&&!is_array($this->args['dir.test'])){
            $this->args['dir.test'] = [$this->args['dir.test']];
        }

    }

    /**
     * get the server host 
     * @if called by `phptest server`, @then generate random port & write to file
     * @if called by running `->get()` tests, @then load port from file
     *
     */
    public function get_server_host(){
        if ($this->server_port!=null)return 'localhost:'.$this->server_port;


        $dir = getcwd() .'/'. $this->args['server.dir'];
        $file = $dir.'/.phptest-host';

        if ($this->command == 'server'){
            $port = random_int(3001, 5000);
            file_put_contents($file, $port);
            return 'localhost:'.$port;
        }

        $this->server_port = file_get_contents($file);
        return 'localhost:'.$this->server_port;
    }

    /**
     * get the server protocol
     * @todo add configurability & dynamic stuff 
     */
    public function get_server_protocol(){
        return 'http://';
    }

    /**
     * Start a localhost server for testing
     * @warning this early implementation will change
     */
    public function start_server($cli, $args){
        $host = $this->get_server_host();
        $dir = $this->pwd .'/'. $this->args['server.dir'];
        $delivery_script = $dir .'/'. $this->args['server.router'];
        $command = "php -S $host -t $dir $delivery_script";
        echo "Execute:\n  $command\n\n";
        system($command);
    }

    /**
     * Copies sample test files into `getcwd().'/test'`
     */
    public function init($cli, $args){
        $dir = getcwd().'/test';
        $continue = readline("Initialize test dir at $dir? (y/n) ");
        if ($continue!=='y')return;
        mkdir($dir);
        mkdir($dir.'/run');
        mkdir($dir.'/src');
        mkdir($dir.'/input');
        mkdir($dir.'/Server');
        mkdir($dir.'/input/Compare');
        $files = [
            'run/Compare.php',
            'run/Server.php',
            "src/Tester.php",
            'input/Compare/Compare.php',
            'input/Compare/Compare.txt',
            'Server/deliver.php',
            'bootstrap.php',
            'config.json',
        ];
        foreach ($files as $f){
            $dest = $dir.'/'.$f;
            if (file_exists($dest)){
                echo "\nSkip: $dest";
            } else {
                echo "\nCreate: $dest";
                copy(dirname(__DIR__).'/test/'.$f, $dir.'/'.$f);
            }
        }
    }

    public function require_directory($path){
        foreach (scandir($path) as $f){
            if ($f=='.'||$f=='..')continue;
            if (is_dir($path.'/'.$f)){
                $this->require_directory($path.'/'.$f);
            } else if (substr($f,-4)=='.php'){
                (function() use ($path, $f){
                    require_once($path.'/'.$f);
                })();
            }
        }
    }

    /**
     * executes all tests inside test directories (config `dir.test`)
     */
    public function run_dir($cli, $args){
        $info = [
            'pass'=>0,
            'fail'=>0,
            'tests_run'=>0,
            'disabled'=>0,
        
        ];
        $dir = $cli->pwd;

        // load required files
        foreach ($args['file.require'] as $file){
            $path = $dir.'/'.$file;
            // var_dump($path);
            if (is_dir($path)){
                $this->require_directory($path);               
            } else {
                (function() use ($path){
                    require_once($path);
                })();
            }
        }

        $phpFiles = $this->get_php_files($dir, $args['dir.test']);

        $excludes = $args['dir.exclude'];
        $test_files = [];
        $classes_to_test = $args['class'] ?? [];
        foreach ($phpFiles as $relPath){
            if ($this->is_excluded($excludes, $relPath))continue;
            $filePath = $dir.'/'.$relPath;
            $class = $this->get_test_class($filePath);
            $class_base = substr($class, strrpos($class, '\\')+1);
            if (count($classes_to_test)>0&&!in_array($class_base, $classes_to_test))continue;
            $test_files[$filePath] = $class;
        }

        //sort test files if you like

        $tests = [];
        foreach ($test_files as $file=>$class){
            $results = $this->test_class($class, $args,$cli);
            if ($results===false)continue;

            $info['pass']+=$results['pass'];
            $info['fail']+=$results['fail'];
            $info['disabled']+=$results['disabled'];
            $info['tests_run']+=$results['tests_run'];
            $info['class'][$class] = $results;
        }
        
        echo "\n";

        echo "\nTests: ".$info['tests_run'];
        echo "\nPass: ".$info['pass'];
        echo "\nFail: ".$info['fail'];
        echo "\nDisabled: ".$info['disabled'];

        echo "\n";

        return $info;
    }

    public function test_class($class, $args, $cli){
        // $ob_level = Utility::startOb();
        //run the test class
        if (!class_exists($class,true))return false;
        $tester = new $class($args, $cli);
        $results = $tester->run();
        // $output = Utility::endOb($ob_level);
        return $results;
    }


    /**
     * Get all php files for testing. @see(get_test_class) is used to filter out files that don't contain a test class.
     *
     * @param string $dir the root directory for the project
     * @param array $sub_dirs the sub-directories where files should be searched for
     */
    public function get_php_files(string $dir, array $sub_dirs){
        // find all files that need testing
        $files = [];
        foreach ($sub_dirs as $sub_dir){
            $search_dir = $dir.'/'.$sub_dir;
            $files = array_merge($files, \Tlf\Tester\Utility::getAllFiles($search_dir,$dir,'.php'));
        }
        return $files;
    }

    /**
     * Check if a file is excluded from testing
     *
     * @param $excludes generally, the 'dir.excludes' config
     * @param $relPath the relative path of a file that we're checking for
     * @return bool true or false
     */
    public function is_excluded($excludes, $relPath){
        //check if file is excluded from testing
        foreach ($excludes as $e){
            $re = $relPath;
            if ($re[0]!='/')$re = '/'.$re;
            if ($e[0]!='/')$e = '/'.$e;
            if (substr($relPath,0,strlen($e))==$e)return true;
        }
        if (in_array($relPath, $excludes))return true;

        return false;
    }

    /**
     * Get name of class in file. The class in the file must be a subclass of \Tlf\Tester
     *
     * @param $filePath the path to the file containing a test class
     * @return class name or null
     * @side_effect require_once the file
     */
    public function get_test_class($filePath){

        (function() use ($filePath){
            require_once($filePath); 
        })();
        $class = Utility::getClassFromFile($filePath);
        if ($class==null){
            $this->report("No class found in $filePath");
            return null;
        }
        if (!is_a($class, '\\Tlf\\Tester', true))return null;
        return $class;
    }

    public function report($msg){
        echo "\n$msg\n";
    }
}