Commit e7220a71e9f for woocommerce

commit e7220a71e9f2c736013cd2e2ae96a428f8b1b6dc
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date:   Sun Nov 15 20:24:54 2026 +0200

    [tests] Fail the run when a test ends the PHPUnit process early (#70969)

    * test: Reset the Analytics reload flag between tests

    Changing the woocommerce_analytics_enabled option sets the static
    Analytics::$is_updated flag, so that the next settings save reloads the
    page: maybe_reload_page() redirects and calls exit(). In production that
    is one admin request. In tests the flag outlives the test that set it,
    and the test bootstrap sets $_SERVER['REQUEST_URI'], so the next test
    that calls WC_Admin_Settings::save() ends the whole PHPUnit process.
    The exit status is 0 and every test after it silently does not run.

    Ten tests change the option. In the default order the settings tests
    run before them, so the full suite finishes; in reverse order, or a
    random order, the run stops early. Three settings redirect tests that
    do run before the stop also fail there, because the save redirects to
    the request URI instead of where they expect.

    Reset the flag in WC_Unit_Test_Case::tearDown(), as the base class
    already does for other static state.

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * test: Fail the run when a test ends the PHPUnit process early

    A test that reaches exit() or die() ends the PHP process. PHPUnit
    prints no summary and the process exits with status 0, so the tests
    after it never run and CI still reports success.

    Add a listener, registered next to the external HTTP guard, that counts
    open suites and tracks the test in progress. A shutdown function reports
    a run that has not finished, naming the test that was running, and
    exits with status 1. A fatal error also lands here: it already prints
    its own message, and the status becomes 1 instead of 255.
    error_get_last() cannot tell the two apart, because WordPress's fatal
    error handler runs first and can raise a warning that replaces it.

    PHPUnit does not load listeners in separate-process children, so those
    tests are covered by the parent process only.

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * test: Check the early exit guard's exit status in a child process

    The listener tests built the guard with its shutdown check turned off
    and only checked the message it would print. Nothing exercised the part
    that fails the run: registering the check, writing the message to
    stderr, and exiting with status 1. Removing any of them left the suite
    green.

    A runner script now builds the real listener with the check registered,
    starts a suite and a test, and ends the process with exit( 0 ). The new
    test runs it with the PHP CLI, as the download handler tests already do,
    and checks that an unfinished run exits with status 1 and names the
    test, while a finished run keeps status 0 and prints nothing.

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * test: Check that the base teardown clears the Analytics reload flag

    The flag test called reset_analytics_reload_flag() directly, so
    removing the call in WC_Unit_Test_Case::tearDown(), which is the actual
    fix, left every test green. The leak would only come back as an early
    exit in reverse or random order.

    The test now changes the option, confirms the flag is set, and asks
    this class's tearDown() to check the flag after the base teardown has
    run. That covers the real wiring inside one test, so it holds in any
    order, unlike a pair of tests that dirty state in one and assert in the
    next. tearDown() clears the flag before asserting, so a failure does
    not end the process at a later settings save.

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * test: Match the test name, not the full message, in the exit status test

    The child-process test exists to protect the exit status and the name
    of the test that was running. It asserted the whole opening sentence of
    the message, so rewording the message would have broken it. Match the
    "while running <class>::<method>" part instead, as the in-process tests
    already match fragments.

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * test: Mention escaped exceptions in the early exit guard's message

    An exception that escapes PHPUnit while a suite is running makes
    PHPUnit print its message and exit with status 2. The suite never
    ended, so the guard also reports the run and replaces the status with
    1. The run still fails and the exception prints first, but the message
    only told readers to look for a fatal error or an exit() call.

    Say in the message and in the check_on_shutdown() docblock that an
    exception message above it is another cause, so the hint is not read
    as proof of an exit() call.

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    ---------

    Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

diff --git a/plugins/woocommerce/changelog/test-early-exit-guard b/plugins/woocommerce/changelog/test-early-exit-guard
new file mode 100644
index 00000000000..d75e2bf2502
--- /dev/null
+++ b/plugins/woocommerce/changelog/test-early-exit-guard
@@ -0,0 +1,3 @@
+Significance: patch
+Type: dev
+Comment: Fail the PHPUnit run when a test ends the process early, and reset the Analytics reload flag between tests; no production change.
diff --git a/plugins/woocommerce/phpunit.xml b/plugins/woocommerce/phpunit.xml
index 54910f5e264..63b7e71210c 100644
--- a/plugins/woocommerce/phpunit.xml
+++ b/plugins/woocommerce/phpunit.xml
@@ -36,6 +36,7 @@
             </arguments>
 		</listener>
 		<listener class="WC_External_HTTP_Guard_Listener" file="./tests/legacy/framework/class-wc-external-http-guard-listener.php" />
+		<listener class="WC_Early_Exit_Guard_Listener" file="./tests/legacy/framework/class-wc-early-exit-guard-listener.php" />
 	</listeners>
 	<coverage includeUncoveredFiles="true">
 		<include>
diff --git a/plugins/woocommerce/tests/legacy/framework/class-wc-early-exit-guard-listener.php b/plugins/woocommerce/tests/legacy/framework/class-wc-early-exit-guard-listener.php
new file mode 100644
index 00000000000..0ff8981c091
--- /dev/null
+++ b/plugins/woocommerce/tests/legacy/framework/class-wc-early-exit-guard-listener.php
@@ -0,0 +1,156 @@
+<?php
+/**
+ * Fails the run when the PHP process ends before PHPUnit finishes, for example when code under test calls exit().
+ *
+ * @package WooCommerce\Tests
+ */
+
+declare( strict_types = 1 );
+
+use PHPUnit\Framework\Test;
+use PHPUnit\Framework\TestCase;
+use PHPUnit\Framework\TestListener;
+use PHPUnit\Framework\TestListenerDefaultImplementation;
+use PHPUnit\Framework\TestSuite;
+
+/**
+ * A test that reaches an exit() or die() ends the whole process. PHPUnit prints no summary and
+ * the process exits with status 0, so every test after it silently does not run and the run
+ * still passes. This listener tracks the suites and the test in progress, and on shutdown, if
+ * the run has not finished, names the test and exits with status 1.
+ *
+ * Registered in phpunit.xml, so it covers every test whatever its base class. Tests that run in
+ * a separate process are covered in the parent process only: PHPUnit does not load listeners in
+ * the child.
+ */
+final class WC_Early_Exit_Guard_Listener implements TestListener {
+
+	use TestListenerDefaultImplementation;
+
+	/**
+	 * Exit status for a run the process ended early.
+	 */
+	public const EXIT_STATUS = 1;
+
+	/**
+	 * Suites started and not yet ended. Zero once the outermost suite ends.
+	 *
+	 * @var int
+	 */
+	private $open_suites = 0;
+
+	/**
+	 * The test in progress, or the last test that ended when none is in progress.
+	 *
+	 * @var string|null
+	 */
+	private $current_test = null;
+
+	/**
+	 * Whether a test is in progress.
+	 *
+	 * @var bool
+	 */
+	private $in_test = false;
+
+	/**
+	 * Register the shutdown check. PHPUnit creates listeners with no arguments; tests of the
+	 * listener pass false so their instances do not check anything at shutdown.
+	 *
+	 * @param bool $register_shutdown_check Whether to check the run at shutdown.
+	 */
+	public function __construct( bool $register_shutdown_check = true ) {
+		if ( $register_shutdown_check ) {
+			register_shutdown_function( array( $this, 'check_on_shutdown' ) );
+		}
+	}
+
+	/**
+	 * Count a started suite.
+	 *
+	 * @param TestSuite $suite The suite.
+	 */
+	public function startTestSuite( TestSuite $suite ): void {
+		++$this->open_suites;
+	}
+
+	/**
+	 * Count an ended suite.
+	 *
+	 * @param TestSuite $suite The suite.
+	 */
+	public function endTestSuite( TestSuite $suite ): void {
+		--$this->open_suites;
+	}
+
+	/**
+	 * Remember the test in progress.
+	 *
+	 * @param Test $test The test about to run.
+	 */
+	public function startTest( Test $test ): void {
+		$this->current_test = self::test_name( $test );
+		$this->in_test      = true;
+	}
+
+	/**
+	 * Mark that no test is in progress.
+	 *
+	 * @param Test  $test The test that just ran.
+	 * @param float $time How long it took, in seconds.
+	 */
+	public function endTest( Test $test, float $time ): void {
+		$this->in_test = false;
+	}
+
+	/**
+	 * Explain why the run ended early, or return null when it finished.
+	 *
+	 * @return string|null
+	 */
+	public function early_exit_message(): ?string {
+		if ( $this->open_suites <= 0 ) {
+			return null;
+		}
+
+		if ( $this->in_test ) {
+			$where = sprintf( 'while running %s', $this->current_test );
+		} elseif ( null !== $this->current_test ) {
+			$where = sprintf( 'outside a test, after %s ended (class setup or teardown)', $this->current_test );
+		} else {
+			$where = 'before the first test started (class setup)';
+		}
+
+		return sprintf(
+			'PHPUnit did not finish: the PHP process ended %s. The tests after it did not run. If no fatal error or exception message is shown above, look for an exit() or die() reached by the code under test, often after a redirect, and stop the test from reaching it.',
+			$where
+		);
+	}
+
+	/**
+	 * Report a run that ended early and fail it. A fatal error lands here too, and so does an exception
+	 * that escapes PHPUnit while a suite is running. Both already show their own message, and this exit
+	 * status replaces PHP's 255 or PHPUnit's 2. error_get_last() cannot tell a fatal error from exit():
+	 * WordPress's fatal error handler runs first and can raise a warning that replaces it.
+	 */
+	public function check_on_shutdown(): void {
+		$message = $this->early_exit_message();
+		if ( null === $message ) {
+			return;
+		}
+
+		fwrite( STDERR, "\n\n" . $message . "\n" ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- Shutdown output for the test runner.
+
+		exit( self::EXIT_STATUS ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- An exit status, not output.
+	}
+
+	/**
+	 * Name a test the way PHPUnit reports it.
+	 *
+	 * @param Test $test The test.
+	 * @return string
+	 */
+	private static function test_name( Test $test ): string {
+		return $test instanceof TestCase ? get_class( $test ) . '::' . $test->getName() : get_class( $test );
+	}
+}
diff --git a/plugins/woocommerce/tests/legacy/framework/class-wc-unit-test-case.php b/plugins/woocommerce/tests/legacy/framework/class-wc-unit-test-case.php
index 6351200c1d1..dc174f8809f 100644
--- a/plugins/woocommerce/tests/legacy/framework/class-wc-unit-test-case.php
+++ b/plugins/woocommerce/tests/legacy/framework/class-wc-unit-test-case.php
@@ -6,6 +6,7 @@
  */

 use Automattic\Jetpack\Constants;
+use Automattic\WooCommerce\Internal\Admin\Analytics;
 use Automattic\WooCommerce\Proxies\LegacyProxy;
 use Automattic\WooCommerce\Testing\Tools\CodeHacking\CodeHacker;
 use Automattic\WooCommerce\Utilities\OrderUtil;
@@ -247,6 +248,7 @@ class WC_Unit_Test_Case extends WP_HTTP_TestCase {
 			// WordPress base class that never runs setUp() on this hierarchy.
 			CodeHacker::reset_hacks();

+			$this->reset_analytics_reload_flag();
 			$this->clear_wc_singleton_state();
 		} finally {
 			// The parent teardown must always run: it rolls back the database
@@ -435,6 +437,21 @@ class WC_Unit_Test_Case extends WP_HTTP_TestCase {
 		}
 	}

+	/**
+	 * Clear the flag that makes Analytics redirect and exit() on the next settings save. Changing the
+	 * Analytics option sets it, and it is static, so a later test that saves settings would end the
+	 * PHPUnit process and every test after it would silently not run.
+	 */
+	protected function reset_analytics_reload_flag(): void {
+		Closure::bind(
+			static function (): void {
+				self::$is_updated = false;
+			},
+			null,
+			Analytics::class
+		)();
+	}
+
 	/**
 	 * Fire rest_api_init with only the provided callbacks attached.
 	 *
diff --git a/plugins/woocommerce/tests/php/framework/class-wc-early-exit-guard-listener-test.php b/plugins/woocommerce/tests/php/framework/class-wc-early-exit-guard-listener-test.php
new file mode 100644
index 00000000000..be9c0bc51da
--- /dev/null
+++ b/plugins/woocommerce/tests/php/framework/class-wc-early-exit-guard-listener-test.php
@@ -0,0 +1,169 @@
+<?php
+/**
+ * Tests for the listener that fails a run the PHP process ended early.
+ *
+ * @package WooCommerce\Tests\Framework
+ */
+
+declare( strict_types = 1 );
+
+use PHPUnit\Framework\TestCase;
+use PHPUnit\Framework\TestSuite;
+
+/**
+ * WC_Early_Exit_Guard_Listener_Test class.
+ *
+ * Drives a listener that registers no shutdown check against a separate, never-run test and
+ * suites, so its state is independent of the run this test is part of. The shutdown check itself
+ * exits the process, so it is tested in a separate PHP process.
+ */
+class WC_Early_Exit_Guard_Listener_Test extends WC_Unit_Test_Case {
+
+	/**
+	 * The System Under Test.
+	 *
+	 * @var WC_Early_Exit_Guard_Listener
+	 */
+	private $sut;
+
+	/**
+	 * The test the listener observes.
+	 *
+	 * @var TestCase
+	 */
+	private $observed_test;
+
+	/**
+	 * A suite for the listener to observe.
+	 *
+	 * @var TestSuite
+	 */
+	private $observed_suite;
+
+	/**
+	 * Create the listener and the test and suite it observes.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+
+		$this->sut            = new WC_Early_Exit_Guard_Listener( false );
+		$this->observed_suite = new TestSuite( 'observed' );
+		$this->observed_test  = new class( 'test_noop' ) extends TestCase {
+			/**
+			 * Never run; the listener only needs a test to observe.
+			 */
+			public function test_noop(): void {}
+		};
+	}
+
+	/**
+	 * @testdox A run whose suites all ended has nothing to report.
+	 */
+	public function test_finished_run_reports_nothing(): void {
+		$this->sut->startTestSuite( $this->observed_suite );
+		$this->sut->startTestSuite( $this->observed_suite );
+		$this->sut->startTest( $this->observed_test );
+		$this->sut->endTest( $this->observed_test, 0.0 );
+		$this->sut->endTestSuite( $this->observed_suite );
+		$this->sut->endTestSuite( $this->observed_suite );
+
+		$this->assertNull( $this->sut->early_exit_message() );
+	}
+
+	/**
+	 * @testdox A process that ends during a test names that test.
+	 */
+	public function test_exit_during_a_test_names_the_test(): void {
+		$this->sut->startTestSuite( $this->observed_suite );
+		$this->sut->startTest( $this->observed_test );
+
+		$message = $this->sut->early_exit_message();
+
+		$this->assertNotNull( $message );
+		$this->assertStringContainsString( 'while running ' . get_class( $this->observed_test ) . '::test_noop', $message );
+	}
+
+	/**
+	 * @testdox A process that ends between tests names the last test that ended.
+	 */
+	public function test_exit_between_tests_names_the_previous_test(): void {
+		$this->sut->startTestSuite( $this->observed_suite );
+		$this->sut->startTest( $this->observed_test );
+		$this->sut->endTest( $this->observed_test, 0.0 );
+
+		$message = $this->sut->early_exit_message();
+
+		$this->assertNotNull( $message );
+		$this->assertStringContainsString( 'outside a test, after ' . get_class( $this->observed_test ) . '::test_noop ended', $message );
+	}
+
+	/**
+	 * @testdox A process that ends before the first test says so.
+	 */
+	public function test_exit_before_the_first_test_is_reported(): void {
+		$this->sut->startTestSuite( $this->observed_suite );
+
+		$message = $this->sut->early_exit_message();
+
+		$this->assertNotNull( $message );
+		$this->assertStringContainsString( 'before the first test started', $message );
+	}
+
+	/**
+	 * @testdox A run is unfinished while an outer suite is still open, even after an inner one ended.
+	 */
+	public function test_open_outer_suite_counts_as_unfinished(): void {
+		$this->sut->startTestSuite( $this->observed_suite );
+		$this->sut->startTestSuite( $this->observed_suite );
+		$this->sut->endTestSuite( $this->observed_suite );
+
+		$this->assertNotNull( $this->sut->early_exit_message() );
+	}
+
+	/**
+	 * @testdox A process that ends before the run finishes exits with status 1 and names the test; one that ends after keeps status 0 and prints nothing.
+	 *
+	 * @dataProvider provider_process_endings
+	 *
+	 * @param string $scenario        Whether the process ends during a test or after the run finished.
+	 * @param int    $expected_status Exit status the process should end with.
+	 * @param string $expected_stderr What the process should write to stderr, or a part of it.
+	 */
+	public function test_shutdown_check_sets_the_exit_status( string $scenario, int $expected_status, string $expected_stderr ): void {
+		// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_proc_open -- Runs the PHP CLI on a fixed script with no user input.
+		$process = proc_open(
+			array( PHP_BINARY, '-d', 'display_errors=stderr', dirname( __DIR__ ) . '/helpers/early-exit-guard-runner.php', $scenario ),
+			array(
+				1 => array( 'pipe', 'w' ),
+				2 => array( 'pipe', 'w' ),
+			),
+			$pipes
+		);
+		$this->assertIsResource( $process, 'The PHP CLI should start the runner script.' );
+		$stdout = stream_get_contents( $pipes[1] );
+		$stderr = stream_get_contents( $pipes[2] );
+		fclose( $pipes[1] ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
+		fclose( $pipes[2] ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
+		$status = proc_close( $process );
+
+		$this->assertSame( '', $stdout, 'The runner script should print nothing to stdout.' );
+		$this->assertSame( $expected_status, $status, "The process should exit with status $expected_status. Its stderr: $stderr" );
+		if ( '' === $expected_stderr ) {
+			$this->assertSame( '', $stderr, 'A finished run should print nothing.' );
+		} else {
+			$this->assertStringContainsString( $expected_stderr, $stderr );
+		}
+	}
+
+	/**
+	 * Ways the runner script's process can end.
+	 *
+	 * @return array<string, array{0: string, 1: int, 2: string}>
+	 */
+	public function provider_process_endings(): array {
+		return array(
+			'exit during a test' => array( 'unfinished', 1, 'while running WC_Early_Exit_Guard_Runner_Test::test_noop' ),
+			'exit after the run' => array( 'finished', 0, '' ),
+		);
+	}
+}
diff --git a/plugins/woocommerce/tests/php/helpers/early-exit-guard-runner.php b/plugins/woocommerce/tests/php/helpers/early-exit-guard-runner.php
new file mode 100644
index 00000000000..f72476311a7
--- /dev/null
+++ b/plugins/woocommerce/tests/php/helpers/early-exit-guard-runner.php
@@ -0,0 +1,43 @@
+<?php
+/**
+ * Runs WC_Early_Exit_Guard_Listener in its own process, with its shutdown check registered, and ends that process with exit( 0 ).
+ *
+ * The check exits the process, so WC_Early_Exit_Guard_Listener_Test runs this script with the PHP CLI and reads its
+ * exit status and stderr. Usage: `php early-exit-guard-runner.php <scenario>`, where the scenario is `unfinished`
+ * (the process ends while a test is running) or `finished` (the suite and test end first).
+ *
+ * @package WooCommerce\Tests
+ */
+
+declare( strict_types = 1 );
+
+use PHPUnit\Framework\TestCase;
+use PHPUnit\Framework\TestSuite;
+
+require dirname( __DIR__, 3 ) . '/vendor/autoload.php';
+require dirname( __DIR__, 2 ) . '/legacy/framework/class-wc-early-exit-guard-listener.php';
+
+/**
+ * A test for the listener to observe. Never run.
+ */
+final class WC_Early_Exit_Guard_Runner_Test extends TestCase {
+
+	/**
+	 * Never run; the listener only needs a test to observe.
+	 */
+	public function test_noop(): void {}
+}
+
+$wc_listener = new WC_Early_Exit_Guard_Listener();
+$wc_suite    = new TestSuite( 'runner' );
+$wc_test     = new WC_Early_Exit_Guard_Runner_Test( 'test_noop' );
+
+$wc_listener->startTestSuite( $wc_suite );
+$wc_listener->startTest( $wc_test );
+
+if ( 'finished' === ( $argv[1] ?? '' ) ) {
+	$wc_listener->endTest( $wc_test, 0.0 );
+	$wc_listener->endTestSuite( $wc_suite );
+}
+
+exit( 0 );
diff --git a/plugins/woocommerce/tests/php/src/UnitTestCaseTearDownTest.php b/plugins/woocommerce/tests/php/src/UnitTestCaseTearDownTest.php
index 3a6be9b253a..89020bdbfe1 100644
--- a/plugins/woocommerce/tests/php/src/UnitTestCaseTearDownTest.php
+++ b/plugins/woocommerce/tests/php/src/UnitTestCaseTearDownTest.php
@@ -4,18 +4,20 @@ declare( strict_types = 1 );
 namespace Automattic\WooCommerce\Tests;

 use Automattic\Jetpack\Constants;
+use Automattic\WooCommerce\Internal\Admin\Analytics;

 /**
  * Tests the state cleanup in WC_Unit_Test_Case: the WC() singleton, $_SERVER, Constants and
- * block type state that survives the parent teardown, and the attribute taxonomies the
- * rollback leaves registered.
+ * block type state that survives the parent teardown, the Analytics reload flag, and the
+ * attribute taxonomies the rollback leaves registered.
  *
  * This drives the cleanup methods directly rather than dirtying state in one test and
  * asserting it is gone in the next. A pair like that only holds under declaration order: run
  * the suite with --order-by=random or reverse and the assertion half runs first, passes
  * against state nothing has dirtied yet, and stops covering anything without ever going red.
- * The one exception stages a leftover taxonomy before parent::setUp() in the same test, so it
- * covers the setup call itself.
+ * Two tests cover the base class's own calls within a single test instead: one stages a
+ * leftover taxonomy before parent::setUp(), and one has tearDown() check the Analytics reload
+ * flag after the base teardown ran.
  */
 class UnitTestCaseTearDownTest extends \WC_Unit_Test_Case {

@@ -45,7 +47,14 @@ class UnitTestCaseTearDownTest extends \WC_Unit_Test_Case {
 	}

 	/**
-	 * Drop the staged leftover in case setup did not.
+	 * Whether tearDown() checks that the base teardown cleared the Analytics reload flag.
+	 *
+	 * @var bool
+	 */
+	private $check_analytics_reload_flag_after_teardown = false;
+
+	/**
+	 * Drop the staged leftover in case setup did not, and run the Analytics flag check when a test asked for it.
 	 */
 	public function tearDown(): void {
 		global $wc_product_attributes;
@@ -54,6 +63,13 @@ class UnitTestCaseTearDownTest extends \WC_Unit_Test_Case {
 		unset( $wc_product_attributes[ self::STALE_TAXONOMY ] );

 		parent::tearDown();
+
+		if ( $this->check_analytics_reload_flag_after_teardown ) {
+			$flag_after_teardown = self::analytics_reload_flag();
+			// Clear it before asserting, so a failure here does not end the process at a later settings save.
+			$this->reset_analytics_reload_flag();
+			$this->assertFalse( $flag_after_teardown, 'The base teardown should clear the Analytics reload flag.' );
+		}
 	}

 	/**
@@ -224,6 +240,33 @@ class UnitTestCaseTearDownTest extends \WC_Unit_Test_Case {
 		$this->assertEqualsCanonicalizing( array_keys( $types_at_setup ), array_keys( $registry->get_all_registered() ), 'The registered names should match setUp().' );
 	}

+	/**
+	 * @testdox The base teardown clears the flag that makes the next settings save redirect and exit, set by changing the Analytics option.
+	 */
+	public function test_teardown_clears_the_analytics_reload_flag_an_option_change_sets(): void {
+		update_option( Analytics::TOGGLE_OPTION_NAME, 'yes' );
+		update_option( Analytics::TOGGLE_OPTION_NAME, 'no' );
+		$this->assertTrue( self::analytics_reload_flag(), 'Changing the Analytics option should set the flag, or this test covers nothing.' );
+
+		// The assertion runs in this class's tearDown(), after WC_Unit_Test_Case::tearDown().
+		$this->check_analytics_reload_flag_after_teardown = true;
+	}
+
+	/**
+	 * Read the static flag Analytics::maybe_reload_page() checks before it redirects and exits.
+	 *
+	 * @return bool
+	 */
+	private static function analytics_reload_flag(): bool {
+		return \Closure::bind(
+			static function (): bool {
+				return self::$is_updated;
+			},
+			null,
+			Analytics::class
+		)();
+	}
+
 	/**
 	 * @testdox The restores leave state alone for a test whose setUp() never reached the base class.
 	 */