> $steps Ordered steps from Optimize_Plan::build(). * @param array $baseline Sample taken before anything changed. * @param array{ * apply:callable, * revert:callable, * purge:callable, * sample:callable, * now?:callable * } $io Injected side effects. * @param int $budget_seconds Wall-clock cap; 0 = uncapped. * @return array{applied:array>,reverted:array>,skipped:array>} */ public static function run( array $steps, array $baseline, array $io, int $budget_seconds = 0 ): array { $apply = $io['apply']; $revert = $io['revert']; $purge = $io['purge']; $sample = $io['sample']; $now = $io['now'] ?? static function () { return time(); }; $started = (int) $now(); $applied = array(); $reverted = array(); $skipped = array(); foreach ( $steps as $i => $step ) { // Budget check BEFORE applying, never mid-step: stopping between // "wrote the setting" and "verified it" is the one state this // loop must never end in. if ( $budget_seconds > 0 && ( (int) $now() - $started ) >= $budget_seconds ) { foreach ( array_slice( $steps, $i ) as $rest ) { $skipped[] = array( 'id' => (string) $rest['id'], 'why' => __( 'Ran out of time before this step.', 'xspeed' ), ); } break; } $before = self::snapshot_of( $step ); $apply( $step ); $purge(); $current = $sample(); // A sample we could not take is NOT a pass. Treating an // unreachable site as "probably fine" is how an autopilot leaves // a site broken and reports success. if ( ! is_array( $current ) ) { $revert( $step, $before ); $purge(); $reverted[] = array( 'id' => (string) $step['id'], 'why' => __( 'Could not load the page to check it, so the change was undone.', 'xspeed' ), ); continue; } $check = Optimize_Verifier::compare( $baseline, $current ); if ( ! $check['ok'] ) { $revert( $step, $before ); $purge(); $reverted[] = array( 'id' => (string) $step['id'], 'why' => implode( ' ', $check['failures'] ), ); continue; } $applied[] = array( 'id' => (string) $step['id'], 'change' => (string) $step['label'], 'verified' => true, ); } return array( 'applied' => $applied, 'reverted' => $reverted, 'skipped' => $skipped, ); } /** * The values to put back if this step has to be undone. * * Every step in the catalog turns something ON, so the inverse is the * same keys set to false. Kept as its own method so a future step whose * inverse is not simply `false` has one obvious place to say so, rather * than the revert path quietly writing the wrong thing. * * @param array $step Step definition. * @return array */ private static function snapshot_of( array $step ): array { $out = array(); foreach ( (array) $step['values'] as $key => $value ) { $out[ $key ] = is_bool( $value ) ? ! $value : false; } return $out; } }