]> granicus.if.org Git - php/commitdiff
run-tests.php: class for test file loading
authorMax Semenik <maxsem.wiki@gmail.com>
Wed, 10 Feb 2021 12:17:03 +0000 (15:17 +0300)
committerNikita Popov <nikita.ppv@gmail.com>
Tue, 16 Mar 2021 10:51:55 +0000 (11:51 +0100)
This moves a bunch of code outside of run_tests(), making it a bit
more manageable. Additionally, accessors provide better readability
than isset() and friends.

This is a minimal patch that moves the code but does not refactor
much. For the sake of reviewing experience, it does not involve
further refactoring which could include:
* Removing setSection()
* Fixing up the mess with hasSection() vs. sectionNotEmpty(), only
  one of which is really needed.
* Moving more repetitive code into the new class.
All of this will be done with later commits.

Closes GH-6678.

run-tests.php

index c04d2f4a97bf23065e5ca546bdca2deff0ca2f71..825718407c2c145ee3352c1d9accbabcf9c5e43a 100755 (executable)
@@ -1862,141 +1862,36 @@ TEST $file
 ";
     }
 
-    // Load the sections of the test file.
-    $section_text = ['TEST' => ''];
-
-    $fp = fopen($file, "rb") or error("Cannot open test file: $file");
-
-    $bork_info = null;
-
-    if (!feof($fp)) {
-        $line = fgets($fp);
-
-        if ($line === false) {
-            $bork_info = "cannot read test";
-        }
-    } else {
-        $bork_info = "empty test [$file]";
-    }
-    if ($bork_info === null && strncmp('--TEST--', $line, 8)) {
-        $bork_info = "tests must start with --TEST-- [$file]";
-    }
-
-    $section = 'TEST';
-    $secfile = false;
-    $secdone = false;
-
-    while (!feof($fp)) {
-        $line = fgets($fp);
-
-        if ($line === false) {
-            break;
-        }
-
-        // Match the beginning of a section.
-        if (preg_match('/^--([_A-Z]+)--/', $line, $r)) {
-            $section = (string) $r[1];
-
-            if (isset($section_text[$section]) && $section_text[$section]) {
-                $bork_info = "duplicated $section section";
-            }
-
-            // check for unknown sections
-            if (!in_array($section, [
-                'EXPECT', 'EXPECTF', 'EXPECTREGEX', 'EXPECTREGEX_EXTERNAL', 'EXPECT_EXTERNAL', 'EXPECTF_EXTERNAL', 'EXPECTHEADERS',
-                'POST', 'POST_RAW', 'GZIP_POST', 'DEFLATE_POST', 'PUT', 'GET', 'COOKIE', 'ARGS',
-                'FILE', 'FILEEOF', 'FILE_EXTERNAL', 'REDIRECTTEST',
-                'CAPTURE_STDIO', 'STDIN', 'CGI', 'PHPDBG',
-                'INI', 'ENV', 'EXTENSIONS',
-                'SKIPIF', 'XFAIL', 'XLEAK', 'CLEAN',
-                'CREDITS', 'DESCRIPTION', 'CONFLICTS', 'WHITESPACE_SENSITIVE',
-            ])) {
-                $bork_info = 'Unknown section "' . $section . '"';
-            }
-
-            $section_text[$section] = '';
-            $secfile = $section == 'FILE' || $section == 'FILEEOF' || $section == 'FILE_EXTERNAL';
-            $secdone = false;
-            continue;
-        }
-
-        // Add to the section text.
-        if (!$secdone) {
-            $section_text[$section] .= $line;
-        }
-
-        // End of actual test?
-        if ($secfile && preg_match('/^===DONE===\s*$/', $line)) {
-            $secdone = true;
-        }
-    }
-
     $shortname = str_replace(TEST_PHP_SRCDIR . '/', '', $file);
     $tested_file = $shortname;
-    $tested = trim($section_text['TEST']);
-
-    // the redirect section allows a set of tests to be reused outside of
-    // a given test dir
-    if ($bork_info === null) {
-        if (isset($section_text['REDIRECTTEST'])) {
-            if ($IN_REDIRECT) {
-                $bork_info = "Can't redirect a test from within a redirected test";
-            }
-        } else {
-            if (!isset($section_text['PHPDBG']) && isset($section_text['FILE']) + isset($section_text['FILEEOF']) + isset($section_text['FILE_EXTERNAL']) != 1) {
-                $bork_info = "missing section --FILE--";
-            }
-
-            if (isset($section_text['FILEEOF'])) {
-                $section_text['FILE'] = preg_replace("/[\r\n]+$/", '', $section_text['FILEEOF']);
-                unset($section_text['FILEEOF']);
-            }
-
-            if ($num_repeats > 1 && isset($section_text['FILE_EXTERNAL'])) {
-                return skip_test($tested, $tested_file, $shortname, 'Test with FILE_EXTERNAL might not be repeatable');
-            }
-
-            foreach (['FILE', 'EXPECT', 'EXPECTF', 'EXPECTREGEX'] as $prefix) {
-                $key = $prefix . '_EXTERNAL';
-
-                if (isset($section_text[$key])) {
-                    // don't allow tests to retrieve files from anywhere but this subdirectory
-                    $section_text[$key] = dirname($file) . '/' . trim(str_replace('..', '', $section_text[$key]));
 
-                    if (file_exists($section_text[$key])) {
-                        $section_text[$prefix] = file_get_contents($section_text[$key]);
-                        unset($section_text[$key]);
-                    } else {
-                        $bork_info = "could not load --" . $key . "-- " . dirname($file) . '/' . trim($section_text[$key]);
-                    }
-                }
-            }
-
-            if ((isset($section_text['EXPECT']) + isset($section_text['EXPECTF']) + isset($section_text['EXPECTREGEX'])) != 1) {
-                $bork_info = "missing section --EXPECT--, --EXPECTF-- or --EXPECTREGEX--";
-            }
-        }
-    }
-    fclose($fp);
-
-    if ($bork_info !== null) {
-        show_result("BORK", $bork_info, $tested_file);
+    try {
+        $test = new TestFile($file, (bool)$IN_REDIRECT);
+    } catch (BorkageException $ex) {
+        show_result("BORK", $ex->getMessage(), $tested_file);
         $PHP_FAILED_TESTS['BORKED'][] = [
             'name' => $file,
             'test_name' => '',
             'output' => '',
             'diff' => '',
-            'info' => "$bork_info [$file]",
+            'info' => "{$ex->getMessage()} [$file]",
         ];
 
-        $junit->markTestAs('BORK', $shortname, $tested_file, 0, $bork_info);
+        $junit->markTestAs('BORK', $shortname, $tested_file, 0, $ex->getMessage());
         return 'BORKED';
     }
 
-    if (isset($section_text['CAPTURE_STDIO'])) {
-        $captureStdIn = stripos($section_text['CAPTURE_STDIO'], 'STDIN') !== false;
-        $captureStdOut = stripos($section_text['CAPTURE_STDIO'], 'STDOUT') !== false;
-        $captureStdErr = stripos($section_text['CAPTURE_STDIO'], 'STDERR') !== false;
+    $tested = $test->getName();
+
+    if ($num_repeats > 1 && $test->hasSection('FILE_EXTERNAL')) {
+        return skip_test($tested, $tested_file, $shortname, 'Test with FILE_EXTERNAL might not be repeatable');
+    }
+
+    if ($test->hasSection('CAPTURE_STDIO')) {
+        $capture = $test->getSection('CAPTURE_STDIO');
+        $captureStdIn = stripos($capture, 'STDIN') !== false;
+        $captureStdOut = stripos($capture, 'STDOUT') !== false;
+        $captureStdErr = stripos($capture, 'STDERR') !== false;
     } else {
         $captureStdIn = true;
         $captureStdOut = true;
@@ -2009,7 +1904,7 @@ TEST $file
     }
 
     /* For GET/POST/PUT tests, check if cgi sapi is available and if it is, use it. */
-    if (array_key_exists('CGI', $section_text) || !empty($section_text['GET']) || !empty($section_text['POST']) || !empty($section_text['GZIP_POST']) || !empty($section_text['DEFLATE_POST']) || !empty($section_text['POST_RAW']) || !empty($section_text['PUT']) || !empty($section_text['COOKIE']) || !empty($section_text['EXPECTHEADERS'])) {
+    if ($test->isCGI()) {
         if (!$php_cgi) {
             return skip_test($tested, $tested_file, $shortname, 'CGI not available');
         }
@@ -2022,11 +1917,7 @@ TEST $file
 
     /* For phpdbg tests, check if phpdbg sapi is available and if it is, use it. */
     $extra_options = '';
-    if (array_key_exists('PHPDBG', $section_text)) {
-        if (!isset($section_text['STDIN'])) {
-            $section_text['STDIN'] = $section_text['PHPDBG'] . "\n";
-        }
-
+    if ($test->hasSection('PHPDBG')) {
         if (isset($phpdbg)) {
             $php = $phpdbg . ' -qIb';
 
@@ -2042,13 +1933,13 @@ TEST $file
     }
 
     if ($num_repeats > 1) {
-        if (array_key_exists('CLEAN', $section_text)) {
+        if ($test->hasSection('CLEAN')) {
             return skip_test($tested, $tested_file, $shortname, 'Test with CLEAN might not be repeatable');
         }
-        if (array_key_exists('STDIN', $section_text)) {
+        if ($test->hasSection('STDIN')) {
             return skip_test($tested, $tested_file, $shortname, 'Test with STDIN might not be repeatable');
         }
-        if (array_key_exists('CAPTURE_STDIO', $section_text)) {
+        if ($test->hasSection('CAPTURE_STDIO')) {
             return skip_test($tested, $tested_file, $shortname, 'Test with CAPTURE_STDIO might not be repeatable');
         }
     }
@@ -2095,8 +1986,8 @@ TEST $file
             mkdir(dirname($copy_file), 0777, true) or error("Cannot create output directory - " . dirname($copy_file));
         }
 
-        if (isset($section_text['FILE'])) {
-            save_text($copy_file, $section_text['FILE']);
+        if ($test->hasSection('FILE')) {
+            save_text($copy_file, $test->getSection('FILE'));
         }
 
         $temp_filenames = [
@@ -2114,7 +2005,7 @@ TEST $file
     }
 
     if (is_array($IN_REDIRECT)) {
-        $tested = $IN_REDIRECT['prefix'] . ' ' . trim($section_text['TEST']);
+        $tested = $IN_REDIRECT['prefix'] . ' ' . $tested;
         $tested_file = $tmp_relative_file;
         $shortname = str_replace(TEST_PHP_SRCDIR . '/', '', $tested_file);
     }
@@ -2145,8 +2036,8 @@ TEST $file
     $env['CONTENT_LENGTH'] = '';
     $env['TZ'] = '';
 
-    if (!empty($section_text['ENV'])) {
-        foreach (explode("\n", trim($section_text['ENV'])) as $e) {
+    if ($test->sectionNotEmpty('ENV')) {
+        foreach (explode("\n", $test->getSection('ENV')) as $e) {
             $e = explode('=', trim($e), 2);
 
             if (!empty($e[0]) && isset($e[1])) {
@@ -2159,11 +2050,11 @@ TEST $file
     $ini_settings = $workerID ? ['opcache.cache_id' => "worker$workerID"] : [];
 
     // Additional required extensions
-    if (array_key_exists('EXTENSIONS', $section_text)) {
+    if ($test->hasSection('EXTENSIONS')) {
         $ext_params = [];
         settings2array($ini_overwrites, $ext_params);
         $ext_params = settings2params($ext_params);
-        $extensions = preg_split("/[\n\r]+/", trim($section_text['EXTENSIONS']));
+        $extensions = preg_split("/[\n\r]+/", trim($test->getSection('EXTENSIONS')));
         [$ext_dir, $loaded] = $skipCache->getExtensions("$php $pass_options $extra_options $ext_params $no_file_cache");
         $ext_prefix = IS_WINDOWS ? "php_" : "";
         foreach ($extensions as $req_ext) {
@@ -2201,12 +2092,12 @@ TEST $file
 
     // Any special ini settings
     // these may overwrite the test defaults...
-    if (array_key_exists('INI', $section_text)) {
-        $section_text['INI'] = str_replace('{PWD}', dirname($file), $section_text['INI']);
-        $section_text['INI'] = str_replace('{TMP}', sys_get_temp_dir(), $section_text['INI']);
+    if ($test->hasSection('INI')) {
+        $ini = str_replace('{PWD}', dirname($file), $test->getSection('INI'));
+        $ini = str_replace('{TMP}', sys_get_temp_dir(), $ini);
         $replacement = IS_WINDOWS ? '"' . PHP_BINARY . ' -r \"while ($in = fgets(STDIN)) echo $in;\" > $1"' : 'tee $1 >/dev/null';
-        $section_text['INI'] = preg_replace('/{MAIL:(\S+)}/', $replacement, $section_text['INI']);
-        settings2array(preg_split("/[\n\r]+/", $section_text['INI']), $ini_settings);
+        $ini = preg_replace('/{MAIL:(\S+)}/', $replacement, $ini);
+        settings2array(preg_split("/[\n\r]+/", $ini), $ini_settings);
 
         if ($num_repeats > 1 && isset($ini_settings['opcache.opt_debug_level'])) {
             return skip_test($tested, $tested_file, $shortname, 'opt_debug_level tests are not repeatable');
@@ -2221,89 +2112,89 @@ TEST $file
     $info = '';
     $warn = false;
 
-    if (array_key_exists('SKIPIF', $section_text)) {
-        if (trim($section_text['SKIPIF'])) {
-            show_file_block('skip', $section_text['SKIPIF']);
-            $extra = !IS_WINDOWS ?
-                "unset REQUEST_METHOD; unset QUERY_STRING; unset PATH_TRANSLATED; unset SCRIPT_FILENAME; unset REQUEST_METHOD;" : "";
+    if ($test->sectionNotEmpty('SKIPIF')) {
+        show_file_block('skip', $test->getSection('SKIPIF'));
+        $extra = !IS_WINDOWS ?
+            "unset REQUEST_METHOD; unset QUERY_STRING; unset PATH_TRANSLATED; unset SCRIPT_FILENAME; unset REQUEST_METHOD;" : "";
 
-            if ($valgrind) {
-                $env['USE_ZEND_ALLOC'] = '0';
-                $env['ZEND_DONT_UNLOAD_MODULES'] = 1;
-            }
+        if ($valgrind) {
+            $env['USE_ZEND_ALLOC'] = '0';
+            $env['ZEND_DONT_UNLOAD_MODULES'] = 1;
+        }
 
-            $junit->startTimer($shortname);
+        $junit->startTimer($shortname);
 
-            $startTime = microtime(true);
-            $commandLine = "$extra $php $pass_options $extra_options -q $orig_ini_settings $no_file_cache -d display_errors=1 -d display_startup_errors=0";
-            $output = $skipCache->checkSkip($commandLine, $section_text['SKIPIF'], $test_skipif, $temp_skipif, $env);
+        $startTime = microtime(true);
+        $commandLine = "$extra $php $pass_options $extra_options -q $orig_ini_settings $no_file_cache -d display_errors=1 -d display_startup_errors=0";
+        $output = $skipCache->checkSkip($commandLine, $test->getSection('SKIPIF'), $test_skipif, $temp_skipif, $env);
 
-            $time = microtime(true) - $startTime;
+        $time = microtime(true) - $startTime;
+        $junit->stopTimer($shortname);
 
-            $junit->stopTimer($shortname);
+        if ($time > $slow_min_ms / 1000) {
+            $PHP_FAILED_TESTS['SLOW'][] = [
+                'name' => $file,
+                'test_name' => 'SKIPIF of ' . $tested . " [$tested_file]",
+                'output' => '',
+                'diff' => '',
+                'info' => $time,
+            ];
+        }
 
-            if ($time > $slow_min_ms / 1000) {
-                $PHP_FAILED_TESTS['SLOW'][] = [
-                    'name' => $file,
-                    'test_name' => 'SKIPIF of ' . $tested . " [$tested_file]",
-                    'output' => '',
-                    'diff' => '',
-                    'info' => $time,
-                ];
+        if (!$cfg['keep']['skip']) {
+            @unlink($test_skipif);
+        }
+
+        if (!strncasecmp('skip', $output, 4)) {
+            if (preg_match('/^skip\s*(.+)/i', $output, $m)) {
+                show_result('SKIP', $tested, $tested_file, "reason: $m[1]", $temp_filenames);
+            } else {
+                show_result('SKIP', $tested, $tested_file, '', $temp_filenames);
             }
 
-            if (!strncasecmp('skip', $output, 4)) {
-                if (preg_match('/^skip\s*(.+)/i', $output, $m)) {
-                    show_result('SKIP', $tested, $tested_file, "reason: $m[1]", $temp_filenames);
-                } else {
-                    show_result('SKIP', $tested, $tested_file, '', $temp_filenames);
-                }
+            $message = !empty($m[1]) ? $m[1] : '';
+            $junit->markTestAs('SKIP', $shortname, $tested, null, $message);
+            return 'SKIPPED';
+        }
 
-                $message = !empty($m[1]) ? $m[1] : '';
-                $junit->markTestAs('SKIP', $shortname, $tested, null, $message);
-                return 'SKIPPED';
-            }
 
-            if (!strncasecmp('info', $output, 4) && preg_match('/^info\s*(.+)/i', $output, $m)) {
-                $info = " (info: $m[1])";
-            } elseif (!strncasecmp('warn', $output, 4) && preg_match('/^warn\s+(.+)/i', $output, $m)) {
-                $warn = true; /* only if there is a reason */
-                $info = " (warn: $m[1])";
-            } elseif (!strncasecmp('xfail', $output, 5)) {
-                // Pretend we have an XFAIL section
-                $section_text['XFAIL'] = ltrim(substr($output, 5));
-            } elseif ($output !== '') {
-                show_result("BORK", $output, $tested_file, 'reason: invalid output from SKIPIF', $temp_filenames);
-                $PHP_FAILED_TESTS['BORKED'][] = [
-                    'name' => $file,
-                    'test_name' => '',
-                    'output' => '',
-                    'diff' => '',
-                    'info' => "$output [$file]",
-                ];
+        if (!strncasecmp('info', $output, 4) && preg_match('/^info\s*(.+)/i', $output, $m)) {
+            $info = " (info: $m[1])";
+        } elseif (!strncasecmp('warn', $output, 4) && preg_match('/^warn\s+(.+)/i', $output, $m)) {
+            $warn = true; /* only if there is a reason */
+            $info = " (warn: $m[1])";
+        } elseif (!strncasecmp('xfail', $output, 5)) {
+            // Pretend we have an XFAIL section
+            $test->setSection('XFAIL', ltrim(substr($output, 5)));
+        } elseif ($output !== '') {
+            show_result("BORK", $output, $tested_file, 'reason: invalid output from SKIPIF', $temp_filenames);
+            $PHP_FAILED_TESTS['BORKED'][] = [
+                'name' => $file,
+                'test_name' => '',
+                'output' => '',
+                'diff' => '',
+                'info' => "$output [$file]",
+            ];
 
-                $junit->markTestAs('BORK', $shortname, $tested, null, $output);
-                return 'BORKED';
-            }
+            $junit->markTestAs('BORK', $shortname, $tested, null, $output);
+            return 'BORKED';
         }
     }
 
-    if (!extension_loaded("zlib")
-        && (array_key_exists("GZIP_POST", $section_text)
-        || array_key_exists("DEFLATE_POST", $section_text))) {
+    if (!extension_loaded("zlib") && $test->hasAnySections("GZIP_POST", "DEFLATE_POST")) {
         $message = "ext/zlib required";
         show_result('SKIP', $tested, $tested_file, "reason: $message", $temp_filenames);
         $junit->markTestAs('SKIP', $shortname, $tested, null, $message);
         return 'SKIPPED';
     }
 
-    if (isset($section_text['REDIRECTTEST'])) {
+    if ($test->hasSection('REDIRECTTEST')) {
         $test_files = [];
 
-        $IN_REDIRECT = eval($section_text['REDIRECTTEST']);
+        $IN_REDIRECT = eval($test->getSection('REDIRECTTEST'));
         $IN_REDIRECT['via'] = "via [$shortname]\n\t";
         $IN_REDIRECT['dir'] = realpath(dirname($file));
-        $IN_REDIRECT['prefix'] = trim($section_text['TEST']);
+        $IN_REDIRECT['prefix'] = $tested;
 
         if (!empty($IN_REDIRECT['TESTS'])) {
             if (is_array($org_file)) {
@@ -2348,7 +2239,7 @@ TEST $file
         }
     }
 
-    if (is_array($org_file) || isset($section_text['REDIRECTTEST'])) {
+    if (is_array($org_file) || $test->hasSection('REDIRECTTEST')) {
         if (is_array($org_file)) {
             $file = $org_file[0];
         }
@@ -2369,15 +2260,15 @@ TEST $file
     }
 
     // We've satisfied the preconditions - run the test!
-    if (isset($section_text['FILE'])) {
-        show_file_block('php', $section_text['FILE'], 'TEST');
-        save_text($test_file, $section_text['FILE'], $temp_file);
+    if ($test->hasSection('FILE')) {
+        show_file_block('php', $test->getSection('FILE'), 'TEST');
+        save_text($test_file, $test->getSection('FILE'), $temp_file);
     } else {
         $test_file = $temp_file = "";
     }
 
-    if (array_key_exists('GET', $section_text)) {
-        $query_string = trim($section_text['GET']);
+    if ($test->hasSection('GET')) {
+        $query_string = trim($test->getSection('GET'));
     } else {
         $query_string = '';
     }
@@ -2393,13 +2284,13 @@ TEST $file
         $env['SCRIPT_FILENAME'] = $test_file;
     }
 
-    if (array_key_exists('COOKIE', $section_text)) {
-        $env['HTTP_COOKIE'] = trim($section_text['COOKIE']);
+    if ($test->hasSection('COOKIE')) {
+        $env['HTTP_COOKIE'] = trim($test->getSection('COOKIE'));
     } else {
         $env['HTTP_COOKIE'] = '';
     }
 
-    $args = isset($section_text['ARGS']) ? ' -- ' . $section_text['ARGS'] : '';
+    $args = $test->hasSection('ARGS') ? ' -- ' . $test->getSection('ARGS') : '';
 
     if ($preload && !empty($test_file)) {
         save_text($preload_filename, "<?php opcache_compile_file('$test_file');");
@@ -2409,8 +2300,8 @@ TEST $file
         $pass_options .= " -d opcache.preload=" . $preload_filename;
     }
 
-    if (array_key_exists('POST_RAW', $section_text) && !empty($section_text['POST_RAW'])) {
-        $post = trim($section_text['POST_RAW']);
+    if ($test->sectionNotEmpty('POST_RAW')) {
+        $post = trim($test->getSection('POST_RAW'));
         $raw_lines = explode("\n", $post);
 
         $request = '';
@@ -2440,8 +2331,8 @@ TEST $file
 
         save_text($tmp_post, $request);
         $cmd = "$php $pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\"";
-    } elseif (array_key_exists('PUT', $section_text) && !empty($section_text['PUT'])) {
-        $post = trim($section_text['PUT']);
+    } elseif ($test->sectionNotEmpty('PUT')) {
+        $post = trim($test->getSection('PUT'));
         $raw_lines = explode("\n", $post);
 
         $request = '';
@@ -2471,8 +2362,8 @@ TEST $file
 
         save_text($tmp_post, $request);
         $cmd = "$php $pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\"";
-    } elseif (array_key_exists('POST', $section_text) && !empty($section_text['POST'])) {
-        $post = trim($section_text['POST']);
+    } elseif ($test->sectionNotEmpty('POST')) {
+        $post = trim($test->getSection('POST'));
         $content_length = strlen($post);
         save_text($tmp_post, $post);
 
@@ -2486,8 +2377,8 @@ TEST $file
         }
 
         $cmd = "$php $pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\"";
-    } elseif (array_key_exists('GZIP_POST', $section_text) && !empty($section_text['GZIP_POST'])) {
-        $post = trim($section_text['GZIP_POST']);
+    } elseif ($test->sectionNotEmpty('GZIP_POST')) {
+        $post = trim($test->getSection('GZIP_POST'));
         $post = gzencode($post, 9, FORCE_GZIP);
         $env['HTTP_CONTENT_ENCODING'] = 'gzip';
 
@@ -2499,8 +2390,8 @@ TEST $file
         $env['CONTENT_LENGTH'] = $content_length;
 
         $cmd = "$php $pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\"";
-    } elseif (array_key_exists('DEFLATE_POST', $section_text) && !empty($section_text['DEFLATE_POST'])) {
-        $post = trim($section_text['DEFLATE_POST']);
+    } elseif ($test->sectionNotEmpty('DEFLATE_POST')) {
+        $post = trim($test->getSection('DEFLATE_POST'));
         $post = gzcompress($post, 9);
         $env['HTTP_CONTENT_ENCODING'] = 'deflate';
         save_text($tmp_post, $post);
@@ -2546,7 +2437,8 @@ COMMAND $cmd
     $hrtime = hrtime();
     $startTime = $hrtime[0] * 1000000000 + $hrtime[1];
 
-    $out = system_with_timeout($cmd, $env, $section_text['STDIN'] ?? null, $captureStdIn, $captureStdOut, $captureStdErr);
+    $stdin = $test->hasSection('STDIN') ? $test->getSection('STDIN') : null;
+    $out = system_with_timeout($cmd, $env, $stdin, $captureStdIn, $captureStdOut, $captureStdErr);
 
     $junit->stopTimer($shortname);
     $hrtime = hrtime();
@@ -2561,20 +2453,18 @@ COMMAND $cmd
         ];
     }
 
-    if (array_key_exists('CLEAN', $section_text) && (!$no_clean || $cfg['keep']['clean'])) {
-        if (trim($section_text['CLEAN'])) {
-            show_file_block('clean', $section_text['CLEAN']);
-            save_text($test_clean, trim($section_text['CLEAN']), $temp_clean);
+    if ($test->sectionNotEmpty('CLEAN') && (!$no_clean || $cfg['keep']['clean'])) {
+        show_file_block('clean', $test->getSection('CLEAN'));
+        save_text($test_clean, trim($test->getSection('CLEAN')), $temp_clean);
 
-            if (!$no_clean) {
-                $extra = !IS_WINDOWS ?
-                    "unset REQUEST_METHOD; unset QUERY_STRING; unset PATH_TRANSLATED; unset SCRIPT_FILENAME; unset REQUEST_METHOD;" : "";
-                system_with_timeout("$extra $php $pass_options $extra_options -q $orig_ini_settings $no_file_cache \"$test_clean\"", $env);
-            }
+        if (!$no_clean) {
+            $extra = !IS_WINDOWS ?
+                "unset REQUEST_METHOD; unset QUERY_STRING; unset PATH_TRANSLATED; unset SCRIPT_FILENAME; unset REQUEST_METHOD;" : "";
+            system_with_timeout("$extra $php $pass_options $extra_options -q $orig_ini_settings $no_file_cache \"$test_clean\"", $env);
+        }
 
-            if (!$cfg['keep']['clean']) {
-                @unlink($test_clean);
-            }
+        if (!$cfg['keep']['clean']) {
+            @unlink($test_clean);
         }
     }
 
@@ -2630,10 +2520,10 @@ COMMAND $cmd
 
     $failed_headers = false;
 
-    if (isset($section_text['EXPECTHEADERS'])) {
+    if ($test->hasSection('EXPECTHEADERS')) {
         $want = [];
         $wanted_headers = [];
-        $lines = preg_split("/[\n\r]+/", $section_text['EXPECTHEADERS']);
+        $lines = preg_split("/[\n\r]+/", $test->getSection('EXPECTHEADERS'));
 
         foreach ($lines as $line) {
             if (strpos($line, ':') !== false) {
@@ -2667,17 +2557,17 @@ COMMAND $cmd
         $output = trim(preg_replace("/\n?Warning: Can't preload [^\n]*\n?/", "", $output));
     }
 
-    if (isset($section_text['EXPECTF']) || isset($section_text['EXPECTREGEX'])) {
-        if (isset($section_text['EXPECTF'])) {
-            $wanted = trim($section_text['EXPECTF']);
+    if ($test->hasAnySections('EXPECTF', 'EXPECTREGEX')) {
+        if ($test->hasSection('EXPECTF')) {
+            $wanted = trim($test->getSection('EXPECTF'));
         } else {
-            $wanted = trim($section_text['EXPECTREGEX']);
+            $wanted = trim($test->getSection('EXPECTREGEX'));
         }
 
         show_file_block('exp', $wanted);
         $wanted_re = preg_replace('/\r\n/', "\n", $wanted);
 
-        if (isset($section_text['EXPECTF'])) {
+        if ($test->hasSection('EXPECTF')) {
             // do preg_quote, but miss out any %r delimited sections
             $temp = "";
             $r = "%r";
@@ -2729,10 +2619,10 @@ COMMAND $cmd
             @unlink($tmp_post);
 
             if (!$leaked && !$failed_headers) {
-                if (isset($section_text['XFAIL'])) {
+                if ($test->hasSection('XFAIL')) {
                     $warn = true;
                     $info = " (warn: XFAIL section but test passes)";
-                } elseif (isset($section_text['XLEAK'])) {
+                } elseif ($test->hasSection('XLEAK')) {
                     $warn = true;
                     $info = " (warn: XLEAK section but test passes)";
                 } else {
@@ -2743,7 +2633,7 @@ COMMAND $cmd
             }
         }
     } else {
-        $wanted = trim($section_text['EXPECT']);
+        $wanted = trim($test->getSection('EXPECT'));
         $wanted = preg_replace('/\r\n/', "\n", $wanted);
         show_file_block('exp', $wanted);
 
@@ -2757,10 +2647,10 @@ COMMAND $cmd
             @unlink($tmp_post);
 
             if (!$leaked && !$failed_headers) {
-                if (isset($section_text['XFAIL'])) {
+                if ($test->hasSection('XFAIL')) {
                     $warn = true;
                     $info = " (warn: XFAIL section but test passes)";
-                } elseif (isset($section_text['XLEAK'])) {
+                } elseif ($test->hasSection('XLEAK')) {
                     $warn = true;
                     $info = " (warn: XLEAK section but test passes)";
                 } else {
@@ -2786,7 +2676,7 @@ COMMAND $cmd
     }
 
     if ($leaked) {
-        $restype[] = isset($section_text['XLEAK']) ?
+        $restype[] = $test->hasSection('XLEAK') ?
                         'XLEAK' : 'LEAK';
     }
 
@@ -2795,12 +2685,12 @@ COMMAND $cmd
     }
 
     if (!$passed) {
-        if (isset($section_text['XFAIL'])) {
+        if ($test->hasSection('XFAIL')) {
             $restype[] = 'XFAIL';
-            $info = '  XFAIL REASON: ' . rtrim($section_text['XFAIL']);
-        } elseif (isset($section_text['XLEAK'])) {
+            $info = '  XFAIL REASON: ' . rtrim($test->getSection('XFAIL'));
+        } elseif ($test->hasSection('XLEAK')) {
             $restype[] = 'XLEAK';
-            $info = '  XLEAK REASON: ' . rtrim($section_text['XLEAK']);
+            $info = '  XLEAK REASON: ' . rtrim($test->getSection('XLEAK'));
         } else {
             $restype[] = 'FAIL';
         }
@@ -3431,6 +3321,10 @@ function show_result(
 
 }
 
+class BorkageException extends Exception
+{
+}
+
 class JUnit
 {
     private bool $enabled = true;
@@ -3843,6 +3737,206 @@ class RuntestsValgrind
     }
 }
 
+class TestFile
+{
+    private string $fileName;
+
+    private array $sections = ['TEST' => ''];
+
+    private const ALLOWED_SECTIONS = [
+        'EXPECT', 'EXPECTF', 'EXPECTREGEX', 'EXPECTREGEX_EXTERNAL', 'EXPECT_EXTERNAL', 'EXPECTF_EXTERNAL', 'EXPECTHEADERS',
+        'POST', 'POST_RAW', 'GZIP_POST', 'DEFLATE_POST', 'PUT', 'GET', 'COOKIE', 'ARGS',
+        'FILE', 'FILEEOF', 'FILE_EXTERNAL', 'REDIRECTTEST',
+        'CAPTURE_STDIO', 'STDIN', 'CGI', 'PHPDBG',
+        'INI', 'ENV', 'EXTENSIONS',
+        'SKIPIF', 'XFAIL', 'XLEAK', 'CLEAN',
+        'CREDITS', 'DESCRIPTION', 'CONFLICTS', 'WHITESPACE_SENSITIVE',
+    ];
+
+    public function __construct(string $fileName, bool $inRedirect)
+    {
+        $this->fileName = $fileName;
+
+        $this->readFile();
+        $this->validateAndProcess($inRedirect);
+    }
+
+    public function hasSection(string $name): bool
+    {
+        return isset($this->sections[$name]);
+    }
+
+    public function hasAllSections(string ...$names): bool
+    {
+        foreach ($names as $section) {
+            if (!isset($this->sections[$section])) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    public function hasAnySections(string ...$names): bool
+    {
+        foreach ($names as $section) {
+            if (isset($this->sections[$section])) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    public function sectionNotEmpty(string $name): bool
+    {
+        return !empty($this->sections[$name]);
+    }
+
+    public function getSection(string $name): string
+    {
+        if (!isset($this->sections[$name])) {
+            throw new Exception("Section $name not found");
+        }
+        return $this->sections[$name];
+    }
+
+    public function getName(): string
+    {
+        return trim($this->getSection('TEST'));
+    }
+
+    public function isCGI(): bool
+    {
+        return $this->sectionNotEmpty('CGI')
+            || $this->sectionNotEmpty('GET')
+            || $this->sectionNotEmpty('POST')
+            || $this->sectionNotEmpty('GZIP_POST')
+            || $this->sectionNotEmpty('DEFLATE_POST')
+            || $this->sectionNotEmpty('POST_RAW')
+            || $this->sectionNotEmpty('PUT')
+            || $this->sectionNotEmpty('COOKIE')
+            || $this->sectionNotEmpty('EXPECTHEADERS');
+    }
+
+    /**
+     * TODO Refactor to make it not needed
+     */
+    public function setSection(string $name, string $value): void
+    {
+        $this->sections[$name] = $value;
+    }
+
+    /**
+     * Load the sections of the test file
+     */
+    private function readFile(): void
+    {
+        $fp = fopen($this->fileName, "rb") or error("Cannot open test file: {$this->fileName}");
+
+        if (!feof($fp)) {
+            $line = fgets($fp);
+
+            if ($line === false) {
+                throw new BorkageException("cannot read test");
+            }
+        } else {
+            throw new BorkageException("empty test [{$this->fileName}]");
+        }
+        if (strncmp('--TEST--', $line, 8)) {
+            throw new BorkageException("tests must start with --TEST-- [{$this->fileName}]");
+        }
+
+        $section = 'TEST';
+        $secfile = false;
+        $secdone = false;
+
+        while (!feof($fp)) {
+            $line = fgets($fp);
+
+            if ($line === false) {
+                break;
+            }
+
+            // Match the beginning of a section.
+            if (preg_match('/^--([_A-Z]+)--/', $line, $r)) {
+                $section = (string) $r[1];
+
+                if (isset($this->sections[$section]) && $this->sections[$section]) {
+                    throw new BorkageException("duplicated $section section");
+                }
+
+                // check for unknown sections
+                if (!in_array($section, self::ALLOWED_SECTIONS)) {
+                    throw new BorkageException('Unknown section "' . $section . '"');
+                }
+
+                $this->sections[$section] = '';
+                $secfile = $section == 'FILE' || $section == 'FILEEOF' || $section == 'FILE_EXTERNAL';
+                $secdone = false;
+                continue;
+            }
+
+            // Add to the section text.
+            if (!$secdone) {
+                $this->sections[$section] .= $line;
+            }
+
+            // End of actual test?
+            if ($secfile && preg_match('/^===DONE===\s*$/', $line)) {
+                $secdone = true;
+            }
+        }
+
+        fclose($fp);
+    }
+
+    private function validateAndProcess(bool $inRedirect): void
+    {
+        // the redirect section allows a set of tests to be reused outside of
+        // a given test dir
+        if ($this->hasSection('REDIRECTTEST')) {
+            if ($inRedirect) {
+                throw new BorkageException("Can't redirect a test from within a redirected test");
+            }
+            return;
+        }
+        if (!$this->hasSection('PHPDBG') && $this->hasSection('FILE') + $this->hasSection('FILEEOF') + $this->hasSection('FILE_EXTERNAL') != 1) {
+            throw new BorkageException("missing section --FILE--");
+        }
+
+        if ($this->hasSection('FILEEOF')) {
+            $this->sections['FILE'] = preg_replace("/[\r\n]+$/", '', $this->sections['FILEEOF']);
+            unset($this->sections['FILEEOF']);
+        }
+
+        foreach (['FILE', 'EXPECT', 'EXPECTF', 'EXPECTREGEX'] as $prefix) {
+            // For grepping: FILE_EXTERNAL, EXPECT_EXTERNAL, EXPECTF_EXTERNAL, EXPECTREGEX_EXTERNAL
+            $key = $prefix . '_EXTERNAL';
+
+            if ($this->hasSection($key)) {
+                // don't allow tests to retrieve files from anywhere but this subdirectory
+                $dir = dirname($this->fileName);
+                $fileName = $dir . '/' . trim(str_replace('..', '', $this->getSection($key)));
+
+                if (file_exists($fileName)) {
+                    $this->sections[$prefix] = file_get_contents($fileName);
+                } else {
+                    throw new BorkageException("could not load --" . $key . "-- " . $dir . '/' . trim($fileName));
+                }
+            }
+        }
+
+        if (($this->hasSection('EXPECT') + $this->hasSection('EXPECTF') + $this->hasSection('EXPECTREGEX')) != 1) {
+            throw new BorkageException("missing section --EXPECT--, --EXPECTF-- or --EXPECTREGEX--");
+        }
+
+        if ($this->hasSection('PHPDBG') && !$this->hasSection('STDIN')) {
+            $this->sections['STDIN'] = $this->sections['PHPDBG'] . "\n";
+        }
+    }
+}
+
 function init_output_buffers(): void
 {
     // Delete as much output buffers as possible.