<?php
// Standalone check for run_paged_parser: php -d zend.assertions=1 tests/paged-parser.php
// No WordPress, no framework. It fails if a slow page can no longer be interrupted.

// Stubs for the handful of WP helpers run_paged_parser touches.
function sanitize_key( $k ) { return strtolower( preg_replace( '/[^a-z0-9_\-]/i', '', $k ) ); }
function __( $s, $d = null ) { return $s; }
function is_wp_error( $t ) { return false; }
class FakeWpdb { public $last_error = ''; public $num_queries = 0; }
$wpdb = new FakeWpdb();
require __DIR__ . '/../classes/core.php';

class Probe extends Meow_WPMC_Core {
	public $sizes = array();
	public $slow_from = PHP_INT_MAX; // page index after which every fetch is expensive
	function timeout_check() {}
	function write_references() {}
	public function parser_budget_check( $s, $l = 'Parser' ) {}
}

// Returns the page sizes the loop asked for, and every row it handed to the parser.
function drive( $total, $slow_from = PHP_INT_MAX, $declared = 100 ) {
	$p = ( new ReflectionClass( 'Probe' ) )->newInstanceWithoutConstructor();
	$p->slow_from = $slow_from;
	$seen = array();
	$p->run_paged_parser( 'probe', function( $offset, $limit ) use ( $total, $p ) {
		if ( count( $p->sizes ) >= $p->slow_from ) usleep( 2100000 );
		$p->sizes[] = $limit;
		$rows = array();
		for ( $i = $offset; $i < min( $total, $offset + $limit ); $i++ ) $rows[] = $i;
		return $rows;
	}, function( $rows ) use ( &$seen ) { foreach ( $rows as $r ) $seen[] = $r; }, $declared );
	return array( $p->sizes, $seen );
}

// Every row is visited exactly once, whatever the page size does.
list( $sizes, $seen ) = drive( 253 );
assert( $seen === range( 0, 252 ), 'all rows seen once, in order' );

// The first page is a cheap probe, so a parser whose rows cost seconds cannot spend the
// whole request before the loop ever gets to look at the clock.
assert( $sizes[0] === 5, 'first page is a probe, not the declared 100: ' . $sizes[0] );

// Cheap pages grow back up to the declared page size, and never past it.
assert( max( $sizes ) === 100, 'grows to the declared cap: ' . max( $sizes ) );
assert( $sizes === array( 5, 10, 20, 40, 80, 100 ), 'doubles: ' . implode( ',', $sizes ) );

// An expensive page halves the next one instead of repeating the cost.
list( $sizes, $seen ) = drive( 120, 2 );
assert( $sizes[2] === 20 && $sizes[3] === 10, 'slow page halves the next: ' . implode( ',', $sizes ) );
assert( count( $seen ) === 120, 'still complete after shrinking' );

// Shrinking has a floor, so the loop cannot degenerate into one row per query.
assert( min( $sizes ) === 5, 'never shrinks below the probe size: ' . min( $sizes ) );

// A parser that declares a small page never gets a bigger one.
list( $sizes, ) = drive( 40, PHP_INT_MAX, 10 );
assert( max( $sizes ) === 10, 'declared size is a cap, not a target' );

echo "ok\n";
