Question

Laravel 11: Is it possible to force PHPUnit to trigger / "press" CTRL+C to simulate interruption of an artisan command?

I have an artisan command that can run for minutes or hours, processing multitude of records in a loop within the handle() method.

I have introduced the ability to interrupt such a long process by humans, by allowing them to press CTRL+C during command runtime.

The following code works flawlessly:

pcntl_async_signals(true);
pcntl_signal(SIGINT, [$this, 'shutdown']);
pcntl_signal(SIGTERM, [$this, 'shutdown']);

and as soon as user presses CTRL+C, the shutdown() method is being called. It doesn't matter what shutdown() does, it matters that I can trigger it.

However when testing, I don't know how can I simulate pressing CTRL+C to trigger shutdown, so that code-coverage shows body of shutdown as green (called), rather than red (not reached).

I'm already testing whether necessary methods are available:

public function test_sync_process_can_be_interrupted_by_user_via_terminal(): void
{
    $this->assertTrue(function_exists('pcntl_async_signals'));
    $this->assertTrue(function_exists('pcntl_signal'));
}

This test is green, but this is not enough for me. I want unit test to "press" these keys. How can I force PHPUnit to do so when testing my artisan command $this->artisan('my:long-command')?

 2  52  2
1 Jan 1970

Solution

 0

With pcntl_signal() you register the signal handler. with posix_kill() you can send a signal (cf. kill(1), full example on the PHP manual pcntl_signal page).

Now if you have the process id (PID) of the artisan command, you can signal it with any of those signals you registered a handler for to try this out (when you run it in background).

The artisan command tester uses the symfony console library under the hood IIRC and it perhaps is able to provide the process id of the appropriate process. On Linux and Unix it is required that the actual PHP executable is invoked prefixed with exec so that the PID that is communicated back to the console library is the correct PID.

From what I know Symfony Console has support for that, otherwise modifying the command may suffice if it does not work out of the box.

This is just a rough outline, but hopefully it already suffices or gives enough pointers.

/Edit:

When re-visiting the answer I remembered that in the past that the artisan command tester is suited for straight forward command testing. Trying to use it for anything different is rather cumbersome in my experience and the experiment I did went into /dev/null afterwards not being able to get grip on the underlying output having implementation to inspect actual output. One reason was that it's not the way that tester works as I learned and I wanted something entry-level, not as specific in specific you're looking for.

From that experiment, what you'd perhaps want is a tester for your own for any commands signal testing. How could it look like?

Here some rough code for inspiration, proc_open allows to have the command run while the test code continues and the rest is aligned with the description above. The timing may vary, do your experiments. Output capturing and environment control can be done with proc_opens additional parameters, now as I write it, command as array may spare the exec call.

$command = "php /path/to/artisan.php command parameters";

try {
    $handle = proc_open("exec $command");
    assert(false !== $handle);
    usleep(10_000); # time command has to start and setup signal handlers
    $pid = proc_get_status($handle);
    posix_kill($pid, SIGINT);
    usleep(10_000); # time command has to handle signals
    assert(false === proc_get_status($handle)['running']);
} finally {
    isset($handle) && $handle && proc_close($handle);
}

Always use the latest PHP version, proc_open is constantly improved. Otherwise look into pcntl and posix functions as well for forking subprocesses from PHP.

2024-07-20
hakre