Remove explicit DB access

* Uses BagOfStuff caching instead of custom-made DB cache
* By configuring the BagOfStuff cache in a way that
  it writes to the database no performance implications are expected
* For WMF-use this should have no effect since restbase
  is used to cache stuff
* Replaces Ib2c216f54e6817ee2c3be0355ba72bd4769ba6ea

Bug: T349442
Change-Id: I1ce8ad9cf4c1a9ae71f447e4e067b39ee2601640
This commit is contained in:
Moritz Schubotz (physikerwelt) 2023-11-19 20:23:09 +01:00 committed by Physikerwelt
parent 7f8a359a84
commit 9236575a1a
23 changed files with 192 additions and 592 deletions

View file

@ -38,7 +38,8 @@ return [
), ),
Math::getMathConfig( $services ), Math::getMathConfig( $services ),
$services->getUserOptionsLookup(), $services->getUserOptionsLookup(),
LoggerFactory::getInstance( 'Math' ) LoggerFactory::getInstance( 'Math' ),
$services->getMainWANObjectCache()
); );
}, },
'Math.WikibaseConnector' => static function ( MediaWikiServices $services ): MathWikibaseConnector { 'Math.WikibaseConnector' => static function ( MediaWikiServices $services ): MathWikibaseConnector {

View file

@ -1,59 +0,0 @@
[
{
"name": "mathlatexml",
"columns": [
{
"name": "math_inputhash",
"type": "binary",
"options": {
"notnull": true,
"length": 16,
"fixed": false
}
},
{
"name": "math_inputtex",
"type": "text",
"options": {
"notnull": true,
"length": 65535
}
},
{
"name": "math_tex",
"type": "text",
"options": {
"notnull": false,
"length": 65535
}
},
{
"name": "math_mathml",
"type": "text",
"options": {
"notnull": false,
"length": 16777215
}
},
{
"name": "math_svg",
"type": "text",
"options": {
"notnull": false,
"length": 65535
}
},
{
"name": "math_style",
"type": "mwtinyint",
"options": {
"notnull": false
}
}
],
"indexes": [],
"pk": [
"math_inputhash"
]
}
]

View file

@ -1,74 +0,0 @@
[
{
"name": "mathoid",
"columns": [
{
"name": "math_inputhash",
"type": "binary",
"options": {
"notnull": true,
"length": 16,
"fixed": false
}
},
{
"name": "math_input",
"type": "text",
"options": {
"notnull": true,
"length": 65535
}
},
{
"name": "math_tex",
"type": "text",
"options": {
"notnull": false,
"length": 65535
}
},
{
"name": "math_mathml",
"type": "text",
"options": {
"notnull": false,
"length": 65535
}
},
{
"name": "math_svg",
"type": "text",
"options": {
"notnull": false,
"length": 65535
}
},
{
"name": "math_style",
"type": "mwtinyint",
"options": {
"notnull": false
}
},
{
"name": "math_input_type",
"type": "mwtinyint",
"options": {
"notnull": false
}
},
{
"name": "math_png",
"type": "blob",
"options": {
"notnull": false,
"length": 16777215
}
}
],
"indexes": [],
"pk": [
"math_inputhash"
]
}
]

View file

@ -1,13 +0,0 @@
-- This file is automatically generated using maintenance/generateSchemaSql.php.
-- Source: Math/sql/mathlatexml.json
-- Do not modify this file directly.
-- See https://www.mediawiki.org/wiki/Manual:Schema_changes
CREATE TABLE /*_*/mathlatexml (
math_inputhash VARBINARY(16) NOT NULL,
math_inputtex TEXT NOT NULL,
math_tex TEXT DEFAULT NULL,
math_mathml MEDIUMTEXT DEFAULT NULL,
math_svg TEXT DEFAULT NULL,
math_style TINYINT DEFAULT NULL,
PRIMARY KEY(math_inputhash)
) /*$wgDBTableOptions*/;

View file

@ -1,15 +0,0 @@
-- This file is automatically generated using maintenance/generateSchemaSql.php.
-- Source: Math/sql/mathoid.json
-- Do not modify this file directly.
-- See https://www.mediawiki.org/wiki/Manual:Schema_changes
CREATE TABLE /*_*/mathoid (
math_inputhash VARBINARY(16) NOT NULL,
math_input TEXT NOT NULL,
math_tex TEXT DEFAULT NULL,
math_mathml TEXT DEFAULT NULL,
math_svg TEXT DEFAULT NULL,
math_style TINYINT DEFAULT NULL,
math_input_type TINYINT DEFAULT NULL,
math_png MEDIUMBLOB DEFAULT NULL,
PRIMARY KEY(math_inputhash)
) /*$wgDBTableOptions*/;

View file

@ -1 +0,0 @@
ALTER TABLE /*_*/mathoid ADD math_png mediumblob;

View file

@ -1,13 +0,0 @@
-- This file is automatically generated using maintenance/generateSchemaSql.php.
-- Source: Math/sql/mathlatexml.json
-- Do not modify this file directly.
-- See https://www.mediawiki.org/wiki/Manual:Schema_changes
CREATE TABLE mathlatexml (
math_inputhash TEXT NOT NULL,
math_inputtex TEXT NOT NULL,
math_tex TEXT DEFAULT NULL,
math_mathml TEXT DEFAULT NULL,
math_svg TEXT DEFAULT NULL,
math_style SMALLINT DEFAULT NULL,
PRIMARY KEY(math_inputhash)
);

View file

@ -1,15 +0,0 @@
-- This file is automatically generated using maintenance/generateSchemaSql.php.
-- Source: Math/sql/mathoid.json
-- Do not modify this file directly.
-- See https://www.mediawiki.org/wiki/Manual:Schema_changes
CREATE TABLE mathoid (
math_inputhash TEXT NOT NULL,
math_input TEXT NOT NULL,
math_tex TEXT DEFAULT NULL,
math_mathml TEXT DEFAULT NULL,
math_svg TEXT DEFAULT NULL,
math_style SMALLINT DEFAULT NULL,
math_input_type SMALLINT DEFAULT NULL,
math_png TEXT DEFAULT NULL,
PRIMARY KEY(math_inputhash)
);

View file

@ -1,13 +0,0 @@
-- This file is automatically generated using maintenance/generateSchemaSql.php.
-- Source: Math/sql/mathlatexml.json
-- Do not modify this file directly.
-- See https://www.mediawiki.org/wiki/Manual:Schema_changes
CREATE TABLE /*_*/mathlatexml (
math_inputhash BLOB NOT NULL,
math_inputtex CLOB NOT NULL,
math_tex CLOB DEFAULT NULL,
math_mathml CLOB DEFAULT NULL,
math_svg CLOB DEFAULT NULL,
math_style SMALLINT DEFAULT NULL,
PRIMARY KEY(math_inputhash)
);

View file

@ -1,15 +0,0 @@
-- This file is automatically generated using maintenance/generateSchemaSql.php.
-- Source: Math/sql/mathoid.json
-- Do not modify this file directly.
-- See https://www.mediawiki.org/wiki/Manual:Schema_changes
CREATE TABLE /*_*/mathoid (
math_inputhash BLOB NOT NULL,
math_input CLOB NOT NULL,
math_tex CLOB DEFAULT NULL,
math_mathml CLOB DEFAULT NULL,
math_svg CLOB DEFAULT NULL,
math_style SMALLINT DEFAULT NULL,
math_input_type SMALLINT DEFAULT NULL,
math_png BLOB DEFAULT NULL,
PRIMARY KEY(math_inputhash)
);

View file

@ -3,7 +3,6 @@
namespace MediaWiki\Extension\Math\HookHandlers; namespace MediaWiki\Extension\Math\HookHandlers;
use DatabaseUpdater; use DatabaseUpdater;
use LogicException;
use MediaWiki\Installer\Hook\LoadExtensionSchemaUpdatesHook; use MediaWiki\Installer\Hook\LoadExtensionSchemaUpdatesHook;
/** /**
@ -19,22 +18,11 @@ class SchemaHooksHandler implements LoadExtensionSchemaUpdatesHook {
public function onLoadExtensionSchemaUpdates( $updater ) { public function onLoadExtensionSchemaUpdates( $updater ) {
$type = $updater->getDB()->getType(); $type = $updater->getDB()->getType();
if ( !in_array( $type, [ 'mysql', 'sqlite', 'postgres' ], true ) ) { if ( !in_array( $type, [ 'mysql', 'sqlite', 'postgres' ], true ) ) {
throw new LogicException( "Math extension does not currently support $type database." ); return;
} }
foreach ( [ 'mathoid', 'mathlatexml' ] as $mode ) { foreach ( [ 'mathoid', 'mathlatexml' ] as $mode ) {
$updater->addExtensionTable( $updater->dropExtensionTable( $mode );
$mode,
__DIR__ . "/../../sql/$type/$mode.sql"
);
}
if ( $type === 'mysql' ) {
$updater->addExtensionField(
'mathoid',
'math_png',
__DIR__ . '/../../sql/' . $type . '/patch-mathoid.add_png.sql'
);
} }
} }
} }

View file

@ -21,9 +21,9 @@ class MathLaTeXML extends MathMathML {
/** @var string settings for LaTeXML daemon */ /** @var string settings for LaTeXML daemon */
private $LaTeXMLSettings = ''; private $LaTeXMLSettings = '';
public function __construct( $tex = '', $params = [] ) { public function __construct( $tex = '', $params = [], $cache = null ) {
global $wgMathLaTeXMLUrl; global $wgMathLaTeXMLUrl;
parent::__construct( $tex, $params ); parent::__construct( $tex, $params, $cache );
$this->host = $wgMathLaTeXMLUrl; $this->host = $wgMathLaTeXMLUrl;
$this->setMode( MathConfig::MODE_LATEXML ); $this->setMode( MathConfig::MODE_LATEXML );
} }

View file

@ -51,9 +51,9 @@ class MathMathML extends MathRenderer {
/** @var string|null */ /** @var string|null */
private $mathoidStyle; private $mathoidStyle;
public function __construct( string $tex = '', array $params = [] ) { public function __construct( string $tex = '', array $params = [], $cache = null ) {
global $wgMathMathMLUrl; global $wgMathMathMLUrl;
parent::__construct( $tex, $params ); parent::__construct( $tex, $params, $cache );
$this->setMode( MathConfig::MODE_MATHML ); $this->setMode( MathConfig::MODE_MATHML );
$this->host = $wgMathMathMLUrl; $this->host = $wgMathMathMLUrl;
if ( isset( $params['type'] ) ) { if ( isset( $params['type'] ) ) {
@ -354,7 +354,7 @@ class MathMathML extends MathRenderer {
return $this->svgPath; return $this->svgPath;
} }
return SpecialPage::getTitleFor( 'MathShowImage' )->getLocalURL( [ return SpecialPage::getTitleFor( 'MathShowImage' )->getLocalURL( [
'hash' => $this->getMd5(), 'hash' => $this->getInputHash(),
'mode' => $this->getMode(), 'mode' => $this->getMode(),
'noRender' => $noRender 'noRender' => $noRender
] ]
@ -493,7 +493,7 @@ class MathMathML extends MathRenderer {
protected function dbOutArray() { protected function dbOutArray() {
$out = parent::dbOutArray(); $out = parent::dbOutArray();
if ( $this->getMathTableName() == 'mathoid' ) { if ( $this->getMathTableName() === 'mathoid' ) {
$out['math_input'] = $out['math_inputtex']; $out['math_input'] = $out['math_inputtex'];
unset( $out['math_inputtex'] ); unset( $out['math_inputtex'] );
} }
@ -502,20 +502,20 @@ class MathMathML extends MathRenderer {
protected function dbInArray() { protected function dbInArray() {
$out = parent::dbInArray(); $out = parent::dbInArray();
if ( $this->getMathTableName() == 'mathoid' ) { if ( $this->getMathTableName() === 'mathoid' ) {
$out = array_diff( $out, [ 'math_inputtex' ] ); $out = array_diff( $out, [ 'math_inputtex' ] );
$out[] = 'math_input'; $out[] = 'math_input';
} }
return $out; return $out;
} }
protected function initializeFromDatabaseRow( $rpage ) { public function initializeFromCache( $rpage ) {
// mathoid allows different input formats // mathoid allows different input formats
// therefore the column name math_inputtex was changed to math_input // therefore the column name math_inputtex was changed to math_input
if ( $this->getMathTableName() == 'mathoid' && !empty( $rpage->math_input ) ) { if ( $this->getMathTableName() === 'mathoid' && isset( $rpage['math_input'] ) ) {
$this->userInputTex = $rpage->math_input; $this->userInputTex = $rpage['math_input'];
} }
parent::initializeFromDatabaseRow( $rpage ); parent::initializeFromCache( $rpage );
} }
/** /**

View file

@ -48,7 +48,7 @@ class MathMathMLCli extends MathMathML {
*/ */
private function initializeFromCliResponse( $res ) { private function initializeFromCliResponse( $res ) {
global $wgMathoidCli; global $wgMathoidCli;
if ( !property_exists( $res, $this->getMd5() ) ) { if ( !property_exists( $res, $this->getInputHash() ) ) {
$this->lastError = $this->lastError =
$this->getError( 'math_mathoid_error', 'cli', $this->getError( 'math_mathoid_error', 'cli',
var_export( get_object_vars( $res ), true ) ); var_export( get_object_vars( $res ), true ) );
@ -58,7 +58,7 @@ class MathMathMLCli extends MathMathML {
$this->lastError = $this->getError( 'math_empty_tex' ); $this->lastError = $this->getError( 'math_empty_tex' );
return false; return false;
} }
$response = $res->{$this->getMd5()}; $response = $res->{$this->getInputHash()};
if ( !$response->success ) { if ( !$response->success ) {
$this->lastError = $this->renderError( $response ); $this->lastError = $this->renderError( $response );
return false; return false;
@ -103,7 +103,7 @@ class MathMathMLCli extends MathMathML {
'query' => [ 'query' => [
'q' => $this->getTex(), 'q' => $this->getTex(),
'type' => $this->getInputType(), 'type' => $this->getInputType(),
'hash' => $this->getMd5(), 'hash' => $this->getInputHash(),
], ],
]; ];
} }

View file

@ -21,8 +21,8 @@ use StatusValue;
class MathNativeMML extends MathMathML { class MathNativeMML extends MathMathML {
private LocalChecker $checker; private LocalChecker $checker;
public function __construct( $tex = '', $params = [] ) { public function __construct( $tex = '', $params = [], $cache = null ) {
parent::__construct( $tex, $params ); parent::__construct( $tex, $params, $cache );
$this->setMode( MathConfig::MODE_NATIVE_MML ); $this->setMode( MathConfig::MODE_NATIVE_MML );
} }
@ -65,7 +65,7 @@ class MathNativeMML extends MathMathML {
return $this->getMathml(); return $this->getMathml();
} }
public function readFromDatabase() { public function readFromCache(): bool {
return false; return false;
} }

View file

@ -11,7 +11,6 @@
namespace MediaWiki\Extension\Math; namespace MediaWiki\Extension\Math;
use MediaWiki\Deferred\DeferredUpdates;
use MediaWiki\Extension\Math\InputCheck\BaseChecker; use MediaWiki\Extension\Math\InputCheck\BaseChecker;
use MediaWiki\Logger\LoggerFactory; use MediaWiki\Logger\LoggerFactory;
use MediaWiki\MediaWikiServices; use MediaWiki\MediaWikiServices;
@ -20,14 +19,14 @@ use Message;
use Parser; use Parser;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use RequestContext; use RequestContext;
use stdClass;
use StringUtils; use StringUtils;
use WANObjectCache;
/** /**
* Abstract base class with static methods for rendering the <math> tags using * Abstract base class with static methods for rendering the <math> tags using
* different technologies. These static methods create a new instance of the * different technologies. These static methods create a new instance of the
* extending classes and render the math tags based on the mode setting of the user. * extending classes and render the math tags based on the mode setting of the user.
* Furthermore this class handles the caching of the rendered output. * Furthermore, this class handles the caching of the rendered output.
* *
* @author Tomasz Wegrzanowski * @author Tomasz Wegrzanowski
* @author Brion Vibber * @author Brion Vibber
@ -58,13 +57,11 @@ abstract class MathRenderer {
/** @var bool has the mathematical content changed */ /** @var bool has the mathematical content changed */
protected $changed = false; protected $changed = false;
/** @var bool is there a database entry for the mathematical content */ /** @var bool is there a database entry for the mathematical content */
protected $storedInDatabase = null; protected $storedInCache = null;
/** @var bool is there a request to purge the existing mathematical content */ /** @var bool is there a request to purge the existing mathematical content */
protected $purge = false; protected $purge = false;
/** @var string with last occurred error */ /** @var string with last occurred error */
protected $lastError = ''; protected $lastError = '';
/** @var string md5 value from userInputTex */
protected $md5 = '';
/** @var string binary packed inputhash */ /** @var string binary packed inputhash */
protected $inputHash = ''; protected $inputHash = '';
/** @var string rendering mode */ /** @var string rendering mode */
@ -78,13 +75,17 @@ abstract class MathRenderer {
/** @var LoggerInterface */ /** @var LoggerInterface */
private $logger; private $logger;
private WANObjectCache $cache;
/** /**
* Constructs a base MathRenderer * Constructs a base MathRenderer
* *
* @param string $tex (optional) LaTeX markup * @param string $tex (optional) LaTeX markup
* @param array $params (optional) HTML attributes * @param array $params (optional) HTML attributes
* @param WANObjectCache|null $cache (optional)
*/ */
public function __construct( $tex = '', $params = [] ) { public function __construct( string $tex = '', $params = [], $cache = null ) {
$this->cache = $cache ?? MediaWikiServices::getInstance()->getMainWANObjectCache();
$this->params = $params; $this->params = $params;
if ( isset( $params['id'] ) ) { if ( isset( $params['id'] ) ) {
$this->id = $params['id']; $this->id = $params['id'];
@ -119,18 +120,6 @@ abstract class MathRenderer {
$this->logger = LoggerFactory::getInstance( 'Math' ); $this->logger = LoggerFactory::getInstance( 'Math' );
} }
/**
* @param string $md5
* @return self the MathRenderer generated from md5
*/
public static function newFromMd5( $md5 ) {
// @phan-suppress-next-line PhanTypeInstantiateAbstractStatic
$instance = new static();
$instance->setMd5( $md5 );
$instance->readFromDatabase();
return $instance;
}
/** /**
* Static factory method for getting a renderer based on mode * Static factory method for getting a renderer based on mode
* *
@ -181,64 +170,31 @@ abstract class MathRenderer {
* *
* @return string hash * @return string hash
*/ */
public function getMd5() { public function getInputHash(): string {
if ( !$this->md5 ) {
$this->md5 = md5( $this->userInputTex );
}
return $this->md5;
}
/**
* Set the input hash (if user input tex is not available)
* @param string $md5
*/
public function setMd5( $md5 ) {
$this->md5 = $md5;
}
/**
* Return hash of input
*
* @return string hash
*/
public function getInputHash() {
// TODO: What happens if $tex is empty?
if ( !$this->inputHash ) { if ( !$this->inputHash ) {
$dbr = MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->getReplicaDatabase(); $this->inputHash = hash( 'md5', // xxh128 might be better when dropping php 7 support
return $dbr->encodeBlob( pack( "H32", $this->getMd5() ) ); # Binary packed, not hex $this->mode .
$this->userInputTex .
implode( $this->params )
);
} }
return $this->inputHash; return $this->inputHash;
} }
/**
* Decode binary packed hash from the database to md5 of input_tex
* @param string $hash (binary)
* @return string md5
*/
private static function dbHash2md5( $hash ) {
$dbr = wfGetDB( DB_REPLICA );
$xhash = unpack( 'H32md5', $dbr->decodeBlob( $hash ) . " " );
return $xhash['md5'];
}
/** /**
* Reads rendering data from database * Reads rendering data from database
* *
* @return bool true if read successfully, false otherwise * @return bool true if read successfully, false otherwise
*/ */
public function readFromDatabase() { public function readFromCache(): bool {
$dbr = MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->getReplicaDatabase(); $rpage = $this->cache->get( $this->getCacheKey() );
$rpage = $dbr->selectRow( $this->getMathTableName(),
$this->dbInArray(),
[ 'math_inputhash' => $this->getInputHash() ],
__METHOD__ );
if ( $rpage !== false ) { if ( $rpage !== false ) {
$this->initializeFromDatabaseRow( $rpage ); $this->initializeFromCache( $rpage );
$this->storedInDatabase = true; $this->storedInCache = true;
return true; return true;
} else { } else {
# Missing from the database and/or the render cache # Missing from the database and/or the render cache
$this->storedInDatabase = false; $this->storedInCache = false;
return false; return false;
} }
} }
@ -258,82 +214,38 @@ abstract class MathRenderer {
/** /**
* Reads the values from the database but does not overwrite set values with empty values * Reads the values from the database but does not overwrite set values with empty values
* @param stdClass $rpage (a database row) * @param array $rpage (a database row)
*/ */
protected function initializeFromDatabaseRow( $rpage ) { public function initializeFromCache( $rpage ) {
$this->inputHash = $rpage->math_inputhash; // MUST NOT BE NULL $this->inputHash = $rpage['math_inputhash']; // MUST NOT BE NULL
$this->md5 = self::dbHash2md5( $this->inputHash ); if ( isset( $rpage['math_mathml'] ) ) {
if ( !empty( $rpage->math_mathml ) ) { $this->mathml = $rpage['math_mathml'];
$this->mathml = $rpage->math_mathml;
} }
if ( !empty( $rpage->math_inputtex ) ) { if ( isset( $rpage['math_inputtex'] ) ) {
// in the current database the field is probably not set. $this->userInputTex = $rpage['math_inputtex'];
$this->userInputTex = $rpage->math_inputtex;
} }
if ( !empty( $rpage->math_tex ) ) { if ( isset( $rpage['math_tex'] ) ) {
$this->tex = $rpage->math_tex; $this->tex = $rpage['math_tex'];
} }
if ( !empty( $rpage->math_svg ) ) { if ( isset( $rpage['math_svg'] ) ) {
$this->svg = $rpage->math_svg; $this->svg = $rpage['math_svg'];
} }
$this->changed = false; $this->changed = false;
} }
/** /**
* Writes rendering entry to database. * Writes rendering entry to cache.
* *
* WARNING: Use writeCache() instead of this method to be sure that all * WARNING: Use writeCache() instead of this method to be sure that all
* renderer specific (such as squid caching) are taken into account. * renderer specific (such as squid caching) are taken into account.
* This function stores the values that are currently present in the class * This function stores the values that are currently present in the class
* to the database even if they are empty. * to the cache even if they are empty.
* *
* This function can be seen as protected function. * This function can be seen as protected function.
* @param \Wikimedia\Rdbms\IDatabase|null $dbw
*/ */
public function writeToDatabase( $dbw = null ) { public function writeToCache() {
# Now save it back to the DB:
if ( MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly() ) {
return;
}
$outArray = $this->dbOutArray(); $outArray = $this->dbOutArray();
$mathTableName = $this->getMathTableName(); $this->cache->set( $this->getCacheKey(), $outArray );
$fname = __METHOD__;
if ( $this->isInDatabase() ) {
$this->debug( 'Update database entry' );
$inputHash = $this->getInputHash();
DeferredUpdates::addCallableUpdate( function () use (
$dbw, $outArray, $inputHash, $mathTableName, $fname
) {
$dbw = $dbw ?: wfGetDB( DB_PRIMARY );
$dbw->update( $mathTableName, $outArray,
[ 'math_inputhash' => $inputHash ], $fname );
$this->logger->debug(
'Row updated after db transaction was idle: ' .
var_export( $outArray, true ) . " to database" );
} );
} else {
$this->storedInDatabase = true;
$this->debug( 'Store new entry in database' );
DeferredUpdates::addCallableUpdate( function () use (
$dbw, $outArray, $mathTableName, $fname
) {
$dbw = $dbw ?: wfGetDB( DB_PRIMARY );
$dbw->insert( $mathTableName, $outArray, $fname, [ 'IGNORE' ] );
LoggerFactory::getInstance( 'Math' )->debug(
'Row inserted after db transaction was idle {out}.',
[
'out' => var_export( $outArray, true ),
]
);
if ( $dbw->affectedRows() == 0 ) {
// That's the price for the delayed update.
$this->logger->warning(
'Entry could not be written. Might be changed in between.' );
}
} );
}
} }
/** /**
@ -346,7 +258,8 @@ abstract class MathRenderer {
'math_mathml' => $this->mathml, 'math_mathml' => $this->mathml,
'math_inputtex' => $this->userInputTex, 'math_inputtex' => $this->userInputTex,
'math_tex' => $this->tex, 'math_tex' => $this->tex,
'math_svg' => $this->svg 'math_svg' => $this->svg,
'math_mode' => $this->mode
]; ];
return $out; return $out;
} }
@ -409,7 +322,7 @@ abstract class MathRenderer {
$this->debug( 'Writing of cache requested' ); $this->debug( 'Writing of cache requested' );
if ( $this->isChanged() ) { if ( $this->isChanged() ) {
$this->debug( 'Change detected. Perform writing' ); $this->debug( 'Change detected. Perform writing' );
$this->writeToDatabase(); $this->writeToCache();
return true; return true;
} else { } else {
$this->debug( "Nothing was changed. Don't write to database" ); $this->debug( "Nothing was changed. Don't write to database" );
@ -591,7 +504,7 @@ abstract class MathRenderer {
return true; return true;
} else { } else {
if ( $texCheckDisabled === MathConfig::NEW && $this->mode != MathConfig::MODE_SOURCE ) { if ( $texCheckDisabled === MathConfig::NEW && $this->mode != MathConfig::MODE_SOURCE ) {
if ( $this->readFromDatabase() ) { if ( $this->readFromCache() ) {
$this->debug( 'Skip TeX check' ); $this->debug( 'Skip TeX check' );
$this->texSecure = true; $this->texSecure = true;
return true; return true;
@ -603,10 +516,10 @@ abstract class MathRenderer {
} }
public function isInDatabase() { public function isInDatabase() {
if ( $this->storedInDatabase === null ) { if ( $this->storedInCache === null ) {
$this->readFromDatabase(); $this->readFromCache();
} }
return $this->storedInDatabase; return $this->storedInCache;
} }
/** /**
@ -697,4 +610,11 @@ abstract class MathRenderer {
protected function debug( $msg ) { protected function debug( $msg ) {
$this->logger->debug( "$msg for \"{tex}\".", [ 'tex' => $this->userInputTex ] ); $this->logger->debug( "$msg for \"{tex}\".", [ 'tex' => $this->userInputTex ] );
} }
private function getCacheKey() {
return $this->cache->makeGlobalKey(
self::class,
$this->getInputHash()
);
}
} }

View file

@ -2,6 +2,7 @@
namespace MediaWiki\Extension\Math\Render; namespace MediaWiki\Extension\Math\Render;
use InvalidArgumentException;
use MediaWiki\Config\ServiceOptions; use MediaWiki\Config\ServiceOptions;
use MediaWiki\Extension\Math\MathConfig; use MediaWiki\Extension\Math\MathConfig;
use MediaWiki\Extension\Math\MathLaTeXML; use MediaWiki\Extension\Math\MathLaTeXML;
@ -12,6 +13,7 @@ use MediaWiki\Extension\Math\MathRenderer;
use MediaWiki\Extension\Math\MathSource; use MediaWiki\Extension\Math\MathSource;
use MediaWiki\User\Options\UserOptionsLookup; use MediaWiki\User\Options\UserOptionsLookup;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use WANObjectCache;
class RendererFactory { class RendererFactory {
@ -34,23 +36,28 @@ class RendererFactory {
/** @var LoggerInterface */ /** @var LoggerInterface */
private $logger; private $logger;
private WANObjectCache $cache;
/** /**
* @param ServiceOptions $serviceOptions * @param ServiceOptions $serviceOptions
* @param MathConfig $mathConfig * @param MathConfig $mathConfig
* @param UserOptionsLookup $userOptionsLookup * @param UserOptionsLookup $userOptionsLookup
* @param LoggerInterface $logger * @param LoggerInterface $logger
* @param WANObjectCache $cache
*/ */
public function __construct( public function __construct(
ServiceOptions $serviceOptions, ServiceOptions $serviceOptions,
MathConfig $mathConfig, MathConfig $mathConfig,
UserOptionsLookup $userOptionsLookup, UserOptionsLookup $userOptionsLookup,
LoggerInterface $logger LoggerInterface $logger,
WANObjectCache $cache
) { ) {
$serviceOptions->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); $serviceOptions->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
$this->options = $serviceOptions; $this->options = $serviceOptions;
$this->mathConfig = $mathConfig; $this->mathConfig = $mathConfig;
$this->userOptionsLookup = $userOptionsLookup; $this->userOptionsLookup = $userOptionsLookup;
$this->logger = $logger; $this->logger = $logger;
$this->cache = $cache;
} }
/** /**
@ -91,17 +98,17 @@ class RendererFactory {
$renderer = new MathSource( $tex, $params ); $renderer = new MathSource( $tex, $params );
break; break;
case MathConfig::MODE_NATIVE_MML: case MathConfig::MODE_NATIVE_MML:
$renderer = new MathNativeMML( $tex, $params ); $renderer = new MathNativeMML( $tex, $params, $this->cache );
break; break;
case MathConfig::MODE_LATEXML: case MathConfig::MODE_LATEXML:
$renderer = new MathLaTeXML( $tex, $params ); $renderer = new MathLaTeXML( $tex, $params, $this->cache );
break; break;
case MathConfig::MODE_MATHML: case MathConfig::MODE_MATHML:
default: default:
if ( $this->options->get( 'MathoidCli' ) ) { if ( $this->options->get( 'MathoidCli' ) ) {
$renderer = new MathMathMLCli( $tex, $params ); $renderer = new MathMathMLCli( $tex, $params, $this->cache );
} else { } else {
$renderer = new MathMathML( $tex, $params ); $renderer = new MathMathML( $tex, $params, $this->cache );
} }
} }
$this->logger->debug( $this->logger->debug(
@ -113,4 +120,15 @@ class RendererFactory {
); );
return $renderer; return $renderer;
} }
public function getFromHash( $hash ) {
$rpage = $this->cache->get( $hash );
if ( $rpage === false ) {
throw new InvalidArgumentException( 'Cache key is invalid' );
}
$mode = $rpage['math_mode'];
$renderer = $this->getRenderer( '', [], $mode );
$renderer->initializeFromCache( $rpage );
return $renderer;
}
} }

View file

@ -81,10 +81,9 @@ class SpecialMathShowImage extends SpecialPage {
echo $this->printSvgError( 'No Inputhash specified' ); echo $this->printSvgError( 'No Inputhash specified' );
} else { } else {
if ( $tex === '' && $asciimath === '' ) { if ( $tex === '' && $asciimath === '' ) {
$this->renderer = $this->rendererFactory->getRenderer( '', [], $this->mode ); $this->renderer = $this->rendererFactory->getFromHash( $hash );
$this->renderer->setMd5( $hash );
$this->noRender = $request->getBool( 'noRender', false ); $this->noRender = $request->getBool( 'noRender', false );
$isInDatabase = $this->renderer->readFromDatabase(); $isInDatabase = $this->renderer->readFromCache();
if ( $isInDatabase || $this->noRender ) { if ( $isInDatabase || $this->noRender ) {
$success = $isInDatabase; $success = $isInDatabase;
} else { } else {

View file

@ -0,0 +1,79 @@
<?php
use MediaWiki\Extension\Math\MathMathML;
use MediaWiki\Extension\Math\MathRenderer;
/**
* Test the database access and core functionality of MathRenderer.
*
* @covers \MediaWiki\Extension\Math\MathRenderer
*
* @group Math
*
* @license GPL-2.0-or-later
*/
class MathCacheTest extends MediaWikiIntegrationTestCase {
/**
* @var MathRenderer
*/
private $renderer;
private const SOME_TEX = "a+b";
private const SOME_MATHML = "iℏ∂_tΨ=H^Ψ<mrow><\ci>";
protected function setUp(): void {
parent::setUp();
$this->renderer = new MathMathML( self::SOME_TEX );
}
/**
* Checks the tex and hash functions
* @covers \MediaWiki\Extension\Math\MathRenderer::getInputHash
*/
public function testInputHash() {
$this->assertEquals( 'beb7506b16f7c36aa0f9c8c9ef41b40b', $this->renderer->getInputHash() );
}
/**
* Helper function to set the current state of the sample renderer instance to the test values
*/
public function setValues() {
// set some values
$this->renderer->setTex( self::SOME_TEX );
$this->renderer->setMathml( self::SOME_MATHML );
}
/**
* Checks database access. Writes an entry and reads it back.
* @covers \MediaWiki\Extension\Math\MathRenderer::writeToCache
* @covers \MediaWiki\Extension\Math\MathRenderer::readFromCache
*/
public function testDBBasics() {
$this->setValues();
$this->renderer->writeToCache();
$renderer2 = new MathMathML( self::SOME_TEX, [ 'display' => '' ] );
$this->assertTrue( $renderer2->readFromCache(), 'Reading from database failed' );
// comparing the class object does now work due to null values etc.
$this->assertEquals(
$this->renderer->getTex(), $renderer2->getTex(), "test if tex is the same"
);
$this->assertEquals(
$this->renderer->getMathml(), $renderer2->getMathml(), "Check MathML encoding"
);
$this->assertEquals(
$this->renderer->getHtmlOutput(), $renderer2->getHtmlOutput(), 'test if HTML is the same'
);
}
/**
* This test checks if no additional write operation
* is performed, if the entry already existed in the database.
*/
public function testNoWrite() {
$this->setValues();
$inputHash = $this->renderer->getInputHash();
$this->assertTrue( $this->renderer->isChanged() );
$this->assertTrue( $this->renderer->writeCache(), "Write new entry" );
$this->assertTrue( $this->renderer->readFromCache(), "Read entry from database" );
$this->assertFalse( $this->renderer->isChanged() );
}
}

View file

@ -1,126 +0,0 @@
<?php
use MediaWiki\Extension\Math\MathConfig;
use MediaWiki\Extension\Math\MathMathML;
use MediaWiki\Extension\Math\MathRenderer;
/**
* Test the database access and core functionality of MathRenderer.
*
* @covers \MediaWiki\Extension\Math\MathRenderer
*
* @group Math
* @group Database
*
* @license GPL-2.0-or-later
*/
class MathDatabaseTest extends MediaWikiIntegrationTestCase {
/**
* @var MathRenderer
*/
private $renderer;
private const SOME_TEX = "a+b";
private const SOME_MATHML = "iℏ∂_tΨ=H^Ψ<mrow><\ci>";
/**
* creates a new database connection and a new math renderer
* TODO: Check if there is a way to get database access without creating
* the connection to the database explicitly
* function addDBData() {
* $this->tablesUsed[] = 'math';
* }
* was not sufficient.
* @throws Exception
*/
protected function setUp(): void {
parent::setUp();
// TODO: figure out why this is necessary
$this->db = $this->getServiceContainer()
->getDBLoadBalancer()
->getConnection( DB_PRIMARY );
$this->renderer = new MathMathML( self::SOME_TEX );
$this->tablesUsed[] = 'mathoid';
}
/**
* Checks the tex and hash functions
* @covers \MediaWiki\Extension\Math\MathRenderer::getInputHash
*/
public function testInputHash() {
$expectedhash = $this->db->encodeBlob( pack( "H32", md5( self::SOME_TEX ) ) );
$this->assertEquals( $expectedhash, $this->renderer->getInputHash() );
}
/**
* Helper function to set the current state of the sample renderer instance to the test values
*/
public function setValues() {
// set some values
$this->renderer->setTex( self::SOME_TEX );
$this->renderer->setMathml( self::SOME_MATHML );
}
/**
* Checks database access. Writes an entry and reads it back.
* @covers \MediaWiki\Extension\Math\MathRenderer::writeToDatabase
* @covers \MediaWiki\Extension\Math\MathRenderer::readFromDatabase
*/
public function testDBBasics() {
$this->setValues();
$this->renderer->writeToDatabase( $this->db );
$renderer2 = new MathMathML( self::SOME_TEX );
$this->assertTrue( $renderer2->readFromDatabase(), 'Reading from database failed' );
// comparing the class object does now work due to null values etc.
$this->assertEquals(
$this->renderer->getTex(), $renderer2->getTex(), "test if tex is the same"
);
$this->assertEquals(
$this->renderer->getMathml(), $renderer2->getMathml(), "Check MathML encoding"
);
$this->assertEquals(
$this->renderer->getHtmlOutput(), $renderer2->getHtmlOutput(), 'test if HTML is the same'
);
}
/**
* Checks the creation of the math table.
* @covers \MediaWiki\Extension\Math\HookHandlers\SchemaHooksHandler::onLoadExtensionSchemaUpdates
*/
public function testCreateTable() {
$this->markTestSkippedIfDbType( 'postgres' );
$this->markTestSkippedIfDbType( 'sqlite' );
$this->setMwGlobals( 'wgMathValidModes', [ MathConfig::MODE_MATHML ] );
$this->db->dropTable( "mathoid", __METHOD__ );
$dbu = DatabaseUpdater::newForDB( $this->db );
$dbu->doUpdates( [ "extensions" ] );
$this->expectOutputRegex( '/(.*)Creating mathoid table(.*)/' );
$this->setValues();
$this->renderer->writeToDatabase();
$res = $this->db->select( "mathoid", "*" );
$row = $res->fetchRow();
$this->assertCount( 16, $row );
}
/**
* This test checks if no additional write operation
* is performed, if the entry already existed in the database.
*/
public function testNoWrite() {
$this->setValues();
$inputHash = $this->renderer->getInputHash();
$this->assertTrue( $this->renderer->isChanged() );
$this->assertTrue( $this->renderer->writeCache(), "Write new entry" );
$res = $this->db->selectField( "mathoid", "math_inputhash",
[ "math_inputhash" => $inputHash ] );
$this->assertTrue( $res !== false, "Check database entry" );
$this->assertTrue( $this->renderer->readFromDatabase(), "Read entry from database" );
$this->assertFalse( $this->renderer->isChanged() );
// modify the database entry manually
$this->db->delete( "mathoid", [ "math_inputhash" => $inputHash ] );
// the renderer should not be aware of the modification and should not recreate the entry
$this->assertFalse( $this->renderer->writeCache() );
// as a result no entry can be found in the database.
$this->assertFalse( $this->renderer->readFromDatabase() );
}
}

View file

@ -1,22 +0,0 @@
<?php
use MediaWiki\Extension\Math\InputCheck\BaseChecker;
/**
* @covers \MediaWiki\Extension\Math\InputCheck\BaseChecker
*
* @group Math
*
* @license GPL-2.0-or-later
*/
class MathInputCheckTest extends MediaWikiIntegrationTestCase {
public function testAbstractClass() {
$InputCheck = $this->getMockForAbstractClass( BaseChecker::class );
/** @var BaseChecker $InputCheck */
$this->assertFalse( $InputCheck->IsValid() );
$this->assertNull( $InputCheck->getError() );
$this->assertNull( $InputCheck->getValidTex() );
}
}

View file

@ -1,24 +1,18 @@
<?php <?php
use MediaWiki\Extension\Math\MathConfig;
use MediaWiki\Extension\Math\MathLaTeXML; use MediaWiki\Extension\Math\MathLaTeXML;
/** /**
* @covers \MediaWiki\Extension\Math\MathLaTeXML * @covers \MediaWiki\Extension\Math\MathLaTeXML
* *
* @group Math * @group Math
* @group Database
* *
* @license GPL-2.0-or-later * @license GPL-2.0-or-later
*/ */
class MathLaTeXMLDatabaseTest extends MediaWikiIntegrationTestCase { class MathLaTeXMLCacheTest extends MediaWikiIntegrationTestCase {
public $renderer; public $renderer;
private const SOME_TEX = "a+b"; private const SOME_TEX = "a+b";
private const SOME_HTML = "a<sub>b</sub>";
private const SOME_MATHML = "iℏ∂_tΨ=H^Ψ<mrow><\ci>"; private const SOME_MATHML = "iℏ∂_tΨ=H^Ψ<mrow><\ci>";
private const SOME_LOG = "Sample Log Text.";
private const SOME_TIMESTAMP = 1272509157;
private const SOME_SVG = "<?xml </svg >>%%LIKE;'\" DROP TABLE math;";
/** /**
* Helper function to test protected/private Methods * Helper function to test protected/private Methods
@ -32,21 +26,9 @@ class MathLaTeXMLDatabaseTest extends MediaWikiIntegrationTestCase {
return $method; return $method;
} }
/**
* creates a new database connection and a new math renderer
* TODO: Check if there is a way to get database access without creating
* the connection to the database explicitly
* function addDBData() {
* $this->tablesUsed[] = 'math';
* }
* was not sufficient.
*/
protected function setUp(): void { protected function setUp(): void {
parent::setUp(); parent::setUp();
// TODO: figure out why this is necessary
$this->db = wfGetDB( DB_PRIMARY );
$this->renderer = new MathLaTeXML( self::SOME_TEX ); $this->renderer = new MathLaTeXML( self::SOME_TEX );
self::setupTestDB( $this->db, "mathtest" );
} }
/** /**
@ -54,8 +36,8 @@ class MathLaTeXMLDatabaseTest extends MediaWikiIntegrationTestCase {
* @covers \MediaWiki\Extension\Math\MathRenderer::getInputHash * @covers \MediaWiki\Extension\Math\MathRenderer::getInputHash
*/ */
public function testInputHash() { public function testInputHash() {
$expectedhash = $this->db->encodeBlob( pack( "H32", md5( self::SOME_TEX ) ) ); $this->assertIsString( $this->renderer->getInputHash() );
$this->assertEquals( $expectedhash, $this->renderer->getInputHash() ); $this->assertStringMatchesFormat( '%x', $this->renderer->getInputHash() );
} }
/** /**
@ -77,38 +59,17 @@ class MathLaTeXMLDatabaseTest extends MediaWikiIntegrationTestCase {
$this->assertEquals( "mathlatexml", $tableName, "Wrong latexml table name" ); $this->assertEquals( "mathlatexml", $tableName, "Wrong latexml table name" );
} }
/**
* Checks the creation of the math table.
* @covers \MediaWiki\Extension\Math\HookHandlers\SchemaHooksHandler::onLoadExtensionSchemaUpdates
*/
public function testCreateTable() {
$this->markTestSkippedIfDbType( 'postgres' );
$this->markTestSkippedIfDbType( 'sqlite' );
$this->setMwGlobals( 'wgMathValidModes', [ MathConfig::MODE_LATEXML ] );
$this->db->dropTable( "mathlatexml", __METHOD__ );
$dbu = DatabaseUpdater::newForDB( $this->db );
$dbu->doUpdates( [ "extensions" ] );
$this->expectOutputRegex( '/(.*)Creating mathlatexml table(.*)/' );
$this->setValues();
$this->renderer->writeToDatabase();
$res = $this->db->select( "mathlatexml", "*" );
$row = $res->fetchRow();
$this->assertCount( 12, $row );
}
/** /**
* Checks database access. Writes an entry and reads it back. * Checks database access. Writes an entry and reads it back.
* @depends testCreateTable * @covers \MediaWiki\Extension\Math\MathRenderer::writeToCache
* @covers \MediaWiki\Extension\Math\MathRenderer::writeToDatabase * @covers \MediaWiki\Extension\Math\MathRenderer::readFromCache
* @covers \MediaWiki\Extension\Math\MathRenderer::readFromDatabase
*/ */
public function testDBBasics() { public function testDBBasics() {
$this->setValues(); $this->setValues();
$this->renderer->writeToDatabase(); $this->renderer->writeToCache();
$renderer2 = $this->renderer = new MathLaTeXML( self::SOME_TEX ); $renderer2 = $this->renderer = new MathLaTeXML( self::SOME_TEX );
$renderer2->readFromDatabase(); $renderer2->readFromCache();
// comparing the class object does now work due to null values etc. // comparing the class object does now work due to null values etc.
$this->assertEquals( $this->assertEquals(
$this->renderer->getTex(), $renderer2->getTex(), "test if tex is the same" $this->renderer->getTex(), $renderer2->getTex(), "test if tex is the same"

View file

@ -39,12 +39,12 @@ class MathRendererTest extends MediaWikiIntegrationTestCase {
public function testWriteCacheSkip() { public function testWriteCacheSkip() {
$renderer = $renderer =
$this->getMockBuilder( MathRenderer::class )->onlyMethods( [ $this->getMockBuilder( MathRenderer::class )->onlyMethods( [
'writeToDatabase', 'writeToCache',
'render', 'render',
'getMathTableName', 'getMathTableName',
'getHtmlOutput' 'getHtmlOutput'
] )->getMock(); ] )->getMock();
$renderer->expects( $this->never() )->method( 'writeToDatabase' ); $renderer->expects( $this->never() )->method( 'writeToCache' );
/** @var MathRenderer $renderer */ /** @var MathRenderer $renderer */
$renderer->writeCache(); $renderer->writeCache();
} }
@ -56,12 +56,12 @@ class MathRendererTest extends MediaWikiIntegrationTestCase {
public function testWriteCache() { public function testWriteCache() {
$renderer = $renderer =
$this->getMockBuilder( MathRenderer::class )->onlyMethods( [ $this->getMockBuilder( MathRenderer::class )->onlyMethods( [
'writeToDatabase', 'writeToCache',
'render', 'render',
'getMathTableName', 'getMathTableName',
'getHtmlOutput' 'getHtmlOutput'
] )->getMock(); ] )->getMock();
$renderer->expects( $this->never() )->method( 'writeToDatabase' ); $renderer->expects( $this->never() )->method( 'writeToCache' );
/** @var MathRenderer $renderer */ /** @var MathRenderer $renderer */
$renderer->writeCache(); $renderer->writeCache();
} }
@ -87,10 +87,10 @@ class MathRendererTest extends MediaWikiIntegrationTestCase {
'render', 'render',
'getMathTableName', 'getMathTableName',
'getHtmlOutput', 'getHtmlOutput',
'readFromDatabase', 'readFromCache',
'setTex' 'setTex'
] )->setConstructorArgs( [ self::TEXVCCHECK_INPUT ] )->getMock(); ] )->setConstructorArgs( [ self::TEXVCCHECK_INPUT ] )->getMock();
$renderer->expects( $this->never() )->method( 'readFromDatabase' ); $renderer->expects( $this->never() )->method( 'readFromCache' );
$renderer->expects( $this->once() )->method( 'setTex' )->with( self::TEXVCCHECK_OUTPUT ); $renderer->expects( $this->once() )->method( 'setTex' )->with( self::TEXVCCHECK_OUTPUT );
/** @var MathRenderer $renderer */ /** @var MathRenderer $renderer */
@ -106,10 +106,10 @@ class MathRendererTest extends MediaWikiIntegrationTestCase {
'render', 'render',
'getMathTableName', 'getMathTableName',
'getHtmlOutput', 'getHtmlOutput',
'readFromDatabase', 'readFromCache',
'setTex' 'setTex'
] )->setConstructorArgs( [ self::TEXVCCHECK_INPUT ] )->getMock(); ] )->setConstructorArgs( [ self::TEXVCCHECK_INPUT ] )->getMock();
$renderer->expects( $this->never() )->method( 'readFromDatabase' ); $renderer->expects( $this->never() )->method( 'readFromCache' );
$renderer->expects( $this->never() )->method( 'setTex' ); $renderer->expects( $this->never() )->method( 'setTex' );
/** @var MathRenderer $renderer */ /** @var MathRenderer $renderer */
@ -125,10 +125,10 @@ class MathRendererTest extends MediaWikiIntegrationTestCase {
'render', 'render',
'getMathTableName', 'getMathTableName',
'getHtmlOutput', 'getHtmlOutput',
'readFromDatabase', 'readFromCache',
'setTex' 'setTex'
] )->setConstructorArgs( [ self::TEXVCCHECK_INPUT ] )->getMock(); ] )->setConstructorArgs( [ self::TEXVCCHECK_INPUT ] )->getMock();
$renderer->expects( $this->once() )->method( 'readFromDatabase' ) $renderer->expects( $this->once() )->method( 'readFromCache' )
->willReturn( false ); ->willReturn( false );
$renderer->expects( $this->once() )->method( 'setTex' )->with( self::TEXVCCHECK_OUTPUT ); $renderer->expects( $this->once() )->method( 'setTex' )->with( self::TEXVCCHECK_OUTPUT );
@ -147,10 +147,10 @@ class MathRendererTest extends MediaWikiIntegrationTestCase {
'render', 'render',
'getMathTableName', 'getMathTableName',
'getHtmlOutput', 'getHtmlOutput',
'readFromDatabase', 'readFromCache',
'setTex' 'setTex'
] )->setConstructorArgs( [ self::TEXVCCHECK_INPUT ] )->getMock(); ] )->setConstructorArgs( [ self::TEXVCCHECK_INPUT ] )->getMock();
$renderer->expects( $this->once() )->method( 'readFromDatabase' ) $renderer->expects( $this->once() )->method( 'readFromCache' )
->willReturn( true ); ->willReturn( true );
$renderer->expects( $this->never() )->method( 'setTex' ); $renderer->expects( $this->never() )->method( 'setTex' );