diff --git a/core/lib/Drupal/Core/Test/JUnitHelper.php b/core/lib/Drupal/Core/Test/JUnitHelper.php
new file mode 100644
index 0000000000..c0a70b8bb5
--- /dev/null
+++ b/core/lib/Drupal/Core/Test/JUnitHelper.php
@@ -0,0 +1,171 @@
+<?php
+
+namespace Drupal\Core\Test;
+
+/**
+ * Convert PHPUnit 6+ JUnit reports to a form usable by run-tests.sh.
+ *
+ * BC for some simpletest functions is supported by
+ * \Drupal\Core\Test\JUnitHelperLegacy.
+ *
+ * @see \Drupal\Core\Test\JUnitHelperLegacy
+ */
+class JUnitHelper {
+
+  /**
+   * The test ID.
+   *
+   * @var string
+   */
+  protected $testId;
+
+  /**
+   * Full path of the JUnit XML file to process.
+   *
+   * @var string
+   */
+  protected $jUnitFile;
+
+  /**
+   * Parsed log file contents.
+   *
+   * @var \SimpleXMLElement
+   */
+  protected $jUnitXml = NULL;
+
+  /**
+   *
+   * @param string $test_id
+   *   The test ID for this test.
+   * @param string $junit_file
+   *   Full path to the JUnit XML file.
+   */
+  public function __construct($test_id, $junit_file) {
+    $this->testId = $test_id;
+    $this->jUnitFile = $junit_file;
+  }
+
+  /**
+   * Get the JUnit XML information transformed into the {simpletest} schema.
+   *
+   * @return mixed[]
+   */
+  public function getRecords() {
+    $junit_report = $this->getXmlForFile();
+    if (empty($junit_report)) {
+      return [];
+    }
+    $records = [];
+    foreach (static::findTestcases($junit_report) as $testcase) {
+      $records[] = $this->testCaseToRow($testcase);
+    }
+    return $records;
+  }
+
+  /**
+   * Gets the parsed XML for the JUnit file.
+   *
+   * @return \SimpleXMLElement|null
+   *   The parsed XML for the JUnit file, or NULL if the file was empty.
+   */
+  protected function getXmlForFile() {
+    if ($this->jUnitXml !== NULL) {
+      return $this->jUnitXml;
+    }
+    if (($log_file_contents = @file_get_contents($this->jUnitFile)) !== FALSE) {
+      $this->jUnitXml = new \SimpleXMLElement($log_file_contents);
+    }
+    else {
+      // We were unable to parse the file, so there are no results.
+      $this->jUnitXml = new \SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><testsuites></testsuites>');
+    }
+    return $this->jUnitXml;
+  }
+
+  /**
+   * Get all the testcase records within the JUnit XML document.
+   *
+   * @param \SimpleXMLElement $junit
+   *   The JUnit XML to search for test cases.
+   *
+   * @return \SimpleXMLElement[]
+   *   All the testcase elements of the JUnit DOM, or empty array if there are
+   *   none.
+   */
+  protected static function findTestcases(\SimpleXMLElement $junit) {
+    return $junit->xpath('.//testcase');
+  }
+
+  /**
+   * Determine a status string for the given testcase.
+   *
+   * JUnit from PHPUnit < 6 will not have a skipped child element, so we can't
+   * determine it from this record. It's still safe to check for the skipped
+   * child element, however, which gives us forward compatiblity for PHPUnit 6+.
+   *
+   * @param \SimpleXMLElement $testcase
+   *
+   * @return string
+   *   The status value to insert into the {simpletest} record. Allowed values:
+   *   'pass', 'fail', 'risky', 'skipped', 'incomplete'.
+   */
+  protected static function getStatus(\SimpleXMLElement $testcase) {
+    $status = 'pass';
+    if ($testcase->failure || $testcase->error) {
+      $status = 'fail';
+    }
+    elseif ($testcase->skipped) {
+      $status = $testcase->skipped->attributes()->message;
+    }
+    return $status;
+  }
+
+  /**
+   * Convert a single JUnit DOM testcase into a {simpletest} database row.
+   *
+   * @param \SimpleXMLElement $testcase
+   *   DOM representation of an individual JUnit testcase.
+   *
+   * @return mixed[]
+   *   A record suitable to insert into the {simpletest} database.
+   */
+  protected function testCaseToRow(\SimpleXMLElement $testcase) {
+    $status = $this->getStatus($testcase);
+
+    // Empty message for 'pass' status.
+    $message = '';
+    if ($status == 'fail') {
+      if ($testcase->failure) {
+        $message = (string) $testcase->failure[0];
+      }
+      elseif ($testcase->error) {
+        $message = (string) $testcase->error[0];
+      }
+    }
+    elseif ($status == 'risky') {
+      $message = 'Risky';
+    }
+    elseif ($status == 'skipped') {
+      $message = 'Skipped';
+    }
+    elseif ($status == 'incomplete') {
+      $message = 'Incomplete';
+    }
+
+    $attributes = $testcase->attributes();
+
+    $record = [
+      'test_id' => $this->testId,
+      'test_class' => (string) $attributes->class,
+      'status' => $status,
+      'message' => $message,
+      // @todo: Check on the proper values for this.
+      'message_group' => 'Other',
+      'function' => $attributes->class . '->' . $attributes->name . '()',
+      'line' => (int) $attributes->line[0] ?: 0,
+      'file' => (string) $attributes->file[0],
+    ];
+    return $record;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Test/JUnitHelperLegacy.php b/core/lib/Drupal/Core/Test/JUnitHelperLegacy.php
new file mode 100644
index 0000000000..1eb6e2f99e
--- /dev/null
+++ b/core/lib/Drupal/Core/Test/JUnitHelperLegacy.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Drupal\Core\Test;
+
+/**
+ * Provides a BC layer for \Drupal\Core\Test\JUnitHelper.
+ *
+ * @deprecated in Drupal 8.8.x for removal before Drupal 9.0.0. Use
+ *   \Drupal\Core\Test\JUnitHelper to convert JUnit to simpletest database rows.
+ *
+ * @see https://www.drupal.org/node/2966954
+ */
+class JUnitHelperLegacy extends JUnitHelper {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function findTestcases(\SimpleXMLElement $junit) {
+    return parent::findTestcases($junit);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function testcaseToRow(\SimpleXMLElement $testcase) {
+    return parent::testCaseToRow($testcase);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Test/JUnitListener.php b/core/lib/Drupal/Core/Test/JUnitListener.php
new file mode 100644
index 0000000000..0f586337c7
--- /dev/null
+++ b/core/lib/Drupal/Core/Test/JUnitListener.php
@@ -0,0 +1,354 @@
+<?php
+
+namespace Drupal\Core\Test;
+
+use PHPUnit\Framework\AssertionFailedError;
+use PHPUnit\Framework\ExceptionWrapper;
+use PHPUnit\Framework\SelfDescribing;
+use PHPUnit\Framework\Test;
+use PHPUnit\Framework\TestCase;
+use PHPUnit\Framework\TestFailure;
+use PHPUnit\Framework\TestListener;
+use PHPUnit\Framework\TestSuite;
+use PHPUnit\Framework\Warning;
+use PHPUnit\Util\Filter;
+use PHPUnit\Util\Xml;
+
+/**
+ * A TestListener that generates a logfile of the test execution in XML markup.
+ *
+ * The XML markup used is the same as the one that is used by the JUnit Ant task.
+ */
+class JUnitListener implements TestListener {
+
+  /**
+   * @var \DOMDocument
+   */
+  protected $document;
+
+  /**
+   * @var \DOMElement
+   */
+  protected $root;
+
+  /**
+   * @var bool
+   */
+  protected $writeDocument = TRUE;
+
+  /**
+   * @var \DOMElement[]
+   */
+  protected $testSuites = [];
+
+  /**
+   * @var array
+   */
+  protected $testSuiteRuns = [];
+
+  /**
+   * @var int
+   */
+  protected $testSuiteLevel = 0;
+
+  /**
+   * @var \DOMElement
+   */
+  protected $currentTestCase;
+
+  /**
+   * Constructor.
+   */
+  public function __construct() {
+    $this->document = new \DOMDocument('1.0', 'UTF-8');
+    $this->document->formatOutput = TRUE;
+
+    $this->root = $this->document->createElement('testsuites');
+    $this->document->appendChild($this->root);
+  }
+
+  /**
+   * Destructor.
+   */
+  public function __destruct() {
+    $junit_file = getenv('SIMPLETEST_JUNIT_FILE');
+    if (!empty($junit_file)) {
+      @file_put_contents($junit_file, $this->getXML());
+    }
+  }
+
+  /**
+   * An error occurred.
+   *
+   * @param \PHPUnit\Framework\Test $test
+   * @param \Exception $e
+   * @param float $time
+   */
+  public function addError(Test $test, \Exception $e, $time) {
+    $this->doAddFault($test, $e, $time, 'error');
+    $this->testSuiteRuns[$this->testSuiteLevel]['errors']++;
+  }
+
+  /**
+   * A warning occurred.
+   *
+   * @param \PHPUnit\Framework\Test $test
+   * @param \PHPUnit\Framework\Warning $e
+   * @param float $time
+   */
+  public function addWarning(Test $test, Warning $e, $time) {
+    $this->doAddFault($test, $e, $time, 'warning');
+    $this->testSuiteRuns[$this->testSuiteLevel]['failures']++;
+  }
+
+  /**
+   * A failure occurred.
+   *
+   * @param \PHPUnit\Framework\Test $test
+   * @param \PHPUnit\Framework\AssertionFailedError $e
+   * @param float $time
+   */
+  public function addFailure(Test $test, AssertionFailedError $e, $time) {
+    $this->doAddFault($test, $e, $time, 'failure');
+    $this->testSuiteRuns[$this->testSuiteLevel]['failures']++;
+  }
+
+  /**
+   * Incomplete test.
+   *
+   * @param \PHPUnit\Framework\Test $test
+   * @param \Exception $e
+   * @param float $time
+   */
+  public function addIncompleteTest(Test $test, \Exception $e, $time) {
+    $this->doAddSkipped($test, 'incomplete');
+  }
+
+  /**
+   * Risky test.
+   *
+   * @param \PHPUnit\Framework\Test $test
+   * @param \Exception $e
+   * @param float $time
+   */
+  public function addRiskyTest(Test $test, \Exception $e, $time) {
+    $this->doAddSkipped($test, 'risky');
+  }
+
+  /**
+   * Skipped test.
+   *
+   * @param \PHPUnit\Framework\Test $test
+   * @param \Exception $e
+   * @param float $time
+   */
+  public function addSkippedTest(Test $test, \Exception $e, $time) {
+    $this->doAddSkipped($test, 'skipped');
+  }
+
+  /**
+   * A testsuite started.
+   *
+   * @param \PHPUnit\Framework\TestSuite $suite
+   */
+  public function startTestSuite(TestSuite $suite) {
+    $testSuite = $this->document->createElement('testsuite');
+    $testSuite->setAttribute('name', $suite->getName());
+    if (class_exists($suite->getName(), FALSE)) {
+      try {
+        $class = new \ReflectionClass($suite->getName());
+        $testSuite->setAttribute('file', $class->getFileName());
+      }
+      catch (\ReflectionException $e) {
+        // Do nothing.
+      }
+    }
+    if ($this->testSuiteLevel > 0) {
+      $this->testSuites[$this->testSuiteLevel]->appendChild($testSuite);
+    }
+    else {
+      $this->root->appendChild($testSuite);
+    }
+    $this->testSuiteLevel++;
+    $this->testSuites[$this->testSuiteLevel] = $testSuite;
+    $this->testSuiteRuns[$this->testSuiteLevel] = [
+      'tests' => 0,
+      'assertions' => 0,
+      'errors' => 0,
+      'failures' => 0,
+      'risky' => 0,
+      'skipped' => 0,
+      'incomplete' => 0,
+      'time' => 0,
+    ];
+  }
+
+  /**
+   * A testsuite ended.
+   *
+   * @param \PHPUnit\Framework\TestSuite $suite
+   */
+  public function endTestSuite(TestSuite $suite) {
+    $properties = [
+      'tests',
+      'assertions',
+      'errors',
+      'failures',
+      'risky',
+      'skipped',
+      'incomplete',
+    ];
+
+    // Add summary to the <testsuite> DOM element.
+    foreach ($properties as $property) {
+      $this->testSuites[$this->testSuiteLevel]->setAttribute($property, $this->testSuiteRuns[$this->testSuiteLevel][$property]);
+    }
+    $this->testSuites[$this->testSuiteLevel]->setAttribute('time', sprintf('%F', $this->testSuiteRuns[$this->testSuiteLevel]['time']));
+
+    // When nesting, sum up results to the parent testsuite.
+    if ($this->testSuiteLevel > 1) {
+      foreach ($properties as $property) {
+        $this->testSuiteRuns[$this->testSuiteLevel - 1][$property] += $this->testSuiteRuns[$this->testSuiteLevel][$property];
+      }
+      $this->testSuiteRuns[$this->testSuiteLevel - 1]['time'] += $this->testSuiteRuns[$this->testSuiteLevel]['time'];
+    }
+    $this->testSuiteLevel--;
+  }
+
+  /**
+   * A test started.
+   *
+   * @param \PHPUnit\Framework\Test $test
+   */
+  public function startTest(Test $test) {
+    $testCase = $this->document->createElement('testcase');
+    $testCase->setAttribute('name', $test->getName());
+
+    if ($test instanceof TestCase) {
+      $class = new \ReflectionClass($test);
+      $methodName = $test->getName(!$test->usesDataProvider());
+
+      if ($class->hasMethod($methodName)) {
+        $method = $class->getMethod($methodName);
+
+        $testCase->setAttribute('class', $class->getName());
+        $testCase->setAttribute('classname', str_replace('\\', '.', $class->getName()));
+        $testCase->setAttribute('file', $class->getFileName());
+        $testCase->setAttribute('line', $method->getStartLine());
+      }
+    }
+
+    $this->currentTestCase = $testCase;
+  }
+
+  /**
+   * A test ended.
+   *
+   * @param \PHPUnit\Framework\Test $test
+   * @param float $time
+   */
+  public function endTest(Test $test, $time) {
+    if ($test instanceof TestCase) {
+      $num_assertions = $test->getNumAssertions();
+      $this->testSuiteRuns[$this->testSuiteLevel]['assertions'] += $num_assertions;
+
+      $this->currentTestCase->setAttribute('assertions', $num_assertions);
+    }
+
+    $this->currentTestCase->setAttribute('time', sprintf('%F', $time));
+
+    $this->testSuites[$this->testSuiteLevel]->appendChild(
+      $this->currentTestCase
+    );
+
+    $this->testSuiteRuns[$this->testSuiteLevel]['tests']++;
+    $this->testSuiteRuns[$this->testSuiteLevel]['time'] += $time;
+
+    if (method_exists($test, 'hasOutput') && $test->hasOutput()) {
+      $systemOut = $this->document->createElement('system-out', Xml::prepareString($test->getActualOutput()));
+
+      $this->currentTestCase->appendChild($systemOut);
+    }
+
+    $this->currentTestCase = NULL;
+  }
+
+  /**
+   * Returns the XML as a string.
+   *
+   * @return string
+   */
+  public function getXML() {
+    return $this->document->saveXML();
+  }
+
+  /**
+   * Method which generalizes addError() and addFailure()
+   *
+   * @param \PHPUnit\Framework\Test $test
+   * @param \Exception $e
+   * @param float $time
+   * @param string $type
+   */
+  private function doAddFault(Test $test, \Exception $e, $time, $type) {
+    if ($this->currentTestCase === NULL) {
+      return;
+    }
+
+    if ($test instanceof SelfDescribing) {
+      $buffer = $test->toString() . "\n";
+    }
+    else {
+      $buffer = '';
+    }
+
+    $buffer .= TestFailure::exceptionToString($e) . "\n" . Filter::getFilteredStacktrace($e);
+
+    $fault = $this->document->createElement($type, Xml::prepareString($buffer));
+
+    if ($e instanceof ExceptionWrapper) {
+      $fault->setAttribute('type', $e->getClassName());
+    }
+    else {
+      $fault->setAttribute('type', get_class($e));
+    }
+
+    $this->currentTestCase->appendChild($fault);
+  }
+
+  /**
+   * Method that generalizes unproductive tests.
+   *
+   * @see ::addSkippedTest
+   * @see ::addIncompleteTest
+   * @see ::addRiskyTest
+   *
+   * @param \PHPUnit\Framework\Test $test
+   * @param string $message
+   */
+  private function doAddSkipped(Test $test, $message) {
+    if ($this->currentTestCase === NULL) {
+      return;
+    }
+
+    $skipped = $this->document->createElement('skipped');
+    $skipped->setAttribute('message', $message);
+    $this->currentTestCase->appendChild($skipped);
+
+    switch ($message) {
+      case 'risky':
+        $this->testSuiteRuns[$this->testSuiteLevel]['risky']++;
+        break;
+
+      case 'skipped':
+        $this->testSuiteRuns[$this->testSuiteLevel]['skipped']++;
+        break;
+
+      case 'incomplete':
+        $this->testSuiteRuns[$this->testSuiteLevel]['incomplete']++;
+        break;
+
+    }
+  }
+
+}
diff --git a/core/modules/simpletest/css/simpletest.module.css b/core/modules/simpletest/css/simpletest.module.css
index f7a932e286..2978ab44f9 100644
--- a/core/modules/simpletest/css/simpletest.module.css
+++ b/core/modules/simpletest/css/simpletest.module.css
@@ -60,6 +60,13 @@ tr.simpletest-fail.odd {
 tr.simpletest-fail.even {
   background-color: #ffacac;
 }
+tr.simpletest-skipped,
+tr.simpletest-skipped.odd {
+  background-color: #ffc9c9;
+}
+tr.simpletest-skipped.even {
+  background-color: #ffacac;
+}
 tr.simpletest-exception,
 tr.simpletest-exception.odd {
   background-color: #f4ea71;
diff --git a/core/modules/simpletest/simpletest.install b/core/modules/simpletest/simpletest.install
index 3ecc6046b5..12b8361265 100644
--- a/core/modules/simpletest/simpletest.install
+++ b/core/modules/simpletest/simpletest.install
@@ -114,10 +114,10 @@ function simpletest_schema() {
       ],
       'status' => [
         'type' => 'varchar',
-        'length' => 9,
+        'length' => 12,
         'not null' => TRUE,
         'default' => '',
-        'description' => 'Message status. Core understands pass, fail, exception.',
+        'description' => 'Message status.',
       ],
       'message' => [
         'type' => 'text',
@@ -198,3 +198,19 @@ function simpletest_uninstall() {
   }
 
 }
+
+/**
+ * Increase the size of the 'status' column.
+ */
+function simpletest_update_8801() {
+  $schema = \Drupal::database()->schema();
+  if ($schema->tableExists('simpletest')) {
+    $schema->changeField('simpletest', 'status', 'status', [
+      'type' => 'varchar',
+      'length' => 12,
+      'not null' => TRUE,
+      'default' => '',
+      'description' => 'Message status.',
+    ]);
+  }
+}
diff --git a/core/modules/simpletest/simpletest.module b/core/modules/simpletest/simpletest.module
index ed3641239c..d5c5beae5e 100644
--- a/core/modules/simpletest/simpletest.module
+++ b/core/modules/simpletest/simpletest.module
@@ -9,6 +9,7 @@
 use Drupal\Core\Asset\AttachedAssetsInterface;
 use Drupal\Core\Database\Database;
 use Drupal\Core\File\Exception\FileException;
+use Drupal\Core\File\FileSystemInterface;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Test\TestDatabase;
@@ -16,6 +17,9 @@
 use Drupal\Tests\Listeners\SimpletestUiPrinter;
 use PHPUnit\Framework\TestCase;
 use Symfony\Component\Process\PhpExecutableFinder;
+use Symfony\Component\Process\Process;
+use Drupal\Core\Test\JUnitHelper;
+use Drupal\Core\Test\JUnitHelperLegacy;
 use Drupal\Core\Test\TestStatus;
 
 /**
@@ -47,7 +51,17 @@ function simpletest_help($route_name, RouteMatchInterface $route_match) {
 function simpletest_theme() {
   return [
     'simpletest_result_summary' => [
-      'variables' => ['label' => NULL, 'items' => [], 'pass' => 0, 'fail' => 0, 'exception' => 0, 'debug' => 0],
+      'variables' => [
+        'label' => NULL,
+        'items' => [],
+        'pass' => 0,
+        'fail' => 0,
+        'risky' => 0,
+        'skipped' => 0,
+        'incomplete' => 0,
+        'exception' => 0,
+        'debug' => 0,
+      ],
     ],
   ];
 }
@@ -95,6 +109,9 @@ function _simpletest_build_summary_line($summary) {
   $translation = \Drupal::translation();
   $items['pass'] = $translation->formatPlural($summary['pass'], '1 pass', '@count passes');
   $items['fail'] = $translation->formatPlural($summary['fail'], '1 fail', '@count fails');
+  $items['risky'] = $translation->formatPlural($summary['risky'], '1 risky', '@count risky');
+  $items['skipped'] = $translation->formatPlural($summary['skipped'], '1 skipped', '@count skipped');
+  $items['incomplete'] = $translation->formatPlural($summary['incomplete'], '1 incomplete', '@count incomplete');
   $items['exception'] = $translation->formatPlural($summary['exception'], '1 exception', '@count exceptions');
   if ($summary['debug']) {
     $items['debug'] = $translation->formatPlural($summary['debug'], '1 debug message', '@count debug messages');
@@ -188,13 +205,14 @@ function simpletest_run_tests($test_list) {
  *   The parsed results of PHPUnit's JUnit XML output, in the format of
  *   {simpletest}'s schema.
  */
-function simpletest_run_phpunit_tests($test_id, array $unescaped_test_classnames, &$status = NULL) {
+function simpletest_run_phpunit_tests($test_id, array $unescaped_test_classnames, &$status = NULL, $environment_variables = []) {
   $phpunit_file = simpletest_phpunit_xml_filepath($test_id);
-  simpletest_phpunit_run_command($unescaped_test_classnames, $phpunit_file, $status, $output);
+  simpletest_phpunit_run_command($unescaped_test_classnames, $phpunit_file, $status, $output, $environment_variables);
 
   $rows = [];
   if ($status == TestStatus::PASS) {
-    $rows = simpletest_phpunit_xml_to_rows($test_id, $phpunit_file);
+    $junit = new JUnitHelper($test_id, $phpunit_file);
+    $rows = $junit->getRecords();
   }
   else {
     $rows[] = [
@@ -215,7 +233,7 @@ function simpletest_run_phpunit_tests($test_id, array $unescaped_test_classnames
  * Inserts the parsed PHPUnit results into {simpletest}.
  *
  * @param array[] $phpunit_results
- *   An array of test results returned from simpletest_phpunit_xml_to_rows().
+ *   An array of test results returned from JUnitHelper::getRecords().
  */
 function simpletest_process_phpunit_results($phpunit_results) {
   // Insert the results of the PHPUnit test run into the database so the results
@@ -247,6 +265,9 @@ function simpletest_summarize_phpunit_result($results) {
       $summaries[$result['test_class']] = [
         '#pass' => 0,
         '#fail' => 0,
+        '#risky' => 0,
+        '#skipped' => 0,
+        '#incomplete' => 0,
         '#exception' => 0,
         '#debug' => 0,
       ];
@@ -261,6 +282,18 @@ function simpletest_summarize_phpunit_result($results) {
         $summaries[$result['test_class']]['#fail']++;
         break;
 
+      case 'risky':
+        $summaries[$result['test_class']]['#risky']++;
+        break;
+
+      case 'skipped':
+        $summaries[$result['test_class']]['#skipped']++;
+        break;
+
+      case 'incomplete':
+        $summaries[$result['test_class']]['#incomplete']++;
+        break;
+
       case 'exception':
         $summaries[$result['test_class']]['#exception']++;
         break;
@@ -283,7 +316,10 @@ function simpletest_summarize_phpunit_result($results) {
  *   Path to the PHPUnit XML file to use for the current $test_id.
  */
 function simpletest_phpunit_xml_filepath($test_id) {
-  return \Drupal::service('file_system')->realpath('public://simpletest') . '/phpunit-' . $test_id . '.xml';
+  $file_system = \Drupal::service('file_system');
+  $simpletest_path = $file_system->realpath('public://simpletest');
+  $file_system->prepareDirectory($simpletest_path, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS);
+  return $simpletest_path . '/phpunit-' . $test_id . '.xml';
 }
 
 /**
@@ -320,34 +356,35 @@ function simpletest_phpunit_configuration_filepath() {
  * @return string
  *   The results as returned by exec().
  */
-function simpletest_phpunit_run_command(array $unescaped_test_classnames, $phpunit_file, &$status = NULL, &$output = NULL) {
+function simpletest_phpunit_run_command(array $unescaped_test_classnames, $phpunit_file, &$status = NULL, &$output = NULL, $environment_variables = []) {
   global $base_url;
   // Setup an environment variable containing the database connection so that
   // functional tests can connect to the database.
-  putenv('SIMPLETEST_DB=' . Database::getConnectionInfoAsUrl());
+  $process_environment_variables = array_merge($environment_variables, [
+    'SIMPLETEST_DB' => Database::getConnectionInfoAsUrl(),
+    'SIMPLETEST_JUNIT_FILE' => $phpunit_file,
+  ]);
 
   // Setup an environment variable containing the base URL, if it is available.
   // This allows functional tests to browse the site under test. When running
   // tests via CLI, core/phpunit.xml.dist or core/scripts/run-tests.sh can set
   // this variable.
   if ($base_url) {
-    putenv('SIMPLETEST_BASE_URL=' . $base_url);
-    putenv('BROWSERTEST_OUTPUT_DIRECTORY=' . \Drupal::service('file_system')->realpath('public://simpletest'));
+    $process_environment_variables['SIMPLETEST_BASE_URL'] = $base_url;
+    $process_environment_variables['BROWSERTEST_OUTPUT_DIRECTORY'] = \Drupal::service('file_system')->realpath('public://simpletest');
   }
   $phpunit_bin = simpletest_phpunit_command();
 
   $command = [
     $phpunit_bin,
-    '--log-junit',
-    escapeshellarg($phpunit_file),
     '--printer',
-    escapeshellarg(SimpletestUiPrinter::class),
+    SimpletestUiPrinter::class,
   ];
 
   // Optimized for running a single test.
   if (count($unescaped_test_classnames) == 1) {
     $class = new \ReflectionClass($unescaped_test_classnames[0]);
-    $command[] = escapeshellarg($class->getFileName());
+    $command[] = $class->getFileName();
   }
   else {
     // Double escape namespaces so they'll work in a regexp.
@@ -358,26 +395,18 @@ function simpletest_phpunit_run_command(array $unescaped_test_classnames, $phpun
     $filter_string = implode("|", $escaped_test_classnames);
     $command = array_merge($command, [
       '--filter',
-      escapeshellarg($filter_string),
+      $filter_string,
     ]);
   }
 
-  // Need to change directories before running the command so that we can use
-  // relative paths in the configuration file's exclusions.
-  $old_cwd = getcwd();
-  chdir(\Drupal::root() . "/core");
-
-  // exec in a subshell so that the environment is isolated when running tests
-  // via the simpletest UI.
-  $ret = exec(implode(" ", $command), $output, $status);
+  $process = new Process($command, \Drupal::root() . "/core", $process_environment_variables);
+  $process->inheritEnvironmentVariables();
+  $process->setTimeout(NULL);
+  $process->run();
+  $output = explode("\n", $process->getOutput());
+  $status = $process->getExitCode();
 
-  chdir($old_cwd);
-  putenv('SIMPLETEST_DB=');
-  if ($base_url) {
-    putenv('SIMPLETEST_BASE_URL=');
-    putenv('BROWSERTEST_OUTPUT_DIRECTORY=');
-  }
-  return $ret;
+  return '';
 }
 
 /**
@@ -395,7 +424,7 @@ function simpletest_phpunit_command() {
 
   // The file in Composer's bin dir is a *nix link, which does not work when
   // extracted from a tarball and generally not on Windows.
-  $command = escapeshellarg($vendor_dir . '/phpunit/phpunit/phpunit');
+  $command = $vendor_dir . '/phpunit/phpunit/phpunit';
   if (substr(PHP_OS, 0, 3) == 'WIN') {
     // On Windows it is necessary to run the script using the PHP executable.
     $php_executable_finder = new PhpExecutableFinder();
@@ -415,7 +444,15 @@ function _simpletest_batch_operation($test_list_init, $test_id, &$context) {
     // First iteration: initialize working values.
     $test_list = $test_list_init;
     $context['sandbox']['max'] = count($test_list);
-    $test_results = ['#pass' => 0, '#fail' => 0, '#exception' => 0, '#debug' => 0];
+    $test_results = [
+      '#pass' => 0,
+      '#fail' => 0,
+      '#risky' => 0,
+      '#skipped' => 0,
+      '#incomplete' => 0,
+      '#exception' => 0,
+      '#debug' => 0,
+    ];
   }
   else {
     // Nth iteration: get the current values where we last stored them.
@@ -842,18 +879,18 @@ function simpletest_mail_alter(&$message) {
  *   The results as array of rows in a format that can be inserted into
  *   {simpletest}. If the phpunit_xml_file does not have any contents then the
  *   function will return NULL.
+ *
+ * @todo https://www.drupal.org/project/drupal/issues/2968538
+ *
+ * @deprecated in Drupal 8.8.x for removal before Drupal 9.0.0. Use
+ *   \Drupal\Core\Test\JUnitHelper to convert JUnit to simpletest database rows.
+ *
+ * @see https://www.drupal.org/node/2966954
  */
 function simpletest_phpunit_xml_to_rows($test_id, $phpunit_xml_file) {
-  $contents = @file_get_contents($phpunit_xml_file);
-  if (!$contents) {
-    return;
-  }
-  $records = [];
-  $testcases = simpletest_phpunit_find_testcases(new SimpleXMLElement($contents));
-  foreach ($testcases as $testcase) {
-    $records[] = simpletest_phpunit_testcase_to_row($test_id, $testcase);
-  }
-  return $records;
+  @trigger_error(__FUNCTION__ . ' is deprecated in Drupal 8.8.x for removal before Drupal 9.0.0. Use \Drupal\Core\Test\JUnitHelper to convert JUnit to simpletest database rows. See https://www.drupal.org/node/2966954.', E_USER_DEPRECATED);
+  $junit = new JUnitHelper($test_id, $phpunit_xml_file);
+  return $junit->getRecords();
 }
 
 /**
@@ -862,38 +899,21 @@ function simpletest_phpunit_xml_to_rows($test_id, $phpunit_xml_file) {
  * @param \SimpleXMLElement $element
  *   The PHPUnit xml to search for test cases.
  * @param \SimpleXMLElement $parent
- *   (Optional) The parent of the current element. Defaults to NULL.
+ *   (Optional) Unused. The parent of the current element. Defaults to NULL.
  *
  * @return array
  *   A list of all test cases.
+ *
+ * @todo https://www.drupal.org/project/drupal/issues/2968538
+ *
+ * @deprecated in Drupal 8.8.x for removal before Drupal 9.0.0. Use
+ *   \Drupal\Core\Test\JUnitHelper to convert JUnit to simpletest database rows.
+ *
+ * @see https://www.drupal.org/node/2966954
  */
 function simpletest_phpunit_find_testcases(\SimpleXMLElement $element, \SimpleXMLElement $parent = NULL) {
-  $testcases = [];
-
-  if (!isset($parent)) {
-    $parent = $element;
-  }
-
-  if ($element->getName() === 'testcase' && (int) $parent->attributes()->tests > 0) {
-    // Add the class attribute if the testcase does not have one. This is the
-    // case for tests using a data provider. The name of the parent testsuite
-    // will be in the format class::method.
-    if (!$element->attributes()->class) {
-      $name = explode('::', $parent->attributes()->name, 2);
-      $element->addAttribute('class', $name[0]);
-    }
-    $testcases[] = $element;
-  }
-  else {
-    foreach ($element as $child) {
-      $file = (string) $parent->attributes()->file;
-      if ($file && !$child->attributes()->file) {
-        $child->addAttribute('file', $file);
-      }
-      $testcases = array_merge($testcases, simpletest_phpunit_find_testcases($child, $element));
-    }
-  }
-  return $testcases;
+  @trigger_error(__FUNCTION__ . ' is deprecated in Drupal 8.8.x for removal before Drupal 9.0.0. Use \Drupal\Core\Test\JUnitHelper to convert JUnit to simpletest database rows. See https://www.drupal.org/node/2966954.', E_USER_DEPRECATED);
+  return JUnitHelperLegacy::findTestcases($element);
 }
 
 /**
@@ -906,35 +926,16 @@ function simpletest_phpunit_find_testcases(\SimpleXMLElement $element, \SimpleXM
  *
  * @return array
  *   An array containing the {simpletest} result row.
+ *
+ * @todo https://www.drupal.org/project/drupal/issues/2968538
+ *
+ * @deprecated in Drupal 8.8.x for removal before Drupal 9.0.0. Use
+ *   \Drupal\Core\Test\JUnitHelper to convert JUnit to simpletest database rows.
+ *
+ * @see https://www.drupal.org/node/2966954
  */
 function simpletest_phpunit_testcase_to_row($test_id, \SimpleXMLElement $testcase) {
-  $message = '';
-  $pass = TRUE;
-  if ($testcase->failure) {
-    $lines = explode("\n", $testcase->failure);
-    $message = $lines[2];
-    $pass = FALSE;
-  }
-  if ($testcase->error) {
-    $message = $testcase->error;
-    $pass = FALSE;
-  }
-
-  $attributes = $testcase->attributes();
-
-  $function = $attributes->class . '->' . $attributes->name . '()';
-  $record = [
-    'test_id' => $test_id,
-    'test_class' => (string) $attributes->class,
-    'status' => $pass ? 'pass' : 'fail',
-    'message' => $message,
-    // @todo: Check on the proper values for this.
-    'message_group' => 'Other',
-    'function' => $function,
-    'line' => $attributes->line ?: 0,
-    // There are situations when the file will not be present because a PHPUnit
-    // @requires has caused a test to be skipped.
-    'file' => $attributes->file ?: $function,
-  ];
-  return $record;
+  @trigger_error(__FUNCTION__ . ' is deprecated in Drupal 8.8.x for removal before Drupal 9.0.0. Use \Drupal\Core\Test\JUnitHelper to convert JUnit to simpletest database rows. See https://www.drupal.org/node/2966954.', E_USER_DEPRECATED);
+  $junit = new JUnitHelperLegacy($test_id, '');
+  return $junit->testcaseToRow($testcase);
 }
diff --git a/core/modules/simpletest/src/Form/SimpletestResultsForm.php b/core/modules/simpletest/src/Form/SimpletestResultsForm.php
index a2241571eb..f6ade17cf8 100644
--- a/core/modules/simpletest/src/Form/SimpletestResultsForm.php
+++ b/core/modules/simpletest/src/Form/SimpletestResultsForm.php
@@ -92,6 +92,9 @@ protected static function buildStatusImageMap() {
     return [
       'pass' => $image_pass,
       'fail' => $image_fail,
+      'risky' => $image_fail,
+      'skipped' => $image_debug,
+      'incomplete' => $image_debug,
       'exception' => $image_exception,
       'debug' => $image_debug,
     ];
@@ -263,6 +266,9 @@ public static function addResultForm(array &$form, array $results) {
     $filter = [
       'pass' => [],
       'fail' => [],
+      'risky' => [],
+      'skipped' => [],
+      'incomplete' => [],
     ];
 
     // Summary result widget.
@@ -277,6 +283,9 @@ public static function addResultForm(array &$form, array $results) {
       '#theme' => 'simpletest_result_summary',
       '#pass' => 0,
       '#fail' => 0,
+      '#risky' => 0,
+      '#skipped' => 0,
+      '#incomplete' => 0,
       '#exception' => 0,
       '#debug' => 0,
     ];
@@ -333,7 +342,8 @@ public static function addResultForm(array &$form, array $results) {
 
       // Set summary information.
       $group_summary['#ok'] = $group_summary['#fail'] + $group_summary['#exception'] == 0;
-      $form['result']['results'][$group]['#open'] = !$group_summary['#ok'];
+      // Set the group to be revealed if there were fails, errors, risky or incomplete.
+      $form['result']['results'][$group]['#open'] = !$group_summary['#ok'] || $group_summary['#risky'] || $group_summary['#incomplete'];
 
       // Store test group (class) as for use in filter.
       $filter[$group_summary['#ok'] ? 'pass' : 'fail'][] = $group;
diff --git a/core/modules/simpletest/src/TestBase.php b/core/modules/simpletest/src/TestBase.php
index 1af310bc80..baa6426f9f 100644
--- a/core/modules/simpletest/src/TestBase.php
+++ b/core/modules/simpletest/src/TestBase.php
@@ -62,6 +62,9 @@
   public $results = [
     '#pass' => 0,
     '#fail' => 0,
+    '#risky' => 0,
+    '#skipped' => 0,
+    '#incomplete' => 0,
     '#exception' => 0,
     '#debug' => 0,
   ];
@@ -305,8 +308,8 @@ protected function storeAssertion(array $assertion) {
    * Internal helper: stores the assert.
    *
    * @param $status
-   *   Can be 'pass', 'fail', 'exception', 'debug'.
-   *   TRUE is a synonym for 'pass', FALSE for 'fail'.
+   *   Can be 'pass', 'fail', 'risky', 'skipped', 'incomplete' , 'exception',
+   *   'debug'. TRUE is a synonym for 'pass', FALSE for 'fail'.
    * @param string|\Drupal\Component\Render\MarkupInterface $message
    *   (optional) A message to display with the assertion. Do not translate
    *   messages: use \Drupal\Component\Render\FormattableMarkup to embed
diff --git a/core/modules/simpletest/src/Tests/SimpleTestTest.php b/core/modules/simpletest/src/Tests/SimpleTestTest.php
index 392bbb921d..58bf0de7e3 100644
--- a/core/modules/simpletest/src/Tests/SimpleTestTest.php
+++ b/core/modules/simpletest/src/Tests/SimpleTestTest.php
@@ -266,7 +266,7 @@ public function confirmStubTestResults() {
 
     $this->assertAssertion("Debug: 'Foo'", 'Debug', 'Fail', 'SimpleTestTest.php', 'Drupal\simpletest\Tests\SimpleTestTest->stubTest()');
 
-    $this->assertEqual('16 passes, 3 fails, 2 exceptions, 3 debug messages', $this->childTestResults['summary']);
+    $this->assertEqual('16 passes, 3 fails, 0 risky, 0 skipped, 0 incomplete, 2 exceptions, 3 debug messages', $this->childTestResults['summary']);
 
     $this->testIds[] = $test_id = $this->getTestIdFromResults();
     $this->assertTrue($test_id, 'Found test ID in results.');
diff --git a/core/modules/simpletest/tests/fixtures/phpunit_skipped.xml b/core/modules/simpletest/tests/fixtures/phpunit_skipped.xml
new file mode 100644
index 0000000000..bd1d037f36
--- /dev/null
+++ b/core/modules/simpletest/tests/fixtures/phpunit_skipped.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<testsuites>
+  <testsuite name="Drupal\Tests\Core\Access\AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" tests="18" assertions="61" errors="0" failures="0" risky="0" skipped="1" incomplete="0" time="0.308898">
+    <testcase name="testSetChecks" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="138" assertions="0" time="0.106626">
+      <skipped message="skipped"/>
+    </testcase>
+    <testcase name="testSetChecksWithDynamicAccessChecker" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="159" assertions="3" time="0.017037"/>
+    <testcase name="testCheck" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="189" assertions="24" time="0.021573"/>
+    <testcase name="testCheckWithNullAccount" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="231" assertions="1" time="0.010700"/>
+    <testsuite name="Drupal\Tests\Core\Access\AccessManagerTest::testCheckConjunctions" tests="6" assertions="12" errors="0" failures="0" risky="0" skipped="0" incomplete="0" time="0.063584">
+      <testcase name="testCheckConjunctions with data set #0" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="305" assertions="2" time="0.010881"/>
+      <testcase name="testCheckConjunctions with data set #1" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="305" assertions="2" time="0.010670"/>
+      <testcase name="testCheckConjunctions with data set #2" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="305" assertions="2" time="0.010567"/>
+      <testcase name="testCheckConjunctions with data set #3" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="305" assertions="2" time="0.010508"/>
+      <testcase name="testCheckConjunctions with data set #4" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="305" assertions="2" time="0.010574"/>
+      <testcase name="testCheckConjunctions with data set #5" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="305" assertions="2" time="0.010384"/>
+    </testsuite>
+    <testcase name="testCheckNamedRoute" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="333" assertions="5" time="0.012686"/>
+    <testcase name="testCheckNamedRouteWithUpcastedValues" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="368" assertions="5" time="0.012923"/>
+    <testcase name="testCheckNamedRouteWithDefaultValue" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="417" assertions="5" time="0.012138"/>
+    <testcase name="testCheckNamedRouteWithNonExistingRoute" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="464" assertions="2" time="0.010824"/>
+    <testsuite name="Drupal\Tests\Core\Access\AccessManagerTest::testCheckException" tests="4" assertions="4" errors="0" failures="0" risky="0" skipped="0" incomplete="0" time="0.040806">
+      <testcase name="testCheckException with data set #0" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="480" assertions="1" time="0.010990"/>
+      <testcase name="testCheckException with data set #1" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="480" assertions="1" time="0.009883"/>
+      <testcase name="testCheckException with data set #2" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="480" assertions="1" time="0.009928"/>
+      <testcase name="testCheckException with data set #3" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="480" assertions="1" time="0.010005"/>
+    </testsuite>
+  </testsuite>
+</testsuites>
diff --git a/core/modules/simpletest/tests/src/Functional/SimpletestUiTest.php b/core/modules/simpletest/tests/src/Functional/SimpletestUiTest.php
index f91cb6d507..74c8bf957c 100644
--- a/core/modules/simpletest/tests/src/Functional/SimpletestUiTest.php
+++ b/core/modules/simpletest/tests/src/Functional/SimpletestUiTest.php
@@ -56,7 +56,7 @@ public function testTestingThroughUI() {
         "tests[$test]" => TRUE,
       ];
       $this->drupalPostForm($url, $edit, t('Run tests'));
-      $assertion->pageTextContains('0 fails, 0 exceptions');
+      $assertion->pageTextContains('0 fails, 0 risky, 0 skipped, 0 incomplete, 0 exceptions');
     }
   }
 
diff --git a/core/modules/simpletest/tests/src/Kernel/SimpletestDeprecationTest.php b/core/modules/simpletest/tests/src/Kernel/SimpletestDeprecationTest.php
index 384f8c1b9b..331befd6ed 100644
--- a/core/modules/simpletest/tests/src/Kernel/SimpletestDeprecationTest.php
+++ b/core/modules/simpletest/tests/src/Kernel/SimpletestDeprecationTest.php
@@ -25,4 +25,30 @@ public function testDeprecatedFunctions() {
     simpletest_classloader_register();
   }
 
+  /**
+   * @expectedDeprecation simpletest_phpunit_xml_to_rows is deprecated in Drupal 8.8.x for removal before Drupal 9.0.0. Use \Drupal\Core\Test\JUnitHelper to convert JUnit to simpletest database rows. See https://www.drupal.org/node/2966954.
+   * @expectedDeprecation simpletest_phpunit_find_testcases is deprecated in Drupal 8.8.x for removal before Drupal 9.0.0. Use \Drupal\Core\Test\JUnitHelper to convert JUnit to simpletest database rows. See https://www.drupal.org/node/2966954.
+   * @expectedDeprecation simpletest_phpunit_testcase_to_row is deprecated in Drupal 8.8.x for removal before Drupal 9.0.0. Use \Drupal\Core\Test\JUnitHelper to convert JUnit to simpletest database rows. See https://www.drupal.org/node/2966954.
+   */
+  public function testDeprecatedJUnitFunctions() {
+    $file = realpath(__DIR__ . '/../../fixtures/phpunit_skipped.xml');
+    $this->assertFileExists($file);
+    $this->assertCount(18, simpletest_phpunit_xml_to_rows('test_id', $file));
+
+    $xml = new \SimpleXMLElement(file_get_contents($file));
+    $this->assertCount(18, $cases = simpletest_phpunit_find_testcases($xml));
+
+    $xml = new \SimpleXMLElement('<testcase name="name"/>');
+    $this->assertEquals([
+      'test_id' => 'my_id',
+      'test_class' => '',
+      'status' => 'pass',
+      'message' => '',
+      'message_group' => 'Other',
+      'function' => '->name()',
+      'line' => 0,
+      'file' => '',
+      ], \simpletest_phpunit_testcase_to_row('my_id', $xml));
+  }
+
 }
diff --git a/core/modules/simpletest/tests/src/Unit/PhpUnitErrorTest.php b/core/modules/simpletest/tests/src/Unit/PhpUnitErrorTest.php
deleted file mode 100644
index 25211fc404..0000000000
--- a/core/modules/simpletest/tests/src/Unit/PhpUnitErrorTest.php
+++ /dev/null
@@ -1,40 +0,0 @@
-<?php
-
-namespace Drupal\Tests\simpletest\Unit;
-
-use Drupal\Tests\UnitTestCase;
-
-/**
- * Tests PHPUnit errors are getting converted to Simpletest errors.
- *
- * @group simpletest
- */
-class PhpUnitErrorTest extends UnitTestCase {
-
-  /**
-   * Test errors reported.
-   *
-   * @covers ::simpletest_phpunit_xml_to_rows
-   */
-  public function testPhpUnitXmlParsing() {
-    require_once __DIR__ . '/../../../simpletest.module';
-
-    $phpunit_error_xml = __DIR__ . '/../../fixtures/phpunit_error.xml';
-
-    $res = simpletest_phpunit_xml_to_rows(1, $phpunit_error_xml);
-    $this->assertEquals(count($res), 4, 'All testcases got extracted');
-    $this->assertNotEquals($res[0]['status'], 'pass');
-    $this->assertEquals($res[0]['status'], 'fail');
-
-    // Test nested testsuites, which appear when you use @dataProvider.
-    for ($i = 0; $i < 3; $i++) {
-      $this->assertNotEquals($res[$i + 1]['status'], 'pass');
-      $this->assertEquals($res[$i + 1]['status'], 'fail');
-    }
-
-    // Make sure simpletest_phpunit_xml_to_rows() does not balk if the test
-    // didn't run.
-    simpletest_phpunit_xml_to_rows(1, 'foobar');
-  }
-
-}
diff --git a/core/modules/simpletest/tests/src/Unit/SimpletestPhpunitRunCommandTest.php b/core/modules/simpletest/tests/src/Unit/SimpletestPhpunitRunCommandTest.php
index 30617b9560..7fda979f6f 100644
--- a/core/modules/simpletest/tests/src/Unit/SimpletestPhpunitRunCommandTest.php
+++ b/core/modules/simpletest/tests/src/Unit/SimpletestPhpunitRunCommandTest.php
@@ -54,6 +54,8 @@ protected function setUp() {
     $file_system = $this->prophesize(FileSystemInterface::class);
     // The simpletest directory wrapper will always point to /tmp.
     $file_system->realpath('public://simpletest')->willReturn(sys_get_temp_dir());
+    // The simpletest directory wrapper preparation will always be true.
+    $file_system->prepareDirectory(sys_get_temp_dir(), FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS)->willReturn(TRUE);
     $container->set('file_system', $file_system->reveal());
     \Drupal::setContainer($container);
   }
@@ -100,10 +102,8 @@ public function testSimpletestPhpUnitRunCommand($status, $label) {
       ]
     );
     $test_id = basename(tempnam(sys_get_temp_dir(), 'xxx'));
-    putenv('SimpletestPhpunitRunCommandTestWillDie=' . $status);
-    $ret = simpletest_run_phpunit_tests($test_id, [SimpletestPhpunitRunCommandTestWillDie::class]);
-    $this->assertSame($ret[0]['status'], $label);
-    putenv('SimpletestPhpunitRunCommandTestWillDie');
+    $ret = simpletest_run_phpunit_tests($test_id, [SimpletestPhpunitRunCommandTestWillDie::class], $status, ['SimpletestPhpunitRunCommandTestWillDie' => $status]);
+    $this->assertSame($label, $ret[0]['status']);
     unlink(simpletest_phpunit_xml_filepath($test_id));
   }
 
diff --git a/core/phpunit.xml.dist b/core/phpunit.xml.dist
index cde7773b27..438f369ed8 100644
--- a/core/phpunit.xml.dist
+++ b/core/phpunit.xml.dist
@@ -48,6 +48,7 @@
     <!-- The Symfony deprecation listener has to come after the Drupal listener -->
     <listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener">
     </listener>
+    <listener class="\Drupal\Core\Test\JUnitListener"/>
   </listeners>
   <!-- Filter for coverage reports. -->
   <filter>
diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh
index f08956e61f..456347d60f 100755
--- a/core/scripts/run-tests.sh
+++ b/core/scripts/run-tests.sh
@@ -749,10 +749,15 @@ function simpletest_script_execute_batch($test_classes) {
           // Insert a fail for xml results.
           simpletest_insert_assert($child['test_id'], $child['class'], FALSE, $message, 'run-tests.sh check');
           // Ensure that an error line is displayed for the class.
-          simpletest_script_reporter_display_summary(
-            $child['class'],
-            ['#pass' => 0, '#fail' => 1, '#exception' => 0, '#debug' => 0]
-          );
+          simpletest_script_reporter_display_summary($child['class'], [
+            '#pass' => 0,
+            '#fail' => 1,
+            '#risky' => 0,
+            '#skipped' => 0,
+            '#incomplete' => 0,
+            '#exception' => 0,
+            '#debug' => 0
+          ]);
           if ($args['die-on-fail']) {
             list($db_prefix) = simpletest_last_test_get($child['test_id']);
             $test_db = new TestDatabase($db_prefix);
@@ -1215,15 +1220,18 @@ function simpletest_script_reporter_display_summary($class, $results) {
   // Output all test results vertically aligned.
   // Cut off the class name after 60 chars, and pad each group with 3 digits
   // by default (more than 999 assertions are rare).
-  $output = vsprintf('%-60.60s %10s %9s %14s %12s', [
+  $output = vsprintf('%-60.60s %10s %9s %10s %11s %14s %14s %12s', [
     $class,
     $results['#pass'] . ' passes',
     !$results['#fail'] ? '' : $results['#fail'] . ' fails',
+    !$results['#risky'] ? '' : $results['#risky'] . ' risky',
+    !$results['#skipped'] ? '' : $results['#skipped'] . ' skipped',
+    !$results['#incomplete'] ? '' : $results['#incomplete'] . ' incomplete',
     !$results['#exception'] ? '' : $results['#exception'] . ' exceptions',
     !$results['#debug'] ? '' : $results['#debug'] . ' messages',
   ]);
 
-  $status = ($results['#fail'] || $results['#exception'] ? 'fail' : 'pass');
+  $status = ($results['#fail'] || $results['#exception'] || $results['#risky'] ? 'fail' : 'pass');
   simpletest_script_print($output . "\n", simpletest_script_color_code($status));
 }
 
diff --git a/core/tests/Drupal/Tests/Core/Test/JUnitHelperLegacyTest.php b/core/tests/Drupal/Tests/Core/Test/JUnitHelperLegacyTest.php
new file mode 100644
index 0000000000..20ce7dd5cc
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Test/JUnitHelperLegacyTest.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Drupal\Tests\Core\Test;
+
+use Drupal\Core\Test\JUnitHelperLegacy;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Test\JUnitHelperLegacy
+ *
+ * @group Test
+ * @group JUnit
+ * @group simpletest
+ */
+class JUnitHelperLegacyTest extends UnitTestCase {
+
+  /**
+   * @coversNothing
+   */
+  public function testEmptyState() {
+    $junit = new JUnitHelperLegacy('id', 'empty-file');
+    $this->assertEmpty($junit->getRecords());
+  }
+
+  /**
+   * @covers ::findTestcases
+   */
+  public function testFindTestcases() {
+    $file = realpath(__DIR__ . '/../../../../fixtures/phpunit_skipped.xml');
+    $this->assertFileExists($file);
+    $xml = new \SimpleXMLElement(file_get_contents($file));
+
+    $this->assertCount(18, JUnitHelperLegacy::findTestcases($xml));
+  }
+
+  /**
+   * @covers ::testcaseToRow
+   */
+  public function testTestcaseToRow() {
+    $xml = new \SimpleXMLElement('<testcase name="name"/>');
+    $junit = new JUnitHelperLegacy('my_id', '');
+    $this->assertArrayEquals([
+      'test_id' => 'my_id',
+      'test_class' => '',
+      'status' => 'pass',
+      'message' => '',
+      'message_group' => 'Other',
+      'function' => '->name()',
+      'line' => 0,
+      'file' => '',
+    ], $junit->testcaseToRow($xml));
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Test/JUnitHelperTest.php b/core/tests/Drupal/Tests/Core/Test/JUnitHelperTest.php
new file mode 100644
index 0000000000..ce3f280fd1
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Test/JUnitHelperTest.php
@@ -0,0 +1,92 @@
+<?php
+
+namespace Drupal\Tests\Core\Test;
+
+use Drupal\Core\Test\JUnitHelper;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Test\JUnitHelper
+ *
+ * @group Test
+ * @group JUnit
+ * @group simpletest
+ */
+class JUnitHelperTest extends UnitTestCase {
+
+  /**
+   * @coversNothing
+   */
+  public function testEmptyState() {
+    $junit = new JUnitHelper('id', 'empty-file');
+    $this->assertSame([], $junit->getRecords());
+  }
+
+  /**
+   * @coversNothing
+   */
+  public function testError() {
+    $file = realpath(__DIR__ . '/../../../../fixtures/phpunit_error.xml');
+    $this->assertFileExists($file);
+    $junit = new JUnitHelper('id', realpath($file));
+    $records = $junit->getRecords();
+
+    $this->assertCount(4, $records, 'All testcases got extracted');
+    $this->assertNotEquals($records[0]['status'], 'pass');
+    $this->assertEquals($records[0]['status'], 'fail');
+
+    // Test nested testsuites, which appear when you use @dataProvider.
+    for ($i = 0; $i < 3; $i++) {
+      $this->assertNotEquals($records[$i + 1]['status'], 'pass');
+      $this->assertEquals($records[$i + 1]['status'], 'fail');
+    }
+  }
+
+  /**
+   * @coversNothing
+   */
+  public function testSkipped() {
+    $file = realpath(__DIR__ . '/../../../../fixtures/phpunit_skipped.xml');
+    $this->assertFileExists($file);
+
+    $junit = new JUnitHelper('id', realpath($file));
+    $records = $junit->getRecords();
+
+    $this->assertCount(18, $records);
+    // Only item 0 should be skipped.
+    $this->assertEquals('skipped', $records[0]['status']);
+    foreach (range(1, 17) as $index) {
+      $this->assertEquals('pass', $records[$index]['status']);
+    }
+  }
+
+  public function provideGetStatus() {
+    return [
+      'pass' => ['pass', '<testcase name="name"/>'],
+      'fail-for-fail' => ['fail', '<testcase name="name"><failure type="PHPUnit_Framework_ExpectationFailedException">Message</failure></testcase>'],
+      'error-for-fail' => ['fail', '<testcase name="name"><error type="PHPUnit_Framework_Error_Notice">Message</error></testcase>'],
+      'skipped' => ['skipped', '<testcase name="name"><skipped message="skipped"/></testcase>'],
+      'incomplete' => ['incomplete', '<testcase name="name"><skipped message="incomplete"/></testcase>'],
+      'risky' => ['risky', '<testcase name="name"><skipped message="risky"/></testcase>'],
+    ];
+  }
+
+  /**
+   * @dataProvider provideGetStatus
+   * @covers ::getStatus
+   */
+  public function testGetStatus($expected, $xml) {
+    $junit = $this->getMockBuilder(JUnitHelper::class)
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $ref_get_status = new \ReflectionMethod($junit, 'getStatus');
+    $ref_get_status->setAccessible(TRUE);
+
+    $this->assertEquals(
+      $expected,
+      $ref_get_status->invokeArgs($junit, [new \SimpleXMLElement($xml)])
+    );
+  }
+
+}
diff --git a/core/modules/simpletest/tests/fixtures/phpunit_error.xml b/core/tests/fixtures/phpunit_error.xml
similarity index 100%
rename from core/modules/simpletest/tests/fixtures/phpunit_error.xml
rename to core/tests/fixtures/phpunit_error.xml
diff --git a/core/tests/fixtures/phpunit_skipped.xml b/core/tests/fixtures/phpunit_skipped.xml
new file mode 100644
index 0000000000..bd1d037f36
--- /dev/null
+++ b/core/tests/fixtures/phpunit_skipped.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<testsuites>
+  <testsuite name="Drupal\Tests\Core\Access\AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" tests="18" assertions="61" errors="0" failures="0" risky="0" skipped="1" incomplete="0" time="0.308898">
+    <testcase name="testSetChecks" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="138" assertions="0" time="0.106626">
+      <skipped message="skipped"/>
+    </testcase>
+    <testcase name="testSetChecksWithDynamicAccessChecker" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="159" assertions="3" time="0.017037"/>
+    <testcase name="testCheck" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="189" assertions="24" time="0.021573"/>
+    <testcase name="testCheckWithNullAccount" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="231" assertions="1" time="0.010700"/>
+    <testsuite name="Drupal\Tests\Core\Access\AccessManagerTest::testCheckConjunctions" tests="6" assertions="12" errors="0" failures="0" risky="0" skipped="0" incomplete="0" time="0.063584">
+      <testcase name="testCheckConjunctions with data set #0" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="305" assertions="2" time="0.010881"/>
+      <testcase name="testCheckConjunctions with data set #1" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="305" assertions="2" time="0.010670"/>
+      <testcase name="testCheckConjunctions with data set #2" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="305" assertions="2" time="0.010567"/>
+      <testcase name="testCheckConjunctions with data set #3" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="305" assertions="2" time="0.010508"/>
+      <testcase name="testCheckConjunctions with data set #4" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="305" assertions="2" time="0.010574"/>
+      <testcase name="testCheckConjunctions with data set #5" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="305" assertions="2" time="0.010384"/>
+    </testsuite>
+    <testcase name="testCheckNamedRoute" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="333" assertions="5" time="0.012686"/>
+    <testcase name="testCheckNamedRouteWithUpcastedValues" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="368" assertions="5" time="0.012923"/>
+    <testcase name="testCheckNamedRouteWithDefaultValue" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="417" assertions="5" time="0.012138"/>
+    <testcase name="testCheckNamedRouteWithNonExistingRoute" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="464" assertions="2" time="0.010824"/>
+    <testsuite name="Drupal\Tests\Core\Access\AccessManagerTest::testCheckException" tests="4" assertions="4" errors="0" failures="0" risky="0" skipped="0" incomplete="0" time="0.040806">
+      <testcase name="testCheckException with data set #0" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="480" assertions="1" time="0.010990"/>
+      <testcase name="testCheckException with data set #1" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="480" assertions="1" time="0.009883"/>
+      <testcase name="testCheckException with data set #2" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="480" assertions="1" time="0.009928"/>
+      <testcase name="testCheckException with data set #3" class="Drupal\Tests\Core\Access\AccessManagerTest" classname="Drupal.Tests.Core.Access.AccessManagerTest" file="/var/www/lab01/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php" line="480" assertions="1" time="0.010005"/>
+    </testsuite>
+  </testsuite>
+</testsuites>
