Page MenuHomeWickedGov Phorge

UnitTestingHelper.php
No OneTemporary

Size
8 KB
Referenced Files
None
Subscribers
None

UnitTestingHelper.php

<?php
/**
* @license GPL-2.0-or-later
* @file
*/
declare( strict_types=1 );
namespace Wikimedia\Stats;
use InvalidArgumentException;
use MediaWiki\Logger\LoggerFactory;
use OutOfBoundsException;
use OutOfRangeException;
use Psr\Log\LoggerInterface;
use Wikimedia\Stats\Emitters\NullEmitter;
use Wikimedia\Stats\Formatters\DogStatsdFormatter;
use Wikimedia\Stats\Metrics\MetricInterface;
/**
* A helper class for testing metrics in Unit Tests.
*
* @author Cole White
* @since 1.44
*/
class UnitTestingHelper {
public const EQUALS = '=';
public const NOT_EQUALS = '!=';
public const EQUALS_REGEX = '=~';
public const NOT_EQUALS_REGEX = '!~';
private StatsCache $cache;
private StatsFactory $factory;
private string $component = '';
private LoggerInterface $logger;
/**
* @internal Use StatsFactory::newUnitTestingHelper() instead.
*/
public function __construct() {
$this->cache = new StatsCache();
$this->logger = LoggerFactory::getInstance( 'Stats' );
// Disable StatsFactory::flush() and its StatsCache::clear() calls, because automatic
// flushes would otherwise delete metrics before we can assert them, e.g. after whenever
// a subject under test commits a database transaction or when a tested Maintenance script
// prints output.
$this->factory = new class( $this->cache, new NullEmitter(), $this->logger ) extends StatsFactory {
public function flush(): void {
}
};
}
/**
* Set a component on the underlying StatsFactory
*/
public function withComponent( string $component ): self {
$this->factory = $this->factory->withComponent( $component );
$this->component = $component;
return $this;
}
/**
* Get the underlying StatsFactory, to pass to your subject under test.
*/
public function getStatsFactory(): StatsFactory {
return $this->factory;
}
/**
* Get all samples in dogstatsd format
*
* @return string[]
*/
public function getAllFormatted(): array {
$output = [];
$dogFmt = new DogStatsdFormatter();
foreach ( $this->cache->getAllMetrics() as $metric ) {
$output = array_merge( $output, $dogFmt->getFormattedSamples( 'mediawiki', $metric ) );
}
return $output;
}
/**
* Get all samples in dogstatsd format and clear the buffer
*
* @return string[]
*/
public function consumeAllFormatted(): array {
$output = $this->getAllFormatted();
$this->cache->clear();
return $output;
}
/**
* How many samples were observed for a given metric.
*
* Example:
* ```php
* $unitTestingHelper->count( 'the_metric_name{fooLabel="bar"}' );
* ```
*/
public function count( string $selector ): int {
return count( $this->getFilteredSamples( $selector ) );
}
/**
* The last recorded sample value for a given metric.
*
* Example:
* ```php
* $unitTestingHelper->last( 'the_metric_name{fooLabel="bar"}' );
* ```
*/
public function last( string $selector ): float {
$samples = $this->getFilteredSamples( $selector );
return $samples[array_key_last( $samples )]->getValue();
}
/**
* The sum of all sample values for a given metric.
*
* Example:
* ```php
* $unitTestingHelper->sum( 'the_metric_name{fooLabel="bar"}' );
* ```
*/
public function sum( string $selector ): float {
$output = 0;
foreach ( $this->getFilteredSamples( $selector ) as $sample ) {
$output += $sample->getValue();
}
return $output;
}
/**
* The max of all sample values for a given metric.
*
* Example:
* ```php
* $unitTestingHelper->max( 'the_metric_name{fooLabel="bar"}' );
* ```
*/
public function max( string $selector ): float {
$output = 0;
foreach ( $this->getFilteredSamples( $selector ) as $sample ) {
if ( $sample->getValue() > $output ) {
$output = $sample->getValue();
}
}
return $output;
}
/**
* The median of all sample values for a given metric.
*
* Example:
* ```php
* $unitTestingHelper->median( 'the_metric_name{fooLabel="bar"}' );
* ```
*/
public function median( string $selector ): float {
return $this->sum( $selector ) / $this->count( $selector );
}
/**
* The min of all sample values for a given metric.
*
* Example:
* ```php
* $unitTestingHelper->min( 'the_metric_name{fooLabel="bar"}' );
* ```
*/
public function min( string $selector ): float {
$output = INF;
foreach ( $this->getFilteredSamples( $selector ) as $sample ) {
if ( $sample->getValue() < $output ) {
$output = $sample->getValue();
}
}
return $output;
}
private function getMetricFromSelector( string $selector ): MetricInterface {
$key = StatsCache::cacheKey( $this->component, $this->getName( $selector ) );
$metric = $this->cache->getAllMetrics()[$key] ?? null;
if ( $metric === null ) {
$actual = 'Actual metrics:';
foreach ( $this->cache->getAllMetrics() as $metric ) {
$name = $metric->getName();
$sampleCount = $metric->getSampleCount();
$actual .= "\n $name ($sampleCount samples)";
}
throw new OutOfBoundsException( "Could not find metric with key '$key'\n\n$actual" );
}
return $metric;
}
private function getFilteredSamples( string $selector ): array {
$metric = $this->getMetricFromSelector( $selector );
$filters = $this->getFilters( $selector );
$labelKeys = $metric->getLabelKeys();
$left = $metric->getSamples();
foreach ( $filters as $filter ) {
$right = [];
[ $key, $value, $operator ] = $filter;
$labelPosition = array_search( $key, $labelKeys );
foreach ( $left as $sample ) {
if ( $this->matches( $sample->getLabelValues()[$labelPosition], $value, $operator ) ) {
$right[] = $sample;
}
}
$left = $right;
}
if ( !$left ) {
$dogFmt = new DogStatsdFormatter();
$actual = 'Actual samples:'
. "\n" . implode( "\n", $dogFmt->getFormattedSamples( 'mediawiki', $metric ) );
throw new OutOfRangeException( "Metric selector '$selector' matched zero samples.\n\n$actual" );
}
return $left;
}
private function getName( string $selector ): string {
$selector = preg_replace( '/\'/', '"', $selector );
if ( str_contains( $selector, '{' ) ) {
$selector = substr( $selector, 0, strpos( $selector, '{' ) );
}
if ( !$selector ) {
throw new InvalidArgumentException( 'Selector cannot be empty.' );
}
return $selector;
}
private function getFilters( string $selector ): array {
$selector = preg_replace( '/\'/', '"', $selector );
if ( !str_contains( $selector, '{' ) && !str_contains( $selector, ',' ) ) {
return [];
}
$output = [];
$filters = substr( $selector, strpos( $selector, '{' ) + 1, -1 );
$filters = explode( ',', $filters );
foreach ( $filters as $filter ) {
$output[] = $this->getFilterComponents( $filter );
}
return $output;
}
private function getFilterComponents( string $filter ): array {
$key = null;
$value = null;
$operator = null;
if ( str_contains( $filter, self::EQUALS ) ) {
[ $key, $value ] = explode( self::EQUALS, $filter );
$operator = self::EQUALS;
}
if ( str_contains( $filter, self::EQUALS_REGEX ) ) {
[ $key, $value ] = explode( self::EQUALS_REGEX, $filter );
$operator = self::EQUALS_REGEX;
}
if ( str_contains( $filter, self::NOT_EQUALS_REGEX ) ) {
[ $key, $value ] = explode( self::NOT_EQUALS_REGEX, $filter );
$operator = self::NOT_EQUALS_REGEX;
}
if ( str_contains( $filter, self::NOT_EQUALS ) ) {
[ $key, $value ] = explode( self::NOT_EQUALS, $filter );
$operator = self::NOT_EQUALS;
}
if ( !$key || !$value || !$operator ) {
$this->logger->debug(
'Got filter expression: {' . $filter . '}',
[ 'key' => $key, 'value' => $value, 'operator' => $operator ]
);
throw new InvalidArgumentException( "Filter components cannot be empty." );
}
$key = preg_replace( '/[^a-zA-Z0-9_]+/', '', $key );
$value = preg_replace( '/[^a-zA-Z0-9_]+/', '', $value );
return [ $key, $value, $operator ];
}
/**
* Return the boolean result of stored and expected values according to the operator.
*/
private function matches( string $stored, string $expected, string $operator ): bool {
if ( $operator === self::NOT_EQUALS ) {
return $stored != $expected;
}
if ( $operator === self::EQUALS_REGEX ) {
return (bool)preg_match( "/$expected/", $stored );
}
if ( $operator === self::NOT_EQUALS_REGEX ) {
return !preg_match( "/$expected/", $stored );
}
return $stored === $expected;
}
}

File Metadata

Mime Type
text/x-php
Expires
Aug 19 2026, 20:50 (7 w, 5 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
f1/07/3f1b140110c5a58f916567972fba
Default Alt Text
UnitTestingHelper.php (8 KB)

Event Timeline