I know how to test classes and functions but I was wondering how to test a file and pass parameters via the console to this file.
For example I have index.php which needs 1 integer num via the console using fgets(STDIN). Can I make a PHPUnit file and test index.php ?
Unit Tested can be pretty much any code on the planet. In your case - pipe input data to script with echo bash command. (Of course depends on OS how to pass data through command shell) :
use PHPUnit\Framework\TestCase;
class ConsoleAppTest extends TestCase
{
public function testIndexFile()
{
$out = shell_exec("echo 123 | php index.php");
// check standard output or some DB modifications if script mangles DB
$is_ok = verify_results($out);
$this->assertSame($is_ok, true);
}
}
Related
I was writing a very basic test for a Laravel Artisan Console Command, like this:
$this->artisan("my-command", ["--some-option" => "some-value"])
->expectsOutput("the expected output");
The test didn't pass. I got really troubled because "the expected output" was exactly what the command was outputting when executed mannualy.
But that's ok, I just have to inspect what the command output actually is when executed through automated tests, right? But wait, how do I do that?
I tried the following:
$output = new BufferedConsoleOutput();
Artisan::call("my-command", ["--some-option", "some-value"], $output);
// dd($output->fetch()
$this->assertTrue($output->fetch() === "the expected output");
But $output->fetch() seems to be always empty.
In brief: How do I print the actual output of a Laravel command in the context of a test?
This isn't something I would want to set up and leave in your tests, but if you prevent Laravel from mocking the console output, you can capture it for inspection.
Assuming you have this test that fails:
public function testStuffDoesntBreak(): void
{
$this->artisan("my-command", ["--some-option" => "some-value"])
->expectsOutput("the expected output");
}
You can rewrite it to this:
use Illuminate\Support\Facades\Artisan;
...
public function testStuffDoesntBreak(): void
{
$this->withoutMockingConsoleOutput()
->artisan("my-command", ["--some-option" => "some-value"]);
// capture the text output from the command
$result = Artisan::output();
// use standard text assertions
$this->assertEquals("the expected output", $result);
}
When you've disabled console mocking, the artisan() method stops being fluent and instead returns the exit code of the command. But it does allow the Artisan facade to access its output. Whether you want to rewrite tests or just change them on the fly in case of an error is personal preference. I've done the latter, as I'd rather not miss out on features like expectsTable().
I have an application which is built in CakePHP 3.
It uses Console Commands to execute several intensive processes in the background using cron.
The application consists of 5 individual commands:
src/Command/Stage1Command.php
src/Command/Stage2Command.php
src/Command/Stage3Command.php
src/Command/Stage4Command.php
src/Command/Stage5Command.php
These can be executed manually by running each one individually, e.g. to execute Stage1Command.php:
$ php bin/cake.php stage1
To make them run via Cron, I created a 6th command (src/Command/RunAllCommand.php) which goes through these in order.
// src/Command/RunAllCommand.php
class RunAllCommand extends Command
{
public function execute(Arguments $args, ConsoleIo $io)
{
$stage1 = new Step1Command();
$this->executeCommand($stage1);
// ...
$stage5 = new Stage5Command();
$this->executeCommand($stage5);
}
}
This works fine so I can now execute everything with 1 command, php bin/cake.php run_all, which will be added as a cron task to automate running the 5 processes.
The problem I'm having is that each of the 5 commands (Stage1Command ... Stage5Command) produces output which appears on standard output in the console.
I need to be able to write the output produced by each of the 5 commands individually into dynamically named files.
So I can't do something like this
$ php bin/cake.php run_all > output.log
Because
output.log would contain everything, i.e. the output from all 5 commands.
output.log isn't a dynamic filename, it has been entered manually on the command line (or as the output destination of the cron task).
I looked at Redirecting PHP output to a text file and tried the following.
Added ob_start(); to RunAllCommand.php:
namespace App\Command;
ob_start();
class RunAllCommand extends Command { ... }
After executing the first task (Stage1Command) capturing ob_get_clean() to a variable called $content:
$stage1 = new Step1Command();
$this->executeCommand($stage1);
$content = ob_get_clean();
When I var_dump($content); it comes out as an empty string:
string(0) ""
But the output is still produced on the command line when executing php bin/cake.php run_all (RunAllCommand.php).
My plan for the dynamic filename was to generate it with PHP inside RunAllCommand.php, e.g.
// $id is a dynamic ID generated from a database call.
// This $id is being generated inside a foreach() loop so is different on each iteration (hence the dynamic nature of the filename).
$id = 234343;
$filename_stage1 = 'logs/stage1_' . $id . '.txt'; // e.g. "logs/stage1_234343.txt"
Then write $content to the above file, e.g.
file_put_contents($filename_stage1, $content);
So I have 2 problems:
The output is being echoed to the console, and unavailable in $content.
Assuming (1) is fixed, how to "reset" the output buffering such that I can use file_put_contents with 5 different filenames to capture the output for the relevant stage.
On each command file you could use the LogTrait then output what file is outputting before any commands to seperate what command is logging or setup the log config with different scopes to output to different files. example of outputting to the cli-debug.log file.
use Cake\Log\LogTrait;
class Stage1Command extends Command
{
use LogTrait;
public function execute(Arguments $args, ConsoleIo $io)
{
$this->log('Stage 1 Output: ', 'debug');
//do stuff
$this->log('output stage 1 stuff', 'debug');
}
}
I have two suggestions for solving your issue.
Option 1 - Using shell_exec
shell_exec returns a string of the output, so you can write it to a log file directly.
public function execute(Arguments $args, ConsoleIo $io)
{
$stage1_log = shell_exec('bin/cake stage1 arguments');
file_put_contents('stage1_dynamic_log_file.txt', $stage1_log);
$stage2_log = shell_exec('bin/cake stage2 arguments');
file_put_contents('stage2_dynamic_log_file.txt', $stage2_log);
}
Option 2 - Overwrite the ConsoleOut stream
Alternatively a more CakePHP style would be to call the command slightly differently. If you look at the contents of executeCommand() it does a few checks and then calls command->run($args, $io)
Also if you look at how the ConsoleIo is constructed, we can override the output method so instead of using php://stdout we could use a file instead, if you look at the code for ConsoleOutput it's just using normal fopen and fwrite.
use Cake\Console\ConsoleIo;
use Cake\Console\ConsoleOutput;
public function execute(Arguments $args, ConsoleIo $io)
{
// File names
$id = 234343;
$filename_stage1 = 'logs/stage1_' . $id . '.txt';
// Create command object
$stage1 = new Stage1Command();
// Define output as this filename
$output = new ConsoleOutput($filename_stage1);
// Create a new ConsoleIo using this new output method
$stage1_io = new ConsoleIo($output);
// Execute passing in the ConsoleIo with text file for output
$this->executeCommand($stage1, ['arguments'], $stage1_io);
}
I have a couple of classes (services) which I use in the main Laravel app as well as via artisan commands.
What I would like to do is being able to display some output when invoking such class methods via artisan (as $this->info(...) inside the command itself) and just eventually return a value (response, result object or similar) when using them within the main app.
Right now I just used them as commands and achieved the output effect by echoing the statements I needed directly from within the methods, but I'm convinced this is not the correct approach because as soon as I start using them in the app I guess I'm gonna have my responses polluted with these info statements (as well as double headers or similar issues, I imagine).
I wonder what would be a better way to approach such scenario.
Below a code example of what I'm trying to achieve:
// DoSequenceCommand
public function fire() {
MyClass::doSequence();
}
// MyClass (Facade)
public function doSequence() {
echo "Beginning Step 1\n"; // THIS I WOULD BE ABLE TO OUTPUT ONLY IF EXECUTED VIA COMMAND LINE
$s1 = $this->step1();
echo "Step 1 " . ( $s1 ? "succeded" : "failed" ) . "!\n";
echo "Beginning Step 2\n";
$s2 = $this->step2();
echo "Step 2 " . ( $s2 ? "succeded" : "failed" ) . "!\n";
return [ "s1" => $s1, "s2" => $s2];
}
You could either:
pass a flag into doSequence() to determine if it should output info statements
fire events in doSequenence() and listen for them in your command to output info from there
break out the sequence into multiple functions, and call them independently in your command, along with your info statements
The events option might be the cleanest way.
I am creating a cron job that will run every few minutes and it will read from database data to see which function needs to be called and act accordingly to the data but half the crons are written in codeigniter and the other half in native php.
Is there any way to do this? I have been googling but the anwser. I came up with is that it is impossible. I tried changing directory and than including or requiring index.php from the codeigniter, in which the function is that i need to call.
while doing this if my class is written in native php, It returns some errors that don't make sense and if I correct those errors, I would say that half the system function from codeigniter would be gone. It would still be a question if it will work even then.
If my class is written in codeigniter, when I include index.php, it just breaks. No errors no response or it says that the "ENVIRONMENT" variables are already defined. Thus I have been looking for a way to undefine those variables from config file or overwrite them to null or empty string but nothing works.
If you have any ideas it would be much appreciated.
I saw a question question link about some cron jobs in php where the user #michal kralik gave an answer about what i am doing in general with database and one cron class that will call other crons (coould use help for that too).
btw forgot to mention that using curl and exec will not work, because on our servers they sometimes just stop working for no reason.
UPDATE 1:
this is my class currently after so many tries:
class Unicron extends MY_Controller {
public $config;
function __construct() {
parent::__construct();
}
public function init(){
$config['base_url'] = "http://localhost/test";
define('EXT_CALL', true); // Added EXT_CALL constant to mark external calls
$_GET['controller/method'] = ''; // add pair to $_GET with call route as key
$current = getcwd(); // Save current directory path
chdir('C:/inetpub/wwwroot/test/'); // change directory to CI_INSTALLATION
include_once 'index.php'; // Add index.php (CI) to script
define('BASEPATH', 'C:/inetpub/wwwroot/test/system/');
$this->load->library("../../application/controllers/controller.php");
$job = new $this->controller->Class();
$isDone = $job->exportExcel(somekey);
echo $isDone;
$CI =& get_instance(); // Get instance of CI
$CI->exportExcel('baseparts', 'exportExcel');
// FOR STATIC CALLING!!
//$CI->method('controller','method');
//replace controller and method with call route
// eg: $CI->list('welcome','list'); If calling welcome/list route.
//$OUT->_display(); // to display output. (quick one)
// Or if you need output in variable,
//$output = $CI->load->view('VIEW_NAME',array(),TRUE);
//To call any specific view file (bit slow)
// You can pass variables in array. View file will pick those as it works in CI
chdir($current); // Change back to current directory
echo $current;
}
where i try to define BASEPATH it does not define it nor override the previous value.
In the index.php of other codeigniter i put:
if(!defined('ENVIRONMENT'))
define('ENVIRONMENT', 'development');
this way i resolved my issue with ENVIRONMENT already being set error.
this is a few things i found and combined together, hoping it could work but still when i call it via command line it shows nothing (even tried echo anything everywhere and nothing).
This may be a long comment rather than a answer as the code supplied requires a lot of work to make it useful.
Running multiple instances of 'codeigniter' - executed from codeigniter.
Using the 'execute programs via the shell' from PHP. Each instance runs in its own environment.
There are some excellent answers already available:
By default the PHP commands 'shell' wait for the command to complete...
732832/php-exec-vs-system-vs-passthru.
However, we want to 'fire and forget' quite often so this answer is quite useful...
1019867/is-there-a-way-to-use-shell-exec-without-waiting-for-the-command-to-complete
All i did was use this show an example of how to use 'codeigniter' to do this. The example was the 'Hello World' cli example from user manual. The version of ci is 2.1.14. I haven't used 'ci' before.
It is tested and works on 'PHP 5.3.18' on windows xp.
As well as the usual 'Hello World' example, i used an example of a a command that uses 'sleep' for a total of 20 seconds so that we can easily see that the 'ci' instances are separate from each other while executing.
Examples:
<?php
class Tools extends CI_Controller {
// the usual 'hello world' program
public function message($to = 'World')
{
echo "Hello {$to}!".PHP_EOL;
}
// so you can see that the processes are independant and 'standalone'
// run for 20 seconds and show progress every second.
public function waitMessage($to = 'World')
{
$countDown = 20;
while ($countDown >= 0) {
echo "Hello {$to}! - ending in {$countDown} seconds".PHP_EOL;
sleep(1);
$countDown--;
}
}
}
'ci' code to run 'ci' code...
<?php
class Runtools extends CI_Controller {
/*
* Executing background processes from PHP on Windows
* http://www.somacon.com/p395.php
*/
// spawn a process and do not wait for it to complete
public function runci_nowait($controller, $method, $param)
{
$runit = "php index.php {$controller} {$method} {$param}" ;
pclose(popen("start \"{$controller} {$method}\" {$runit}", "r"));
return;
}
// spawn a process and wait for the output.
public function runci_wait($controller, $method, $param)
{
$runit = "php index.php {$controller} {$method} {$param}";
$output = exec("{$runit}");
echo $output;
}
}
How to run them from the cli...
To run the 'ci' 'nowait' routine then do:
php index.php runtools runci_nowait <controller> <method> <param>
where the parameters are the ci controller you want to run. Chnge to 'runci_wait' for the other one.
'Hello World: 'wait for output' - (ci: tools message )
codeigniter>php index.php runtools runci_wait tools message ryan3
Hello ryan3!
The waitMessage - 'do not wait for output' - (ci : tools waitMessage )
codeigniter>php index.php runtools runci_nowait tools waitMessage ryan1
codeigniter>php index.php runtools runci_nowait tools waitMessage ryan2
These will start and run two separate 'ci' processes.
In the web application we use same code and modules with different configs like:
in index.php file app will decide, wchin config to turn:
switch($_SERVER['HTTP_HOST']){
default:
$yii=$webRoot.'/framework/yiilite.php';
$config = $webRoot.'/protected/config/main.php';
break;
case 'someurl.com':
...
break;
...
}
But, how can I do it with console application?
The reason in that I use different databases ant etc.
is it possible to do something like this:
$ ./protected/yiic --application=myappname [all defined commands as default]
in the code a
--application
will set with which console config to work
more explanation
my answer to #Joe Miller
But the problem is, how choose theme?
I did in the files foloowings:
in protectes/yiic
$__appId = null;
for( $__i=1,$__max=count($argv); $__i<$__max; ++$__i ) {
if ( strpos($argv[$__i],'--appid',0) === 0 ) {
$__appId = substr($argv[$__i], 8);
unset($argv[$__i]);
}
}
require_once(dirname(__FILE__).'/yiic.php');
and in protected/yiic.php
$__appIdsList = array(
'my_site_1',
'my_site_2',
'my_site_3',
'my_site_4',
);
$yiic=dirname(__FILE__).'/../framework/yiic.php';
$config=dirname(__FILE__).'/config/console_'.$__appId.'.php';
require_once($yiic);
and it works and it catchs that config file what I need
./protected/yiic --appid=my_site_1
bu when I`m trying to do migrate
./protected/yiic --appid=my_site_1 migrate
the app cant recognize comman and gives me migrates help list
And final conslusion (I solved it)
I`d like to add transperent console command without affecting it to other execution of builtin console commands and custom console commands.
Another requirement is, solve this issue on a low-level approach, without inheritance or overloading other classes or methods.
So, my solution is:
in protected/yiic
#!/usr/bin/env php
<?php
$__appId = null;
for( $__i=1,$__max=count($argv); $__i<$__max; ++$__i ) {
if ( strpos($argv[$__i],'--appid',0) === 0 ) {
$__appId = substr($argv[$__i], 8);
unset($argv[$__i]);
unset($_SERVER['argv'][$__i]);
$argv = $_SERVER['argv'] = array_values($argv);
}
}
require_once(dirname(__FILE__).'/yiic.php');
and in /protected/yiic.php
<?php
// change the following paths if necessary
$__appIdsList = array(
'app_1',
'app_2',
);
$yiic=dirname(__FILE__).'/../framework/yiic.php';
$config=dirname(__FILE__).'/config/console_'.$__appId.'.php';
if ( !is_file($config) ) {
die("Error: There is no or wrong parametr appid. Please set parametr or correct. Example -appid={application_name}\n\tThe list of available appid:\n\t\t - ".implode("\n\t\t - ", $__appIdsList));
}
require_once($yiic);
and now it is possible to set param "appid" in any place of command line, like
./protected/yiic migrate --appid=app_1
and it acts only in that app what we need
PS: in any case, thanks #Joe Miller
Copy yiic.php for example to cron.php and modify the config file in the cron.php
then use as if it were yiic, for example:
cd ~/protected;php ~/protected/cron.php app command --param=value >> ~/runtime/crontab.log
If I've understood what you're trying to do correctly, I think you might need something like this. I've referred to this article http://www.yiiframework.com/doc/guide/1.1/en/topics.console#creating-commands. I've not tried this, so I'm just interpreting the article.
Create a base command class, from which you will extend all the other commands. The base class run() method selects the config file to load.
In protected>commands you need a file migrate.php. This must contain the class MigrateCommand, and must extend CConsoleCommand. You can then override the run() method of this class to allow parameters to be passed to the method. e.g.
In protected>commands>baseCommand.php
class MyBaseCommand extends CConsoleCommand{
public function run($args){
//Code here to select the config file to load
//$args are any arguments you have passed in the command line
}
}
In protected>commands>migrate.php
class Migrate extends MyBaseCommand{
public function run($args){
parent::run($args);
//Do your own stuff here
}
}
you should then be able to call the command as;
./protected/yiic migrate --appid=my_site_1
Note that the name of the command appears first, I'm not sure if this is important, but it's what the guide says! I hope I've understood your question this time!
I think, I founded more confortable solution!
It`s more easy, and solved all my requirements.
in the file
protected/yiic.php
I write:
...
$yiic=dirname(__FILE__).'/../lib/framework/yiic.php';
if ( strpos(__FILE__,{first/place}) !== false ) {
$config=dirname(__FILE__).'/config/first_config.php';
} elseif ( strpos(__FILE__,{second/place}) !== false ) {
$config=dirname(__FILE__).'/config/second_plase.php';
} else {
// by default
$config=dirname(__FILE__).'/config/console.php';
}
require_once($yiic);
...
where {first/place},{second/place} - a part of the project`s path. For example:
Your first project is placed in:
/var/www/aproject/first_one
and the second one on the
/var/www/aproject/second_one
than you checks will be:
// for first porject
strpos(__FILE__,'aproject/first_one') !== false
and etc.