Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
75.00% covered (warning)
75.00%
24 / 32
57.14% covered (warning)
57.14%
4 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
RedefineExit
75.00% covered (warning)
75.00%
24 / 32
57.14% covered (warning)
57.14%
4 / 7
26.25
0.00% covered (danger)
0.00%
0 / 1
 setup
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 setupDangerously
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 restoreAll
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 expirationHandler
62.50% covered (warning)
62.50%
5 / 8
0.00% covered (danger)
0.00%
0 / 1
11.38
 exitHandler
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
4.02
 getBacktrace
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 ignoreExitCall
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * Main package entry point.
4 *
5 * @package automattic/patchwork-redefine-exit
6 */
7
8namespace Automattic;
9
10use Automattic\RedefineExit\ExitException;
11
12/**
13 * Main package entry point.
14 */
15class RedefineExit {
16
17    /**
18     * Function this instance is handling.
19     *
20     * @var string
21     */
22    private $which;
23
24    /**
25     * Set up the redefinitions.
26     */
27    public static function setup() {
28        foreach ( array( 'exit', 'die' ) as $func ) {
29            $instance        = new static();
30            $instance->which = $func;
31
32            $handle = \Patchwork\redefine( $func, array( $instance, 'exitHandler' ) );
33            $handle->addExpirationHandler( array( $instance, 'expirationHandler' ) );
34            $handle->unsilence();
35        }
36    }
37
38    /**
39     * Set up the redefinitions, without registering the expiration handlers.
40     */
41    public static function setupDangerously() {
42        foreach ( array( 'exit', 'die' ) as $func ) {
43            $instance        = new static();
44            $instance->which = $func;
45            \Patchwork\redefine( $func, array( $instance, 'exitHandler' ) );
46        }
47    }
48
49    /**
50     * Restore all Patchwork redefines except ours.
51     */
52    public static function restoreAll() {
53        \Patchwork\restoreAll();
54        static::setup();
55    }
56
57    /**
58     * Handle Patchwork removing the redefinition.
59     *
60     * @private
61     */
62    public function expirationHandler() {
63        // Allow removing the handler when called from Patchwork's own __destruct or our restoreAll function.
64        // Otherwise complain and exit.
65        $bt = $this->getBacktrace();
66        foreach ( $bt as $data ) {
67            if ( isset( $data['class'] ) && $data['class'] === \Patchwork\CallRerouting\Handle::class && $data['function'] === '__destruct' ) {
68                return;
69            }
70            if ( isset( $data['class'] ) && $data['class'] === static::class && $data['function'] === 'restoreAll' ) {
71                return;
72            }
73        }
74
75        fprintf( STDERR, "The Patchwork handler for %s was removed. This breaks tests, don't do it.\nStack trace:\n%s\n", $this->which, ( new \Exception() )->getTraceAsString() );
76        exit( 1 );
77    }
78
79    /**
80     * Handle a call to `exit` or `die`.
81     *
82     * @private
83     * @param string|int|null $arg Argument.
84     * @throws ExitException Whenever `$this->ignoreExitCall()` doesn't return true.
85     */
86    public function exitHandler( $arg = null ) {
87        // While Patchwork does have a way to exclude files from replacement,
88        // it requires non-wildcarded paths in the patchwork.json. Easier to just
89        // check here for calls from within PHPUnit itself.
90        $bt   = $this->getBacktrace();
91        $func = \Patchwork\getFunction();
92        foreach ( $bt as $i => $data ) {
93            if ( $data['function'] === $func ) {
94                $stack                = array_slice( $bt, $i );
95                $stack[0]['function'] = $this->which;
96                if ( $this->ignoreExitCall( $stack ) ) {
97                    return \Patchwork\relay();
98                }
99                break;
100            }
101        }
102
103        throw new ExitException( $this->which, $arg );
104    }
105
106    /**
107     * Call debug_backtrace().
108     *
109     * @return array[]
110     */
111    protected function getBacktrace() {
112        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
113        return debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS );
114    }
115
116    /**
117     * Determine if a call should be ignored.
118     *
119     * @param array[] $stack Call stack to check, as from `debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS )`.
120     *   The top stack frame is the `exit`/`die` call itself.
121     * @return bool True if the `exit`/`die` called via this stack frame should be ignored.
122     */
123    protected function ignoreExitCall( $stack ) {
124        return isset( $stack[1]['class'] ) && substr( $stack[1]['class'], 0, 8 ) === 'PHPUnit\\';
125    }
126}