]> granicus.if.org Git - php/commitdiff
run-tests.php: move JUnit stuff into a class
authorMax Semenik <maxsem.wiki@gmail.com>
Fri, 5 Feb 2021 20:31:31 +0000 (23:31 +0300)
committerNikita Popov <nikita.ppv@gmail.com>
Mon, 8 Feb 2021 17:28:11 +0000 (18:28 +0100)
This is part one of my work that was announced at
https://externals.io/message/110391

Closes GH-6671.

run-tests.php

index 51758ce5076f3b16b42cefd7c535f508cac52aa8..57751309c473f587f1bb2bd4d602a520ea68024f 100755 (executable)
 
 /* $Id$ */
 
+/* Temporary variables while this file is being refactored. */
+/** @var ?JUnit */
+$junit = null;
+
+/* End temporary variables. */
+
 /* Let there be no top-level code beyond this point:
  * Only functions and classes, thanks!
  *
- * Minimum required PHP version: 7.1.0
+ * Minimum required PHP version: 7.4.0
  */
 
 function show_usage(): void
@@ -156,6 +162,10 @@ function main(): void
     global $workers, $workerID;
     global $context_line_count;
 
+    // Temporary for the duration of refactoring
+    /** @var JUnit */
+    global $junit;
+
     define('IS_WINDOWS', substr(PHP_OS, 0, 3) == "WIN");
 
     $workerID = 0;
@@ -247,7 +257,7 @@ function main(): void
         $DETAILED = 0;
     }
 
-    junit_init();
+    $junit = new JUnit($environment, $workerID);
 
     if (getenv('SHOW_ONLY_GROUPS')) {
         $SHOW_ONLY_GROUPS = explode(",", getenv('SHOW_ONLY_GROUPS'));
@@ -772,7 +782,7 @@ function main(): void
         save_or_mail_results();
     }
 
-    junit_save_xml();
+    $junit->saveXML();
     if (getenv('REPORT_EXIT_STATUS') !== '0' && getenv('REPORT_EXIT_STATUS') !== 'no' &&
             ($sum_results['FAILED'] || $sum_results['LEAKED'])) {
         exit(1);
@@ -1364,6 +1374,8 @@ function run_all_tests_parallel(array $test_files, array $env, $redir_tested): v
 {
     global $workers, $test_idx, $test_cnt, $test_results, $failed_tests_file, $result_tests_file, $PHP_FAILED_TESTS, $shuffle, $SHOW_ONLY_GROUPS, $valgrind;
 
+    global $junit;
+
     // The PHP binary running run-tests.php, and run-tests.php itself
     // This PHP executable is *not* necessarily the same as the tested version
     $thisPHP = PHP_BINARY;
@@ -1562,9 +1574,7 @@ escape:
                                     }
                                 }
                             }
-                            if (junit_enabled()) {
-                                junit_merge_results($message["junit"]);
-                            }
+                            $junit->mergeResults($message["junit"]);
                             // no break
                         case "ready":
                             // Schedule sequential tests only once we are down to one worker.
@@ -1704,6 +1714,8 @@ function run_worker(): void
 {
     global $workerID, $workerSock;
 
+    global $junit;
+
     $sockUri = getenv("TEST_PHP_URI");
 
     $workerSock = stream_socket_client($sockUri, $_, $_, 5) or error("Couldn't connect to $sockUri");
@@ -1750,9 +1762,9 @@ function run_worker(): void
                 run_all_tests($command["test_files"], $command["env"], $command["redir_tested"]);
                 send_message($workerSock, [
                     "type" => "tests_finished",
-                    "junit" => junit_enabled() ? $GLOBALS['JUNIT'] : null,
+                    "junit" => $junit->isEnabled() ? $junit : null,
                 ]);
-                junit_init();
+                //junit_init(); TODO is this needed?
                 break;
             default:
                 send_message($workerSock, [
@@ -1789,9 +1801,11 @@ function show_file_block(string $file, string $block, ?string $section = null):
 }
 
 function skip_test(string $tested, string $tested_file, string $shortname, string $reason) {
+    global $junit;
+
     show_result('SKIP', $tested, $tested_file, "reason: $reason");
-    junit_init_suite(junit_get_suitename_for($shortname));
-    junit_mark_test_as('SKIP', $shortname, $tested, 0, $reason);
+    $junit->initSuite($junit->getSuiteName($shortname));
+    $junit->markTestAs('SKIP', $shortname, $tested, 0, $reason);
     return 'SKIPPED';
 }
 
@@ -1814,6 +1828,11 @@ function run_test(string $php, $file, array $env): string
     global $num_repeats;
     // Parallel testing
     global $workerID;
+
+    // Temporary
+    /** @var JUnit */
+    global $junit;
+
     $temp_filenames = null;
     $org_file = $file;
 
@@ -1963,7 +1982,7 @@ TEST $file
             'info' => "$bork_info [$file]",
         ];
 
-        junit_mark_test_as('BORK', $shortname, $tested_file, 0, $bork_info);
+        $junit->markTestAs('BORK', $shortname, $tested_file, 0, $bork_info);
         return 'BORKED';
     }
 
@@ -2208,14 +2227,14 @@ TEST $file
                 $env['ZEND_DONT_UNLOAD_MODULES'] = 1;
             }
 
-            junit_start_timer($shortname);
+            $junit->startTimer($shortname);
 
             $startTime = microtime(true);
             $output = system_with_timeout("$extra $php $pass_options $extra_options -q $orig_ini_settings $no_file_cache -d display_errors=1 -d display_startup_errors=0 \"$test_skipif\"", $env);
             $output = trim($output);
             $time = microtime(true) - $startTime;
 
-            junit_finish_timer($shortname);
+            $junit->stopTimer($shortname);
 
             if ($time > $slow_min_ms / 1000) {
                 $PHP_FAILED_TESTS['SLOW'][] = [
@@ -2243,7 +2262,7 @@ TEST $file
                 }
 
                 $message = !empty($m[1]) ? $m[1] : '';
-                junit_mark_test_as('SKIP', $shortname, $tested, null, $message);
+                $junit->markTestAs('SKIP', $shortname, $tested, null, $message);
                 return 'SKIPPED';
             }
 
@@ -2265,7 +2284,7 @@ TEST $file
                     'info' => "$output [$file]",
                 ];
 
-                junit_mark_test_as('BORK', $shortname, $tested, null, $output);
+                $junit->markTestAs('BORK', $shortname, $tested, null, $output);
                 return 'BORKED';
             }
         }
@@ -2276,7 +2295,7 @@ TEST $file
         || array_key_exists("DEFLATE_POST", $section_text))) {
         $message = "ext/zlib required";
         show_result('SKIP', $tested, $tested_file, "reason: $message", $temp_filenames);
-        junit_mark_test_as('SKIP', $shortname, $tested, null, $message);
+        $junit->markTestAs('SKIP', $shortname, $tested, null, $message);
         return 'SKIPPED';
     }
 
@@ -2316,7 +2335,7 @@ TEST $file
             // a redirected test never fails
             $IN_REDIRECT = false;
 
-            junit_mark_test_as('PASS', $shortname, $tested);
+            $junit->markTestAs('PASS', $shortname, $tested);
             return 'REDIR';
         } else {
             $bork_info = "Redirect info must contain exactly one TEST string to be used as redirect directory.";
@@ -2346,7 +2365,7 @@ TEST $file
             'info' => "$bork_info [$file]",
         ];
 
-        junit_mark_test_as('BORK', $shortname, $tested, null, $bork_info);
+        $junit->markTestAs('BORK', $shortname, $tested, null, $bork_info);
 
         return 'BORKED';
     }
@@ -2417,7 +2436,7 @@ TEST $file
         $env['REQUEST_METHOD'] = 'POST';
 
         if (empty($request)) {
-            junit_mark_test_as('BORK', $shortname, $tested, null, 'empty $request');
+            $junit->markTestAs('BORK', $shortname, $tested, null, 'empty $request');
             return 'BORKED';
         }
 
@@ -2448,7 +2467,7 @@ TEST $file
         $env['REQUEST_METHOD'] = 'PUT';
 
         if (empty($request)) {
-            junit_mark_test_as('BORK', $shortname, $tested, null, 'empty $request');
+            $junit->markTestAs('BORK', $shortname, $tested, null, 'empty $request');
             return 'BORKED';
         }
 
@@ -2525,13 +2544,13 @@ COMMAND $cmd
 ";
     }
 
-    junit_start_timer($shortname);
+    $junit->startTimer($shortname);
     $hrtime = hrtime();
     $startTime = $hrtime[0] * 1000000000 + $hrtime[1];
 
     $out = system_with_timeout($cmd, $env, $section_text['STDIN'] ?? null, $captureStdIn, $captureStdOut, $captureStdErr);
 
-    junit_finish_timer($shortname);
+    $junit->stopTimer($shortname);
     $hrtime = hrtime();
     $time = $hrtime[0] * 1000000000 + $hrtime[1] - $startTime;
     if ($time >= $slow_min_ms * 1000000) {
@@ -2720,7 +2739,7 @@ COMMAND $cmd
                     $info = " (warn: XLEAK section but test passes)";
                 } else {
                     show_result("PASS", $tested, $tested_file, '', $temp_filenames);
-                    junit_mark_test_as('PASS', $shortname, $tested);
+                    $junit->markTestAs('PASS', $shortname, $tested);
                     return 'PASSED';
                 }
             }
@@ -2748,7 +2767,7 @@ COMMAND $cmd
                     $info = " (warn: XLEAK section but test passes)";
                 } else {
                     show_result("PASS", $tested, $tested_file, '', $temp_filenames);
-                    junit_mark_test_as('PASS', $shortname, $tested);
+                    $junit->markTestAs('PASS', $shortname, $tested);
                     return 'PASSED';
                 }
             }
@@ -2870,7 +2889,7 @@ SH;
 
     $diff = empty($diff) ? '' : preg_replace('/\e/', '<esc>', $diff);
 
-    junit_mark_test_as($restype, $shortname, $tested, null, $info, $diff);
+    $junit->markTestAs($restype, $shortname, $tested, null, $info, $diff);
 
     return $restype[0] . 'ED';
 }
@@ -3413,303 +3432,281 @@ function show_result(
 
 }
 
-function junit_init(): void
+class JUnit
 {
-    // Check whether a junit log is wanted.
-    global $workerID;
-    $JUNIT = getenv('TEST_PHP_JUNIT');
-    if (empty($JUNIT)) {
-        $GLOBALS['JUNIT'] = false;
-        return;
-    }
-    if ($workerID) {
-        $fp = null;
-    } elseif (!$fp = fopen($JUNIT, 'w')) {
-        error("Failed to open $JUNIT for writing.");
-    }
-    $GLOBALS['JUNIT'] = [
-        'fp' => $fp,
-        'name' => 'PHP',
+    private bool $enabled = true;
+    private $fp = null;
+    private array $suites = [];
+    private array $rootSuite = self::EMPTY_SUITE + ['name' => 'php'];
+
+    private const EMPTY_SUITE = [
         'test_total' => 0,
         'test_pass' => 0,
         'test_fail' => 0,
         'test_error' => 0,
         'test_skip' => 0,
         'test_warn' => 0,
+        'files' => [],
         'execution_time' => 0,
-        'suites' => [],
-        'files' => []
     ];
-}
 
-function junit_save_xml(): void
-{
-    global $JUNIT;
-    if (!junit_enabled()) {
-        return;
+    public function __construct(array $env, int $workerID)
+    {
+        // Check whether a junit log is wanted.
+        $fileName = $env['TEST_PHP_JUNIT'] ?? null;
+        if (empty($fileName)) {
+            $this->enabled = false;
+            return;
+        }
+        if (!$workerID && !$this->fp = fopen($fileName, 'w')) {
+            throw new Exception("Failed to open $fileName for writing.");
+        }
     }
 
-    $xml = '<' . '?' . 'xml version="1.0" encoding="UTF-8"' . '?' . '>' . PHP_EOL;
-    $xml .= sprintf(
-        '<testsuites name="%s" tests="%s" failures="%d" errors="%d" skip="%d" time="%s">' . PHP_EOL,
-        $JUNIT['name'],
-        $JUNIT['test_total'],
-        $JUNIT['test_fail'],
-        $JUNIT['test_error'],
-        $JUNIT['test_skip'],
-        $JUNIT['execution_time']
-    );
-    $xml .= junit_get_suite_xml();
-    $xml .= '</testsuites>';
-    fwrite($JUNIT['fp'], $xml);
-}
+    public function isEnabled(): bool
+    {
+        return $this->enabled;
+    }
 
-function junit_get_suite_xml(string $suite_name = ''): string
-{
-    global $JUNIT;
-
-    $result = "";
-
-    foreach ($JUNIT['suites'] as $suite_name => $suite) {
-        $result .= sprintf(
-            '<testsuite name="%s" tests="%s" failures="%d" errors="%d" skip="%d" time="%s">' . PHP_EOL,
-            $suite['name'],
-            $suite['test_total'],
-            $suite['test_fail'],
-            $suite['test_error'],
-            $suite['test_skip'],
-            $suite['execution_time']
+    public function saveXML(): void
+    {
+        if (!$this->enabled) {
+            return;
+        }
+
+        $xml = '<' . '?' . 'xml version="1.0" encoding="UTF-8"' . '?' . '>' . PHP_EOL;
+        $xml .= sprintf(
+            '<testsuites name="%s" tests="%s" failures="%d" errors="%d" skip="%d" time="%s">' . PHP_EOL,
+            $this->rootSuite['name'],
+            $this->rootSuite['test_total'],
+            $this->rootSuite['test_fail'],
+            $this->rootSuite['test_error'],
+            $this->rootSuite['test_skip'],
+            $this->rootSuite['execution_time']
         );
+        $xml .= $this->getSuitesXML();
+        $xml .= '</testsuites>';
+        fwrite($this->fp, $xml);
+    }
 
-        if (!empty($suite_name)) {
-            foreach ($suite['files'] as $file) {
-                $result .= $JUNIT['files'][$file]['xml'];
+    private function getSuitesXML(string $suite_name = '')
+    {
+        // FIXME: $suite_name gets overwritten
+        $result = '';
+
+        foreach ($this->suites as $suite_name => $suite) {
+            $result .= sprintf(
+                '<testsuite name="%s" tests="%s" failures="%d" errors="%d" skip="%d" time="%s">' . PHP_EOL,
+                $suite['name'],
+                $suite['test_total'],
+                $suite['test_fail'],
+                $suite['test_error'],
+                $suite['test_skip'],
+                $suite['execution_time']
+            );
+
+            if (!empty($suite_name)) {
+                foreach ($suite['files'] as $file) {
+                    $result .= $this->rootSuite['files'][$file]['xml'];
+                }
             }
+
+            $result .= '</testsuite>' . PHP_EOL;
         }
 
-        $result .= '</testsuite>' . PHP_EOL;
+        return $result;
     }
 
-    return $result;
-}
-
-function junit_enabled(): bool
-{
-    global $JUNIT;
-    return !empty($JUNIT);
-}
+    public function markTestAs(
+        $type,
+        string $file_name,
+        string $test_name,
+        ?int $time = null,
+        string $message = '',
+        string $details = ''
+    ): void {
+        if (!$this->enabled) {
+            return;
+        }
 
-/**
- * @param array|string $type
- */
-function junit_mark_test_as(
-    $type,
-    string $file_name,
-    string $test_name,
-    ?int $time = null,
-    string $message = '',
-    string $details = ''
-): void {
-    global $JUNIT;
-    if (!junit_enabled()) {
-        return;
-    }
+        $suite = $this->getSuiteName($file_name);
 
-    $suite = junit_get_suitename_for($file_name);
+        $this->record($suite, 'test_total');
 
-    junit_suite_record($suite, 'test_total');
+        $time = $time ?? $this->getTimer($file_name);
+        $this->record($suite, 'execution_time', $time);
 
-    $time = $time ?? junit_get_timer($file_name);
-    junit_suite_record($suite, 'execution_time', $time);
+        $escaped_details = htmlspecialchars($details, ENT_QUOTES, 'UTF-8');
+        $escaped_details = preg_replace_callback('/[\0-\x08\x0B\x0C\x0E-\x1F]/', function ($c) {
+            return sprintf('[[0x%02x]]', ord($c[0]));
+        }, $escaped_details);
+        $escaped_message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
 
-    $escaped_details = htmlspecialchars($details, ENT_QUOTES, 'UTF-8');
-    $escaped_details = preg_replace_callback('/[\0-\x08\x0B\x0C\x0E-\x1F]/', function (array $c): string {
-        return sprintf('[[0x%02x]]', ord($c[0]));
-    }, $escaped_details);
-    $escaped_message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
+        $escaped_test_name = htmlspecialchars($file_name . ' (' . $test_name . ')', ENT_QUOTES);
+        $this->rootSuite['files'][$file_name]['xml'] = "<testcase name='$escaped_test_name' time='$time'>\n";
 
-    $escaped_test_name = htmlspecialchars($file_name . ' (' . $test_name . ')', ENT_QUOTES);
-    $JUNIT['files'][$file_name]['xml'] = "<testcase name='$escaped_test_name' time='$time'>\n";
+        if (is_array($type)) {
+            $output_type = $type[0] . 'ED';
+            $temp = array_intersect(['XFAIL', 'XLEAK', 'FAIL', 'WARN'], $type);
+            $type = reset($temp);
+        } else {
+            $output_type = $type . 'ED';
+        }
+
+        if ('PASS' == $type || 'XFAIL' == $type || 'XLEAK' == $type) {
+            $this->record($suite, 'test_pass');
+        } elseif ('BORK' == $type) {
+            $this->record($suite, 'test_error');
+            $this->rootSuite['files'][$file_name]['xml'] .= "<error type='$output_type' message='$escaped_message'/>\n";
+        } elseif ('SKIP' == $type) {
+            $this->record($suite, 'test_skip');
+            $this->rootSuite['files'][$file_name]['xml'] .= "<skipped>$escaped_message</skipped>\n";
+        } elseif ('WARN' == $type) {
+            $this->record($suite, 'test_warn');
+            $this->rootSuite['files'][$file_name]['xml'] .= "<warning>$escaped_message</warning>\n";
+        } elseif ('FAIL' == $type) {
+            $this->record($suite, 'test_fail');
+            $this->rootSuite['files'][$file_name]['xml'] .= "<failure type='$output_type' message='$escaped_message'>$escaped_details</failure>\n";
+        } else {
+            $this->record($suite, 'test_error');
+            $this->rootSuite['files'][$file_name]['xml'] .= "<error type='$output_type' message='$escaped_message'>$escaped_details</error>\n";
+        }
 
-    if (is_array($type)) {
-        $output_type = $type[0] . 'ED';
-        $temp = array_intersect(['XFAIL', 'XLEAK', 'FAIL', 'WARN'], $type);
-        $type = reset($temp);
-    } else {
-        $output_type = $type . 'ED';
-    }
-
-    if ('PASS' == $type || 'XFAIL' == $type || 'XLEAK' == $type) {
-        junit_suite_record($suite, 'test_pass');
-    } elseif ('BORK' == $type) {
-        junit_suite_record($suite, 'test_error');
-        $JUNIT['files'][$file_name]['xml'] .= "<error type='$output_type' message='$escaped_message'/>\n";
-    } elseif ('SKIP' == $type) {
-        junit_suite_record($suite, 'test_skip');
-        $JUNIT['files'][$file_name]['xml'] .= "<skipped>$escaped_message</skipped>\n";
-    } elseif ('WARN' == $type) {
-        junit_suite_record($suite, 'test_warn');
-        $JUNIT['files'][$file_name]['xml'] .= "<warning>$escaped_message</warning>\n";
-    } elseif ('FAIL' == $type) {
-        junit_suite_record($suite, 'test_fail');
-        $JUNIT['files'][$file_name]['xml'] .= "<failure type='$output_type' message='$escaped_message'>$escaped_details</failure>\n";
-    } else {
-        junit_suite_record($suite, 'test_error');
-        $JUNIT['files'][$file_name]['xml'] .= "<error type='$output_type' message='$escaped_message'>$escaped_details</error>\n";
+        $this->rootSuite['files'][$file_name]['xml'] .= "</testcase>\n";
     }
 
-    $JUNIT['files'][$file_name]['xml'] .= "</testcase>\n";
-}
+    private function record(string $suite, string $param, $value = 1): void
+    {
+        $this->rootSuite[$param] += $value;
+        $this->suites[$suite][$param] += $value;
+    }
 
-function junit_suite_record(string $suite, string $param, int $value = 1): void
-{
-    global $JUNIT;
+    private function getTimer(string $file_name)
+    {
+        if (!$this->enabled) {
+            return 0;
+        }
 
-    $JUNIT[$param] += $value;
-    $JUNIT['suites'][$suite][$param] += $value;
-}
+        if (isset($this->rootSuite['files'][$file_name]['total'])) {
+            return number_format($this->rootSuite['files'][$file_name]['total'], 4);
+        }
 
-function junit_get_timer(string $file_name): int
-{
-    global $JUNIT;
-    if (!junit_enabled()) {
         return 0;
     }
 
-    if (isset($JUNIT['files'][$file_name]['total'])) {
-        return number_format($JUNIT['files'][$file_name]['total'], 4);
-    }
+    public function startTimer(string $file_name): void
+    {
+        if (!$this->enabled) {
+            return;
+        }
 
-    return 0;
-}
+        if (!isset($this->rootSuite['files'][$file_name]['start'])) {
+            $this->rootSuite['files'][$file_name]['start'] = microtime(true);
 
-function junit_start_timer(string $file_name): void
-{
-    global $JUNIT;
-    if (!junit_enabled()) {
-        return;
+            $suite = $this->getSuiteName($file_name);
+            $this->initSuite($suite);
+            $this->suites[$suite]['files'][$file_name] = $file_name;
+        }
     }
 
-    if (!isset($JUNIT['files'][$file_name]['start'])) {
-        $JUNIT['files'][$file_name]['start'] = microtime(true);
-
-        $suite = junit_get_suitename_for($file_name);
-        junit_init_suite($suite);
-        $JUNIT['suites'][$suite]['files'][$file_name] = $file_name;
+    public function getSuiteName(string $file_name): string
+    {
+        return $this->pathToClassName(dirname($file_name));
     }
-}
 
-function junit_get_suitename_for(string $file_name): string
-{
-    return junit_path_to_classname(dirname($file_name));
-}
-
-function junit_path_to_classname(string $file_name): string
-{
-    global $JUNIT;
-
-    if (!junit_enabled()) {
-        return '';
-    }
+    private function pathToClassName(string $file_name): string
+    {
+        if (!$this->enabled) {
+            return '';
+        }
 
-    $ret = $JUNIT['name'];
-    $_tmp = [];
+        $ret = $this->rootSuite['name'];
+        $_tmp = [];
 
-    // lookup whether we're in the PHP source checkout
-    $max = 5;
-    if (is_file($file_name)) {
-        $dir = dirname(realpath($file_name));
-    } else {
-        $dir = realpath($file_name);
-    }
-    do {
-        array_unshift($_tmp, basename($dir));
-        $chk = $dir . DIRECTORY_SEPARATOR . "main" . DIRECTORY_SEPARATOR . "php_version.h";
-        $dir = dirname($dir);
-    } while (!file_exists($chk) && --$max > 0);
-    if (file_exists($chk)) {
-        if ($max) {
-            array_shift($_tmp);
+        // lookup whether we're in the PHP source checkout
+        $max = 5;
+        if (is_file($file_name)) {
+            $dir = dirname(realpath($file_name));
+        } else {
+            $dir = realpath($file_name);
         }
-        foreach ($_tmp as $p) {
-            $ret .= "." . preg_replace(",[^a-z0-9]+,i", ".", $p);
+        do {
+            array_unshift($_tmp, basename($dir));
+            $chk = $dir . DIRECTORY_SEPARATOR . "main" . DIRECTORY_SEPARATOR . "php_version.h";
+            $dir = dirname($dir);
+        } while (!file_exists($chk) && --$max > 0);
+        if (file_exists($chk)) {
+            if ($max) {
+                array_shift($_tmp);
+            }
+            foreach ($_tmp as $p) {
+                $ret .= "." . preg_replace(",[^a-z0-9]+,i", ".", $p);
+            }
+            return $ret;
         }
-        return $ret;
+
+        return $this->rootSuite['name'] . '.' . str_replace([DIRECTORY_SEPARATOR, '-'], '.', $file_name);
     }
 
-    return $JUNIT['name'] . '.' . str_replace([DIRECTORY_SEPARATOR, '-'], '.', $file_name);
-}
+    public function initSuite(string $suite_name): void
+    {
+        if (!$this->enabled) {
+            return;
+        }
 
-function junit_init_suite(string $suite_name): void
-{
-    global $JUNIT;
-    if (!junit_enabled()) {
-        return;
-    }
+        if (!empty($this->suites[$suite_name])) {
+            return;
+        }
 
-    if (!empty($JUNIT['suites'][$suite_name])) {
-        return;
+        $this->suites[$suite_name] = self::EMPTY_SUITE + ['name' => $suite_name];
     }
 
-    $JUNIT['suites'][$suite_name] = [
-        'name' => $suite_name,
-        'test_total' => 0,
-        'test_pass' => 0,
-        'test_fail' => 0,
-        'test_error' => 0,
-        'test_skip' => 0,
-        'test_warn' => 0,
-        'files' => [],
-        'execution_time' => 0,
-    ];
-}
+    public function stopTimer(string $file_name): void
+    {
+        if (!$this->enabled) {
+            return;
+        }
 
-function junit_finish_timer(string $file_name): void
-{
-    global $JUNIT;
-    if (!junit_enabled()) {
-        return;
-    }
+        if (!isset($this->rootSuite['files'][$file_name]['start'])) {
+            throw new Exception("Timer for $file_name was not started!");
+        }
 
-    if (!isset($JUNIT['files'][$file_name]['start'])) {
-        error("Timer for $file_name was not started!");
-    }
+        if (!isset($this->rootSuite['files'][$file_name]['total'])) {
+            $this->rootSuite['files'][$file_name]['total'] = 0;
+        }
 
-    if (!isset($JUNIT['files'][$file_name]['total'])) {
-        $JUNIT['files'][$file_name]['total'] = 0;
+        $start = $this->rootSuite['files'][$file_name]['start'];
+        $this->rootSuite['files'][$file_name]['total'] += microtime(true) - $start;
+        unset($this->rootSuite['files'][$file_name]['start']);
     }
 
-    $start = $JUNIT['files'][$file_name]['start'];
-    $JUNIT['files'][$file_name]['total'] += microtime(true) - $start;
-    unset($JUNIT['files'][$file_name]['start']);
-}
+    public function mergeResults(?JUnit $other): void
+    {
+        if (!$this->enabled || !$other) {
+            return;
+        }
 
-function junit_merge_results(array $junit): void
-{
-    global $JUNIT;
-    $JUNIT['test_total'] += $junit['test_total'];
-    $JUNIT['test_pass']  += $junit['test_pass'];
-    $JUNIT['test_fail']  += $junit['test_fail'];
-    $JUNIT['test_error'] += $junit['test_error'];
-    $JUNIT['test_skip']  += $junit['test_skip'];
-    $JUNIT['test_warn']  += $junit['test_warn'];
-    $JUNIT['execution_time'] += $junit['execution_time'];
-    $JUNIT['files'] += $junit['files'];
-    foreach ($junit['suites'] as $name => $suite) {
-        if (!isset($JUNIT['suites'][$name])) {
-            $JUNIT['suites'][$name] = $suite;
-            continue;
+        $this->mergeSuites($this->rootSuite, $other->rootSuite);
+        foreach ($other->suites as $name => $suite) {
+            if (!isset($this->suites[$name])) {
+                $this->suites[$name] = $suite;
+                continue;
+            }
+
+            $this->mergeSuites($this->suites[$name], $suite);
         }
+    }
 
-        $SUITE =& $JUNIT['suites'][$name];
-        $SUITE['test_total'] += $suite['test_total'];
-        $SUITE['test_pass']  += $suite['test_pass'];
-        $SUITE['test_fail']  += $suite['test_fail'];
-        $SUITE['test_error'] += $suite['test_error'];
-        $SUITE['test_skip']  += $suite['test_skip'];
-        $SUITE['test_warn']  += $suite['test_warn'];
-        $SUITE['execution_time'] += $suite['execution_time'];
-        $SUITE['files'] += $suite['files'];
+    private function mergeSuites(array &$dest, array $source): void
+    {
+        $dest['test_total'] += $source['test_total'];
+        $dest['test_pass']  += $source['test_pass'];
+        $dest['test_fail']  += $source['test_fail'];
+        $dest['test_error'] += $source['test_error'];
+        $dest['test_skip']  += $source['test_skip'];
+        $dest['test_warn']  += $source['test_warn'];
+        $dest['execution_time'] += $source['execution_time'];
+        $dest['files'] += $source['files'];
     }
 }