feat(cache): WP Fastest Cache statt WP Super Cache
Die Cache-Integration war für WP Super Cache gebaut, eingesetzt wird aber WP Fastest Cache. Profil (wpfc), Listener, Drosselung, lit/cache-clear und Leeren-Probe laufen jetzt über die öffentlichen WPFC-Funktionen wpfc_clear_post_cache_by_id/wpfc_clear_all_cache; die Probe leert nur die Wegwerf-Seite (singleDeleteCache ohne Elternseiten) und prüft, dass WPFC den Hook noch verarbeitet. Smoke fastest-cache-smoke.sh ersetzt den WPSC-Smoke. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e1fad3dffe
commit
bee9da3a86
@@ -100,11 +100,10 @@ nichts geändert. Vorgehen:
|
|||||||
`compat_incompatible`; der Stand steht im selben Reiter „Kompatibilität“.
|
`compat_incompatible`; der Stand steht im selben Reiter „Kompatibilität“.
|
||||||
|
|
||||||
Seitenweite Änderungen (Optionen, Menüs und Menü-Positionen, Medien, Divi-Design, Theme Builder, globale
|
Seitenweite Änderungen (Optionen, Menüs und Menü-Positionen, Medien, Divi-Design, Theme Builder, globale
|
||||||
Bibliotheksmodule) leeren den Cache von WP Super Cache, Änderungen an einem Beitrag nur dessen Seite. Dabei leert der
|
Bibliotheksmodule) leeren den Cache von WP Fastest Cache, Änderungen an einem Beitrag dessen Seite samt Startseite,
|
||||||
Purge eines einzelnen Beitrags die Cache-Dateien unter dem Host, über den der MCP-Aufruf eingeht; bei verschiedenen
|
Feeds, Sitemap, Nachbarbeiträgen und Archiven seiner Begriffe (so leert WP Fastest Cache einen Beitrag). Ist der Cache
|
||||||
Hosts (z. B. mit und ohne `www`) bleiben die Dateien des anderen Hosts womöglich ungeleert (der Komplett-Purge ist davon
|
dafür gesperrt, bleibt er unverändert; der Zustand steht im Reiter „Kompatibilität“
|
||||||
nicht betroffen). Ist der Cache dafür gesperrt, bleibt er unverändert; der Zustand steht im Reiter „Kompatibilität“
|
und in `lit-site-status` (`wpfc`). Der Nachlauf-Purge, der auf ein gedrosseltes Leeren des ganzen Caches folgt, braucht
|
||||||
und in `lit-site-status` (`wpsc`). Der Nachlauf-Purge, der auf ein gedrosseltes Leeren des ganzen Caches folgt, braucht
|
|
||||||
WP-Cron (oder einen System-Cron, der `wp-cron.php` aufruft). Auf Divi-Seiten leert jede Prüfung im Reiter den ganzen
|
WP-Cron (oder einen System-Cron, der `wp-cron.php` aufruft). Auf Divi-Seiten leert jede Prüfung im Reiter den ganzen
|
||||||
Cache einmal: Divi selbst leert ihn beim Anlegen und Löschen der Testseite. Wurde etwas außerhalb von LIT MCP
|
Cache einmal: Divi selbst leert ihn beim Anlegen und Löschen der Testseite. Wurde etwas außerhalb von LIT MCP
|
||||||
geändert, leert `lit/cache-clear` den Cache auf Wunsch (`scope` `all` oder `post` mit `post_id`; erst Probelauf, dann
|
geändert, leert `lit/cache-clear` den Cache auf Wunsch (`scope` `all` oder `post` mit `post_id`; erst Probelauf, dann
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use Closure;
|
|||||||
|
|
||||||
defined('ABSPATH') || exit;
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
/** The one check step a profile's behaviour probe makes (Profile::$extraSteps), shared by Pods and WP Super Cache. */
|
/** The one check step a profile's behaviour probe makes (Profile::$extraSteps), shared by Pods and WP Fastest Cache. */
|
||||||
final class ProbeStep
|
final class ProbeStep
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ defined('ABSPATH') || exit;
|
|||||||
*/
|
*/
|
||||||
final class ThrowawayPost
|
final class ThrowawayPost
|
||||||
{
|
{
|
||||||
/** Meta key and title prefix the Pods and WP Super Cache probes give their entries (the Divi write probe has its own). */
|
/** Meta key and title prefix the Pods and WP Fastest Cache probes give their entries (the Divi write probe has its own). */
|
||||||
public const MARKER = '_lit_mcp_check_entry';
|
public const MARKER = '_lit_mcp_check_entry';
|
||||||
|
|
||||||
public const TITLE = '[LIT MCP Prüfung]';
|
public const TITLE = '[LIT MCP Prüfung]';
|
||||||
|
|||||||
+18
-33
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace LIT\MCP\Integrations\SuperCache;
|
namespace LIT\MCP\Integrations\FastestCache;
|
||||||
|
|
||||||
use LIT\MCP\Abilities\Ability;
|
use LIT\MCP\Abilities\Ability;
|
||||||
use LIT\MCP\Abilities\Annotations;
|
use LIT\MCP\Abilities\Annotations;
|
||||||
@@ -17,18 +17,19 @@ use WP_Post;
|
|||||||
defined('ABSPATH') || exit;
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* lit/cache-clear: empties the WP Super Cache on request – for changes made outside this plugin (the plugin's own
|
* lit/cache-clear: empties the WP Fastest Cache on request – for changes made outside this plugin (the plugin's own
|
||||||
* writes empty it anyway, SuperCacheListener). Dry run without confirm; with confirm: true the whole cache goes
|
* writes empty it anyway, FastestCacheListener). Dry run without confirm; with confirm: true the whole cache goes
|
||||||
* through the same throttle and trailing purge as the listener (SuperCachePurger), a post through wp_cache_post_edit,
|
* through the same throttle and trailing purge as the listener (FastestCachePurger), a post through
|
||||||
* which skips every status but publish and private. The change log entry is ignored by the listener (NAME).
|
* wpfc_clear_post_cache_by_id (any status: WPFC also finds the address of a trashed post). The change log entry is
|
||||||
|
* ignored by the listener (NAME).
|
||||||
*/
|
*/
|
||||||
final class CacheClearAbility implements Ability, DeclaresRequirements
|
final class CacheClearAbility implements Ability, DeclaresRequirements
|
||||||
{
|
{
|
||||||
public const NAME = 'lit/cache-clear';
|
public const NAME = 'lit/cache-clear';
|
||||||
|
|
||||||
private const OUTCOMES = ['purged', 'throttled', 'nothing', 'cache_off'];
|
private const OUTCOMES = ['purged', 'throttled', 'cache_off'];
|
||||||
|
|
||||||
public function __construct(private SuperCachePurger $purger, private ChangeLog $log)
|
public function __construct(private FastestCachePurger $purger, private ChangeLog $log)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@ final class CacheClearAbility implements Ability, DeclaresRequirements
|
|||||||
|
|
||||||
public function description(): string
|
public function description(): string
|
||||||
{
|
{
|
||||||
return 'Leert den Seiten-Cache von WP Super Cache: scope "all" den ganzen Cache, scope "post" nur die Seite '
|
return 'Leert den Seiten-Cache von WP Fastest Cache: scope "all" den ganzen Cache, scope "post" nur die Seite '
|
||||||
. 'eines Beitrags (post_id). Änderungen durch LIT MCP leeren ihn ohnehin automatisch – das Tool ist für '
|
. 'eines Beitrags (post_id). Änderungen durch LIT MCP leeren ihn ohnehin automatisch – das Tool ist für '
|
||||||
. 'Fälle, in denen etwas außerhalb von LIT MCP geändert wurde. Ohne confirm nur Probelauf (nichts wird '
|
. 'Fälle, in denen etwas außerhalb von LIT MCP geändert wurde. Ohne confirm nur Probelauf (nichts wird '
|
||||||
. 'geleert); mit confirm: true nach Rückfrage beim Nutzer ausführen. Das Leeren des ganzen Caches ist auf '
|
. 'geleert); mit confirm: true nach Rückfrage beim Nutzer ausführen. Das Leeren des ganzen Caches ist auf '
|
||||||
@@ -89,8 +90,8 @@ final class CacheClearAbility implements Ability, DeclaresRequirements
|
|||||||
public function requirements(array $input): array
|
public function requirements(array $input): array
|
||||||
{
|
{
|
||||||
return match ($input['scope'] ?? null) {
|
return match ($input['scope'] ?? null) {
|
||||||
'all' => [SuperCacheFeature::PurgeAll->id()],
|
'all' => [FastestCacheFeature::PurgeAll->id()],
|
||||||
'post' => [SuperCacheFeature::PurgePost->id()],
|
'post' => [FastestCacheFeature::PurgePost->id()],
|
||||||
default => [],
|
default => [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -123,34 +124,26 @@ final class CacheClearAbility implements Ability, DeclaresRequirements
|
|||||||
return $this->result(
|
return $this->result(
|
||||||
$scope,
|
$scope,
|
||||||
'cache_off',
|
'cache_off',
|
||||||
'Der Seiten-Cache von WP Super Cache ist ausgeschaltet; nichts zu leeren.'
|
'Der Seiten-Cache von WP Fastest Cache ist ausgeschaltet; nichts zu leeren.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($post !== null) {
|
if ($post !== null) {
|
||||||
if (! $this->clearsStatus($post)) {
|
$outcome = $this->purger->post((int) $post->ID) ? FastestCachePurger::PURGED : FastestCachePurger::LOCKED;
|
||||||
return $this->result($scope, 'nothing', sprintf(
|
|
||||||
'Nichts zu leeren: "%s" ist ein Entwurf bzw. nicht veröffentlicht (Status %s); WP Super Cache '
|
|
||||||
. 'cacht ihn nicht.',
|
|
||||||
$post->post_title,
|
|
||||||
$post->post_status
|
|
||||||
));
|
|
||||||
}
|
|
||||||
$outcome = $this->purger->post((int) $post->ID) ? SuperCachePurger::PURGED : SuperCachePurger::LOCKED;
|
|
||||||
} else {
|
} else {
|
||||||
$outcome = $this->purger->everything();
|
$outcome = $this->purger->everything();
|
||||||
}
|
}
|
||||||
if ($outcome === SuperCachePurger::LOCKED) {
|
if ($outcome === FastestCachePurger::LOCKED) {
|
||||||
return Errors::make(
|
return Errors::make(
|
||||||
'compat_incompatible',
|
'compat_incompatible',
|
||||||
'WP Super Cache ist gesperrt (siehe Reiter Kompatibilität). Es wurde nichts geleert.',
|
'WP Fastest Cache ist gesperrt (siehe Reiter Kompatibilität). Es wurde nichts geleert.',
|
||||||
'Den Nutzer informieren, dass die Kompatibilität von WP Super Cache geprüft werden muss.',
|
'Den Nutzer informieren, dass die Kompatibilität von WP Fastest Cache geprüft werden muss.',
|
||||||
423
|
423
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$summary = match ($outcome) {
|
$summary = match ($outcome) {
|
||||||
SuperCachePurger::THROTTLED => 'Der ganze Seiten-Cache wurde vor weniger als 30 Sekunden geleert; das '
|
FastestCachePurger::THROTTLED => 'Der ganze Seiten-Cache wurde vor weniger als 30 Sekunden geleert; das '
|
||||||
. 'Leeren folgt als Nachlauf per WP-Cron.',
|
. 'Leeren folgt als Nachlauf per WP-Cron.',
|
||||||
default => ucfirst($what) . ' wurde geleert.',
|
default => ucfirst($what) . ' wurde geleert.',
|
||||||
};
|
};
|
||||||
@@ -166,8 +159,6 @@ final class CacheClearAbility implements Ability, DeclaresRequirements
|
|||||||
$note = ' Der Cache ist ausgeschaltet; es würde nichts geleert.';
|
$note = ' Der Cache ist ausgeschaltet; es würde nichts geleert.';
|
||||||
} elseif ($throttled) {
|
} elseif ($throttled) {
|
||||||
$note = ' Das Leeren wäre gerade gedrosselt und folgt als Nachlauf.';
|
$note = ' Das Leeren wäre gerade gedrosselt und folgt als Nachlauf.';
|
||||||
} elseif ($post !== null && ! $this->clearsStatus($post)) {
|
|
||||||
$note = ' Der Beitrag ist nicht veröffentlicht; es gäbe nichts zu leeren.';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Errors::confirmRequired(
|
return Errors::confirmRequired(
|
||||||
@@ -188,7 +179,7 @@ final class CacheClearAbility implements Ability, DeclaresRequirements
|
|||||||
$logId = FollowUp::run(
|
$logId = FollowUp::run(
|
||||||
fn () => $this->log->record(
|
fn () => $this->log->record(
|
||||||
self::NAME,
|
self::NAME,
|
||||||
'wpsc',
|
'wpfc',
|
||||||
$post === null ? 'all' : (string) $post->ID,
|
$post === null ? 'all' : (string) $post->ID,
|
||||||
$summary
|
$summary
|
||||||
),
|
),
|
||||||
@@ -202,12 +193,6 @@ final class CacheClearAbility implements Ability, DeclaresRequirements
|
|||||||
return $result->toArray();
|
return $result->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** WPSC skips everything but publish and private unless WPSCFORCEUPDATE is defined (wp_cache_post_edit). */
|
|
||||||
private function clearsStatus(WP_Post $post): bool
|
|
||||||
{
|
|
||||||
return defined('WPSCFORCEUPDATE') || in_array($post->post_status, ['publish', 'private'], true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The checked target: the WP_Post of scope "post", true for "all" – or the error. Input is not schema-validated
|
* The checked target: the WP_Post of scope "post", true for "all" – or the error. Input is not schema-validated
|
||||||
* yet when permission() runs.
|
* yet when permission() runs.
|
||||||
+6
-6
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace LIT\MCP\Integrations\SuperCache;
|
namespace LIT\MCP\Integrations\FastestCache;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use LIT\MCP\Compatibility\CheckStep;
|
use LIT\MCP\Compatibility\CheckStep;
|
||||||
@@ -10,8 +10,8 @@ use LIT\MCP\Compatibility\ProbeStep;
|
|||||||
|
|
||||||
defined('ABSPATH') || exit;
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
/** The check step only WP Super Cache has (the purge probe, SuperCacheProbe) and its labels. */
|
/** The check step only WP Fastest Cache has (the purge probe, FastestCacheProbe) and its labels. */
|
||||||
final class SuperCacheCheckSteps
|
final class FastestCacheCheckSteps
|
||||||
{
|
{
|
||||||
public const PROBE = 'probe';
|
public const PROBE = 'probe';
|
||||||
|
|
||||||
@@ -19,10 +19,10 @@ final class SuperCacheCheckSteps
|
|||||||
private const LABEL_CACHE_OFF = self::LABEL . ' (Cache ist ausgeschaltet, nicht geprüft)';
|
private const LABEL_CACHE_OFF = self::LABEL . ' (Cache ist ausgeschaltet, nicht geprüft)';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Profile::$extraSteps of WP Super Cache. A switched-off cache is a green step that says so: a skipped step
|
* The Profile::$extraSteps of WP Fastest Cache. A switched-off cache is a green step that says so: a skipped step
|
||||||
* would lock the purge on a site that caches nothing.
|
* would lock the purge on a site that caches nothing.
|
||||||
*
|
*
|
||||||
* @param Closure(): (list<string>|null) $probe SuperCacheProbe::run().
|
* @param Closure(): (list<string>|null) $probe FastestCacheProbe::run().
|
||||||
* @return Closure(bool): list<CheckStep>
|
* @return Closure(bool): list<CheckStep>
|
||||||
*/
|
*/
|
||||||
public static function extra(Closure $probe): Closure
|
public static function extra(Closure $probe): Closure
|
||||||
@@ -35,7 +35,7 @@ final class SuperCacheCheckSteps
|
|||||||
// Reports saved before labels were stored have none.
|
// Reports saved before labels were stored have none.
|
||||||
return $step->label
|
return $step->label
|
||||||
?? ($step->key === self::PROBE ? self::LABEL : null)
|
?? ($step->key === self::PROBE ? self::LABEL : null)
|
||||||
?? CheckStep::genericLabel($step->key, SuperCacheCompatibility::LABEL)
|
?? CheckStep::genericLabel($step->key, FastestCacheCompatibility::LABEL)
|
||||||
?? $step->key;
|
?? $step->key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Integrations\FastestCache;
|
||||||
|
|
||||||
|
use LIT\MCP\Compatibility\Call;
|
||||||
|
use LIT\MCP\Compatibility\ConstantRequirement;
|
||||||
|
use LIT\MCP\Compatibility\GlobalRequirement;
|
||||||
|
use LIT\MCP\Compatibility\Manifest;
|
||||||
|
use LIT\MCP\Compatibility\Profile;
|
||||||
|
use LIT\MCP\Compatibility\Tier;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WP Fastest Cache as a compatibility profile: the two public purge functions FastestCacheListener calls, checked
|
||||||
|
* against 1.5.5 (wpFastestCache.php). Both only fire an action the WpFastestCache instance hooks in its constructor
|
||||||
|
* (singleDeleteCache, deleteCache); the probe exercises singleDeleteCache and checks that hook (FastestCacheProbe).
|
||||||
|
* The cache switch lives in the global $wp_fastest_cache_options (the decoded option WpFastestCache).
|
||||||
|
*/
|
||||||
|
final class FastestCacheCompatibility
|
||||||
|
{
|
||||||
|
public const LABEL = 'WP Fastest Cache';
|
||||||
|
|
||||||
|
public static function profile(): Profile
|
||||||
|
{
|
||||||
|
$all = FastestCacheFeature::cases();
|
||||||
|
|
||||||
|
return new Profile(
|
||||||
|
id: 'wpfc',
|
||||||
|
label: self::LABEL,
|
||||||
|
tier: Tier::LockOnFailure,
|
||||||
|
capabilities: $all,
|
||||||
|
manifests: [new Manifest(
|
||||||
|
'WP Fastest Cache',
|
||||||
|
calls: [
|
||||||
|
new Call('wpfc_clear_post_cache_by_id', 1, [FastestCacheFeature::PurgePost]),
|
||||||
|
new Call('wpfc_clear_all_cache', 0, [FastestCacheFeature::PurgeAll]),
|
||||||
|
new Call('WpFastestCache::singleDeleteCache', 3, [FastestCacheFeature::PurgePost]),
|
||||||
|
new Call('WpFastestCache::getWpContentDir', 1, [FastestCacheFeature::PurgePost]),
|
||||||
|
],
|
||||||
|
constants: [new ConstantRequirement('WPFC_MAIN_PATH', $all)],
|
||||||
|
globals: [
|
||||||
|
new GlobalRequirement('wp_fastest_cache', $all),
|
||||||
|
new GlobalRequirement('wp_fastest_cache_options', $all),
|
||||||
|
]
|
||||||
|
)],
|
||||||
|
active: static fn (): bool => defined('WPFC_MAIN_PATH'),
|
||||||
|
version: static function (): ?string {
|
||||||
|
if (! defined('WPFC_MAIN_PATH')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$header = get_file_data(WPFC_MAIN_PATH . 'wpFastestCache.php', ['Version' => 'Version']);
|
||||||
|
$version = $header['Version'] ?? '';
|
||||||
|
|
||||||
|
return $version === '' ? null : $version;
|
||||||
|
},
|
||||||
|
testedVersions: ['1.5.5'],
|
||||||
|
extraSteps: FastestCacheCheckSteps::extra(static fn (): ?array => (new FastestCacheProbe())->run()),
|
||||||
|
stepLabel: FastestCacheCheckSteps::label(...)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** WPFC caches only while its switch is on (wpFastestCacheStatus in the decoded option, an object or []). */
|
||||||
|
public static function cacheEnabled(): bool
|
||||||
|
{
|
||||||
|
$options = $GLOBALS['wp_fastest_cache_options'] ?? null;
|
||||||
|
|
||||||
|
return is_object($options) && ! empty($options->wpFastestCacheStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-5
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace LIT\MCP\Integrations\SuperCache;
|
namespace LIT\MCP\Integrations\FastestCache;
|
||||||
|
|
||||||
use LIT\MCP\Compatibility\Capability;
|
use LIT\MCP\Compatibility\Capability;
|
||||||
|
|
||||||
@@ -11,11 +11,11 @@ defined('ABSPATH') || exit;
|
|||||||
// The installed PHPCompatibility sniffs predate enums (PHP 8.1) and read their methods as plain functions.
|
// The installed PHPCompatibility sniffs predate enums (PHP 8.1) and read their methods as plain functions.
|
||||||
// phpcs:disable PHPCompatibility.Variables.ForbiddenThisUseContexts.OutsideObjectContext
|
// phpcs:disable PHPCompatibility.Variables.ForbiddenThisUseContexts.OutsideObjectContext
|
||||||
|
|
||||||
/** What the plugin does to WP Super Cache: empty its cache for one post or entirely. */
|
/** What the plugin does to WP Fastest Cache: empty its cache for one post or entirely. */
|
||||||
enum SuperCacheFeature: string implements Capability
|
enum FastestCacheFeature: string implements Capability
|
||||||
{
|
{
|
||||||
case PurgePost = 'wpsc-purge-post';
|
case PurgePost = 'wpfc-purge-post';
|
||||||
case PurgeAll = 'wpsc-purge-all';
|
case PurgeAll = 'wpfc-purge-all';
|
||||||
|
|
||||||
public function id(): string
|
public function id(): string
|
||||||
{
|
{
|
||||||
+16
-15
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace LIT\MCP\Integrations\SuperCache;
|
namespace LIT\MCP\Integrations\FastestCache;
|
||||||
|
|
||||||
use LIT\MCP\Admin\CompatibilityTab;
|
use LIT\MCP\Admin\CompatibilityTab;
|
||||||
use LIT\MCP\ChangeLog\ChangeLog;
|
use LIT\MCP\ChangeLog\ChangeLog;
|
||||||
@@ -13,22 +13,22 @@ use LIT\MCP\Integrations\Integration;
|
|||||||
defined('ABSPATH') || exit;
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WP Super Cache: the compatibility profile (SuperCacheListener purges after writes, wired by Plugin), the cleanup of
|
* WP Fastest Cache: the compatibility profile (FastestCacheListener purges after writes, wired by Plugin), the
|
||||||
* probe pages a dead check left behind and the tool lit/cache-clear – a tool group only while WP Super Cache is
|
* cleanup of probe pages a dead check left behind and the tool lit/cache-clear – a tool group only while WP Fastest
|
||||||
* loaded.
|
* Cache is loaded.
|
||||||
*/
|
*/
|
||||||
final class SuperCacheIntegration implements Integration, ProvidesCompatibility, Bootable
|
final class FastestCacheIntegration implements Integration, ProvidesCompatibility, Bootable
|
||||||
{
|
{
|
||||||
/** @var callable(): bool */
|
/** @var callable(): bool */
|
||||||
private $available;
|
private $available;
|
||||||
|
|
||||||
/** @param callable(): bool|null $available Whether WP Super Cache is loaded; null = WPCACHEHOME is defined. */
|
/** @param callable(): bool|null $available Whether WP Fastest Cache is loaded; null = WPFC_MAIN_PATH is defined. */
|
||||||
public function __construct(
|
public function __construct(
|
||||||
?callable $available,
|
?callable $available,
|
||||||
private SuperCachePurger $purger,
|
private FastestCachePurger $purger,
|
||||||
private ChangeLog $log
|
private ChangeLog $log
|
||||||
) {
|
) {
|
||||||
$this->available = $available ?? static fn (): bool => defined('WPCACHEHOME');
|
$this->available = $available ?? static fn (): bool => defined('WPFC_MAIN_PATH');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
@@ -37,24 +37,25 @@ final class SuperCacheIntegration implements Integration, ProvidesCompatibility,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* lit_mcp_compat_profile_load('wpsc'), before any output: deletes probe pages a dead check left behind
|
* lit_mcp_compat_profile_load('wpfc'), before any output: deletes probe pages a dead check left behind
|
||||||
* (SuperCacheProbe::removeOrphans()). A GET without nonce on purpose: only the plugin's own leftovers, idempotent.
|
* (FastestCacheProbe::removeOrphans()). A GET without nonce on purpose: only the plugin's own leftovers,
|
||||||
|
* idempotent.
|
||||||
*/
|
*/
|
||||||
public function removeOrphans(string $profileId): void
|
public function removeOrphans(string $profileId): void
|
||||||
{
|
{
|
||||||
if ($profileId === 'wpsc' && $this->isAvailable() && current_user_can('manage_options')) {
|
if ($profileId === 'wpfc' && $this->isAvailable() && current_user_can('manage_options')) {
|
||||||
SuperCacheProbe::removeOrphans();
|
FastestCacheProbe::removeOrphans();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function id(): string
|
public function id(): string
|
||||||
{
|
{
|
||||||
return 'wpsc';
|
return 'wpfc';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function label(): string
|
public function label(): string
|
||||||
{
|
{
|
||||||
return SuperCacheCompatibility::LABEL;
|
return FastestCacheCompatibility::LABEL;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isAvailable(): bool
|
public function isAvailable(): bool
|
||||||
@@ -69,6 +70,6 @@ final class SuperCacheIntegration implements Integration, ProvidesCompatibility,
|
|||||||
|
|
||||||
public function compatibilities(): array
|
public function compatibilities(): array
|
||||||
{
|
{
|
||||||
return [SuperCacheCompatibility::profile()];
|
return [FastestCacheCompatibility::profile()];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+25
-24
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace LIT\MCP\Integrations\SuperCache;
|
namespace LIT\MCP\Integrations\FastestCache;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use LIT\MCP\Admin\CompatibilityTab;
|
use LIT\MCP\Admin\CompatibilityTab;
|
||||||
@@ -14,25 +14,26 @@ use Throwable;
|
|||||||
defined('ABSPATH') || exit;
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Empties the WP Super Cache after a write of this plugin (hook lit_mcp_changed). WPSC purges on its own only what
|
* Empties the WP Fastest Cache after a write of this plugin (hook lit_mcp_changed). WPFC purges on its own only on
|
||||||
* goes through wp_update_post: field, term and featured-image updates, options, menu locations, Divi design,
|
* post status transitions (and only with its "new post"/"update post" settings on) and term edits: field, term and
|
||||||
* theme builder and global library modules it never sees.
|
* featured-image updates, options, menu locations, Divi design, theme builder and global library modules it never
|
||||||
|
* sees.
|
||||||
*
|
*
|
||||||
* Purge with trailing run: the listener only collects, one purge runs at the end of the request (shutdown). A post
|
* Purge with trailing run: the listener only collects, one purge runs at the end of the request (shutdown). A post
|
||||||
* of a public post type (not attachment) that still exists: that post (wp_cache_post_edit, which skips drafts and
|
* of a public post type (not attachment) that still exists: that post (wpfc_clear_post_cache_by_id, which also
|
||||||
* honours the user's "clear all on edit" setting). More than one post, or anything else: the whole cache of this
|
* clears the home page, feeds, sitemap, neighbours and term archives of the post). More than one post, or anything
|
||||||
* site, once. The whole-cache purge runs at most every THROTTLE seconds (a client looping over media updates must
|
* else: the whole cache of this site, once. The whole-cache purge runs at most every THROTTLE seconds (a client
|
||||||
* not keep the cache empty): a purge inside the lock time schedules exactly one trailing purge (TRAILING_HOOK) at
|
* looping over media updates must not keep the cache empty): a purge inside the lock time schedules exactly one
|
||||||
* its end (SuperCachePurger, shared with lit/cache-clear). Single-post purges are never throttled. The release/withdraw
|
* trailing purge (TRAILING_HOOK) at its end (FastestCachePurger, shared with lit/cache-clear). Single-post purges are
|
||||||
* entries of the check workflow change nothing on the site and purge nothing; the entries of lit/cache-clear purged
|
* never throttled. The release/withdraw entries of the check workflow change nothing on the site and purge nothing;
|
||||||
* themselves.
|
* the entries of lit/cache-clear purged themselves.
|
||||||
*/
|
*/
|
||||||
final class SuperCacheListener
|
final class FastestCacheListener
|
||||||
{
|
{
|
||||||
public const TRAILING_HOOK = SuperCachePurger::TRAILING_HOOK;
|
public const TRAILING_HOOK = FastestCachePurger::TRAILING_HOOK;
|
||||||
public const PURGED_AT = SuperCachePurger::PURGED_AT;
|
public const PURGED_AT = FastestCachePurger::PURGED_AT;
|
||||||
|
|
||||||
private SuperCachePurger $purger;
|
private FastestCachePurger $purger;
|
||||||
private Closure $postIdFor;
|
private Closure $postIdFor;
|
||||||
/** @var array<int, true> posts changed in this request */
|
/** @var array<int, true> posts changed in this request */
|
||||||
private array $posts = [];
|
private array $posts = [];
|
||||||
@@ -40,12 +41,12 @@ final class SuperCacheListener
|
|||||||
private bool $queued = false;
|
private bool $queued = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defaults are the real WPSC and WordPress functions; closures only for tests.
|
* Defaults are the real WPFC and WordPress functions; closures only for tests.
|
||||||
*
|
*
|
||||||
* @param Closure(SuperCacheFeature): bool $usable Compatibility::usable().
|
* @param Closure(FastestCacheFeature): bool $usable Compatibility::usable().
|
||||||
* @param (Closure(): bool)|null $cacheEnabled WPSC caching switched on ($cache_enabled).
|
* @param (Closure(): bool)|null $cacheEnabled WPFC caching switched on.
|
||||||
* @param (Closure(int): void)|null $purgePost wp_cache_post_edit($postId).
|
* @param (Closure(int): void)|null $purgePost wpfc_clear_post_cache_by_id($postId).
|
||||||
* @param (Closure(int): void)|null $purgeAll wp_cache_clear_cache($blogId), 0 = all sites.
|
* @param (Closure(): void)|null $purgeAll wpfc_clear_all_cache().
|
||||||
* @param (Closure(string, string): ?int)|null $postIdFor Post ID if objectType/objectId name an existing
|
* @param (Closure(string, string): ?int)|null $postIdFor Post ID if objectType/objectId name an existing
|
||||||
* post of a public type but attachment.
|
* post of a public type but attachment.
|
||||||
*/
|
*/
|
||||||
@@ -56,7 +57,7 @@ final class SuperCacheListener
|
|||||||
?Closure $purgeAll = null,
|
?Closure $purgeAll = null,
|
||||||
?Closure $postIdFor = null
|
?Closure $postIdFor = null
|
||||||
) {
|
) {
|
||||||
$this->purger = new SuperCachePurger($usable, $cacheEnabled, $purgePost, $purgeAll);
|
$this->purger = new FastestCachePurger($usable, $cacheEnabled, $purgePost, $purgeAll);
|
||||||
$this->postIdFor = $postIdFor ?? self::publicPostId(...);
|
$this->postIdFor = $postIdFor ?? self::publicPostId(...);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +92,7 @@ final class SuperCacheListener
|
|||||||
add_action('shutdown', [$this, 'flush']);
|
add_action('shutdown', [$this, 'flush']);
|
||||||
}
|
}
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
Log::failure('WP Super Cache purge', $e);
|
Log::failure('WP Fastest Cache purge', $e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +111,7 @@ final class SuperCacheListener
|
|||||||
$this->purger->post($posts[0]);
|
$this->purger->post($posts[0]);
|
||||||
}
|
}
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
Log::failure('WP Super Cache purge', $e);
|
Log::failure('WP Fastest Cache purge', $e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +123,7 @@ final class SuperCacheListener
|
|||||||
$this->purger->everything();
|
$this->purger->everything();
|
||||||
}
|
}
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
Log::failure('WP Super Cache trailing purge', $e);
|
Log::failure('WP Fastest Cache trailing purge', $e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Integrations\FastestCache;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use LIT\MCP\Compatibility\ThrowawayPost;
|
||||||
|
use LIT\MCP\Log;
|
||||||
|
use WP_Post;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Purge probe of the WP Fastest Cache check: a marked throwaway page gets a cache file where WPFC keeps the cache of
|
||||||
|
* that page (cache/all/<path of its address>/index.html, WPFC's own getWpContentDir), WpFastestCache::
|
||||||
|
* singleDeleteCache – the method behind wpfc_clear_post_cache_by_id, which FastestCacheListener calls – runs for it,
|
||||||
|
* and the file must be gone (WPFC moves the directory to cache/tmpWpfc, which it empties itself). The probe also
|
||||||
|
* checks that WPFC still hooks wpfc_clear_post_cache_by_id to that method: without the hook the public function does
|
||||||
|
* nothing, silently. The page and the cache directory are always removed (ThrowawayPost); entries a dead check left
|
||||||
|
* behind are deleted by removeOrphans(). The whole-cache purge (wpfc_clear_all_cache) is not probed: it would empty
|
||||||
|
* the live cache of the site on every check. Runs only from the check route (Compatibility::check).
|
||||||
|
*
|
||||||
|
* The purge is narrowed to the page ($to_clear_parents = false; otherwise WPFC also empties the home page, author
|
||||||
|
* page, sitemap, neighbours and term archives). NOT avoidable: WPFC still empties the two feed caches and purges the
|
||||||
|
* external caches configured in WPFC (Varnish, Cloudflare) on every call; creating and deleting the page fire the
|
||||||
|
* hooks of every other plugin (Divi 5 empties caches on insert and delete of a page).
|
||||||
|
*
|
||||||
|
* The page stays a draft; WPFC takes the address a draft would have once published. The draft gets a fixed slug
|
||||||
|
* straight in the database, without hooks: an empty slug would make its address the home page.
|
||||||
|
*
|
||||||
|
* shortcut: needs a pretty permalink structure (a "?page_id=" address has no cache directory, nothing to probe then).
|
||||||
|
*/
|
||||||
|
final class FastestCacheProbe
|
||||||
|
{
|
||||||
|
private const SLUG_PREFIX = 'lit-mcp-check-';
|
||||||
|
private const PURGE_HOOK = 'wpfc_clear_post_cache_by_id';
|
||||||
|
|
||||||
|
private const ABORTED = 'Fastest-Cache-Probe abgebrochen (Details im PHP-Error-Log).';
|
||||||
|
private const NOT_HOOKED = 'WP Fastest Cache verarbeitet wpfc_clear_post_cache_by_id nicht mehr; das Leeren eines '
|
||||||
|
. 'Beitrags bliebe wirkungslos.';
|
||||||
|
private const NOT_PURGED = 'Die Testdatei im Cache blieb nach dem Leeren durch WP Fastest Cache bestehen.';
|
||||||
|
private const NO_SLUG = 'Die Adresse der Testseite konnte nicht gesetzt werden (Details im PHP-Error-Log).';
|
||||||
|
private const NOT_WRITTEN = 'Die Testdatei konnte nicht im Cache-Verzeichnis angelegt werden '
|
||||||
|
. '(Details im PHP-Error-Log).';
|
||||||
|
|
||||||
|
private Closure $enabled;
|
||||||
|
private Closure $hooked;
|
||||||
|
private Closure $cacheDir;
|
||||||
|
private Closure $purge;
|
||||||
|
private Closure $setSlug;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defaults are the real WPFC and WordPress calls; closures only for tests.
|
||||||
|
*
|
||||||
|
* @param (Closure(): bool)|null $enabled WPFC caching switched on.
|
||||||
|
* @param (Closure(): bool)|null $hooked WPFC hooks wpfc_clear_post_cache_by_id to singleDeleteCache.
|
||||||
|
* @param (Closure(int): ?string)|null $cacheDir Cache directory (with trailing slash) of a page; null = none.
|
||||||
|
* @param (Closure(int): void)|null $purge singleDeleteCache for this page only.
|
||||||
|
* @param (Closure(int): bool)|null $setSlug Sets the probe slug without hooks; false = not changed.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
?Closure $enabled = null,
|
||||||
|
?Closure $hooked = null,
|
||||||
|
?Closure $cacheDir = null,
|
||||||
|
?Closure $purge = null,
|
||||||
|
?Closure $setSlug = null
|
||||||
|
) {
|
||||||
|
$this->enabled = $enabled ?? FastestCacheCompatibility::cacheEnabled(...);
|
||||||
|
$this->hooked = $hooked ?? static fn (): bool
|
||||||
|
=> has_action(self::PURGE_HOOK, [$GLOBALS['wp_fastest_cache'] ?? null, 'singleDeleteCache']) !== false;
|
||||||
|
$this->cacheDir = $cacheDir ?? self::cacheDirFor(...);
|
||||||
|
$this->purge = $purge ?? static function (int $id): void {
|
||||||
|
$GLOBALS['wp_fastest_cache']->singleDeleteCache(false, $id, false);
|
||||||
|
};
|
||||||
|
$this->setSlug = $setSlug ?? self::writeSlug(...);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<string>|null German problems, [] = green; null = nothing to probe (cache off, no usable path). */
|
||||||
|
public function run(): ?array
|
||||||
|
{
|
||||||
|
if (! ($this->enabled)()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (! ($this->hooked)()) {
|
||||||
|
return [self::NOT_HOOKED];
|
||||||
|
}
|
||||||
|
|
||||||
|
$unchecked = false;
|
||||||
|
$problems = self::throwaway()->run('page', function (int $id) use (&$unchecked): array {
|
||||||
|
if (! ($this->setSlug)($id)) {
|
||||||
|
Log::line('wpfc probe: the database did not set the slug of page ' . $id);
|
||||||
|
|
||||||
|
return [self::NO_SLUG];
|
||||||
|
}
|
||||||
|
$dir = ($this->cacheDir)($id);
|
||||||
|
if ($dir === null) {
|
||||||
|
$unchecked = true;
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->exercise($id, $dir);
|
||||||
|
});
|
||||||
|
|
||||||
|
return $unchecked && $problems === [] ? null : $problems;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<int> IDs of probe pages a dead check left behind, deleted now. */
|
||||||
|
public static function removeOrphans(): array
|
||||||
|
{
|
||||||
|
return self::throwaway()->removeOrphans('page');
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function throwaway(): ThrowawayPost
|
||||||
|
{
|
||||||
|
return new ThrowawayPost(
|
||||||
|
ThrowawayPost::MARKER,
|
||||||
|
ThrowawayPost::TITLE,
|
||||||
|
self::ABORTED,
|
||||||
|
'Die Probe-Seite konnte nicht angelegt werden (Details im PHP-Error-Log).',
|
||||||
|
'Die Probe-Seite %d konnte nicht gelöscht werden; bitte endgültig löschen (Titel „'
|
||||||
|
. ThrowawayPost::TITLE . ' …“).',
|
||||||
|
'wpfc probe'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WPFC's cache directory of the probe page, computed the way singleDeleteCache does (the path of the published
|
||||||
|
* address), or null when it is not exactly <path>/<probe slug>/: a plain "?page_id=" address, a path with "..".
|
||||||
|
*/
|
||||||
|
public static function cacheDirFor(int $postId): ?string
|
||||||
|
{
|
||||||
|
$post = get_post($postId);
|
||||||
|
if (! $post instanceof WP_Post) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$published = clone $post;
|
||||||
|
$published->post_status = 'publish';
|
||||||
|
$address = urldecode((string) get_permalink($published));
|
||||||
|
if (preg_match('#^https?://[^/]+/(.+)$#', $address, $match) !== 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$path = trim($match[1], '/');
|
||||||
|
$slug = self::SLUG_PREFIX . $postId;
|
||||||
|
if (str_contains($path, '..') || ($path !== $slug && ! str_ends_with($path, '/' . $slug))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $GLOBALS['wp_fastest_cache']->getWpContentDir('/cache/all/') . $path . '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
// The probe's own file in WPFC's own cache directory, written the way WPFC does it (plain PHP, no WP_Filesystem).
|
||||||
|
// phpcs:disable WordPress.WP.AlternativeFunctions
|
||||||
|
|
||||||
|
/** @return list<string> */
|
||||||
|
private function exercise(int $id, string $dir): array
|
||||||
|
{
|
||||||
|
$file = $dir . 'index.html';
|
||||||
|
try {
|
||||||
|
if (! wp_mkdir_p($dir) || file_put_contents($file, '<!-- lit-mcp purge probe -->') === false) {
|
||||||
|
Log::line('wpfc probe: could not write the test file ' . $file);
|
||||||
|
|
||||||
|
return [self::NOT_WRITTEN];
|
||||||
|
}
|
||||||
|
|
||||||
|
($this->purge)($id);
|
||||||
|
clearstatcache(true, $file);
|
||||||
|
if (! file_exists($file)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::line('wpfc probe: singleDeleteCache left the test file ' . $file . ' in place.');
|
||||||
|
|
||||||
|
return [self::NOT_PURGED];
|
||||||
|
} finally {
|
||||||
|
@unlink($file);
|
||||||
|
@rmdir($dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// phpcs:enable WordPress.WP.AlternativeFunctions
|
||||||
|
|
||||||
|
/** A fixed slug (an empty one would make the page's address the home page) – no hooks, no clean_post_cache. */
|
||||||
|
private static function writeSlug(int $id): bool
|
||||||
|
{
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$rows = $wpdb->update($wpdb->posts, ['post_name' => self::SLUG_PREFIX . $id], ['ID' => $id]);
|
||||||
|
wp_cache_delete($id, 'posts');
|
||||||
|
|
||||||
|
return $rows !== false && (int) $rows > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
-23
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace LIT\MCP\Integrations\SuperCache;
|
namespace LIT\MCP\Integrations\FastestCache;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use LIT\MCP\Log;
|
use LIT\MCP\Log;
|
||||||
@@ -10,15 +10,15 @@ use LIT\MCP\Log;
|
|||||||
defined('ABSPATH') || exit;
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The two purges of WP Super Cache, shared by SuperCacheListener (after every write) and lit/cache-clear: the
|
* The two purges of WP Fastest Cache, shared by FastestCacheListener (after every write) and lit/cache-clear: the
|
||||||
* capability check, the 30 s throttle of the whole-cache purge with its one trailing purge (TRAILING_HOOK) and the
|
* capability check, the 30 s throttle of the whole-cache purge with its one trailing purge (TRAILING_HOOK) and the
|
||||||
* stamp (PURGED_AT). The state lives in the transient and the cron event, so any number of instances share it.
|
* stamp (PURGED_AT). The state lives in the transient and the cron event, so any number of instances share it.
|
||||||
*/
|
*/
|
||||||
final class SuperCachePurger
|
final class FastestCachePurger
|
||||||
{
|
{
|
||||||
public const TRAILING_HOOK = 'lit_mcp_wpsc_trailing_purge';
|
public const TRAILING_HOOK = 'lit_mcp_wpfc_trailing_purge';
|
||||||
/** Transient with the time of the last whole-cache purge (also in Uninstaller). */
|
/** Transient with the time of the last whole-cache purge (also in Uninstaller). */
|
||||||
public const PURGED_AT = 'lit_mcp_wpsc_purged_at';
|
public const PURGED_AT = 'lit_mcp_wpfc_purged_at';
|
||||||
/** Seconds between two whole-cache purges. */
|
/** Seconds between two whole-cache purges. */
|
||||||
private const THROTTLE = 30;
|
private const THROTTLE = 30;
|
||||||
|
|
||||||
@@ -33,12 +33,12 @@ final class SuperCachePurger
|
|||||||
private bool $warned = false;
|
private bool $warned = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defaults are the real WPSC functions; closures only for tests.
|
* Defaults are the real WPFC functions; closures only for tests.
|
||||||
*
|
*
|
||||||
* @param Closure(SuperCacheFeature): bool $usable Compatibility::usable().
|
* @param Closure(FastestCacheFeature): bool $usable Compatibility::usable().
|
||||||
* @param (Closure(): bool)|null $cacheEnabled WPSC caching switched on ($cache_enabled).
|
* @param (Closure(): bool)|null $cacheEnabled WPFC caching switched on.
|
||||||
* @param (Closure(int): void)|null $purgePost wp_cache_post_edit($postId).
|
* @param (Closure(int): void)|null $purgePost wpfc_clear_post_cache_by_id($postId).
|
||||||
* @param (Closure(int): void)|null $purgeAll wp_cache_clear_cache($blogId), 0 = all sites.
|
* @param (Closure(): void)|null $purgeAll wpfc_clear_all_cache().
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
Closure $usable,
|
Closure $usable,
|
||||||
@@ -47,12 +47,12 @@ final class SuperCachePurger
|
|||||||
?Closure $purgeAll = null
|
?Closure $purgeAll = null
|
||||||
) {
|
) {
|
||||||
$this->usable = $usable;
|
$this->usable = $usable;
|
||||||
$this->cacheEnabled = $cacheEnabled ?? static fn (): bool => ! empty($GLOBALS['cache_enabled']);
|
$this->cacheEnabled = $cacheEnabled ?? FastestCacheCompatibility::cacheEnabled(...);
|
||||||
$this->purgePost = $purgePost ?? static function (int $id): void {
|
$this->purgePost = $purgePost ?? static function (int $id): void {
|
||||||
wp_cache_post_edit($id);
|
wpfc_clear_post_cache_by_id($id);
|
||||||
};
|
};
|
||||||
$this->purgeAll = $purgeAll ?? static function (int $blog): void {
|
$this->purgeAll = $purgeAll ?? static function (): void {
|
||||||
wp_cache_clear_cache($blog);
|
wpfc_clear_all_cache();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,10 +80,10 @@ final class SuperCachePurger
|
|||||||
return is_numeric($last) ? (int) $last : null;
|
return is_numeric($last) ? (int) $last : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** wp_cache_post_edit for one post; false = locked. Never throttled. */
|
/** wpfc_clear_post_cache_by_id for one post; false = locked. Never throttled. */
|
||||||
public function post(int $postId): bool
|
public function post(int $postId): bool
|
||||||
{
|
{
|
||||||
if (! $this->allowed(SuperCacheFeature::PurgePost)) {
|
if (! $this->allowed(FastestCacheFeature::PurgePost)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
($this->purgePost)($postId);
|
($this->purgePost)($postId);
|
||||||
@@ -93,13 +93,13 @@ final class SuperCachePurger
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Whole cache, at most every THROTTLE seconds. Inside the lock time one trailing purge is scheduled (cron events
|
* Whole cache, at most every THROTTLE seconds. Inside the lock time one trailing purge is scheduled (cron events
|
||||||
* are per site: the callback runs on this blog, no blog switch, wp_cache_clear_cache takes the blog ID).
|
* are per site: the callback runs on this blog).
|
||||||
*
|
*
|
||||||
* @return self::PURGED|self::THROTTLED|self::LOCKED
|
* @return self::PURGED|self::THROTTLED|self::LOCKED
|
||||||
*/
|
*/
|
||||||
public function everything(): string
|
public function everything(): string
|
||||||
{
|
{
|
||||||
if (! $this->allowed(SuperCacheFeature::PurgeAll)) {
|
if (! $this->allowed(FastestCacheFeature::PurgeAll)) {
|
||||||
return self::LOCKED;
|
return self::LOCKED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,22 +112,20 @@ final class SuperCachePurger
|
|||||||
return self::THROTTLED;
|
return self::THROTTLED;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Known and unavoidable on a multisite: WPSC also empties the shared blogs/ meta directory of all sites
|
($this->purgeAll)();
|
||||||
// in its blog branch (wp-cache-phase2.php, wp_cache_clear_cache).
|
|
||||||
($this->purgeAll)(is_multisite() ? get_current_blog_id() : 0);
|
|
||||||
set_transient(self::PURGED_AT, time(), self::THROTTLE);
|
set_transient(self::PURGED_AT, time(), self::THROTTLE);
|
||||||
|
|
||||||
return self::PURGED;
|
return self::PURGED;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function allowed(SuperCacheFeature $feature): bool
|
private function allowed(FastestCacheFeature $feature): bool
|
||||||
{
|
{
|
||||||
if (($this->usable)($feature)) {
|
if (($this->usable)($feature)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (! $this->warned) {
|
if (! $this->warned) {
|
||||||
$this->warned = true;
|
$this->warned = true;
|
||||||
Log::line(sprintf('WP Super Cache not purged: %s not usable (see tab Compatibility)', $feature->id()));
|
Log::line(sprintf('WP Fastest Cache not purged: %s not usable (see tab Compatibility)', $feature->id()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace LIT\MCP\Integrations\SuperCache;
|
|
||||||
|
|
||||||
use LIT\MCP\Compatibility\Call;
|
|
||||||
use LIT\MCP\Compatibility\ConstantRequirement;
|
|
||||||
use LIT\MCP\Compatibility\GlobalRequirement;
|
|
||||||
use LIT\MCP\Compatibility\Manifest;
|
|
||||||
use LIT\MCP\Compatibility\Profile;
|
|
||||||
use LIT\MCP\Compatibility\Tier;
|
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* WP Super Cache as a compatibility profile: the two purge functions SuperCacheListener calls, checked against
|
|
||||||
* 3.1.5 (wp-cache-phase2.php). wp_cache_post_edit is no documented API: if a release drops it, only the purge is
|
|
||||||
* locked (visible in the tab and in lit-site-status). Both functions read the global $cache_path, which wp-cache.php
|
|
||||||
* sets on load from wp-cache-config.php or its sample; wp_cache_post_edit also reads $blog_cache_dir
|
|
||||||
* (wp-cache-base.php). The check adds a purge probe (SuperCacheProbe).
|
|
||||||
*/
|
|
||||||
final class SuperCacheCompatibility
|
|
||||||
{
|
|
||||||
public const LABEL = 'WP Super Cache';
|
|
||||||
|
|
||||||
public static function profile(): Profile
|
|
||||||
{
|
|
||||||
$all = SuperCacheFeature::cases();
|
|
||||||
|
|
||||||
return new Profile(
|
|
||||||
id: 'wpsc',
|
|
||||||
label: self::LABEL,
|
|
||||||
tier: Tier::LockOnFailure,
|
|
||||||
capabilities: $all,
|
|
||||||
manifests: [new Manifest(
|
|
||||||
'WP Super Cache',
|
|
||||||
calls: [
|
|
||||||
new Call('wp_cache_post_edit', 1, [SuperCacheFeature::PurgePost]),
|
|
||||||
new Call('wp_cache_clear_cache', 1, [SuperCacheFeature::PurgeAll]),
|
|
||||||
],
|
|
||||||
constants: [new ConstantRequirement('WPCACHEHOME', $all)],
|
|
||||||
globals: [
|
|
||||||
new GlobalRequirement('cache_path', $all),
|
|
||||||
new GlobalRequirement('blog_cache_dir', [SuperCacheFeature::PurgePost]),
|
|
||||||
]
|
|
||||||
)],
|
|
||||||
active: static fn (): bool => defined('WPCACHEHOME'),
|
|
||||||
version: static function (): ?string {
|
|
||||||
if (! defined('WPCACHEHOME')) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// WPSC_VERSION_ID is a build counter, not the plugin version: read the plugin header.
|
|
||||||
$version = get_file_data(WPCACHEHOME . 'wp-cache.php', ['Version' => 'Version'])['Version'] ?? '';
|
|
||||||
|
|
||||||
return $version === '' ? null : $version;
|
|
||||||
},
|
|
||||||
testedVersions: ['3.1.5'],
|
|
||||||
extraSteps: SuperCacheCheckSteps::extra(static fn (): ?array => (new SuperCacheProbe())->run()),
|
|
||||||
stepLabel: SuperCacheCheckSteps::label(...)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,230 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace LIT\MCP\Integrations\SuperCache;
|
|
||||||
|
|
||||||
use Closure;
|
|
||||||
use LIT\MCP\Compatibility\ThrowawayPost;
|
|
||||||
use LIT\MCP\Log;
|
|
||||||
use Throwable;
|
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Purge probe of the WP Super Cache check: a marked throwaway page gets a cache file at the exact place WP Super
|
|
||||||
* Cache keeps the cache of that page (its own path logic), wp_cache_post_edit() – what SuperCacheListener calls –
|
|
||||||
* runs, and the file must be gone (renamed to ".needs-rebuild" counts as gone: that is how WPSC marks it). The
|
|
||||||
* file, its directory and the page are always removed (ThrowawayPost). Entries a dead check left behind are
|
|
||||||
* deleted by removeOrphans(). The whole-cache purge (wp_cache_clear_cache) is not probed: it would empty the live
|
|
||||||
* cache of the site on every check. Runs only from the check route (Compatibility::check).
|
|
||||||
*
|
|
||||||
* The purge of the live site is narrowed for the call: $wp_cache_clear_on_post_edit is 0 (otherwise WPSC empties
|
|
||||||
* the whole cache) and the filter wpsc_delete_related_pages_on_edit returns false (otherwise wp_cache_post_change
|
|
||||||
* clears the home page and page/ as well); both are put back exactly, also when WPSC throws. What WPSC still does
|
|
||||||
* for a page then: it empties the cache directory of that page (wp_cache_post_id_gc, $all = false: no page/, no
|
|
||||||
* home page), deletes the legacy wp-cache meta files of that page and any unreadable legacy meta file of this site
|
|
||||||
* (stray files, unusable anyway), fires the action gc_cache ("prune"/"rebuild" with the page's address – other
|
|
||||||
* plugins hooking it, e.g. preloaders, see the probe page) and, for each public taxonomy term of the page,
|
|
||||||
* wpsc_delete_post_archives() clears the term archive's cache. A fresh page has no terms unless another plugin
|
|
||||||
* gives pages a default term; then that one archive loses its cache file on every check.
|
|
||||||
*
|
|
||||||
* NOT avoidable: creating, changing and deleting the page are real WordPress calls, and other plugins react to them
|
|
||||||
* like to any editor's page. Divi 5 (Loop module, LoopHooks → et_core_clear_wp_cache → wp_cache_clear_cache) empties
|
|
||||||
* the whole WPSC cache on every insert and delete of a page – seen on the DDEV site, so on a Divi site every check
|
|
||||||
* empties the live cache regardless of what the probe does itself.
|
|
||||||
*
|
|
||||||
* The page is a draft; wp_cache_post_edit skips drafts, so for the call it is a private page – its status is set
|
|
||||||
* straight in the database, without hooks: WPSC itself hooks clean_post_cache/edit_post to wp_cache_post_edit and
|
|
||||||
* remembers the last post per request, so a real status change would use up the call before the probe makes it.
|
|
||||||
*
|
|
||||||
* The cache directory is WPSC's own (get_current_url_supercache_dir), which takes the host from the request. It is
|
|
||||||
* used only when it is exactly <home host directory>/<probe slug>/; any other host (www or not) makes the probe
|
|
||||||
* "not checked" instead of looking in the wrong place.
|
|
||||||
*
|
|
||||||
* shortcut: needs a pretty permalink structure with WP in the home address' directory (WPSC does not purge a "?p="
|
|
||||||
* address either, nothing to probe then).
|
|
||||||
*/
|
|
||||||
final class SuperCacheProbe
|
|
||||||
{
|
|
||||||
private const SLUG_PREFIX = 'lit-mcp-check-';
|
|
||||||
private const CLEAR_ON_EDIT = 'wp_cache_clear_on_post_edit';
|
|
||||||
private const RELATED_PAGES_FILTER = 'wpsc_delete_related_pages_on_edit';
|
|
||||||
/** Files WPSC writes per page and protocol, and what a purge renames them to. */
|
|
||||||
private const CACHE_FILES = ['index.html', 'index-https.html'];
|
|
||||||
private const REBUILD_SUFFIX = '.needs-rebuild';
|
|
||||||
|
|
||||||
private const ABORTED = 'Super-Cache-Probe abgebrochen (Details im PHP-Error-Log).';
|
|
||||||
private const NOT_PURGED = 'Die Testdatei im Cache blieb nach dem Leeren durch WP Super Cache bestehen.';
|
|
||||||
private const NOT_PRIVATE = 'Der Status der Testseite konnte nicht gesetzt werden (Details im PHP-Error-Log).';
|
|
||||||
private const NOT_WRITTEN = 'Die Testdatei konnte nicht im Cache-Verzeichnis angelegt werden '
|
|
||||||
. '(Details im PHP-Error-Log).';
|
|
||||||
|
|
||||||
private Closure $enabled;
|
|
||||||
private Closure $cacheDir;
|
|
||||||
private Closure $purge;
|
|
||||||
private Closure $setStatus;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Defaults are the real WPSC and WordPress calls; closures only for tests.
|
|
||||||
*
|
|
||||||
* @param (Closure(): bool)|null $enabled WPSC caching switched on ($cache_enabled).
|
|
||||||
* @param (Closure(int): ?string)|null $cacheDir Cache directory (with trailing slash) of a page; null = none.
|
|
||||||
* @param (Closure(int): void)|null $purge wp_cache_post_edit($postId).
|
|
||||||
* @param (Closure(int, string): bool)|null $setStatus Sets status (and the slug) of the page without hooks;
|
|
||||||
* false = the database did not change it.
|
|
||||||
*/
|
|
||||||
public function __construct(
|
|
||||||
?Closure $enabled = null,
|
|
||||||
?Closure $cacheDir = null,
|
|
||||||
?Closure $purge = null,
|
|
||||||
?Closure $setStatus = null
|
|
||||||
) {
|
|
||||||
$this->enabled = $enabled ?? static fn (): bool => ! empty($GLOBALS['cache_enabled']);
|
|
||||||
$this->cacheDir = $cacheDir ?? self::cacheDirFor(...);
|
|
||||||
$this->purge = $purge ?? self::purgeThisPageOnly(...);
|
|
||||||
$this->setStatus = $setStatus ?? self::writeStatus(...);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return list<string>|null German problems, [] = green; null = nothing to probe (cache off, no usable path). */
|
|
||||||
public function run(): ?array
|
|
||||||
{
|
|
||||||
if (! ($this->enabled)()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$unchecked = false;
|
|
||||||
$problems = self::throwaway()->run('page', function (int $id) use (&$unchecked): array {
|
|
||||||
try {
|
|
||||||
if (! ($this->setStatus)($id, 'private')) {
|
|
||||||
Log::line('wpsc probe: the database did not set page ' . $id . ' to private.');
|
|
||||||
|
|
||||||
return [self::NOT_PRIVATE];
|
|
||||||
}
|
|
||||||
$dir = ($this->cacheDir)($id);
|
|
||||||
if ($dir === null) {
|
|
||||||
$unchecked = true;
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->exercise($id, $dir);
|
|
||||||
} finally {
|
|
||||||
$this->resetToDraft($id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return $unchecked && $problems === [] ? null : $problems;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return list<int> IDs of probe pages a dead check left behind (draft or private), deleted now. */
|
|
||||||
public static function removeOrphans(): array
|
|
||||||
{
|
|
||||||
return self::throwaway()->removeOrphans('page');
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function throwaway(): ThrowawayPost
|
|
||||||
{
|
|
||||||
return new ThrowawayPost(
|
|
||||||
ThrowawayPost::MARKER,
|
|
||||||
ThrowawayPost::TITLE,
|
|
||||||
self::ABORTED,
|
|
||||||
'Die Probe-Seite konnte nicht angelegt werden (Details im PHP-Error-Log).',
|
|
||||||
'Die Probe-Seite %d konnte nicht gelöscht werden; bitte endgültig löschen (Titel „'
|
|
||||||
. ThrowawayPost::TITLE . ' …“).',
|
|
||||||
'wpsc probe'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Must not hide the result of the probe or end the check: the page is deleted right after anyway. */
|
|
||||||
private function resetToDraft(int $id): void
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
if (! ($this->setStatus)($id, 'draft')) {
|
|
||||||
Log::line('wpsc probe: the database did not set page ' . $id . ' back to draft.');
|
|
||||||
}
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
Log::failure('wpsc probe', $e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* WPSC's own cache directory of the probe page, or null when the probe must not use it: anything but exactly
|
|
||||||
* <home host directory>/<probe slug>/ – another host than the home URL's (WPSC takes it from the request), a
|
|
||||||
* plain "?p=" address or an empty slug (both resolve to the home page's directory), a path with "..".
|
|
||||||
*/
|
|
||||||
public static function cacheDirFor(int $postId): ?string
|
|
||||||
{
|
|
||||||
$home = (string) get_supercache_dir();
|
|
||||||
$dir = (string) get_current_url_supercache_dir($postId);
|
|
||||||
|
|
||||||
return $home !== '' && $dir === $home . self::SLUG_PREFIX . $postId . '/' ? $dir : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** wp_cache_post_edit for the probe page only: see the class comment. Global and filter are put back exactly. */
|
|
||||||
private static function purgeThisPageOnly(int $id): void
|
|
||||||
{
|
|
||||||
$hadGlobal = array_key_exists(self::CLEAR_ON_EDIT, $GLOBALS);
|
|
||||||
$before = $GLOBALS[self::CLEAR_ON_EDIT] ?? null;
|
|
||||||
// A closure of its own: remove_filter must not take away a "__return_false" another plugin added.
|
|
||||||
$off = static fn (): bool => false;
|
|
||||||
try {
|
|
||||||
$GLOBALS[self::CLEAR_ON_EDIT] = 0;
|
|
||||||
add_filter(self::RELATED_PAGES_FILTER, $off, PHP_INT_MAX);
|
|
||||||
wp_cache_post_edit($id);
|
|
||||||
} finally {
|
|
||||||
remove_filter(self::RELATED_PAGES_FILTER, $off, PHP_INT_MAX);
|
|
||||||
if ($hadGlobal) {
|
|
||||||
$GLOBALS[self::CLEAR_ON_EDIT] = $before;
|
|
||||||
} else {
|
|
||||||
unset($GLOBALS[self::CLEAR_ON_EDIT]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The probe's own file in WPSC's own cache directory, written the way WPSC does it (plain PHP, no WP_Filesystem).
|
|
||||||
// phpcs:disable WordPress.WP.AlternativeFunctions
|
|
||||||
|
|
||||||
/** @return list<string> */
|
|
||||||
private function exercise(int $id, string $dir): array
|
|
||||||
{
|
|
||||||
$file = $dir . 'index.html';
|
|
||||||
try {
|
|
||||||
if (! wp_mkdir_p($dir) || file_put_contents($file, '<!-- lit-mcp purge probe -->') === false) {
|
|
||||||
Log::line('wpsc probe: could not write the test file ' . $file);
|
|
||||||
|
|
||||||
return [self::NOT_WRITTEN];
|
|
||||||
}
|
|
||||||
|
|
||||||
($this->purge)($id);
|
|
||||||
clearstatcache(true, $file);
|
|
||||||
if (! file_exists($file)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
Log::line('wpsc probe: wp_cache_post_edit left the test file ' . $file . ' in place.');
|
|
||||||
|
|
||||||
return [self::NOT_PURGED];
|
|
||||||
} finally {
|
|
||||||
foreach (self::CACHE_FILES as $name) {
|
|
||||||
@unlink($dir . $name);
|
|
||||||
@unlink($dir . $name . self::REBUILD_SUFFIX);
|
|
||||||
}
|
|
||||||
@rmdir($dir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// phpcs:enable WordPress.WP.AlternativeFunctions
|
|
||||||
|
|
||||||
/** Status and a fixed slug (an empty one would make the page's address the home page) – no hooks, no clean_post_cache. */
|
|
||||||
private static function writeStatus(int $id, string $status): bool
|
|
||||||
{
|
|
||||||
global $wpdb;
|
|
||||||
|
|
||||||
$columns = ['post_status' => $status, 'post_name' => self::SLUG_PREFIX . $id];
|
|
||||||
$rows = $wpdb->update($wpdb->posts, $columns, ['ID' => $id]);
|
|
||||||
wp_cache_delete($id, 'posts');
|
|
||||||
|
|
||||||
return $rows !== false && (int) $rows > 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+9
-9
@@ -37,10 +37,10 @@ use LIT\MCP\Integrations\Pods\PodsCompatibility;
|
|||||||
use LIT\MCP\Integrations\Pods\PodsFeature;
|
use LIT\MCP\Integrations\Pods\PodsFeature;
|
||||||
use LIT\MCP\Integrations\Pods\PodsFieldSource;
|
use LIT\MCP\Integrations\Pods\PodsFieldSource;
|
||||||
use LIT\MCP\Integrations\Pods\PodsIntegration;
|
use LIT\MCP\Integrations\Pods\PodsIntegration;
|
||||||
use LIT\MCP\Integrations\SuperCache\SuperCacheFeature;
|
use LIT\MCP\Integrations\FastestCache\FastestCacheFeature;
|
||||||
use LIT\MCP\Integrations\SuperCache\SuperCacheIntegration;
|
use LIT\MCP\Integrations\FastestCache\FastestCacheIntegration;
|
||||||
use LIT\MCP\Integrations\SuperCache\SuperCachePurger;
|
use LIT\MCP\Integrations\FastestCache\FastestCachePurger;
|
||||||
use LIT\MCP\Integrations\SuperCache\SuperCacheListener;
|
use LIT\MCP\Integrations\FastestCache\FastestCacheListener;
|
||||||
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||||||
use LIT\MCP\Integrations\WordPress\Schema\FieldSource;
|
use LIT\MCP\Integrations\WordPress\Schema\FieldSource;
|
||||||
use LIT\MCP\Integrations\WordPress\Schema\RegisteredMetaSource;
|
use LIT\MCP\Integrations\WordPress\Schema\RegisteredMetaSource;
|
||||||
@@ -100,8 +100,8 @@ final class Plugin
|
|||||||
// Integration-owned hooks (Divi: upgrader hook, check route, CLI command, tab "Divi") – before the settings
|
// Integration-owned hooks (Divi: upgrader hook, check route, CLI command, tab "Divi") – before the settings
|
||||||
// page reads its tabs.
|
// page reads its tabs.
|
||||||
$registry->boot();
|
$registry->boot();
|
||||||
// After every write of the plugin: empty the WP Super Cache where WordPress itself would not.
|
// After every write of the plugin: empty the WP Fastest Cache where WordPress itself would not.
|
||||||
SuperCacheListener::create($registry->compatibility())->register();
|
FastestCacheListener::create($registry->compatibility())->register();
|
||||||
// The admin check of every compatibility profile (POST lit-mcp/v1/<id>-check), REST like the MCP calls.
|
// The admin check of every compatibility profile (POST lit-mcp/v1/<id>-check), REST like the MCP calls.
|
||||||
(new CheckRoute($registry->compatibility()))->register();
|
(new CheckRoute($registry->compatibility()))->register();
|
||||||
|
|
||||||
@@ -216,11 +216,11 @@ final class Plugin
|
|||||||
$divi->library(),
|
$divi->library(),
|
||||||
$divi->themeBuilder(),
|
$divi->themeBuilder(),
|
||||||
new PodsIntegration(),
|
new PodsIntegration(),
|
||||||
new SuperCacheIntegration(
|
new FastestCacheIntegration(
|
||||||
null,
|
null,
|
||||||
// Asked at call time: the compatibility exists only once the registry does.
|
// Asked at call time: the compatibility exists only once the registry does.
|
||||||
new SuperCachePurger(
|
new FastestCachePurger(
|
||||||
static function (SuperCacheFeature $feature) use (&$registry): bool {
|
static function (FastestCacheFeature $feature) use (&$registry): bool {
|
||||||
return $registry instanceof IntegrationRegistry && $registry->compatibility()->usable($feature);
|
return $registry instanceof IntegrationRegistry && $registry->compatibility()->usable($feature);
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
|||||||
+6
-6
@@ -19,11 +19,11 @@ defined('ABSPATH') || exit;
|
|||||||
* Limits::OPTION, ChangeLog\ChangeLogInstaller::VERSION_OPTION, Integrations\Divi\Release\
|
* Limits::OPTION, ChangeLog\ChangeLogInstaller::VERSION_OPTION, Integrations\Divi\Release\
|
||||||
* lit_mcp_<profile>_releases (Compatibility\ReleaseStore), lit_mcp_<profile>_check_report
|
* lit_mcp_<profile>_releases (Compatibility\ReleaseStore), lit_mcp_<profile>_check_report
|
||||||
* (Compatibility\CheckReportStore) and lit_mcp_<profile>_check_lock (Admin\CheckRoute) for the
|
* (Compatibility\CheckReportStore) and lit_mcp_<profile>_check_lock (Admin\CheckRoute) for the
|
||||||
* profiles divi, pods and wpsc (an explicit list: a LIKE pattern could hit foreign options),
|
* profiles divi, pods and wpfc (an explicit list: a LIKE pattern could hit foreign options),
|
||||||
* Integrations\Divi\Design\DesignLock::option(), Admin\ConnectionStatus::ENDPOINT_TRANSIENT,
|
* Integrations\Divi\Design\DesignLock::option(), Admin\ConnectionStatus::ENDPOINT_TRANSIENT,
|
||||||
* Integrations\Divi\Release\ReleaseResolver::CACHE,
|
* Integrations\Divi\Release\ReleaseResolver::CACHE,
|
||||||
* Integrations\SuperCache\SuperCacheListener::PURGED_AT (transient),
|
* Integrations\FastestCache\FastestCacheListener::PURGED_AT (transient),
|
||||||
* ChangeLog\ChangeLogCleanup::HOOK, Integrations\SuperCache\SuperCacheListener::TRAILING_HOOK (cron),
|
* ChangeLog\ChangeLogCleanup::HOOK, Integrations\FastestCache\FastestCacheListener::TRAILING_HOOK (cron),
|
||||||
* ChangeLog\ChangeLog::TABLE, Admin\LogTab::PER_PAGE_OPTION and
|
* ChangeLog\ChangeLog::TABLE, Admin\LogTab::PER_PAGE_OPTION and
|
||||||
* Updater\RecheckOnInstall::OPTION on purpose;
|
* Updater\RecheckOnInstall::OPTION on purpose;
|
||||||
* tests/Unit/UninstallerTest.php and tests/Unit/UninstallTest.php keep them in sync with
|
* tests/Unit/UninstallerTest.php and tests/Unit/UninstallTest.php keep them in sync with
|
||||||
@@ -40,7 +40,7 @@ final class Uninstaller
|
|||||||
'lit_mcp_allowed_roles', 'lit_mcp_exposure', 'lit_mcp_limits', 'lit_mcp_db_version',
|
'lit_mcp_allowed_roles', 'lit_mcp_exposure', 'lit_mcp_limits', 'lit_mcp_db_version',
|
||||||
'lit_mcp_divi_releases', 'lit_mcp_divi_check_report', 'lit_mcp_divi_check_lock',
|
'lit_mcp_divi_releases', 'lit_mcp_divi_check_report', 'lit_mcp_divi_check_lock',
|
||||||
'lit_mcp_pods_releases', 'lit_mcp_pods_check_report', 'lit_mcp_pods_check_lock',
|
'lit_mcp_pods_releases', 'lit_mcp_pods_check_report', 'lit_mcp_pods_check_lock',
|
||||||
'lit_mcp_wpsc_releases', 'lit_mcp_wpsc_check_report', 'lit_mcp_wpsc_check_lock',
|
'lit_mcp_wpfc_releases', 'lit_mcp_wpfc_check_report', 'lit_mcp_wpfc_check_lock',
|
||||||
'lit_mcp_snapshots_colors', 'lit_mcp_snapshots_variables', 'lit_mcp_snapshots_presets',
|
'lit_mcp_snapshots_colors', 'lit_mcp_snapshots_variables', 'lit_mcp_snapshots_presets',
|
||||||
'lit_mcp_snapshots_theme_options', 'lit_mcp_snapshots_custom_css',
|
'lit_mcp_snapshots_theme_options', 'lit_mcp_snapshots_custom_css',
|
||||||
'lit_mcp_lock_divi_options', 'lit_mcp_lock_variables', 'lit_mcp_lock_presets', 'lit_mcp_lock_custom_css',
|
'lit_mcp_lock_divi_options', 'lit_mcp_lock_variables', 'lit_mcp_lock_presets', 'lit_mcp_lock_custom_css',
|
||||||
@@ -48,8 +48,8 @@ final class Uninstaller
|
|||||||
'lit_mcp_lock_colors', 'lit_mcp_lock_theme_options',
|
'lit_mcp_lock_colors', 'lit_mcp_lock_theme_options',
|
||||||
'lit_mcp_installed_version',
|
'lit_mcp_installed_version',
|
||||||
];
|
];
|
||||||
private const TRANSIENTS = ['lit_mcp_endpoint_check', 'lit_mcp_divi_state', 'lit_mcp_wpsc_purged_at'];
|
private const TRANSIENTS = ['lit_mcp_endpoint_check', 'lit_mcp_divi_state', 'lit_mcp_wpfc_purged_at'];
|
||||||
private const CRON_HOOKS = ['lit_mcp_log_cleanup', 'lit_mcp_wpsc_trailing_purge'];
|
private const CRON_HOOKS = ['lit_mcp_log_cleanup', 'lit_mcp_wpfc_trailing_purge'];
|
||||||
private const TABLE = 'lit_mcp_log';
|
private const TABLE = 'lit_mcp_log';
|
||||||
|
|
||||||
/** Screen option "Einträge pro Seite" of the Protokoll tab (Admin\LogTab::PER_PAGE_OPTION): user meta. */
|
/** Screen option "Einträge pro Seite" of the Protokoll tab (Admin\LogTab::PER_PAGE_OPTION): user meta. */
|
||||||
|
|||||||
+70
-147
@@ -1,56 +1,38 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# End-to-end checks of the WP Super Cache profile of the compatibility layer (plan 2026-11-06-kompatibilitaetsschicht,
|
# End-to-end checks of the WP Fastest Cache profile of the compatibility layer: status, check route /wpfc-check with
|
||||||
# 2.5 and 2.6): status, check route /wpsc-check with the purge probe (cache off and on), the purge after a write of
|
# the purge probe (cache off and on), the purge after a write of this plugin without wp_update_post (terms), the
|
||||||
# this plugin without wp_update_post (terms), the whole-cache purge after a setting, its throttle and the trailing
|
# whole-cache purge after a setting, its throttle, the trailing purge by cron and lit/cache-clear. Runs INSIDE the
|
||||||
# purge by cron. Runs INSIDE the DDEV web container:
|
# DDEV web container:
|
||||||
# cd /home/janniclas/Projekte/WordPress && timeout 600 ddev exec bash wp-content/plugins/WP-LIT-MCP/tests/Integration/super-cache-smoke.sh
|
# cd /home/janniclas/Projekte/WordPress && timeout 600 ddev exec bash wp-content/plugins/WP-LIT-MCP/tests/Integration/fastest-cache-smoke.sh
|
||||||
#
|
#
|
||||||
# SIDE EFFECTS, all undone by the EXIT trap and proven at the end (verify_restored): activating WP Super Cache edits
|
# SIDE EFFECTS, all undone by the EXIT trap and proven at the end (verify_restored): WP Fastest Cache is activated if
|
||||||
# wp-config.php (WP_CACHE, WPCACHEHOME), creates wp-content/advanced-cache.php and wp-content/wp-cache-config.php,
|
# it was not, its option WpFastestCache is switched (only wpFastestCacheStatus, nothing that writes .htaccess) and set
|
||||||
# fills wp-content/cache/ and schedules wp_cache_* cron events and options. The script saves wp-config.php (and the
|
# back byte for byte, a test post and two categories are created and deleted. Cache files of the site are emptied by
|
||||||
# files named in SAVED_FILES, if they exist) first, deactivates the plugin and writes the saved state back, byte for byte (sha256).
|
# the whole-cache purges – that is what the run checks; WPFC rebuilds them on the next requests.
|
||||||
# It uses the plain PHP cache mode ($wp_cache_mod_rewrite = 0): no .htaccess rewrite rules are ever written; the run
|
|
||||||
# refuses to start if the cache directory is not empty, WP Super Cache is already active or WP_CACHE is already set.
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd /var/www/html
|
cd /var/www/html
|
||||||
# shellcheck source=lib.sh
|
# shellcheck source=lib.sh
|
||||||
source wp-content/plugins/WP-LIT-MCP/tests/Integration/lib.sh
|
source wp-content/plugins/WP-LIT-MCP/tests/Integration/lib.sh
|
||||||
|
|
||||||
ROOT=/var/www/html
|
ROOT=/var/www/html
|
||||||
CACHE_DIR=$ROOT/wp-content/cache
|
CACHE_ALL=$ROOT/wp-content/cache/all
|
||||||
SUPERCACHE_DIR=$CACHE_DIR/supercache
|
|
||||||
WP_CONFIG=$ROOT/wp-config.php
|
|
||||||
HTACCESS=$ROOT/.htaccess
|
HTACCESS=$ROOT/.htaccess
|
||||||
SAVED_FILES=(wp-content/advanced-cache.php wp-content/wp-cache-config.php .htaccess)
|
CRON_PATTERN='lit_mcp_wpfc'
|
||||||
CRON_PATTERN='wp_cache|wpsc|lit_mcp_wpsc'
|
OPTION_PATTERN='lit_mcp_wpfc'
|
||||||
OPTION_PATTERN='wpsc|wp_super_cache|wp_cache|super_cache|lit_mcp_wpsc'
|
TRAILING_HOOK=lit_mcp_wpfc_trailing_purge
|
||||||
TRAILING_HOOK=lit_mcp_wpsc_trailing_purge
|
PURGED_AT=lit_mcp_wpfc_purged_at
|
||||||
PURGED_AT=lit_mcp_wpsc_purged_at
|
|
||||||
BASE=http://localhost
|
BASE=http://localhost
|
||||||
|
CACHE_ON='{"wpFastestCacheStatus":"on"}'
|
||||||
|
|
||||||
ADMIN_LOGIN=$(wp user list --role=administrator --field=user_login | head -1)
|
ADMIN_LOGIN=$(wp user list --role=administrator --field=user_login | head -1)
|
||||||
PREFIX=$(wp db prefix)
|
PREFIX=$(wp db prefix)
|
||||||
|
|
||||||
# --- Guards: nothing is changed before all of them pass ------------------------------------------------------------
|
# --- Guards and snapshot of everything the run can change ----------------------------------------------------------
|
||||||
wp plugin is-active wp-super-cache 2>/dev/null && fail "WP Super Cache is already active; this smoke expects it inactive"
|
wp plugin is-installed wp-fastest-cache || fail "WP Fastest Cache is not installed"
|
||||||
grep -Eq "^\s*(define\s*\(\s*['\"]WP_CACHE['\"]|const\s+WP_CACHE\s*=)" "$WP_CONFIG" && fail "wp-config.php already sets WP_CACHE"
|
|
||||||
[ -d "$CACHE_DIR" ] && [ -n "$(find "$CACHE_DIR" -mindepth 1 -print -quit)" ] && fail "$CACHE_DIR is not empty; refusing to touch a cache"
|
|
||||||
|
|
||||||
# --- Snapshot of everything the run can change ---------------------------------------------------------------------
|
|
||||||
BACKUP=$(mktemp -d)
|
|
||||||
sums() { sha256sum "$@" 2>/dev/null | awk '{print $1}'; }
|
sums() { sha256sum "$@" 2>/dev/null | awk '{print $1}'; }
|
||||||
CONFIG_SUM_BEFORE=$(sums "$WP_CONFIG")
|
WPFC_WAS_ACTIVE=no; wp plugin is-active wp-fastest-cache 2>/dev/null && WPFC_WAS_ACTIVE=yes
|
||||||
CONFIG_STAT_BEFORE=$(stat -c '%a %U:%G' "$WP_CONFIG")
|
WPFC_OPTION_BEFORE=$(wp option get WpFastestCache 2>/dev/null || echo __absent__)
|
||||||
# cp -a, not -p: wp-config.php carries an ACL entry; WP Super Cache replaces the file (new inode) and loses it, and
|
|
||||||
# only cp -a writes it back with the content.
|
|
||||||
cp -a "$WP_CONFIG" "$BACKUP/wp-config.php"
|
|
||||||
[ "$(sums "$BACKUP/wp-config.php")" = "$CONFIG_SUM_BEFORE" ] || fail "could not save wp-config.php"
|
|
||||||
for f in "${SAVED_FILES[@]}"; do
|
|
||||||
if [ -e "$ROOT/$f" ]; then cp -a "$ROOT/$f" "$BACKUP/$(basename "$f")"; echo "$(sums "$ROOT/$f")" > "$BACKUP/$(basename "$f").sum"; fi
|
|
||||||
done
|
|
||||||
HTACCESS_BEFORE=absent; [ -e "$HTACCESS" ] && HTACCESS_BEFORE=$(sums "$HTACCESS")
|
HTACCESS_BEFORE=absent; [ -e "$HTACCESS" ] && HTACCESS_BEFORE=$(sums "$HTACCESS")
|
||||||
CACHE_DIR_EXISTED=no; CACHE_DIR_MODE=775
|
|
||||||
if [ -d "$CACHE_DIR" ]; then CACHE_DIR_EXISTED=yes; CACHE_DIR_MODE=$(stat -c '%a' "$CACHE_DIR"); fi
|
|
||||||
list_hooks() { wp cron event list --fields=hook --format=csv | tail -n +2 | grep -E "$CRON_PATTERN" | sort -u || true; }
|
list_hooks() { wp cron event list --fields=hook --format=csv | tail -n +2 | grep -E "$CRON_PATTERN" | sort -u || true; }
|
||||||
list_options() { wp db query "SELECT option_name FROM ${PREFIX}options WHERE option_name REGEXP '$OPTION_PATTERN' ORDER BY option_name" --skip-column-names; }
|
list_options() { wp db query "SELECT option_name FROM ${PREFIX}options WHERE option_name REGEXP '$OPTION_PATTERN' ORDER BY option_name" --skip-column-names; }
|
||||||
HOOKS_BEFORE=$(list_hooks)
|
HOOKS_BEFORE=$(list_hooks)
|
||||||
@@ -60,41 +42,20 @@ ORIGINAL_DESCRIPTION=$(wp option get blogdescription)
|
|||||||
CREATED_POSTS=()
|
CREATED_POSTS=()
|
||||||
CREATED_TERMS=()
|
CREATED_TERMS=()
|
||||||
|
|
||||||
# --- Restore ---------------------------------------------------------------------------------------------------------
|
|
||||||
# A saved file is written back atomically: a copy next to the target (same file system), then mv – a run killed in
|
|
||||||
# the middle never leaves a half-written wp-config.php.
|
|
||||||
restore_file() { # $1 = path relative to ROOT
|
|
||||||
local name tmp; name=$(basename "$1")
|
|
||||||
if [ -e "$BACKUP/$name" ]; then
|
|
||||||
tmp=$(mktemp "$ROOT/$(dirname "$1")/.$name.restore.XXXXXX") \
|
|
||||||
&& cp -a "$BACKUP/$name" "$tmp" && mv -f "$tmp" "$ROOT/$1" || { rm -f "$tmp"; return 1; }
|
|
||||||
else
|
|
||||||
rm -f "$ROOT/$1"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
restore() {
|
restore() {
|
||||||
set +e
|
set +e
|
||||||
local id term hook option
|
local id term hook option
|
||||||
for id in "${CREATED_POSTS[@]}"; do wp post delete "$id" --force >/dev/null 2>&1; done
|
for id in "${CREATED_POSTS[@]}"; do wp post delete "$id" --force >/dev/null 2>&1; done
|
||||||
for term in "${CREATED_TERMS[@]}"; do wp term delete category "$term" >/dev/null 2>&1; done
|
for term in "${CREATED_TERMS[@]}"; do wp term delete category "$term" >/dev/null 2>&1; done
|
||||||
wp option update blogdescription "$ORIGINAL_DESCRIPTION" >/dev/null 2>&1
|
wp option update blogdescription "$ORIGINAL_DESCRIPTION" >/dev/null 2>&1
|
||||||
wp option delete lit_mcp_wpsc_check_report lit_mcp_wpsc_releases lit_mcp_wpsc_check_lock >/dev/null 2>&1
|
|
||||||
wp transient delete "$PURGED_AT" >/dev/null 2>&1
|
wp transient delete "$PURGED_AT" >/dev/null 2>&1
|
||||||
cleanup_app_passwords_for "$ADMIN_LOGIN"
|
cleanup_app_passwords_for "$ADMIN_LOGIN"
|
||||||
# Deactivation lets WP Super Cache undo its own edits (WP_CACHE, WPCACHEHOME, advanced-cache.php, cache files) ...
|
if [ "$WPFC_OPTION_BEFORE" = __absent__ ]; then
|
||||||
wp plugin is-active wp-super-cache >/dev/null 2>&1 && wp plugin deactivate wp-super-cache >/dev/null 2>&1
|
wp option delete WpFastestCache >/dev/null 2>&1
|
||||||
# ... and what it leaves, or does differently, is set back from the saved state.
|
|
||||||
restore_file wp-config.php
|
|
||||||
for id in "${SAVED_FILES[@]}"; do restore_file "$id"; done
|
|
||||||
[ -d "$CACHE_DIR" ] && find "$CACHE_DIR" -mindepth 1 -delete
|
|
||||||
if [ "$CACHE_DIR_EXISTED" = no ]; then
|
|
||||||
rmdir "$CACHE_DIR" 2>/dev/null
|
|
||||||
else
|
else
|
||||||
# Deactivation removes the whole cache directory; mkdir -m keeps the inherited ACL of its parent.
|
wp option update WpFastestCache "$WPFC_OPTION_BEFORE" >/dev/null 2>&1
|
||||||
[ -d "$CACHE_DIR" ] || mkdir -m "$CACHE_DIR_MODE" "$CACHE_DIR"
|
|
||||||
fi
|
fi
|
||||||
# The profile's cron event and every option or event the plugin added.
|
[ "$WPFC_WAS_ACTIVE" = no ] && wp plugin deactivate wp-fastest-cache >/dev/null 2>&1
|
||||||
for hook in $(list_hooks); do
|
for hook in $(list_hooks); do
|
||||||
grep -qx "$hook" <<<"$HOOKS_BEFORE" || wp cron event delete "$hook" >/dev/null 2>&1
|
grep -qx "$hook" <<<"$HOOKS_BEFORE" || wp cron event delete "$hook" >/dev/null 2>&1
|
||||||
done
|
done
|
||||||
@@ -106,30 +67,11 @@ restore() {
|
|||||||
|
|
||||||
# Prints the proof; returns non-zero on any difference (used in the trap, so it must not exit).
|
# Prints the proof; returns non-zero on any difference (used in the trap, so it must not exit).
|
||||||
verify_restored() {
|
verify_restored() {
|
||||||
local ok=0 name after
|
local ok=0 htaccess_after=absent
|
||||||
after=$(sums "$WP_CONFIG")
|
[ "$(wp option get WpFastestCache 2>/dev/null || echo __absent__)" = "$WPFC_OPTION_BEFORE" ] \
|
||||||
if [ "$after" = "$CONFIG_SUM_BEFORE" ]; then pass "restored: wp-config.php sha256 $CONFIG_SUM_BEFORE (before) == $after (after)"
|
|| { echo "FAIL: option WpFastestCache differs"; ok=1; }
|
||||||
else echo "FAIL: wp-config.php sha256 $CONFIG_SUM_BEFORE (before) != $after (after)"; ok=1; fi
|
[ -e "$HTACCESS" ] && htaccess_after=$(sums "$HTACCESS")
|
||||||
[ "$(stat -c '%a %U:%G' "$WP_CONFIG")" = "$CONFIG_STAT_BEFORE" ] \
|
|
||||||
|| { echo "FAIL: wp-config.php mode/owner changed: $CONFIG_STAT_BEFORE -> $(stat -c '%a %U:%G' "$WP_CONFIG")"; ok=1; }
|
|
||||||
for f in "${SAVED_FILES[@]}"; do
|
|
||||||
name=$(basename "$f")
|
|
||||||
if [ -e "$BACKUP/$name" ]; then
|
|
||||||
[ "$(sums "$ROOT/$f")" = "$(cat "$BACKUP/$name.sum")" ] || { echo "FAIL: $f differs from the saved one"; ok=1; }
|
|
||||||
else
|
|
||||||
[ ! -e "$ROOT/$f" ] || { echo "FAIL: $f was absent before and exists now"; ok=1; }
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
[ "$ok" = 0 ] && pass "restored: advanced-cache.php and wp-cache-config.php as before (absent: $([ -e "$BACKUP/advanced-cache.php" ] && echo no || echo yes))"
|
|
||||||
local htaccess_after=absent; [ -e "$HTACCESS" ] && htaccess_after=$(sums "$HTACCESS")
|
|
||||||
[ "$htaccess_after" = "$HTACCESS_BEFORE" ] || { echo "FAIL: .htaccess changed ($HTACCESS_BEFORE -> $htaccess_after)"; ok=1; }
|
[ "$htaccess_after" = "$HTACCESS_BEFORE" ] || { echo "FAIL: .htaccess changed ($HTACCESS_BEFORE -> $htaccess_after)"; ok=1; }
|
||||||
if [ "$CACHE_DIR_EXISTED" = yes ]; then
|
|
||||||
[ -d "$CACHE_DIR" ] && [ -z "$(find "$CACHE_DIR" -mindepth 1 -print -quit)" ] && [ "$(stat -c '%a' "$CACHE_DIR")" = "$CACHE_DIR_MODE" ] \
|
|
||||||
|| { echo "FAIL: cache directory not empty, gone or its mode changed"; ok=1; }
|
|
||||||
else
|
|
||||||
[ ! -e "$CACHE_DIR" ] || { echo "FAIL: cache directory was absent before and exists now"; ok=1; }
|
|
||||||
fi
|
|
||||||
wp plugin is-active wp-super-cache 2>/dev/null && { echo "FAIL: WP Super Cache is still active"; ok=1; }
|
|
||||||
[ "$(wp option get active_plugins --format=json)" = "$ACTIVE_PLUGINS_BEFORE" ] || { echo "FAIL: active_plugins differs"; ok=1; }
|
[ "$(wp option get active_plugins --format=json)" = "$ACTIVE_PLUGINS_BEFORE" ] || { echo "FAIL: active_plugins differs"; ok=1; }
|
||||||
[ "$(list_hooks)" = "$HOOKS_BEFORE" ] || { echo "FAIL: cron events differ: $(list_hooks | tr '\n' ' ')"; ok=1; }
|
[ "$(list_hooks)" = "$HOOKS_BEFORE" ] || { echo "FAIL: cron events differ: $(list_hooks | tr '\n' ' ')"; ok=1; }
|
||||||
[ "$(list_options)" = "$OPTIONS_BEFORE" ] || { echo "FAIL: options differ: $(list_options | tr '\n' ' ')"; ok=1; }
|
[ "$(list_options)" = "$OPTIONS_BEFORE" ] || { echo "FAIL: options differ: $(list_options | tr '\n' ' ')"; ok=1; }
|
||||||
@@ -137,8 +79,9 @@ verify_restored() {
|
|||||||
[ "$(wp option get blogdescription)" = "$ORIGINAL_DESCRIPTION" ] || { echo "FAIL: blogdescription not restored"; ok=1; }
|
[ "$(wp option get blogdescription)" = "$ORIGINAL_DESCRIPTION" ] || { echo "FAIL: blogdescription not restored"; ok=1; }
|
||||||
[ "$(wp post list --post_type=any --post_status=any --meta_key=_lit_mcp_check_entry --format=count)" = 0 ] \
|
[ "$(wp post list --post_type=any --post_status=any --meta_key=_lit_mcp_check_entry --format=count)" = 0 ] \
|
||||||
|| { echo "FAIL: a probe page is left"; ok=1; }
|
|| { echo "FAIL: a probe page is left"; ok=1; }
|
||||||
[ "$(wp post list --post_type=post --post_status=any --name=lit-wpsc-smoke --format=count)" = 0 ] \
|
[ "$(wp post list --post_type=post --post_status=any --name=lit-wpfc-smoke --format=count)" = 0 ] \
|
||||||
|| { echo "FAIL: the test post is left"; ok=1; }
|
|| { echo "FAIL: the test post is left"; ok=1; }
|
||||||
|
[ "$ok" = 0 ] && pass "restored: WP Fastest Cache active=$WPFC_WAS_ACTIVE with its option as before, .htaccess untouched"
|
||||||
return "$ok"
|
return "$ok"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +89,6 @@ on_exit() {
|
|||||||
local rc=$1
|
local rc=$1
|
||||||
restore
|
restore
|
||||||
verify_restored || rc=1
|
verify_restored || rc=1
|
||||||
rm -rf "$BACKUP"
|
|
||||||
guard_exit "$rc" || exit $?
|
guard_exit "$rc" || exit $?
|
||||||
exit "$rc"
|
exit "$rc"
|
||||||
}
|
}
|
||||||
@@ -154,11 +96,11 @@ trap 'on_exit $?' EXIT
|
|||||||
trap 'exit 130' INT TERM HUP
|
trap 'exit 130' INT TERM HUP
|
||||||
|
|
||||||
# --- Helpers ---------------------------------------------------------------------------------------------------------
|
# --- Helpers ---------------------------------------------------------------------------------------------------------
|
||||||
wpsc_component() { tool lit-site-status '{}' | jq -c '.components[] | select(.id == "wpsc")'; }
|
wpfc_component() { tool lit-site-status '{}' | jq -c '.components[] | select(.id == "wpfc")'; }
|
||||||
stored_option() { wp option get "$1" --format=json 2>/dev/null || true; }
|
stored_option() { wp option get "$1" --format=json 2>/dev/null || true; }
|
||||||
event_count() { wp cron event list --hook="$TRAILING_HOOK" --format=count 2>/dev/null || echo 0; }
|
event_count() { wp cron event list --hook="$TRAILING_HOOK" --format=count 2>/dev/null || echo 0; }
|
||||||
# The cache file of the test post as WP Super Cache keeps it (index.html; a ".needs-rebuild" copy is a purged one).
|
# The cache file of the test post where WP Fastest Cache keeps it (a purge moves the directory to cache/tmpWpfc).
|
||||||
cache_file() { find "$SUPERCACHE_DIR" -type f -path '*lit-wpsc-smoke*' -name index.html 2>/dev/null | head -1; }
|
cache_file() { local f="$CACHE_ALL${POST_PATH%/}/index.html"; [ -f "$f" ] && echo "$f" || true; }
|
||||||
wait_for() { # $1 = description, rest = command; polls up to 15 s
|
wait_for() { # $1 = description, rest = command; polls up to 15 s
|
||||||
local what=$1 i; shift
|
local what=$1 i; shift
|
||||||
for i in $(seq 1 15); do "$@" && return 0; sleep 1; done
|
for i in $(seq 1 15); do "$@" && return 0; sleep 1; done
|
||||||
@@ -166,97 +108,78 @@ wait_for() { # $1 = description, rest = command; polls up to 15 s
|
|||||||
}
|
}
|
||||||
cache_file_exists() { [ -n "$(cache_file)" ]; }
|
cache_file_exists() { [ -n "$(cache_file)" ]; }
|
||||||
cache_file_gone() { [ -z "$(cache_file)" ]; }
|
cache_file_gone() { [ -z "$(cache_file)" ]; }
|
||||||
front_page_by_wpsc() { [[ "$(curl -s -H "Host: $HOST" "$BASE/")" == *WP-Super-Cache* ]]; }
|
|
||||||
# One public request for the test post; the cache file appears at the end of the request.
|
|
||||||
fetch_post() {
|
fetch_post() {
|
||||||
local code
|
local code
|
||||||
code=$(curl -s -o /dev/null -w '%{http_code}' -H "Host: $HOST" "$BASE$POST_PATH")
|
code=$(curl -s -o /dev/null -w '%{http_code}' -H "Host: $HOST" "$BASE$POST_PATH")
|
||||||
[ "$code" = 200 ] || fail "GET $POST_PATH answered $code"
|
[ "$code" = 200 ] || fail "GET $POST_PATH answered $code"
|
||||||
}
|
}
|
||||||
# Requests the test post until WP Super Cache has written its file (the first request after a purge or a new post
|
# Requests the test post until WP Fastest Cache has written its file.
|
||||||
# is not always the one that is stored).
|
|
||||||
fill_cache() {
|
fill_cache() {
|
||||||
local i
|
local i
|
||||||
for i in $(seq 1 10); do
|
for i in $(seq 1 10); do
|
||||||
fetch_post
|
fetch_post
|
||||||
sleep 1
|
|
||||||
cache_file_exists && return 0
|
cache_file_exists && return 0
|
||||||
|
sleep 1
|
||||||
done
|
done
|
||||||
echo "--- response of $POST_PATH (selected headers) and the cache tree:"
|
echo "--- end of the response of $POST_PATH and the cache tree:"
|
||||||
curl -s -i -H "Host: $HOST" "$BASE$POST_PATH" | grep -iE '^(HTTP|cache-control|set-cookie)|WP Super Cache|Page not cached' | head -10
|
curl -s -H "Host: $HOST" "$BASE$POST_PATH" | tail -c 300
|
||||||
find "$CACHE_DIR" | head -20
|
find "$CACHE_ALL" -maxdepth 4 | head -20
|
||||||
fail "timeout: cache file of the test post"
|
fail "timeout: cache file of the test post"
|
||||||
}
|
}
|
||||||
config_value() { grep -E "^\s*\\\$$1\s*=" "$ROOT/wp-content/wp-cache-config.php" | head -1; }
|
|
||||||
|
|
||||||
# --- 0. Activate and look at what WP Super Cache did ------------------------------------------------------------------
|
# --- 0. Active, cache switched off -----------------------------------------------------------------------------------
|
||||||
wp plugin activate wp-super-cache >/dev/null 2>&1 || fail "could not activate WP Super Cache"
|
[ "$WPFC_WAS_ACTIVE" = yes ] || wp plugin activate wp-fastest-cache >/dev/null 2>&1 || fail "could not activate WP Fastest Cache"
|
||||||
grep -q "define( *'WP_CACHE', *true *)" "$WP_CONFIG" || fail "activation did not set WP_CACHE (the run cannot cache)"
|
wp option update WpFastestCache '{}' >/dev/null
|
||||||
[ -e "$ROOT/wp-content/advanced-cache.php" ] || fail "activation did not create advanced-cache.php"
|
|
||||||
# WP Super Cache keys its files by the host of the request: MCP calls must carry the host the page was cached under.
|
|
||||||
MCP_HOST=$HOST
|
|
||||||
connect "$ADMIN_LOGIN"
|
connect "$ADMIN_LOGIN"
|
||||||
|
|
||||||
# 1. Status
|
# 1. Status
|
||||||
wpsc_component | jq -e '.active == true and .version == "3.1.5" and .problems == []' >/dev/null \
|
wpfc_component | jq -e '.active == true and .version == "1.5.5" and .problems == []' >/dev/null \
|
||||||
|| fail "site status must show WP Super Cache 3.1.5 active without problems: $(wpsc_component)"
|
|| fail "site status must show WP Fastest Cache 1.5.5 active without problems: $(wpfc_component)"
|
||||||
pass "lit-site-status: WP Super Cache 3.1.5 active, no problems"
|
pass "lit-site-status: WP Fastest Cache 1.5.5 active, no problems"
|
||||||
|
|
||||||
# 2. Check route with the cache still switched off: the probe step is green and says "not checked"
|
# 2. Check route with the cache switched off: the probe step is green and says "not checked"
|
||||||
APP_PASSWORD=$(wp user application-password create "$ADMIN_LOGIN" lit-mcp-integration-test --porcelain)
|
APP_PASSWORD=$(wp user application-password create "$ADMIN_LOGIN" lit-mcp-integration-test --porcelain)
|
||||||
run_check() {
|
run_check() {
|
||||||
local headers
|
local headers
|
||||||
headers=$(curl -s -o /dev/null -D - -X POST -H "Host: $HOST" -H 'X-Forwarded-Proto: https' -u "$ADMIN_LOGIN:$APP_PASSWORD" "$BASE/wp-json/lit-mcp/v1/wpsc-check")
|
headers=$(curl -s -o /dev/null -D - -X POST -H "Host: $HOST" -H 'X-Forwarded-Proto: https' -u "$ADMIN_LOGIN:$APP_PASSWORD" "$BASE/wp-json/lit-mcp/v1/wpfc-check")
|
||||||
echo "$headers" | grep -qi '^HTTP/[0-9.]* 303' || fail "check route must answer 303: $headers"
|
echo "$headers" | grep -qi '^HTTP/[0-9.]* 303' || fail "check route must answer 303: $headers"
|
||||||
echo "$headers" | grep -qi '^location:.*tab=compatibility.*lit_mcp_notice=compat_checked' || fail "redirect to the tab: $headers"
|
echo "$headers" | grep -qi '^location:.*tab=compatibility.*lit_mcp_notice=compat_checked' || fail "redirect to the tab: $headers"
|
||||||
}
|
}
|
||||||
[ "$(wp eval 'echo (int) ! empty($GLOBALS["cache_enabled"]);')" = 0 ] || fail "the cache must be off after activation"
|
|
||||||
run_check
|
run_check
|
||||||
REPORT=$(stored_option lit_mcp_wpsc_check_report)
|
REPORT=$(stored_option lit_mcp_wpfc_check_report)
|
||||||
echo "$REPORT" | jq -e '(.steps | length) == 2 and all(.steps[]; .problems == [] and .skipped == false)
|
echo "$REPORT" | jq -e '(.steps | length) == 2 and all(.steps[]; .problems == [] and .skipped == false)
|
||||||
and ([.steps[] | select(.key == "probe")] | length == 1 and (.[0].label | contains("Cache ist ausgeschaltet, nicht geprüft")))' >/dev/null \
|
and ([.steps[] | select(.key == "probe")] | length == 1 and (.[0].label | contains("Cache ist ausgeschaltet, nicht geprüft")))' >/dev/null \
|
||||||
|| fail "cache off: the report must be green and the probe must say it did not check: $REPORT"
|
|| fail "cache off: the report must be green and the probe must say it did not check: $REPORT"
|
||||||
pass "/wpsc-check with the cache off: green, probe step says 'Cache ist ausgeschaltet, nicht geprüft'"
|
pass "/wpfc-check with the cache off: green, probe step says 'Cache ist ausgeschaltet, nicht geprüft'"
|
||||||
|
|
||||||
# 3. Switch the cache on in the plain PHP mode (no mod_rewrite, no .htaccess)
|
# 3. Cache on; the check route runs the real purge probe
|
||||||
wp eval 'wp_cache_enable();' >/dev/null 2>&1 || fail "wp_cache_enable failed"
|
wp option update WpFastestCache "$CACHE_ON" >/dev/null
|
||||||
# PHP-FPM's opcache re-reads a changed wp-cache-config.php at most every 2 s: wait until a public request is served
|
|
||||||
# by WP Super Cache (its HTML comment), the state the checks below need.
|
|
||||||
wait_for "WP Super Cache serves public requests" front_page_by_wpsc
|
|
||||||
config_value cache_enabled | grep -q 'true' || fail "wp-cache-config.php: cache_enabled is not true: $(config_value cache_enabled)"
|
|
||||||
config_value wp_cache_mod_rewrite | grep -Eq '= *0;' || fail "mod_rewrite mode is on: $(config_value wp_cache_mod_rewrite)"
|
|
||||||
config_value super_cache_enabled | grep -q 'true' || fail "super_cache_enabled is not true: $(config_value super_cache_enabled)"
|
|
||||||
[ "$(config_value wp_cache_clear_on_post_edit | grep -Ec '= *0;')" = 1 ] || fail "clear_on_post_edit must be 0: $(config_value wp_cache_clear_on_post_edit)"
|
|
||||||
[ ! -e "$HTACCESS" ] || [ "$(sums "$HTACCESS")" = "$HTACCESS_BEFORE" ] || fail ".htaccess changed"
|
|
||||||
pass "cache on in the plain PHP mode (mod_rewrite 0, clear_on_post_edit 0), .htaccess untouched"
|
|
||||||
|
|
||||||
# 4. Check route with the cache on: the purge probe really runs
|
|
||||||
run_check
|
run_check
|
||||||
REPORT=$(stored_option lit_mcp_wpsc_check_report)
|
REPORT=$(stored_option lit_mcp_wpfc_check_report)
|
||||||
echo "$REPORT" | jq -e '(.steps | length) == 2 and all(.steps[]; .problems == [] and .skipped == false)
|
echo "$REPORT" | jq -e '(.steps | length) == 2 and all(.steps[]; .problems == [] and .skipped == false)
|
||||||
and ([.steps[] | select(.key == "probe")] | length == 1 and (.[0].label | contains("nicht geprüft") | not))
|
and ([.steps[] | select(.key == "probe")] | length == 1 and (.[0].label | contains("nicht geprüft") | not))
|
||||||
and .version == "3.1.5"' >/dev/null || fail "cache on: the report must be green with a real probe step: $REPORT"
|
and .version == "1.5.5"' >/dev/null || fail "cache on: the report must be green with a real probe step: $REPORT"
|
||||||
[ -z "$(stored_option lit_mcp_wpsc_check_lock)" ] || fail "the check lock must be released"
|
[ -z "$(stored_option lit_mcp_wpfc_check_lock)" ] || fail "the check lock must be released"
|
||||||
[ "$(wp post list --post_type=any --post_status=any --meta_key=_lit_mcp_check_entry --format=count)" = 0 ] || fail "the probe left a page behind"
|
[ "$(wp post list --post_type=any --post_status=any --meta_key=_lit_mcp_check_entry --format=count)" = 0 ] || fail "the probe left a page behind"
|
||||||
[ "$(find "$SUPERCACHE_DIR" -path '*lit-mcp-check-*' -print -quit 2>/dev/null)" = "" ] || fail "the probe left a cache directory behind"
|
[ "$(find "$CACHE_ALL" -maxdepth 3 -path '*lit-mcp-check-*' -print -quit 2>/dev/null)" = "" ] || fail "the probe left a cache directory behind"
|
||||||
UNAUTH=$(curl -s -o /dev/null -w '%{http_code}' -X POST -H "Host: $HOST" -H 'X-Forwarded-Proto: https' "$BASE/wp-json/lit-mcp/v1/wpsc-check")
|
UNAUTH=$(curl -s -o /dev/null -w '%{http_code}' -X POST -H "Host: $HOST" -H 'X-Forwarded-Proto: https' "$BASE/wp-json/lit-mcp/v1/wpfc-check")
|
||||||
[ "$UNAUTH" = 401 ] || [ "$UNAUTH" = 403 ] || fail "anonymous check must be refused, got $UNAUTH"
|
[ "$UNAUTH" = 401 ] || [ "$UNAUTH" = 403 ] || fail "anonymous check must be refused, got $UNAUTH"
|
||||||
pass "/wpsc-check with the cache on: green with the purge probe, no page or cache directory left, anonymous refused"
|
pass "/wpfc-check with the cache on: green with the purge probe, no page or cache directory left, anonymous refused"
|
||||||
|
|
||||||
# 5. A published test post, fetched until it is in the cache
|
# 4. A published test post, fetched until it is in the cache
|
||||||
CAT_A=$(wp term create category "LIT WPSC Smoke A" --porcelain); CREATED_TERMS+=("$CAT_A")
|
CAT_A=$(wp term create category "LIT WPFC Smoke A" --porcelain); CREATED_TERMS+=("$CAT_A")
|
||||||
CAT_B=$(wp term create category "LIT WPSC Smoke B" --porcelain); CREATED_TERMS+=("$CAT_B")
|
CAT_B=$(wp term create category "LIT WPFC Smoke B" --porcelain); CREATED_TERMS+=("$CAT_B")
|
||||||
POST_ID=$(wp post create --post_type=post --post_status=publish --post_title="LIT WPSC Smoke" --post_name=lit-wpsc-smoke \
|
POST_ID=$(wp post create --post_type=post --post_status=publish --post_title="LIT WPFC Smoke" --post_name=lit-wpfc-smoke \
|
||||||
--post_content="Testbeitrag fuer den Super-Cache-Smoke." --porcelain)
|
--post_content="Testbeitrag fuer den Fastest-Cache-Smoke." --porcelain)
|
||||||
CREATED_POSTS+=("$POST_ID")
|
CREATED_POSTS+=("$POST_ID")
|
||||||
POST_PATH=$(wp eval "echo wp_parse_url(get_permalink($POST_ID), PHP_URL_PATH);")
|
POST_PATH=$(wp eval "echo wp_parse_url(get_permalink($POST_ID), PHP_URL_PATH);")
|
||||||
fill_cache
|
fill_cache
|
||||||
pass "test post $POST_ID cached: $(cache_file | sed "s#$CACHE_DIR/##")"
|
pass "test post $POST_ID cached: $(cache_file | sed "s#$ROOT/##")"
|
||||||
|
|
||||||
# 6. Control: a term change outside this plugin does not touch the cache (WP Super Cache never sees it) ...
|
# 5. Control: a term change outside this plugin does not touch the cache (WP Fastest Cache never sees it) ...
|
||||||
wp post term set "$POST_ID" category "$CAT_A" >/dev/null
|
wp post term set "$POST_ID" category "$CAT_A" >/dev/null
|
||||||
sleep 2
|
sleep 2
|
||||||
cache_file_exists || fail "control: WP Super Cache itself purged on a plain term change; the next check proves nothing"
|
cache_file_exists || fail "control: WP Fastest Cache itself purged on a plain term change; the next check proves nothing"
|
||||||
# ... but the same kind of change through lit-content-update (no wp_update_post) empties the cache of the post.
|
# ... but the same kind of change through lit-content-update (no wp_update_post) empties the cache of the post.
|
||||||
REV=$(tool lit-content-get "{\"post_id\":$POST_ID}" | jq -r '.rev')
|
REV=$(tool lit-content-get "{\"post_id\":$POST_ID}" | jq -r '.rev')
|
||||||
UPDATED=$(tool lit-content-update "{\"post_id\":$POST_ID,\"rev\":\"$REV\",\"terms\":{\"category\":[$CAT_B]}}")
|
UPDATED=$(tool lit-content-update "{\"post_id\":$POST_ID,\"rev\":\"$REV\",\"terms\":{\"category\":[$CAT_B]}}")
|
||||||
@@ -265,10 +188,10 @@ wait_for "the cache file of the post is gone after lit-content-update" cache_fil
|
|||||||
[ "$(event_count)" = 0 ] || fail "a single-post purge must not schedule a trailing purge"
|
[ "$(event_count)" = 0 ] || fail "a single-post purge must not schedule a trailing purge"
|
||||||
pass "terms-only lit-content-update empties the cache of the post (a plain term change does not), no trailing event"
|
pass "terms-only lit-content-update empties the cache of the post (a plain term change does not), no trailing event"
|
||||||
|
|
||||||
# 7. Whole-cache purge after a setting; the second one inside 30 s is throttled and leaves exactly one cron event
|
# 6. Whole-cache purge after a setting; the second one inside 30 s is throttled and leaves exactly one cron event
|
||||||
fill_cache
|
fill_cache
|
||||||
[ -z "$(wp transient get "$PURGED_AT" 2>/dev/null || true)" ] || fail "transient $PURGED_AT set before the first whole purge"
|
[ -z "$(wp transient get "$PURGED_AT" 2>/dev/null || true)" ] || fail "transient $PURGED_AT set before the first whole purge"
|
||||||
ability lit/wp-settings-update '{"settings":{"blogdescription":"LIT WPSC Smoke 1"}}' | jq -e '.ok == true' >/dev/null || fail "settings update 1"
|
ability lit/wp-settings-update '{"settings":{"blogdescription":"LIT WPFC Smoke 1"}}' | jq -e '.ok == true' >/dev/null || fail "settings update 1"
|
||||||
wait_for "the cache is empty after a setting change" cache_file_gone
|
wait_for "the cache is empty after a setting change" cache_file_gone
|
||||||
wait_for "the purge time is stored" bash -c "[ -n \"\$(wp transient get $PURGED_AT 2>/dev/null)\" ]"
|
wait_for "the purge time is stored" bash -c "[ -n \"\$(wp transient get $PURGED_AT 2>/dev/null)\" ]"
|
||||||
LAST=$(wp transient get "$PURGED_AT")
|
LAST=$(wp transient get "$PURGED_AT")
|
||||||
@@ -276,18 +199,18 @@ LAST=$(wp transient get "$PURGED_AT")
|
|||||||
pass "setting change empties the whole cache (purge time stored, no trailing event)"
|
pass "setting change empties the whole cache (purge time stored, no trailing event)"
|
||||||
|
|
||||||
fill_cache
|
fill_cache
|
||||||
ability lit/wp-settings-update '{"settings":{"blogdescription":"LIT WPSC Smoke 2"}}' | jq -e '.ok == true' >/dev/null || fail "settings update 2"
|
ability lit/wp-settings-update '{"settings":{"blogdescription":"LIT WPFC Smoke 2"}}' | jq -e '.ok == true' >/dev/null || fail "settings update 2"
|
||||||
wait_for "one trailing purge is scheduled" bash -c "[ \"\$(wp cron event list --hook=$TRAILING_HOOK --format=count)\" = 1 ]"
|
wait_for "one trailing purge is scheduled" bash -c "[ \"\$(wp cron event list --hook=$TRAILING_HOOK --format=count)\" = 1 ]"
|
||||||
sleep 2
|
sleep 2
|
||||||
cache_file_exists || fail "throttled: the second whole purge ran inside the lock time"
|
cache_file_exists || fail "throttled: the second whole purge ran inside the lock time"
|
||||||
ability lit/wp-settings-update '{"settings":{"blogdescription":"LIT WPSC Smoke 3"}}' | jq -e '.ok == true' >/dev/null || fail "settings update 3"
|
ability lit/wp-settings-update '{"settings":{"blogdescription":"LIT WPFC Smoke 3"}}' | jq -e '.ok == true' >/dev/null || fail "settings update 3"
|
||||||
sleep 3
|
sleep 3
|
||||||
[ "$(event_count)" = 1 ] || fail "exactly one trailing event must stay scheduled, found $(event_count)"
|
[ "$(event_count)" = 1 ] || fail "exactly one trailing event must stay scheduled, found $(event_count)"
|
||||||
cache_file_exists || fail "throttled: the third whole purge ran inside the lock time"
|
cache_file_exists || fail "throttled: the third whole purge ran inside the lock time"
|
||||||
[ "$(wp transient get "$PURGED_AT")" = "$LAST" ] || fail "throttled purges must not move the purge time"
|
[ "$(wp transient get "$PURGED_AT")" = "$LAST" ] || fail "throttled purges must not move the purge time"
|
||||||
pass "second and third whole purge inside 30 s: throttled (file stays, purge time unchanged), exactly one $TRAILING_HOOK event"
|
pass "second and third whole purge inside 30 s: throttled (file stays, purge time unchanged), exactly one $TRAILING_HOOK event"
|
||||||
|
|
||||||
# 8. Run the trailing purge once the lock is over: the cache is emptied and nothing is scheduled any more
|
# 7. Run the trailing purge once the lock is over: the cache is emptied and nothing is scheduled any more
|
||||||
WAIT=$((LAST + 32 - $(date +%s))); [ "$WAIT" -gt 0 ] && sleep "$WAIT"
|
WAIT=$((LAST + 32 - $(date +%s))); [ "$WAIT" -gt 0 ] && sleep "$WAIT"
|
||||||
wp cron event run "$TRAILING_HOOK" >/dev/null 2>&1 || fail "wp cron event run $TRAILING_HOOK"
|
wp cron event run "$TRAILING_HOOK" >/dev/null 2>&1 || fail "wp cron event run $TRAILING_HOOK"
|
||||||
cache_file_gone || fail "the trailing purge did not empty the cache"
|
cache_file_gone || fail "the trailing purge did not empty the cache"
|
||||||
@@ -295,7 +218,7 @@ cache_file_gone || fail "the trailing purge did not empty the cache"
|
|||||||
[ "$(wp transient get "$PURGED_AT")" -gt "$LAST" ] || fail "the trailing purge must store a new purge time"
|
[ "$(wp transient get "$PURGED_AT")" -gt "$LAST" ] || fail "the trailing purge must store a new purge time"
|
||||||
pass "trailing purge by cron: cache emptied, event gone, purge time renewed"
|
pass "trailing purge by cron: cache emptied, event gone, purge time renewed"
|
||||||
|
|
||||||
# 8b. lit/cache-clear: the dry run changes nothing, confirm empties the post's page / the whole cache
|
# 8. lit/cache-clear: the dry run changes nothing, confirm empties the post's page / the whole cache
|
||||||
fill_cache
|
fill_cache
|
||||||
for ARGS in "{\"scope\":\"post\",\"post_id\":$POST_ID}" '{"scope":"all"}'; do
|
for ARGS in "{\"scope\":\"post\",\"post_id\":$POST_ID}" '{"scope":"all"}'; do
|
||||||
DRY=$(ability lit/cache-clear "$ARGS")
|
DRY=$(ability lit/cache-clear "$ARGS")
|
||||||
@@ -339,6 +262,6 @@ pass "lit/cache-clear scope all inside 30 s: throttled, one trailing event, cron
|
|||||||
# 9. Everything back as before (the EXIT trap would do it too; here it is part of the verdict)
|
# 9. Everything back as before (the EXIT trap would do it too; here it is part of the verdict)
|
||||||
restore
|
restore
|
||||||
verify_restored || fail "the state before the run was not restored"
|
verify_restored || fail "the state before the run was not restored"
|
||||||
pass "cleanup: plugin inactive, test post, categories, options, transient and cron events gone"
|
pass "cleanup: test post, categories, options, transient and cron events gone"
|
||||||
|
|
||||||
finish
|
finish
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
# Requires jq and curl (both present in the DDEV web image).
|
# Requires jq and curl (both present in the DDEV web image).
|
||||||
|
|
||||||
# Mutual exclusion of ALL smokes (every one sources this file inside the same web container): two at once would
|
# Mutual exclusion of ALL smokes (every one sources this file inside the same web container): two at once would
|
||||||
# share one site state – WP Super Cache edits wp-config.php, every smoke empties the test application passwords of the
|
# share one site state – WP Fastest Cache edits wp-config.php, every smoke empties the test application passwords of the
|
||||||
# admin (cleanup_app_passwords_for, harmless only because of this lock), Pods/Divi release options. The lock lives as
|
# admin (cleanup_app_passwords_for, harmless only because of this lock), Pods/Divi release options. The lock lives as
|
||||||
# long as the script (fd 9 closes with it); a smoke that runs a second one itself must not source this file twice.
|
# long as the script (fd 9 closes with it); a smoke that runs a second one itself must not source this file twice.
|
||||||
exec 9>/tmp/lit-mcp-smoke.lock
|
exec 9>/tmp/lit-mcp-smoke.lock
|
||||||
@@ -22,7 +22,7 @@ ENDPOINT=http://localhost/wp-json/mcp/mcp-oauth-server
|
|||||||
HOST=$(wp option get home | sed -E 's#^[a-zA-Z]+://##')
|
HOST=$(wp option get home | sed -E 's#^[a-zA-Z]+://##')
|
||||||
|
|
||||||
# Optional: the Host header MCP requests carry (default: curl's own, "localhost"). A cache plugin that keys its files
|
# Optional: the Host header MCP requests carry (default: curl's own, "localhost"). A cache plugin that keys its files
|
||||||
# by the request's host (WP Super Cache) needs the site's real host here (super-cache-smoke.sh sets it).
|
# by the request's host (WP Fastest Cache) needs the site's real host here (none of the current smokes needs it).
|
||||||
MCP_HOST=${MCP_HOST:-}
|
MCP_HOST=${MCP_HOST:-}
|
||||||
|
|
||||||
TOKEN=""
|
TOKEN=""
|
||||||
|
|||||||
@@ -173,11 +173,11 @@ final class CompatibilityTabTest extends TestCase
|
|||||||
return (string) ob_get_clean();
|
return (string) ob_get_clean();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** One source for the ability name of the release and withdraw entries (SuperCacheListener reads it too). */
|
/** One source for the ability name of the release and withdraw entries (FastestCacheListener reads it too). */
|
||||||
public function testTheReleaseAbilityNameHasOneSource(): void
|
public function testTheReleaseAbilityNameHasOneSource(): void
|
||||||
{
|
{
|
||||||
self::assertSame('lit/divi-release', CompatibilityTab::releaseAbility('divi'));
|
self::assertSame('lit/divi-release', CompatibilityTab::releaseAbility('divi'));
|
||||||
self::assertTrue(CompatibilityTab::isReleaseAbility(CompatibilityTab::releaseAbility('wpsc')));
|
self::assertTrue(CompatibilityTab::isReleaseAbility(CompatibilityTab::releaseAbility('wpfc')));
|
||||||
foreach (['lit/content-update', 'lit/-release', 'lit/release', 'lit/divi-releases'] as $other) {
|
foreach (['lit/content-update', 'lit/-release', 'lit/release', 'lit/divi-releases'] as $other) {
|
||||||
self::assertFalse(CompatibilityTab::isReleaseAbility($other), $other);
|
self::assertFalse(CompatibilityTab::isReleaseAbility($other), $other);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ final class ThrowawayPostTest extends TestCase
|
|||||||
self::assertSame([], $this->throwaway()->removeOrphans('page'));
|
self::assertSame([], $this->throwaway()->removeOrphans('page'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A probe killed while its page was private (WP Super Cache) leaves a private page, not a draft. */
|
/** Private entries count too (the old WP Super Cache probe passed through private). */
|
||||||
public function testShortStatusListCoversAProbeKilledWhilePrivate(): void
|
public function testShortStatusListCoversAProbeKilledWhilePrivate(): void
|
||||||
{
|
{
|
||||||
$query = null;
|
$query = null;
|
||||||
|
|||||||
+23
-24
@@ -2,15 +2,15 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace LIT\MCP\Tests\Unit\Integrations\SuperCache;
|
namespace LIT\MCP\Tests\Unit\Integrations\FastestCache;
|
||||||
|
|
||||||
use Brain\Monkey\Functions;
|
use Brain\Monkey\Functions;
|
||||||
use LIT\MCP\Abilities\Ability;
|
use LIT\MCP\Abilities\Ability;
|
||||||
use LIT\MCP\Abilities\Annotations;
|
use LIT\MCP\Abilities\Annotations;
|
||||||
use LIT\MCP\Abilities\DeclaresRequirements;
|
use LIT\MCP\Abilities\DeclaresRequirements;
|
||||||
use LIT\MCP\Integrations\SuperCache\CacheClearAbility;
|
use LIT\MCP\Integrations\FastestCache\CacheClearAbility;
|
||||||
use LIT\MCP\Integrations\SuperCache\SuperCacheFeature;
|
use LIT\MCP\Integrations\FastestCache\FastestCacheFeature;
|
||||||
use LIT\MCP\Integrations\SuperCache\SuperCachePurger;
|
use LIT\MCP\Integrations\FastestCache\FastestCachePurger;
|
||||||
use LIT\MCP\Tests\Support\FakeChangeLog;
|
use LIT\MCP\Tests\Support\FakeChangeLog;
|
||||||
use LIT\MCP\Tests\Unit\TestCase;
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
use WP_Error;
|
use WP_Error;
|
||||||
@@ -51,20 +51,20 @@ final class CacheClearAbilityTest extends TestCase
|
|||||||
|
|
||||||
protected function tearDown(): void
|
protected function tearDown(): void
|
||||||
{
|
{
|
||||||
unset($GLOBALS['cache_enabled']);
|
unset($GLOBALS['wp_fastest_cache_options']);
|
||||||
parent::tearDown();
|
parent::tearDown();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function ability(?FakeChangeLog $log = null, bool $cacheOn = true, bool $usable = true): CacheClearAbility
|
private function ability(?FakeChangeLog $log = null, bool $cacheOn = true, bool $usable = true): CacheClearAbility
|
||||||
{
|
{
|
||||||
$purger = new SuperCachePurger(
|
$purger = new FastestCachePurger(
|
||||||
static fn (SuperCacheFeature $feature): bool => $usable,
|
static fn (FastestCacheFeature $feature): bool => $usable,
|
||||||
static fn (): bool => $cacheOn,
|
static fn (): bool => $cacheOn,
|
||||||
function (int $id): void {
|
function (int $id): void {
|
||||||
$this->purges[] = 'post:' . $id;
|
$this->purges[] = 'post:' . $id;
|
||||||
},
|
},
|
||||||
function (int $blog): void {
|
function (): void {
|
||||||
$this->purges[] = 'all:' . $blog;
|
$this->purges[] = 'all';
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ final class CacheClearAbilityTest extends TestCase
|
|||||||
self::assertFalse($ability->isDirect());
|
self::assertFalse($ability->isDirect());
|
||||||
self::assertSame(Annotations::write(true), $ability->annotations());
|
self::assertSame(Annotations::write(true), $ability->annotations());
|
||||||
self::assertStringContainsString('confirm: true', $ability->description());
|
self::assertStringContainsString('confirm: true', $ability->description());
|
||||||
self::assertStringContainsString('WP Super Cache', $ability->description());
|
self::assertStringContainsString('WP Fastest Cache', $ability->description());
|
||||||
|
|
||||||
$schema = $ability->inputSchema();
|
$schema = $ability->inputSchema();
|
||||||
self::assertSame(['scope'], $schema['required']);
|
self::assertSame(['scope'], $schema['required']);
|
||||||
@@ -95,8 +95,8 @@ final class CacheClearAbilityTest extends TestCase
|
|||||||
$ability = $this->ability();
|
$ability = $this->ability();
|
||||||
|
|
||||||
self::assertInstanceOf(DeclaresRequirements::class, $ability);
|
self::assertInstanceOf(DeclaresRequirements::class, $ability);
|
||||||
self::assertSame(['wpsc-purge-all'], $ability->requirements(['scope' => 'all']));
|
self::assertSame(['wpfc-purge-all'], $ability->requirements(['scope' => 'all']));
|
||||||
self::assertSame(['wpsc-purge-post'], $ability->requirements(['scope' => 'post', 'post_id' => 7]));
|
self::assertSame(['wpfc-purge-post'], $ability->requirements(['scope' => 'post', 'post_id' => 7]));
|
||||||
self::assertSame([], $ability->requirements(['scope' => 'x']));
|
self::assertSame([], $ability->requirements(['scope' => 'x']));
|
||||||
self::assertSame([], $ability->requirements([]));
|
self::assertSame([], $ability->requirements([]));
|
||||||
}
|
}
|
||||||
@@ -165,7 +165,7 @@ final class CacheClearAbilityTest extends TestCase
|
|||||||
|
|
||||||
public function testTheDryRunClearsNothingAndReportsTheState(): void
|
public function testTheDryRunClearsNothingAndReportsTheState(): void
|
||||||
{
|
{
|
||||||
$GLOBALS['cache_enabled'] = true;
|
$GLOBALS['wp_fastest_cache_options'] = (object) ['wpFastestCacheStatus' => 'on'];
|
||||||
$this->lastPurge = time() - 5;
|
$this->lastPurge = time() - 5;
|
||||||
|
|
||||||
$payload = self::assertToolError($this->ability()->execute(['scope' => 'all']), 'confirm_required');
|
$payload = self::assertToolError($this->ability()->execute(['scope' => 'all']), 'confirm_required');
|
||||||
@@ -202,17 +202,16 @@ final class CacheClearAbilityTest extends TestCase
|
|||||||
self::assertTrue($result['ok']);
|
self::assertTrue($result['ok']);
|
||||||
self::assertSame('purged', $result['outcome']);
|
self::assertSame('purged', $result['outcome']);
|
||||||
self::assertSame(4, $result['log_id']);
|
self::assertSame(4, $result['log_id']);
|
||||||
$log->assertRecorded(['ability' => 'lit/cache-clear', 'object_type' => 'wpsc', 'object_id' => '7']);
|
$log->assertRecorded(['ability' => 'lit/cache-clear', 'object_type' => 'wpfc', 'object_id' => '7']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testADraftHasNothingToClear(): void
|
/** WPFC finds the address of any post (a trashed one too), so no status is skipped. */
|
||||||
|
public function testADraftIsClearedToo(): void
|
||||||
{
|
{
|
||||||
$GLOBALS['cache_enabled'] = true;
|
$result = $this->ability(new FakeChangeLog(2))->execute(['scope' => 'post', 'post_id' => 8, 'confirm' => true]);
|
||||||
$result = $this->ability()->execute(['scope' => 'post', 'post_id' => 8, 'confirm' => true]);
|
|
||||||
|
|
||||||
self::assertSame([], $this->purges);
|
self::assertSame(['post:8'], $this->purges);
|
||||||
self::assertSame('nothing', $result['outcome']);
|
self::assertSame('purged', $result['outcome']);
|
||||||
self::assertStringContainsString('Entwurf', $result['summary']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testConfirmClearsTheWholeCacheAtOnce(): void
|
public function testConfirmClearsTheWholeCacheAtOnce(): void
|
||||||
@@ -221,9 +220,9 @@ final class CacheClearAbilityTest extends TestCase
|
|||||||
|
|
||||||
$result = $this->ability($log)->execute(['scope' => 'all', 'confirm' => true]);
|
$result = $this->ability($log)->execute(['scope' => 'all', 'confirm' => true]);
|
||||||
|
|
||||||
self::assertSame(['all:0'], $this->purges);
|
self::assertSame(['all'], $this->purges);
|
||||||
self::assertSame('purged', $result['outcome']);
|
self::assertSame('purged', $result['outcome']);
|
||||||
$log->assertRecorded(['ability' => 'lit/cache-clear', 'object_type' => 'wpsc', 'object_id' => 'all']);
|
$log->assertRecorded(['ability' => 'lit/cache-clear', 'object_type' => 'wpfc', 'object_id' => 'all']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testAWholePurgeInsideTheLockTimeIsThrottledWithATrailingPurge(): void
|
public function testAWholePurgeInsideTheLockTimeIsThrottledWithATrailingPurge(): void
|
||||||
@@ -233,7 +232,7 @@ final class CacheClearAbilityTest extends TestCase
|
|||||||
$result = $this->ability(new FakeChangeLog(1))->execute(['scope' => 'all', 'confirm' => true]);
|
$result = $this->ability(new FakeChangeLog(1))->execute(['scope' => 'all', 'confirm' => true]);
|
||||||
|
|
||||||
self::assertSame([], $this->purges);
|
self::assertSame([], $this->purges);
|
||||||
self::assertSame([SuperCachePurger::TRAILING_HOOK], $this->scheduled);
|
self::assertSame([FastestCachePurger::TRAILING_HOOK], $this->scheduled);
|
||||||
self::assertSame('throttled', $result['outcome']);
|
self::assertSame('throttled', $result['outcome']);
|
||||||
self::assertStringContainsString('Nachlauf', $result['summary']);
|
self::assertStringContainsString('Nachlauf', $result['summary']);
|
||||||
}
|
}
|
||||||
@@ -271,7 +270,7 @@ final class CacheClearAbilityTest extends TestCase
|
|||||||
self::assertInstanceOf(WP_Error::class, $result);
|
self::assertInstanceOf(WP_Error::class, $result);
|
||||||
self::assertSame('lit_compat_incompatible', $result->get_error_code());
|
self::assertSame('lit_compat_incompatible', $result->get_error_code());
|
||||||
self::assertStringContainsString(
|
self::assertStringContainsString(
|
||||||
'WP Super Cache ist gesperrt (siehe Reiter Kompatibilität). Es wurde nichts geleert.',
|
'WP Fastest Cache ist gesperrt (siehe Reiter Kompatibilität). Es wurde nichts geleert.',
|
||||||
$result->get_error_message()
|
$result->get_error_message()
|
||||||
);
|
);
|
||||||
self::assertSame([], $this->purges);
|
self::assertSame([], $this->purges);
|
||||||
+12
-12
@@ -2,33 +2,33 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace LIT\MCP\Tests\Unit\Integrations\SuperCache;
|
namespace LIT\MCP\Tests\Unit\Integrations\FastestCache;
|
||||||
|
|
||||||
use LIT\MCP\Compatibility\CheckStep;
|
use LIT\MCP\Compatibility\CheckStep;
|
||||||
use LIT\MCP\Integrations\SuperCache\SuperCacheCheckSteps;
|
use LIT\MCP\Integrations\FastestCache\FastestCacheCheckSteps;
|
||||||
use LIT\MCP\Integrations\SuperCache\SuperCacheCompatibility;
|
use LIT\MCP\Integrations\FastestCache\FastestCacheCompatibility;
|
||||||
use LIT\MCP\Tests\Unit\TestCase;
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
|
|
||||||
final class SuperCacheCheckStepsTest extends TestCase
|
final class FastestCacheCheckStepsTest extends TestCase
|
||||||
{
|
{
|
||||||
public function testTheProbeIsSkippedWithARedManifestAndNotRun(): void
|
public function testTheProbeIsSkippedWithARedManifestAndNotRun(): void
|
||||||
{
|
{
|
||||||
$ran = false;
|
$ran = false;
|
||||||
$steps = SuperCacheCheckSteps::extra(static function () use (&$ran): array {
|
$steps = FastestCacheCheckSteps::extra(static function () use (&$ran): array {
|
||||||
$ran = true;
|
$ran = true;
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
})(false);
|
})(false);
|
||||||
|
|
||||||
self::assertCount(1, $steps);
|
self::assertCount(1, $steps);
|
||||||
self::assertSame(SuperCacheCheckSteps::PROBE, $steps[0]->key);
|
self::assertSame(FastestCacheCheckSteps::PROBE, $steps[0]->key);
|
||||||
self::assertTrue($steps[0]->skipped);
|
self::assertTrue($steps[0]->skipped);
|
||||||
self::assertFalse($ran);
|
self::assertFalse($ran);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testTheStepCarriesTheProbeProblems(): void
|
public function testTheStepCarriesTheProbeProblems(): void
|
||||||
{
|
{
|
||||||
$steps = SuperCacheCheckSteps::extra(static fn (): array => ['rot'])(true);
|
$steps = FastestCacheCheckSteps::extra(static fn (): array => ['rot'])(true);
|
||||||
|
|
||||||
self::assertSame(['rot'], $steps[0]->problems);
|
self::assertSame(['rot'], $steps[0]->problems);
|
||||||
self::assertFalse($steps[0]->skipped);
|
self::assertFalse($steps[0]->skipped);
|
||||||
@@ -38,7 +38,7 @@ final class SuperCacheCheckStepsTest extends TestCase
|
|||||||
/** A switched-off cache is no red step: a skipped one would lock the purge of a site that simply caches nothing. */
|
/** A switched-off cache is no red step: a skipped one would lock the purge of a site that simply caches nothing. */
|
||||||
public function testWithoutAnActiveCacheTheStepIsGreenAndSaysSo(): void
|
public function testWithoutAnActiveCacheTheStepIsGreenAndSaysSo(): void
|
||||||
{
|
{
|
||||||
$steps = SuperCacheCheckSteps::extra(static fn (): ?array => null)(true);
|
$steps = FastestCacheCheckSteps::extra(static fn (): ?array => null)(true);
|
||||||
|
|
||||||
self::assertTrue($steps[0]->green());
|
self::assertTrue($steps[0]->green());
|
||||||
self::assertStringContainsString('Cache ist ausgeschaltet, nicht geprüft', $steps[0]->label());
|
self::assertStringContainsString('Cache ist ausgeschaltet, nicht geprüft', $steps[0]->label());
|
||||||
@@ -46,11 +46,11 @@ final class SuperCacheCheckStepsTest extends TestCase
|
|||||||
|
|
||||||
public function testTheLabelOfAnOldStepComesFromTheKey(): void
|
public function testTheLabelOfAnOldStepComesFromTheKey(): void
|
||||||
{
|
{
|
||||||
self::assertSame('Leeren-Test an einer Wegwerf-Seite im Cache', SuperCacheCheckSteps::label(new CheckStep(SuperCacheCheckSteps::PROBE, [])));
|
self::assertSame('Leeren-Test an einer Wegwerf-Seite im Cache', FastestCacheCheckSteps::label(new CheckStep(FastestCacheCheckSteps::PROBE, [])));
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
SuperCacheCompatibility::LABEL . '-Funktionen, Klassen und Registrierungen',
|
FastestCacheCompatibility::LABEL . '-Funktionen, Klassen und Registrierungen',
|
||||||
SuperCacheCheckSteps::label(new CheckStep(CheckStep::MANIFEST, []))
|
FastestCacheCheckSteps::label(new CheckStep(CheckStep::MANIFEST, []))
|
||||||
);
|
);
|
||||||
self::assertSame('other', SuperCacheCheckSteps::label(new CheckStep('other', [])));
|
self::assertSame('other', FastestCacheCheckSteps::label(new CheckStep('other', [])));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Tests\Unit\Integrations\FastestCache;
|
||||||
|
|
||||||
|
use Brain\Monkey\Functions;
|
||||||
|
use LIT\MCP\Compatibility\Call;
|
||||||
|
use LIT\MCP\Compatibility\CheckStep;
|
||||||
|
use LIT\MCP\Compatibility\Compatibility;
|
||||||
|
use LIT\MCP\Compatibility\Tier;
|
||||||
|
use LIT\MCP\Integrations\Divi\Release\DiviProfile;
|
||||||
|
use LIT\MCP\Integrations\Pods\PodsCompatibility;
|
||||||
|
use LIT\MCP\Integrations\FastestCache\FastestCacheCheckSteps;
|
||||||
|
use LIT\MCP\Integrations\FastestCache\FastestCacheCompatibility;
|
||||||
|
use LIT\MCP\Integrations\FastestCache\FastestCacheFeature;
|
||||||
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
|
|
||||||
|
final class FastestCacheCompatibilityTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testDescribesFastestCacheAsALockOnFailurePlugin(): void
|
||||||
|
{
|
||||||
|
$profile = FastestCacheCompatibility::profile();
|
||||||
|
|
||||||
|
self::assertSame('wpfc', $profile->id);
|
||||||
|
self::assertSame('WP Fastest Cache', $profile->label);
|
||||||
|
self::assertSame(Tier::LockOnFailure, $profile->tier);
|
||||||
|
self::assertSame(['1.5.5'], $profile->testedVersions);
|
||||||
|
self::assertNull($profile->minVersion);
|
||||||
|
self::assertSame(FastestCacheFeature::cases(), $profile->capabilities);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Argument counts are those the purger and the probe pass. */
|
||||||
|
public function testTheManifestHoldsTheTwoPurgeFunctionsWithTheirArgumentCounts(): void
|
||||||
|
{
|
||||||
|
$manifest = FastestCacheCompatibility::profile()->manifests[0];
|
||||||
|
$calls = [];
|
||||||
|
foreach ($manifest->calls as $call) {
|
||||||
|
$calls[$call->symbol] = [$call->args, $call->features()];
|
||||||
|
}
|
||||||
|
|
||||||
|
self::assertSame([
|
||||||
|
'wpfc_clear_post_cache_by_id' => [1, [FastestCacheFeature::PurgePost]],
|
||||||
|
'wpfc_clear_all_cache' => [0, [FastestCacheFeature::PurgeAll]],
|
||||||
|
'WpFastestCache::singleDeleteCache' => [3, [FastestCacheFeature::PurgePost]],
|
||||||
|
'WpFastestCache::getWpContentDir' => [1, [FastestCacheFeature::PurgePost]],
|
||||||
|
], $calls);
|
||||||
|
self::assertContainsOnlyInstancesOf(Call::class, $manifest->calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The instance that hooks the purge actions and the decoded options with the cache switch. */
|
||||||
|
public function testTheGlobalsTheFunctionsReadAreRequired(): void
|
||||||
|
{
|
||||||
|
$manifest = FastestCacheCompatibility::profile()->manifests[0];
|
||||||
|
|
||||||
|
self::assertSame(['WPFC_MAIN_PATH'], array_map(static fn ($c) => $c->name, $manifest->constants));
|
||||||
|
$globals = [];
|
||||||
|
foreach ($manifest->globals as $global) {
|
||||||
|
$globals[$global->name] = $global->features();
|
||||||
|
}
|
||||||
|
self::assertSame(
|
||||||
|
['wp_fastest_cache' => FastestCacheFeature::cases(), 'wp_fastest_cache_options' => FastestCacheFeature::cases()],
|
||||||
|
$globals
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @runInSeparateProcess
|
||||||
|
* @preserveGlobalState disabled
|
||||||
|
*/
|
||||||
|
public function testActiveAndVersionFollowTheMainPathConstantAndThePluginHeader(): void
|
||||||
|
{
|
||||||
|
$profile = FastestCacheCompatibility::profile();
|
||||||
|
self::assertFalse(($profile->active)());
|
||||||
|
self::assertNull(($profile->version)());
|
||||||
|
|
||||||
|
define('WPFC_MAIN_PATH', '/plugins/wp-fastest-cache/');
|
||||||
|
Functions\expect('get_file_data')
|
||||||
|
->once()
|
||||||
|
->with('/plugins/wp-fastest-cache/wpFastestCache.php', ['Version' => 'Version'])
|
||||||
|
->andReturn(['Version' => '1.5.5']);
|
||||||
|
|
||||||
|
self::assertTrue(($profile->active)());
|
||||||
|
self::assertSame('1.5.5', ($profile->version)());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @runInSeparateProcess
|
||||||
|
* @preserveGlobalState disabled
|
||||||
|
*/
|
||||||
|
public function testAnUnreadableHeaderIsAnUnknownVersion(): void
|
||||||
|
{
|
||||||
|
define('WPFC_MAIN_PATH', '/plugins/wp-fastest-cache/');
|
||||||
|
Functions\when('get_file_data')->justReturn(['Version' => '']);
|
||||||
|
|
||||||
|
self::assertNull((FastestCacheCompatibility::profile()->version)());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheCheckHasTheProbeAsExtraStep(): void
|
||||||
|
{
|
||||||
|
$profile = FastestCacheCompatibility::profile();
|
||||||
|
|
||||||
|
self::assertNotNull($profile->extraSteps);
|
||||||
|
$steps = ($profile->extraSteps)(false);
|
||||||
|
self::assertSame([FastestCacheCheckSteps::PROBE], array_map(static fn ($step): string => $step->key, $steps));
|
||||||
|
self::assertTrue($steps[0]->skipped);
|
||||||
|
self::assertNotNull($profile->stepLabel);
|
||||||
|
self::assertSame(
|
||||||
|
'Leeren-Test an einer Wegwerf-Seite im Cache',
|
||||||
|
($profile->stepLabel)(new CheckStep(FastestCacheCheckSteps::PROBE, []))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheCacheIsOnlyEnabledWhileTheSwitchInTheDecodedOptionsIsOn(): void
|
||||||
|
{
|
||||||
|
unset($GLOBALS['wp_fastest_cache_options']);
|
||||||
|
self::assertFalse(FastestCacheCompatibility::cacheEnabled());
|
||||||
|
$GLOBALS['wp_fastest_cache_options'] = [];
|
||||||
|
self::assertFalse(FastestCacheCompatibility::cacheEnabled(), 'WPFC stores [] when the option is missing');
|
||||||
|
$GLOBALS['wp_fastest_cache_options'] = (object) ['wpFastestCacheMinifyHtml' => 'on'];
|
||||||
|
self::assertFalse(FastestCacheCompatibility::cacheEnabled());
|
||||||
|
$GLOBALS['wp_fastest_cache_options'] = (object) ['wpFastestCacheStatus' => 'on'];
|
||||||
|
self::assertTrue(FastestCacheCompatibility::cacheEnabled());
|
||||||
|
unset($GLOBALS['wp_fastest_cache_options']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAllProfilesCoexistWithoutIdCollision(): void
|
||||||
|
{
|
||||||
|
$compatibility = new Compatibility(
|
||||||
|
[DiviProfile::profile(), PodsCompatibility::profile(), FastestCacheCompatibility::profile()]
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(['divi', 'pods', 'wpfc'], array_map(static fn ($p) => $p->id, $compatibility->profiles()));
|
||||||
|
self::assertSame(FastestCacheFeature::PurgeAll, $compatibility->capability('wpfc-purge-all'));
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-6
@@ -2,25 +2,25 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace LIT\MCP\Tests\Unit\Integrations\SuperCache;
|
namespace LIT\MCP\Tests\Unit\Integrations\FastestCache;
|
||||||
|
|
||||||
use LIT\MCP\Compatibility\Capability;
|
use LIT\MCP\Compatibility\Capability;
|
||||||
use LIT\MCP\Integrations\SuperCache\SuperCacheFeature;
|
use LIT\MCP\Integrations\FastestCache\FastestCacheFeature;
|
||||||
use LIT\MCP\Tests\Unit\TestCase;
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
|
|
||||||
final class SuperCacheFeatureTest extends TestCase
|
final class FastestCacheFeatureTest extends TestCase
|
||||||
{
|
{
|
||||||
public function testEveryFeatureIsAWritingCapabilityWithAPrefixedId(): void
|
public function testEveryFeatureIsAWritingCapabilityWithAPrefixedId(): void
|
||||||
{
|
{
|
||||||
foreach (SuperCacheFeature::cases() as $feature) {
|
foreach (FastestCacheFeature::cases() as $feature) {
|
||||||
self::assertInstanceOf(Capability::class, $feature);
|
self::assertInstanceOf(Capability::class, $feature);
|
||||||
self::assertSame($feature->value, $feature->id());
|
self::assertSame($feature->value, $feature->id());
|
||||||
self::assertNotSame('', $feature->label());
|
self::assertNotSame('', $feature->label());
|
||||||
self::assertTrue($feature->isWrite());
|
self::assertTrue($feature->isWrite());
|
||||||
}
|
}
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
['wpsc-purge-post', 'wpsc-purge-all'],
|
['wpfc-purge-post', 'wpfc-purge-all'],
|
||||||
array_map(static fn (SuperCacheFeature $f): string => $f->id(), SuperCacheFeature::cases())
|
array_map(static fn (FastestCacheFeature $f): string => $f->id(), FastestCacheFeature::cases())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Tests\Unit\Integrations\FastestCache;
|
||||||
|
|
||||||
|
use Brain\Monkey\Functions;
|
||||||
|
use LIT\MCP\Admin\CompatibilityTab;
|
||||||
|
use LIT\MCP\Compatibility\ProvidesCompatibility;
|
||||||
|
use LIT\MCP\Integrations\Bootable;
|
||||||
|
use LIT\MCP\Integrations\Integration;
|
||||||
|
use LIT\MCP\Integrations\FastestCache\FastestCacheCompatibility;
|
||||||
|
use LIT\MCP\Integrations\FastestCache\FastestCacheIntegration;
|
||||||
|
use LIT\MCP\Integrations\FastestCache\FastestCachePurger;
|
||||||
|
use LIT\MCP\Tests\Support\FakeChangeLog;
|
||||||
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
|
|
||||||
|
final class FastestCacheIntegrationTest extends TestCase
|
||||||
|
{
|
||||||
|
private static function purger(): FastestCachePurger
|
||||||
|
{
|
||||||
|
return new FastestCachePurger(static fn (): bool => true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsAnIntegrationThatProvidesItsProfileAndTheCacheClearTool(): void
|
||||||
|
{
|
||||||
|
$integration = new FastestCacheIntegration(null, self::purger(), FakeChangeLog::forbidden());
|
||||||
|
|
||||||
|
self::assertInstanceOf(Integration::class, $integration);
|
||||||
|
self::assertInstanceOf(ProvidesCompatibility::class, $integration);
|
||||||
|
self::assertSame('wpfc', $integration->id());
|
||||||
|
self::assertSame('WP Fastest Cache', $integration->label());
|
||||||
|
self::assertEquals(['lit/cache-clear'], array_map(static fn ($a): string => $a->name(), $integration->abilities()));
|
||||||
|
self::assertEquals([FastestCacheCompatibility::profile()], $integration->compatibilities());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The same constant the profile's active() reads: WP Fastest Cache defines it when its plugin file loads.
|
||||||
|
*
|
||||||
|
* @runInSeparateProcess
|
||||||
|
* @preserveGlobalState disabled
|
||||||
|
*/
|
||||||
|
public function testIsAvailableOnlyWithFastestCache(): void
|
||||||
|
{
|
||||||
|
$integration = new FastestCacheIntegration(null, self::purger(), FakeChangeLog::forbidden());
|
||||||
|
self::assertFalse($integration->isAvailable());
|
||||||
|
|
||||||
|
define('WPFC_MAIN_PATH', '/plugins/wp-fastest-cache/');
|
||||||
|
self::assertTrue($integration->isAvailable());
|
||||||
|
self::assertTrue((FastestCacheCompatibility::profile()->active)());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testBootHooksTheOrphanCleanupIntoTheProfileLoad(): void
|
||||||
|
{
|
||||||
|
$integration = new FastestCacheIntegration(null, self::purger(), FakeChangeLog::forbidden());
|
||||||
|
$integration->boot();
|
||||||
|
|
||||||
|
self::assertInstanceOf(Bootable::class, $integration);
|
||||||
|
self::assertNotFalse(has_action(CompatibilityTab::LOAD_ACTION, [$integration, 'removeOrphans']));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNoCleanupForAnotherProfileWithoutWpscOrWithoutTheCapability(): void
|
||||||
|
{
|
||||||
|
Functions\expect('get_posts')->never();
|
||||||
|
Functions\when('current_user_can')->justReturn(true);
|
||||||
|
(new FastestCacheIntegration(static fn (): bool => true, self::purger(), FakeChangeLog::forbidden()))->removeOrphans('pods');
|
||||||
|
(new FastestCacheIntegration(static fn (): bool => false, self::purger(), FakeChangeLog::forbidden()))->removeOrphans('wpfc');
|
||||||
|
|
||||||
|
Functions\when('current_user_can')->justReturn(false);
|
||||||
|
(new FastestCacheIntegration(static fn (): bool => true, self::purger(), FakeChangeLog::forbidden()))->removeOrphans('wpfc');
|
||||||
|
$this->addToAssertionCount(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCleansUpPagesForTheAdministratorWithWpsc(): void
|
||||||
|
{
|
||||||
|
Functions\when('current_user_can')->justReturn(true);
|
||||||
|
Functions\expect('get_posts')->once()->andReturnUsing(static function (array $args): array {
|
||||||
|
self::assertSame('page', $args['post_type']);
|
||||||
|
self::assertSame(['draft', 'private'], $args['post_status']);
|
||||||
|
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
(new FastestCacheIntegration(static fn (): bool => true, self::purger(), FakeChangeLog::forbidden()))->removeOrphans('wpfc');
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
-52
@@ -2,19 +2,19 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace LIT\MCP\Tests\Unit\Integrations\SuperCache;
|
namespace LIT\MCP\Tests\Unit\Integrations\FastestCache;
|
||||||
|
|
||||||
use Brain\Monkey\Functions;
|
use Brain\Monkey\Functions;
|
||||||
use Closure;
|
use Closure;
|
||||||
use LIT\MCP\Admin\CompatibilityTab;
|
use LIT\MCP\Admin\CompatibilityTab;
|
||||||
use LIT\MCP\Compatibility\Compatibility;
|
use LIT\MCP\Compatibility\Compatibility;
|
||||||
use LIT\MCP\Integrations\SuperCache\SuperCacheCompatibility;
|
use LIT\MCP\Integrations\FastestCache\FastestCacheCompatibility;
|
||||||
use LIT\MCP\Integrations\SuperCache\SuperCacheFeature;
|
use LIT\MCP\Integrations\FastestCache\FastestCacheFeature;
|
||||||
use LIT\MCP\Integrations\SuperCache\SuperCacheListener;
|
use LIT\MCP\Integrations\FastestCache\FastestCacheListener;
|
||||||
use LIT\MCP\Tests\Unit\TestCase;
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
use WP_Post;
|
use WP_Post;
|
||||||
|
|
||||||
final class SuperCacheListenerTest extends TestCase
|
final class FastestCacheListenerTest extends TestCase
|
||||||
{
|
{
|
||||||
/** @var list<string> */
|
/** @var list<string> */
|
||||||
private array $purges = [];
|
private array $purges = [];
|
||||||
@@ -56,7 +56,7 @@ final class SuperCacheListenerTest extends TestCase
|
|||||||
{
|
{
|
||||||
ini_set('error_log', (string) $this->previousErrorLog);
|
ini_set('error_log', (string) $this->previousErrorLog);
|
||||||
unlink($this->errorLog);
|
unlink($this->errorLog);
|
||||||
unset($GLOBALS['cache_enabled']);
|
unset($GLOBALS['wp_fastest_cache_options']);
|
||||||
parent::tearDown();
|
parent::tearDown();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,17 +66,17 @@ final class SuperCacheListenerTest extends TestCase
|
|||||||
bool $usablePost = true,
|
bool $usablePost = true,
|
||||||
bool $usableAll = true,
|
bool $usableAll = true,
|
||||||
?Closure $postIdFor = null
|
?Closure $postIdFor = null
|
||||||
): SuperCacheListener {
|
): FastestCacheListener {
|
||||||
return new SuperCacheListener(
|
return new FastestCacheListener(
|
||||||
static fn (SuperCacheFeature $feature): bool => $feature === SuperCacheFeature::PurgePost
|
static fn (FastestCacheFeature $feature): bool => $feature === FastestCacheFeature::PurgePost
|
||||||
? $usablePost
|
? $usablePost
|
||||||
: $usableAll,
|
: $usableAll,
|
||||||
static fn (): bool => $cacheOn,
|
static fn (): bool => $cacheOn,
|
||||||
function (int $id): void {
|
function (int $id): void {
|
||||||
$this->purges[] = 'post:' . $id;
|
$this->purges[] = 'post:' . $id;
|
||||||
},
|
},
|
||||||
function (int $blog): void {
|
function (): void {
|
||||||
$this->purges[] = 'all:' . $blog;
|
$this->purges[] = 'all';
|
||||||
},
|
},
|
||||||
$postIdFor ?? static fn (string $type, string $id): ?int => in_array($type, ['page', 'post'], true)
|
$postIdFor ?? static fn (string $type, string $id): ?int => in_array($type, ['page', 'post'], true)
|
||||||
? (int) $id
|
? (int) $id
|
||||||
@@ -120,7 +120,7 @@ final class SuperCacheListenerTest extends TestCase
|
|||||||
$listener->onChange('lit/content-update', 'post', '14', 7);
|
$listener->onChange('lit/content-update', 'post', '14', 7);
|
||||||
$listener->flush();
|
$listener->flush();
|
||||||
|
|
||||||
self::assertSame(['all:0'], $this->purges);
|
self::assertSame(['all'], $this->purges);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testThePurgeIsDoneOncePerFlush(): void
|
public function testThePurgeIsDoneOncePerFlush(): void
|
||||||
@@ -143,7 +143,7 @@ final class SuperCacheListenerTest extends TestCase
|
|||||||
$listener->flush();
|
$listener->flush();
|
||||||
$listener->onChange('lit/content-update', 'page', '14', 7);
|
$listener->onChange('lit/content-update', 'page', '14', 7);
|
||||||
$listener->flush();
|
$listener->flush();
|
||||||
self::assertSame(['all:0', 'post:14'], $this->purges);
|
self::assertSame(['all', 'post:14'], $this->purges);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @dataProvider siteWideChanges */
|
/** @dataProvider siteWideChanges */
|
||||||
@@ -153,7 +153,7 @@ final class SuperCacheListenerTest extends TestCase
|
|||||||
$listener->onChange($ability, $type, $id, null);
|
$listener->onChange($ability, $type, $id, null);
|
||||||
$listener->flush();
|
$listener->flush();
|
||||||
|
|
||||||
self::assertSame(['all:0'], $this->purges);
|
self::assertSame(['all'], $this->purges);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array<string, array{string, string, string}> */
|
/** @return array<string, array{string, string, string}> */
|
||||||
@@ -175,7 +175,7 @@ final class SuperCacheListenerTest extends TestCase
|
|||||||
$listener->onChange('lit/post-trash', 'post', '99', 1);
|
$listener->onChange('lit/post-trash', 'post', '99', 1);
|
||||||
$listener->flush();
|
$listener->flush();
|
||||||
|
|
||||||
self::assertSame(['all:0'], $this->purges);
|
self::assertSame(['all'], $this->purges);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** One whole-cache change among single posts: still one purge, no single ones on top. */
|
/** One whole-cache change among single posts: still one purge, no single ones on top. */
|
||||||
@@ -186,7 +186,7 @@ final class SuperCacheListenerTest extends TestCase
|
|||||||
$listener->onChange('lit/wp-settings-update', 'option', 'a', 2);
|
$listener->onChange('lit/wp-settings-update', 'option', 'a', 2);
|
||||||
$listener->flush();
|
$listener->flush();
|
||||||
|
|
||||||
self::assertSame(['all:0'], $this->purges);
|
self::assertSame(['all'], $this->purges);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Release and withdraw entries of the check workflow change nothing on the site (shared ability name). */
|
/** Release and withdraw entries of the check workflow change nothing on the site (shared ability name). */
|
||||||
@@ -204,8 +204,8 @@ final class SuperCacheListenerTest extends TestCase
|
|||||||
public function testTheCacheClearToolsOwnEntriesPurgeNothing(): void
|
public function testTheCacheClearToolsOwnEntriesPurgeNothing(): void
|
||||||
{
|
{
|
||||||
$listener = $this->listener();
|
$listener = $this->listener();
|
||||||
$listener->onChange('lit/cache-clear', 'wpsc', 'all', 1);
|
$listener->onChange('lit/cache-clear', 'wpfc', 'all', 1);
|
||||||
$listener->onChange('lit/cache-clear', 'wpsc', '12', 2);
|
$listener->onChange('lit/cache-clear', 'wpfc', '12', 2);
|
||||||
$listener->flush();
|
$listener->flush();
|
||||||
|
|
||||||
self::assertSame([], $this->purges);
|
self::assertSame([], $this->purges);
|
||||||
@@ -233,7 +233,7 @@ final class SuperCacheListenerTest extends TestCase
|
|||||||
$listener->flush();
|
$listener->flush();
|
||||||
|
|
||||||
self::assertSame([], $this->purges);
|
self::assertSame([], $this->purges);
|
||||||
self::assertSame(1, substr_count($this->logged(), 'wpsc-purge-post'));
|
self::assertSame(1, substr_count($this->logged(), 'wpfc-purge-post'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The capabilities lock separately: a gone purge-post does not stop the whole-cache purge. */
|
/** The capabilities lock separately: a gone purge-post does not stop the whole-cache purge. */
|
||||||
@@ -244,7 +244,7 @@ final class SuperCacheListenerTest extends TestCase
|
|||||||
$listener->onChange('lit/wp-settings-update', 'option', 'blogname', 1);
|
$listener->onChange('lit/wp-settings-update', 'option', 'blogname', 1);
|
||||||
$listener->flush();
|
$listener->flush();
|
||||||
|
|
||||||
self::assertSame(['all:0'], $this->purges);
|
self::assertSame(['all'], $this->purges);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testALockedWholeCachePurgeSchedulesNothing(): void
|
public function testALockedWholeCachePurgeSchedulesNothing(): void
|
||||||
@@ -258,18 +258,6 @@ final class SuperCacheListenerTest extends TestCase
|
|||||||
self::assertSame([], $this->scheduled);
|
self::assertSame([], $this->scheduled);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testOnAMultisiteTheCurrentBlogIsPurged(): void
|
|
||||||
{
|
|
||||||
Functions\when('is_multisite')->justReturn(true);
|
|
||||||
Functions\when('get_current_blog_id')->justReturn(3);
|
|
||||||
|
|
||||||
$listener = $this->listener();
|
|
||||||
$listener->onChange('lit/wp-settings-update', 'option', 'blogname', 1);
|
|
||||||
$listener->flush();
|
|
||||||
|
|
||||||
self::assertSame(['all:3'], $this->purges);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testAWholeCachePurgeStampsTheTimeForThirtySeconds(): void
|
public function testAWholeCachePurgeStampsTheTimeForThirtySeconds(): void
|
||||||
{
|
{
|
||||||
$listener = $this->listener();
|
$listener = $this->listener();
|
||||||
@@ -277,7 +265,7 @@ final class SuperCacheListenerTest extends TestCase
|
|||||||
$listener->flush();
|
$listener->flush();
|
||||||
|
|
||||||
self::assertCount(1, $this->stamped);
|
self::assertCount(1, $this->stamped);
|
||||||
self::assertSame('lit_mcp_wpsc_purged_at', $this->stamped[0][0]);
|
self::assertSame('lit_mcp_wpfc_purged_at', $this->stamped[0][0]);
|
||||||
self::assertEqualsWithDelta(time(), $this->stamped[0][1], 2);
|
self::assertEqualsWithDelta(time(), $this->stamped[0][1], 2);
|
||||||
self::assertSame(30, $this->stamped[0][2]);
|
self::assertSame(30, $this->stamped[0][2]);
|
||||||
}
|
}
|
||||||
@@ -305,7 +293,7 @@ final class SuperCacheListenerTest extends TestCase
|
|||||||
|
|
||||||
self::assertSame([], $this->purges);
|
self::assertSame([], $this->purges);
|
||||||
self::assertSame([], $this->stamped);
|
self::assertSame([], $this->stamped);
|
||||||
self::assertSame([[$this->lastPurge + 30, 'lit_mcp_wpsc_trailing_purge']], $this->scheduled);
|
self::assertSame([[$this->lastPurge + 30, 'lit_mcp_wpfc_trailing_purge']], $this->scheduled);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Options-table transients come back as strings. */
|
/** Options-table transients come back as strings. */
|
||||||
@@ -337,7 +325,7 @@ final class SuperCacheListenerTest extends TestCase
|
|||||||
$listener = $this->listener();
|
$listener = $this->listener();
|
||||||
$listener->trailingPurge();
|
$listener->trailingPurge();
|
||||||
|
|
||||||
self::assertSame(['all:0'], $this->purges);
|
self::assertSame(['all'], $this->purges);
|
||||||
self::assertCount(1, $this->stamped);
|
self::assertCount(1, $this->stamped);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,7 +339,7 @@ final class SuperCacheListenerTest extends TestCase
|
|||||||
|
|
||||||
public function testAThrowingTrailingPurgeIsLogged(): void
|
public function testAThrowingTrailingPurgeIsLogged(): void
|
||||||
{
|
{
|
||||||
$listener = new SuperCacheListener(
|
$listener = new FastestCacheListener(
|
||||||
static fn (): bool => true,
|
static fn (): bool => true,
|
||||||
static fn (): bool => true,
|
static fn (): bool => true,
|
||||||
null,
|
null,
|
||||||
@@ -367,13 +355,13 @@ final class SuperCacheListenerTest extends TestCase
|
|||||||
|
|
||||||
public function testAThrowingPurgeIsLoggedAndNotRethrown(): void
|
public function testAThrowingPurgeIsLoggedAndNotRethrown(): void
|
||||||
{
|
{
|
||||||
$listener = new SuperCacheListener(
|
$listener = new FastestCacheListener(
|
||||||
static fn (): bool => true,
|
static fn (): bool => true,
|
||||||
static fn (): bool => true,
|
static fn (): bool => true,
|
||||||
static function (): void {
|
static function (): void {
|
||||||
throw new \RuntimeException('disk full');
|
throw new \RuntimeException('disk full');
|
||||||
},
|
},
|
||||||
static fn (int $blog): null => null,
|
static fn (): null => null,
|
||||||
static fn (): int => 12
|
static fn (): int => 12
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -389,37 +377,37 @@ final class SuperCacheListenerTest extends TestCase
|
|||||||
$listener->register();
|
$listener->register();
|
||||||
|
|
||||||
self::assertSame(10, has_action('lit_mcp_changed', [$listener, 'onChange']));
|
self::assertSame(10, has_action('lit_mcp_changed', [$listener, 'onChange']));
|
||||||
self::assertSame(10, has_action('lit_mcp_wpsc_trailing_purge', [$listener, 'trailingPurge']));
|
self::assertSame(10, has_action('lit_mcp_wpfc_trailing_purge', [$listener, 'trailingPurge']));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Plugin wires create(): without WP Super Cache nothing is usable, and with the cache off nothing is asked. */
|
/** Plugin wires create(): without WP Fastest Cache nothing is usable, and with the cache off nothing is asked. */
|
||||||
public function testCreateAsksTheCompatibilityOnlyWhenTheCacheIsOn(): void
|
public function testCreateAsksTheCompatibilityOnlyWhenTheCacheIsOn(): void
|
||||||
{
|
{
|
||||||
$compatibility = new Compatibility([SuperCacheCompatibility::profile()]);
|
$compatibility = new Compatibility([FastestCacheCompatibility::profile()]);
|
||||||
$listener = SuperCacheListener::create($compatibility);
|
$listener = FastestCacheListener::create($compatibility);
|
||||||
|
|
||||||
$listener->onChange('lit/wp-settings-update', 'option', 'blogname', 1);
|
$listener->onChange('lit/wp-settings-update', 'option', 'blogname', 1);
|
||||||
$listener->flush();
|
$listener->flush();
|
||||||
self::assertSame('', $this->logged());
|
self::assertSame('', $this->logged());
|
||||||
|
|
||||||
$GLOBALS['cache_enabled'] = true;
|
$GLOBALS['wp_fastest_cache_options'] = (object) ['wpFastestCacheStatus' => 'on'];
|
||||||
$listener->onChange('lit/wp-settings-update', 'option', 'blogname', 1);
|
$listener->onChange('lit/wp-settings-update', 'option', 'blogname', 1);
|
||||||
$listener->flush();
|
$listener->flush();
|
||||||
self::assertStringContainsString('wpsc-purge-all not usable', $this->logged());
|
self::assertStringContainsString('wpfc-purge-all not usable', $this->logged());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testTheDefaultCacheSwitchIsTheWpscGlobal(): void
|
public function testTheDefaultCacheSwitchIsTheStatusOfWpfc(): void
|
||||||
{
|
{
|
||||||
$post = new WP_Post();
|
$post = new WP_Post();
|
||||||
Functions\when('get_post')->justReturn($post);
|
Functions\when('get_post')->justReturn($post);
|
||||||
Functions\when('get_post_type_object')->justReturn((object) ['public' => true]);
|
Functions\when('get_post_type_object')->justReturn((object) ['public' => true]);
|
||||||
$listener = new SuperCacheListener(static fn (): bool => true);
|
$listener = new FastestCacheListener(static fn (): bool => true);
|
||||||
|
|
||||||
$GLOBALS['cache_enabled'] = false;
|
$GLOBALS['wp_fastest_cache_options'] = (object) [];
|
||||||
$listener->onChange('lit/content-update', 'page', '12', 5);
|
$listener->onChange('lit/content-update', 'page', '12', 5);
|
||||||
$listener->flush();
|
$listener->flush();
|
||||||
|
|
||||||
self::assertSame('', $this->logged(), 'unset or false cache_enabled: silent, nothing purged');
|
self::assertSame('', $this->logged(), 'cache switched off: silent, nothing purged');
|
||||||
self::assertFalse(has_action('shutdown', [$listener, 'flush']));
|
self::assertFalse(has_action('shutdown', [$listener, 'flush']));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,18 +418,18 @@ final class SuperCacheListenerTest extends TestCase
|
|||||||
bool $public,
|
bool $public,
|
||||||
bool $exists
|
bool $exists
|
||||||
): void {
|
): void {
|
||||||
$GLOBALS['cache_enabled'] = true;
|
$GLOBALS['wp_fastest_cache_options'] = (object) ['wpFastestCacheStatus' => 'on'];
|
||||||
Functions\when('get_post_type_object')->justReturn((object) ['public' => $public]);
|
Functions\when('get_post_type_object')->justReturn((object) ['public' => $public]);
|
||||||
Functions\when('get_post')->justReturn($exists ? new WP_Post() : null);
|
Functions\when('get_post')->justReturn($exists ? new WP_Post() : null);
|
||||||
$purged = [];
|
$purged = [];
|
||||||
Functions\when('wp_cache_clear_cache')->alias(static function () use (&$purged): void {
|
Functions\when('wpfc_clear_all_cache')->alias(static function () use (&$purged): void {
|
||||||
$purged[] = 'all';
|
$purged[] = 'all';
|
||||||
});
|
});
|
||||||
Functions\when('wp_cache_post_edit')->alias(static function () use (&$purged): void {
|
Functions\when('wpfc_clear_post_cache_by_id')->alias(static function () use (&$purged): void {
|
||||||
$purged[] = 'post';
|
$purged[] = 'post';
|
||||||
});
|
});
|
||||||
|
|
||||||
$listener = new SuperCacheListener(static fn (): bool => true);
|
$listener = new FastestCacheListener(static fn (): bool => true);
|
||||||
$listener->onChange('lit/x', $type, $id, 1);
|
$listener->onChange('lit/x', $type, $id, 1);
|
||||||
$listener->flush();
|
$listener->flush();
|
||||||
|
|
||||||
@@ -0,0 +1,421 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Tests\Unit\Integrations\FastestCache;
|
||||||
|
|
||||||
|
use Brain\Monkey\Functions;
|
||||||
|
use Closure;
|
||||||
|
use LIT\MCP\Compatibility\ThrowawayPost;
|
||||||
|
use LIT\MCP\Integrations\FastestCache\FastestCacheProbe;
|
||||||
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
|
use Mockery;
|
||||||
|
use RuntimeException;
|
||||||
|
use WP_Post;
|
||||||
|
|
||||||
|
final class FastestCacheProbeTest extends TestCase
|
||||||
|
{
|
||||||
|
private string $cacheRoot = '';
|
||||||
|
|
||||||
|
/** @var list<int> */
|
||||||
|
private array $deleted = [];
|
||||||
|
|
||||||
|
/** @var array<string, mixed>|null The wp_insert_post arguments. */
|
||||||
|
private ?array $inserted = null;
|
||||||
|
|
||||||
|
/** @var array<string, mixed> Globals the test changes (only those that existed). */
|
||||||
|
private array $savedGlobals = [];
|
||||||
|
|
||||||
|
private const GLOBALS_CHANGED = ['wp_fastest_cache', 'wp_fastest_cache_options', 'wpdb'];
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->cacheRoot = sys_get_temp_dir() . '/lit-mcp-wpfc-' . bin2hex(random_bytes(4)) . '/';
|
||||||
|
mkdir($this->cacheRoot . 'cache/all/', 0777, true);
|
||||||
|
$this->deleted = [];
|
||||||
|
$this->inserted = null;
|
||||||
|
$this->savedGlobals = [];
|
||||||
|
foreach (self::GLOBALS_CHANGED as $name) {
|
||||||
|
if (array_key_exists($name, $GLOBALS)) {
|
||||||
|
$this->savedGlobals[$name] = $GLOBALS[$name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Functions\when('wp_insert_post')->alias(function (array $post): int {
|
||||||
|
$this->inserted = $post;
|
||||||
|
|
||||||
|
return 77;
|
||||||
|
});
|
||||||
|
Functions\when('wp_delete_post')->alias(function (int $id, bool $force): WP_Post {
|
||||||
|
self::assertTrue($force, 'never to the trash');
|
||||||
|
$this->deleted[] = $id;
|
||||||
|
|
||||||
|
return new WP_Post(['ID' => $id]);
|
||||||
|
});
|
||||||
|
Functions\when('wp_mkdir_p')->alias(static fn (string $dir): bool => is_dir($dir) || @mkdir($dir, 0777, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
foreach (self::GLOBALS_CHANGED as $name) {
|
||||||
|
if (array_key_exists($name, $this->savedGlobals)) {
|
||||||
|
$GLOBALS[$name] = $this->savedGlobals[$name];
|
||||||
|
} else {
|
||||||
|
unset($GLOBALS[$name]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self::removeTree($this->cacheRoot);
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function removeTree(string $path): void
|
||||||
|
{
|
||||||
|
foreach (glob(rtrim($path, '/') . '/{,.}*', GLOB_BRACE) ?: [] as $entry) {
|
||||||
|
if (in_array(basename($entry), ['.', '..'], true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
is_dir($entry) ? self::removeTree($entry) : unlink($entry);
|
||||||
|
}
|
||||||
|
@rmdir($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function pageDir(): string
|
||||||
|
{
|
||||||
|
return $this->cacheRoot . 'cache/all/lit-mcp-check-77/';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param (Closure(int): void)|null $purge
|
||||||
|
* @param (Closure(int): ?string)|null $cacheDir
|
||||||
|
*/
|
||||||
|
private function probe(
|
||||||
|
?Closure $purge = null,
|
||||||
|
?Closure $cacheDir = null,
|
||||||
|
bool $enabled = true,
|
||||||
|
bool $hooked = true,
|
||||||
|
bool $slugSet = true
|
||||||
|
): FastestCacheProbe {
|
||||||
|
return new FastestCacheProbe(
|
||||||
|
static fn (): bool => $enabled,
|
||||||
|
static fn (): bool => $hooked,
|
||||||
|
$cacheDir ?? fn (int $id): string => $this->pageDir(),
|
||||||
|
$purge ?? fn (int $id) => unlink($this->pageDir() . 'index.html'),
|
||||||
|
static fn (int $id): bool => $slugSet
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Captures what Log writes to the PHP error log while $run runs. */
|
||||||
|
private function logged(Closure $run): string
|
||||||
|
{
|
||||||
|
$log = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log');
|
||||||
|
$previous = ini_set('error_log', $log);
|
||||||
|
try {
|
||||||
|
$run();
|
||||||
|
|
||||||
|
return (string) file_get_contents($log);
|
||||||
|
} finally {
|
||||||
|
ini_set('error_log', (string) $previous);
|
||||||
|
unlink($log);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGreenWhenThePurgeRemovesTheCacheFileAndNothingIsLeftBehind(): void
|
||||||
|
{
|
||||||
|
$probe = $this->probe(function (int $id): void {
|
||||||
|
self::assertFileExists($this->pageDir() . 'index.html', 'the file is there before the purge');
|
||||||
|
unlink($this->pageDir() . 'index.html');
|
||||||
|
});
|
||||||
|
|
||||||
|
self::assertSame([], $probe->run());
|
||||||
|
self::assertDirectoryDoesNotExist($this->pageDir());
|
||||||
|
self::assertDirectoryExists($this->cacheRoot . 'cache/all/');
|
||||||
|
self::assertSame([77], $this->deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCreatesAMarkedDraftPage(): void
|
||||||
|
{
|
||||||
|
$this->probe()->run();
|
||||||
|
|
||||||
|
self::assertSame('page', $this->inserted['post_type']);
|
||||||
|
self::assertSame('draft', $this->inserted['post_status']);
|
||||||
|
self::assertStringStartsWith(ThrowawayPost::TITLE, $this->inserted['post_title']);
|
||||||
|
self::assertSame([ThrowawayPost::MARKER => '1'], $this->inserted['meta_input']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** WPFC moves the whole directory away (to cache/tmpWpfc). */
|
||||||
|
public function testADirectoryThePurgeMovedAwayIsGreen(): void
|
||||||
|
{
|
||||||
|
$problems = $this->probe(function (int $id): void {
|
||||||
|
rename($this->pageDir(), $this->cacheRoot . 'moved');
|
||||||
|
})->run();
|
||||||
|
|
||||||
|
self::assertSame([], $problems);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testACacheFileThatSurvivesThePurgeIsAFixedTextAndIsRemovedAnyway(): void
|
||||||
|
{
|
||||||
|
$problems = [];
|
||||||
|
$logged = $this->logged(function () use (&$problems): void {
|
||||||
|
$problems = $this->probe(static fn (int $id) => null)->run();
|
||||||
|
});
|
||||||
|
|
||||||
|
self::assertSame(['Die Testdatei im Cache blieb nach dem Leeren durch WP Fastest Cache bestehen.'], $problems);
|
||||||
|
self::assertStringContainsString('lit-mcp-check-77', $logged);
|
||||||
|
self::assertDirectoryDoesNotExist($this->pageDir());
|
||||||
|
self::assertSame([77], $this->deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAForeignFileInThePageDirectoryIsLeftAlone(): void
|
||||||
|
{
|
||||||
|
$problems = $this->probe(function (int $id): void {
|
||||||
|
unlink($this->pageDir() . 'index.html');
|
||||||
|
touch($this->pageDir() . 'fremd.txt');
|
||||||
|
})->run();
|
||||||
|
|
||||||
|
self::assertSame([], $problems);
|
||||||
|
self::assertFileExists($this->pageDir() . 'fremd.txt');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAnExceptionOfThePurgeIsAFixedTextALogLineAndEverythingIsCleanedUp(): void
|
||||||
|
{
|
||||||
|
$problems = [];
|
||||||
|
$logged = $this->logged(function () use (&$problems): void {
|
||||||
|
$problems = $this->probe(static function (int $id): never {
|
||||||
|
throw new RuntimeException('WPFC kaputt');
|
||||||
|
})->run();
|
||||||
|
});
|
||||||
|
|
||||||
|
self::assertSame(['Fastest-Cache-Probe abgebrochen (Details im PHP-Error-Log).'], $problems);
|
||||||
|
self::assertStringContainsString('WPFC kaputt', $logged);
|
||||||
|
self::assertDirectoryDoesNotExist($this->pageDir());
|
||||||
|
self::assertSame([77], $this->deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNothingToProbeWhileTheCacheIsSwitchedOff(): void
|
||||||
|
{
|
||||||
|
self::assertNull($this->probe(enabled: false)->run());
|
||||||
|
self::assertNull($this->inserted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Without the hook wpfc_clear_post_cache_by_id does nothing: the purge of a post would silently fail. */
|
||||||
|
public function testAMissingPurgeHookIsAProblemWithoutAnyPage(): void
|
||||||
|
{
|
||||||
|
self::assertSame(
|
||||||
|
['WP Fastest Cache verarbeitet wpfc_clear_post_cache_by_id nicht mehr; das Leeren eines Beitrags bliebe wirkungslos.'],
|
||||||
|
$this->probe(hooked: false)->run()
|
||||||
|
);
|
||||||
|
self::assertNull($this->inserted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNothingToProbeWithoutACachePathForThePage(): void
|
||||||
|
{
|
||||||
|
self::assertNull($this->probe(cacheDir: static fn (int $id): ?string => null)->run());
|
||||||
|
self::assertSame([77], $this->deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testARefusedSlugIsItsOwnFixedProblemAndTheProbeNeverPurges(): void
|
||||||
|
{
|
||||||
|
$purged = false;
|
||||||
|
$problems = [];
|
||||||
|
$logged = $this->logged(function () use (&$problems, &$purged): void {
|
||||||
|
$problems = $this->probe(static function (int $id) use (&$purged): void {
|
||||||
|
$purged = true;
|
||||||
|
}, slugSet: false)->run();
|
||||||
|
});
|
||||||
|
|
||||||
|
self::assertSame(['Die Adresse der Testseite konnte nicht gesetzt werden (Details im PHP-Error-Log).'], $problems);
|
||||||
|
self::assertStringContainsString('77', $logged);
|
||||||
|
self::assertFalse($purged);
|
||||||
|
self::assertSame([77], $this->deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testACacheDirectoryThatCannotBeCreatedIsAFixedText(): void
|
||||||
|
{
|
||||||
|
$blocker = $this->cacheRoot . 'blocker';
|
||||||
|
file_put_contents($blocker, 'x');
|
||||||
|
$problems = [];
|
||||||
|
$logged = $this->logged(function () use (&$problems, $blocker): void {
|
||||||
|
$problems = $this->probe(cacheDir: static fn (int $id): string => $blocker . '/seite/')->run();
|
||||||
|
});
|
||||||
|
|
||||||
|
self::assertSame(['Die Testdatei konnte nicht im Cache-Verzeichnis angelegt werden (Details im PHP-Error-Log).'], $problems);
|
||||||
|
self::assertStringContainsString('seite', $logged);
|
||||||
|
self::assertSame([77], $this->deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Defaults: the real WPFC and WordPress calls ----------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Records calls; getWpContentDir like WPFC without WPML/multisite: content dir plus path. */
|
||||||
|
private function fakeWpfc(): object
|
||||||
|
{
|
||||||
|
return new class ($this->cacheRoot) {
|
||||||
|
/** @var list<array{mixed, mixed, mixed}> */
|
||||||
|
public array $calls = [];
|
||||||
|
|
||||||
|
public function __construct(private string $root)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function singleDeleteCache(mixed $comment = false, mixed $post = false, mixed $parents = true): void
|
||||||
|
{
|
||||||
|
$this->calls[] = [$comment, $post, $parents];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getWpContentDir(string $path): string
|
||||||
|
{
|
||||||
|
return rtrim($this->root, '/') . $path;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheDefaultPurgeIsSingleDeleteCacheWithoutTheParentPages(): void
|
||||||
|
{
|
||||||
|
$GLOBALS['wp_fastest_cache'] = $wpfc = $this->fakeWpfc();
|
||||||
|
$probe = new FastestCacheProbe(
|
||||||
|
static fn (): bool => true,
|
||||||
|
static fn (): bool => true,
|
||||||
|
fn (int $id): string => $this->pageDir(),
|
||||||
|
null,
|
||||||
|
static fn (int $id): bool => true
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->logged(static function () use ($probe): void {
|
||||||
|
$probe->run();
|
||||||
|
});
|
||||||
|
|
||||||
|
self::assertSame([[false, 77, false]], $wpfc->calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheDefaultHookCheckAsksForTheMethodOfTheWpfcInstance(): void
|
||||||
|
{
|
||||||
|
$GLOBALS['wp_fastest_cache'] = $wpfc = $this->fakeWpfc();
|
||||||
|
Functions\expect('has_action')->twice()
|
||||||
|
->with('wpfc_clear_post_cache_by_id', [$wpfc, 'singleDeleteCache'])
|
||||||
|
->andReturn(10, false);
|
||||||
|
$probe = new FastestCacheProbe(
|
||||||
|
static fn (): bool => true,
|
||||||
|
null,
|
||||||
|
static fn (int $id): ?string => null,
|
||||||
|
null,
|
||||||
|
static fn (int $id): bool => true
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertNull($probe->run(), 'hooked: on to the page, no path: not checked');
|
||||||
|
self::assertCount(1, $probe->run() ?? [], 'not hooked: one problem');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheDefaultEnabledCheckReadsTheWpfcSwitch(): void
|
||||||
|
{
|
||||||
|
$GLOBALS['wp_fastest_cache_options'] = (object) [];
|
||||||
|
self::assertNull((new FastestCacheProbe())->run());
|
||||||
|
|
||||||
|
$GLOBALS['wp_fastest_cache_options'] = [];
|
||||||
|
self::assertNull((new FastestCacheProbe())->run());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheDefaultSlugIsWrittenStraightToTheDatabaseWithoutHooks(): void
|
||||||
|
{
|
||||||
|
$wpdb = Mockery::mock();
|
||||||
|
$wpdb->posts = 'wp_posts';
|
||||||
|
$wpdb->expects('update')->with('wp_posts', ['post_name' => 'lit-mcp-check-77'], ['ID' => 77])->andReturn(1);
|
||||||
|
$GLOBALS['wpdb'] = $wpdb;
|
||||||
|
Functions\expect('clean_post_cache')->never();
|
||||||
|
$dropped = [];
|
||||||
|
Functions\when('wp_cache_delete')->alias(static function (int $id, string $group) use (&$dropped): bool {
|
||||||
|
$dropped[] = $group . ':' . $id;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
$probe = new FastestCacheProbe(
|
||||||
|
static fn (): bool => true,
|
||||||
|
static fn (): bool => true,
|
||||||
|
fn (int $id): string => $this->pageDir(),
|
||||||
|
fn (int $id) => unlink($this->pageDir() . 'index.html')
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame([], $probe->run());
|
||||||
|
self::assertSame(['posts:77'], $dropped, 'the object cache must not keep the empty slug');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, array{0: int|false}> */
|
||||||
|
public static function refusedUpdates(): array
|
||||||
|
{
|
||||||
|
return ['database error' => [false], 'no row changed' => [0]];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @dataProvider refusedUpdates */
|
||||||
|
public function testTheDefaultSlugWriterTreatsAFailedOrEmptyUpdateAsRefused(int|false $result): void
|
||||||
|
{
|
||||||
|
$wpdb = Mockery::mock();
|
||||||
|
$wpdb->posts = 'wp_posts';
|
||||||
|
$wpdb->shouldReceive('update')->andReturn($result);
|
||||||
|
$GLOBALS['wpdb'] = $wpdb;
|
||||||
|
Functions\when('wp_cache_delete')->justReturn(true);
|
||||||
|
|
||||||
|
$problems = [];
|
||||||
|
$this->logged(function () use (&$problems): void {
|
||||||
|
$problems = (new FastestCacheProbe(
|
||||||
|
static fn (): bool => true,
|
||||||
|
static fn (): bool => true,
|
||||||
|
fn (int $id): string => $this->pageDir(),
|
||||||
|
static fn (int $id) => self::fail('no purge without the slug')
|
||||||
|
))->run();
|
||||||
|
});
|
||||||
|
|
||||||
|
self::assertSame(['Die Adresse der Testseite konnte nicht gesetzt werden (Details im PHP-Error-Log).'], $problems);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function permalink(string $address): void
|
||||||
|
{
|
||||||
|
$GLOBALS['wp_fastest_cache'] = $this->fakeWpfc();
|
||||||
|
Functions\when('get_post')->justReturn(new WP_Post(['ID' => 77, 'post_status' => 'draft']));
|
||||||
|
Functions\when('get_permalink')->alias(static function (WP_Post $post) use ($address): string {
|
||||||
|
self::assertSame('publish', $post->post_status, 'the address the draft would have once published');
|
||||||
|
|
||||||
|
return $address;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheDefaultCachePathIsTheProbePageDirectoryBelowCacheAll(): void
|
||||||
|
{
|
||||||
|
$this->permalink('https://example.test/lit-mcp-check-77/');
|
||||||
|
|
||||||
|
self::assertSame($this->pageDir(), FastestCacheProbe::cacheDirFor(77));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheDefaultCachePathKeepsTheSubdirectoryOfTheSite(): void
|
||||||
|
{
|
||||||
|
$this->permalink('https://example.test/wp/lit-mcp-check-77/');
|
||||||
|
|
||||||
|
self::assertSame($this->cacheRoot . 'cache/all/wp/lit-mcp-check-77/', FastestCacheProbe::cacheDirFor(77));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, array{0: string}> */
|
||||||
|
public static function foreignAddresses(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'plain address' => ['https://example.test/?page_id=77'],
|
||||||
|
'home page' => ['https://example.test/'],
|
||||||
|
'other slug' => ['https://example.test/seite/'],
|
||||||
|
'other probe' => ['https://example.test/lit-mcp-check-78/'],
|
||||||
|
'slug as suffix only' => ['https://example.test/x-lit-mcp-check-77/'],
|
||||||
|
'traversal' => ['https://example.test/../lit-mcp-check-77/'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @dataProvider foreignAddresses */
|
||||||
|
public function testTheDefaultCachePathIsNullForAnythingButTheProbePage(string $address): void
|
||||||
|
{
|
||||||
|
$this->permalink($address);
|
||||||
|
|
||||||
|
self::assertNull(FastestCacheProbe::cacheDirFor(77));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheDefaultCachePathIsNullForAMissingPost(): void
|
||||||
|
{
|
||||||
|
Functions\when('get_post')->justReturn(null);
|
||||||
|
|
||||||
|
self::assertNull(FastestCacheProbe::cacheDirFor(77));
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
-11
@@ -2,27 +2,27 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace LIT\MCP\Tests\Unit\Integrations\SuperCache;
|
namespace LIT\MCP\Tests\Unit\Integrations\FastestCache;
|
||||||
|
|
||||||
use Brain\Monkey\Functions;
|
use Brain\Monkey\Functions;
|
||||||
use LIT\MCP\Integrations\SuperCache\SuperCacheFeature;
|
use LIT\MCP\Integrations\FastestCache\FastestCacheFeature;
|
||||||
use LIT\MCP\Integrations\SuperCache\SuperCachePurger;
|
use LIT\MCP\Integrations\FastestCache\FastestCachePurger;
|
||||||
use LIT\MCP\Tests\Unit\TestCase;
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
|
|
||||||
final class SuperCachePurgerTest extends TestCase
|
final class FastestCachePurgerTest extends TestCase
|
||||||
{
|
{
|
||||||
public function testALockedWholePurgeDoesNothingAndSaysSo(): void
|
public function testALockedWholePurgeDoesNothingAndSaysSo(): void
|
||||||
{
|
{
|
||||||
$previous = ini_set('error_log', '/dev/null');
|
$previous = ini_set('error_log', '/dev/null');
|
||||||
Functions\expect('get_transient')->never();
|
Functions\expect('get_transient')->never();
|
||||||
$purger = new SuperCachePurger(
|
$purger = new FastestCachePurger(
|
||||||
static fn (SuperCacheFeature $feature): bool => false,
|
static fn (FastestCacheFeature $feature): bool => false,
|
||||||
static fn (): bool => true,
|
static fn (): bool => true,
|
||||||
static fn (int $id) => self::fail('no post purge'),
|
static fn (int $id) => self::fail('no post purge'),
|
||||||
static fn (int $blog) => self::fail('no whole purge')
|
static fn () => self::fail('no whole purge')
|
||||||
);
|
);
|
||||||
|
|
||||||
self::assertSame(SuperCachePurger::LOCKED, $purger->everything());
|
self::assertSame(FastestCachePurger::LOCKED, $purger->everything());
|
||||||
self::assertFalse($purger->post(3));
|
self::assertFalse($purger->post(3));
|
||||||
ini_set('error_log', (string) $previous);
|
ini_set('error_log', (string) $previous);
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ final class SuperCachePurgerTest extends TestCase
|
|||||||
Functions\when('get_transient')->alias(static function () use (&$stamp) {
|
Functions\when('get_transient')->alias(static function () use (&$stamp) {
|
||||||
return $stamp;
|
return $stamp;
|
||||||
});
|
});
|
||||||
$purger = new SuperCachePurger(static fn (): bool => true);
|
$purger = new FastestCachePurger(static fn (): bool => true);
|
||||||
|
|
||||||
self::assertFalse($purger->throttled());
|
self::assertFalse($purger->throttled());
|
||||||
$stamp = (string) (time() - 5);
|
$stamp = (string) (time() - 5);
|
||||||
@@ -56,9 +56,9 @@ final class SuperCachePurgerTest extends TestCase
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
$purger = new SuperCachePurger(static fn (): bool => true);
|
$purger = new FastestCachePurger(static fn (): bool => true);
|
||||||
|
|
||||||
self::assertSame(SuperCachePurger::THROTTLED, $purger->everything());
|
self::assertSame(FastestCachePurger::THROTTLED, $purger->everything());
|
||||||
|
|
||||||
self::assertSame(1, $reads);
|
self::assertSame(1, $reads);
|
||||||
self::assertCount(1, $scheduledAt);
|
self::assertCount(1, $scheduledAt);
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace LIT\MCP\Tests\Unit\Integrations\SuperCache;
|
|
||||||
|
|
||||||
use Brain\Monkey\Functions;
|
|
||||||
use LIT\MCP\Compatibility\Call;
|
|
||||||
use LIT\MCP\Compatibility\CheckStep;
|
|
||||||
use LIT\MCP\Compatibility\Compatibility;
|
|
||||||
use LIT\MCP\Compatibility\Tier;
|
|
||||||
use LIT\MCP\Integrations\Divi\Release\DiviProfile;
|
|
||||||
use LIT\MCP\Integrations\Pods\PodsCompatibility;
|
|
||||||
use LIT\MCP\Integrations\SuperCache\SuperCacheCheckSteps;
|
|
||||||
use LIT\MCP\Integrations\SuperCache\SuperCacheCompatibility;
|
|
||||||
use LIT\MCP\Integrations\SuperCache\SuperCacheFeature;
|
|
||||||
use LIT\MCP\Tests\Unit\TestCase;
|
|
||||||
|
|
||||||
final class SuperCacheCompatibilityTest extends TestCase
|
|
||||||
{
|
|
||||||
public function testDescribesSuperCacheAsALockOnFailurePlugin(): void
|
|
||||||
{
|
|
||||||
$profile = SuperCacheCompatibility::profile();
|
|
||||||
|
|
||||||
self::assertSame('wpsc', $profile->id);
|
|
||||||
self::assertSame('WP Super Cache', $profile->label);
|
|
||||||
self::assertSame(Tier::LockOnFailure, $profile->tier);
|
|
||||||
self::assertSame(['3.1.5'], $profile->testedVersions);
|
|
||||||
self::assertNull($profile->minVersion);
|
|
||||||
self::assertSame(SuperCacheFeature::cases(), $profile->capabilities);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Argument counts are those the listener passes; wp_cache_clear_cache($blog_id = 0) gets the blog. */
|
|
||||||
public function testTheManifestHoldsTheTwoPurgeFunctionsWithTheirArgumentCounts(): void
|
|
||||||
{
|
|
||||||
$manifest = SuperCacheCompatibility::profile()->manifests[0];
|
|
||||||
$calls = [];
|
|
||||||
foreach ($manifest->calls as $call) {
|
|
||||||
$calls[$call->symbol] = [$call->args, $call->features()];
|
|
||||||
}
|
|
||||||
|
|
||||||
self::assertSame([
|
|
||||||
'wp_cache_post_edit' => [1, [SuperCacheFeature::PurgePost]],
|
|
||||||
'wp_cache_clear_cache' => [1, [SuperCacheFeature::PurgeAll]],
|
|
||||||
], $calls);
|
|
||||||
self::assertContainsOnlyInstancesOf(Call::class, $manifest->calls);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Both purge functions read $cache_path; wp_cache_post_edit also $blog_cache_dir. */
|
|
||||||
public function testTheGlobalsTheFunctionsReadAreRequired(): void
|
|
||||||
{
|
|
||||||
$manifest = SuperCacheCompatibility::profile()->manifests[0];
|
|
||||||
|
|
||||||
self::assertSame(['WPCACHEHOME'], array_map(static fn ($c) => $c->name, $manifest->constants));
|
|
||||||
$globals = [];
|
|
||||||
foreach ($manifest->globals as $global) {
|
|
||||||
$globals[$global->name] = $global->features();
|
|
||||||
}
|
|
||||||
self::assertSame(
|
|
||||||
['cache_path' => SuperCacheFeature::cases(), 'blog_cache_dir' => [SuperCacheFeature::PurgePost]],
|
|
||||||
$globals
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @runInSeparateProcess
|
|
||||||
* @preserveGlobalState disabled
|
|
||||||
*/
|
|
||||||
public function testActiveAndVersionFollowTheCacheHomeConstantAndThePluginHeader(): void
|
|
||||||
{
|
|
||||||
$profile = SuperCacheCompatibility::profile();
|
|
||||||
self::assertFalse(($profile->active)());
|
|
||||||
self::assertNull(($profile->version)());
|
|
||||||
|
|
||||||
define('WPCACHEHOME', '/plugins/wp-super-cache/');
|
|
||||||
Functions\expect('get_file_data')
|
|
||||||
->once()
|
|
||||||
->with('/plugins/wp-super-cache/wp-cache.php', ['Version' => 'Version'])
|
|
||||||
->andReturn(['Version' => '3.1.5']);
|
|
||||||
|
|
||||||
self::assertTrue(($profile->active)());
|
|
||||||
self::assertSame('3.1.5', ($profile->version)());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @runInSeparateProcess
|
|
||||||
* @preserveGlobalState disabled
|
|
||||||
*/
|
|
||||||
public function testAnUnreadableHeaderIsAnUnknownVersion(): void
|
|
||||||
{
|
|
||||||
define('WPCACHEHOME', '/plugins/wp-super-cache/');
|
|
||||||
Functions\when('get_file_data')->justReturn(['Version' => '']);
|
|
||||||
|
|
||||||
self::assertNull((SuperCacheCompatibility::profile()->version)());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testTheCheckHasTheProbeAsExtraStep(): void
|
|
||||||
{
|
|
||||||
$profile = SuperCacheCompatibility::profile();
|
|
||||||
|
|
||||||
self::assertNotNull($profile->extraSteps);
|
|
||||||
$steps = ($profile->extraSteps)(false);
|
|
||||||
self::assertSame([SuperCacheCheckSteps::PROBE], array_map(static fn ($step): string => $step->key, $steps));
|
|
||||||
self::assertTrue($steps[0]->skipped);
|
|
||||||
self::assertNotNull($profile->stepLabel);
|
|
||||||
self::assertSame(
|
|
||||||
'Leeren-Test an einer Wegwerf-Seite im Cache',
|
|
||||||
($profile->stepLabel)(new CheckStep(SuperCacheCheckSteps::PROBE, []))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testAllProfilesCoexistWithoutIdCollision(): void
|
|
||||||
{
|
|
||||||
$compatibility = new Compatibility(
|
|
||||||
[DiviProfile::profile(), PodsCompatibility::profile(), SuperCacheCompatibility::profile()]
|
|
||||||
);
|
|
||||||
|
|
||||||
self::assertSame(['divi', 'pods', 'wpsc'], array_map(static fn ($p) => $p->id, $compatibility->profiles()));
|
|
||||||
self::assertSame(SuperCacheFeature::PurgeAll, $compatibility->capability('wpsc-purge-all'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace LIT\MCP\Tests\Unit\Integrations\SuperCache;
|
|
||||||
|
|
||||||
use Brain\Monkey\Functions;
|
|
||||||
use LIT\MCP\Admin\CompatibilityTab;
|
|
||||||
use LIT\MCP\Compatibility\ProvidesCompatibility;
|
|
||||||
use LIT\MCP\Integrations\Bootable;
|
|
||||||
use LIT\MCP\Integrations\Integration;
|
|
||||||
use LIT\MCP\Integrations\SuperCache\SuperCacheCompatibility;
|
|
||||||
use LIT\MCP\Integrations\SuperCache\SuperCacheIntegration;
|
|
||||||
use LIT\MCP\Integrations\SuperCache\SuperCachePurger;
|
|
||||||
use LIT\MCP\Tests\Support\FakeChangeLog;
|
|
||||||
use LIT\MCP\Tests\Unit\TestCase;
|
|
||||||
|
|
||||||
final class SuperCacheIntegrationTest extends TestCase
|
|
||||||
{
|
|
||||||
private static function purger(): SuperCachePurger
|
|
||||||
{
|
|
||||||
return new SuperCachePurger(static fn (): bool => true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testIsAnIntegrationThatProvidesItsProfileAndTheCacheClearTool(): void
|
|
||||||
{
|
|
||||||
$integration = new SuperCacheIntegration(null, self::purger(), FakeChangeLog::forbidden());
|
|
||||||
|
|
||||||
self::assertInstanceOf(Integration::class, $integration);
|
|
||||||
self::assertInstanceOf(ProvidesCompatibility::class, $integration);
|
|
||||||
self::assertSame('wpsc', $integration->id());
|
|
||||||
self::assertSame('WP Super Cache', $integration->label());
|
|
||||||
self::assertEquals(['lit/cache-clear'], array_map(static fn ($a): string => $a->name(), $integration->abilities()));
|
|
||||||
self::assertEquals([SuperCacheCompatibility::profile()], $integration->compatibilities());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testIsAvailableOnlyWithSuperCache(): void
|
|
||||||
{
|
|
||||||
self::assertSame(defined('WPCACHEHOME'), (new SuperCacheIntegration(null, self::purger(), FakeChangeLog::forbidden()))->isAvailable());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testBootHooksTheOrphanCleanupIntoTheProfileLoad(): void
|
|
||||||
{
|
|
||||||
$integration = new SuperCacheIntegration(null, self::purger(), FakeChangeLog::forbidden());
|
|
||||||
$integration->boot();
|
|
||||||
|
|
||||||
self::assertInstanceOf(Bootable::class, $integration);
|
|
||||||
self::assertNotFalse(has_action(CompatibilityTab::LOAD_ACTION, [$integration, 'removeOrphans']));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testNoCleanupForAnotherProfileWithoutWpscOrWithoutTheCapability(): void
|
|
||||||
{
|
|
||||||
Functions\expect('get_posts')->never();
|
|
||||||
Functions\when('current_user_can')->justReturn(true);
|
|
||||||
(new SuperCacheIntegration(static fn (): bool => true, self::purger(), FakeChangeLog::forbidden()))->removeOrphans('pods');
|
|
||||||
(new SuperCacheIntegration(static fn (): bool => false, self::purger(), FakeChangeLog::forbidden()))->removeOrphans('wpsc');
|
|
||||||
|
|
||||||
Functions\when('current_user_can')->justReturn(false);
|
|
||||||
(new SuperCacheIntegration(static fn (): bool => true, self::purger(), FakeChangeLog::forbidden()))->removeOrphans('wpsc');
|
|
||||||
$this->addToAssertionCount(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testCleansUpPagesForTheAdministratorWithWpsc(): void
|
|
||||||
{
|
|
||||||
Functions\when('current_user_can')->justReturn(true);
|
|
||||||
Functions\expect('get_posts')->once()->andReturnUsing(static function (array $args): array {
|
|
||||||
self::assertSame('page', $args['post_type']);
|
|
||||||
self::assertSame(['draft', 'private'], $args['post_status']);
|
|
||||||
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
|
|
||||||
(new SuperCacheIntegration(static fn (): bool => true, self::purger(), FakeChangeLog::forbidden()))->removeOrphans('wpsc');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,525 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace LIT\MCP\Tests\Unit\Integrations\SuperCache;
|
|
||||||
|
|
||||||
use Brain\Monkey\Functions;
|
|
||||||
use Closure;
|
|
||||||
use LIT\MCP\Compatibility\ThrowawayPost;
|
|
||||||
use LIT\MCP\Integrations\SuperCache\SuperCacheProbe;
|
|
||||||
use LIT\MCP\Tests\Unit\TestCase;
|
|
||||||
use Mockery;
|
|
||||||
use RuntimeException;
|
|
||||||
use WP_Post;
|
|
||||||
|
|
||||||
final class SuperCacheProbeTest extends TestCase
|
|
||||||
{
|
|
||||||
private string $cacheRoot = '';
|
|
||||||
|
|
||||||
/** @var list<int> */
|
|
||||||
private array $deleted = [];
|
|
||||||
|
|
||||||
/** @var list<string> "<id>:<status>" in call order. */
|
|
||||||
private array $statuses = [];
|
|
||||||
|
|
||||||
/** @var array<string, mixed>|null The wp_insert_post arguments. */
|
|
||||||
private ?array $inserted = null;
|
|
||||||
|
|
||||||
/** @var array<string, mixed> Globals the test changes (only those that existed). */
|
|
||||||
private array $savedGlobals = [];
|
|
||||||
|
|
||||||
private const GLOBALS_CHANGED = ['cache_enabled', 'cache_path', 'wpdb', 'wp_cache_clear_on_post_edit'];
|
|
||||||
|
|
||||||
/** The status the fake status switch refuses to set (returns false); null = none. */
|
|
||||||
private ?string $refusedStatus = null;
|
|
||||||
|
|
||||||
/** The status the fake status switch throws for; null = none. */
|
|
||||||
private ?string $throwingStatus = null;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
$this->cacheRoot = sys_get_temp_dir() . '/lit-mcp-wpsc-' . bin2hex(random_bytes(4)) . '/';
|
|
||||||
mkdir($this->cacheRoot . 'supercache/example.test/', 0777, true);
|
|
||||||
$this->deleted = [];
|
|
||||||
$this->statuses = [];
|
|
||||||
$this->inserted = null;
|
|
||||||
$this->refusedStatus = null;
|
|
||||||
$this->throwingStatus = null;
|
|
||||||
$this->savedGlobals = [];
|
|
||||||
foreach (self::GLOBALS_CHANGED as $name) {
|
|
||||||
if (array_key_exists($name, $GLOBALS)) {
|
|
||||||
$this->savedGlobals[$name] = $GLOBALS[$name];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Functions\when('wp_insert_post')->alias(function (array $post): int {
|
|
||||||
$this->inserted = $post;
|
|
||||||
|
|
||||||
return 77;
|
|
||||||
});
|
|
||||||
Functions\when('wp_delete_post')->alias(function (int $id, bool $force): WP_Post {
|
|
||||||
self::assertTrue($force, 'never to the trash');
|
|
||||||
$this->deleted[] = $id;
|
|
||||||
|
|
||||||
return new WP_Post(['ID' => $id]);
|
|
||||||
});
|
|
||||||
Functions\when('wp_mkdir_p')->alias(static fn (string $dir): bool => is_dir($dir) || @mkdir($dir, 0777, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function tearDown(): void
|
|
||||||
{
|
|
||||||
foreach (self::GLOBALS_CHANGED as $name) {
|
|
||||||
if (array_key_exists($name, $this->savedGlobals)) {
|
|
||||||
$GLOBALS[$name] = $this->savedGlobals[$name];
|
|
||||||
} else {
|
|
||||||
unset($GLOBALS[$name]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self::removeTree($this->cacheRoot);
|
|
||||||
parent::tearDown();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function removeTree(string $path): void
|
|
||||||
{
|
|
||||||
foreach (glob(rtrim($path, '/') . '/{,.}*', GLOB_BRACE) ?: [] as $entry) {
|
|
||||||
if (in_array(basename($entry), ['.', '..'], true)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
is_dir($entry) ? self::removeTree($entry) : unlink($entry);
|
|
||||||
}
|
|
||||||
@rmdir($path);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function pageDir(): string
|
|
||||||
{
|
|
||||||
return $this->cacheRoot . 'supercache/example.test/lit-mcp-check-77/';
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param (Closure(int): void)|null $purge */
|
|
||||||
private function probe(?Closure $purge = null, ?Closure $cacheDir = null, bool $enabled = true): SuperCacheProbe
|
|
||||||
{
|
|
||||||
return new SuperCacheProbe(
|
|
||||||
static fn (): bool => $enabled,
|
|
||||||
$cacheDir ?? fn (int $id): string => $this->pageDir(),
|
|
||||||
$purge ?? fn (int $id) => unlink($this->pageDir() . 'index.html'),
|
|
||||||
function (int $id, string $status): bool {
|
|
||||||
$this->statuses[] = $id . ':' . $status;
|
|
||||||
if ($status === $this->throwingStatus) {
|
|
||||||
throw new RuntimeException('Status kaputt');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $status !== $this->refusedStatus;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Purge fake that does what WPSC does: the page's cache file disappears. */
|
|
||||||
private function purgingProbe(): SuperCacheProbe
|
|
||||||
{
|
|
||||||
return $this->probe(function (int $id): void {
|
|
||||||
self::assertFileExists($this->pageDir() . 'index.html', 'the file is there before the purge');
|
|
||||||
unlink($this->pageDir() . 'index.html');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Captures what Log writes to the PHP error log while $run runs. */
|
|
||||||
private function logged(Closure $run): string
|
|
||||||
{
|
|
||||||
$log = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log');
|
|
||||||
$previous = ini_set('error_log', $log);
|
|
||||||
try {
|
|
||||||
$run();
|
|
||||||
|
|
||||||
return (string) file_get_contents($log);
|
|
||||||
} finally {
|
|
||||||
ini_set('error_log', (string) $previous);
|
|
||||||
unlink($log);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testGreenWhenThePurgeRemovesTheCacheFileAndNothingIsLeftBehind(): void
|
|
||||||
{
|
|
||||||
self::assertSame([], $this->purgingProbe()->run());
|
|
||||||
|
|
||||||
self::assertDirectoryDoesNotExist($this->pageDir());
|
|
||||||
self::assertSame([77], $this->deleted);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testCreatesAMarkedDraftPageAndOnlyPassesThroughPrivateForThePurge(): void
|
|
||||||
{
|
|
||||||
$this->purgingProbe()->run();
|
|
||||||
|
|
||||||
self::assertSame('page', $this->inserted['post_type']);
|
|
||||||
self::assertSame('draft', $this->inserted['post_status']);
|
|
||||||
self::assertStringStartsWith(ThrowawayPost::TITLE, $this->inserted['post_title']);
|
|
||||||
self::assertSame([ThrowawayPost::MARKER => '1'], $this->inserted['meta_input']);
|
|
||||||
self::assertSame(['77:private', '77:draft'], $this->statuses);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testACacheFileThatSurvivesThePurgeIsAFixedTextAndIsRemovedAnyway(): void
|
|
||||||
{
|
|
||||||
$problems = [];
|
|
||||||
$logged = $this->logged(function () use (&$problems): void {
|
|
||||||
$problems = $this->probe(static fn (int $id) => null)->run();
|
|
||||||
});
|
|
||||||
|
|
||||||
self::assertSame(['Die Testdatei im Cache blieb nach dem Leeren durch WP Super Cache bestehen.'], $problems);
|
|
||||||
self::assertStringContainsString('lit-mcp-check-77', $logged);
|
|
||||||
self::assertDirectoryDoesNotExist($this->pageDir());
|
|
||||||
self::assertSame([77], $this->deleted);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testARenamedRebuildFileCountsAsPurgedAndIsCleanedUp(): void
|
|
||||||
{
|
|
||||||
$problems = $this->probe(function (int $id): void {
|
|
||||||
rename($this->pageDir() . 'index.html', $this->pageDir() . 'index.html.needs-rebuild');
|
|
||||||
})->run();
|
|
||||||
|
|
||||||
self::assertSame([], $problems);
|
|
||||||
self::assertDirectoryDoesNotExist($this->pageDir());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testADirectoryThePurgeAlreadyRemovedIsNoProblem(): void
|
|
||||||
{
|
|
||||||
$problems = $this->probe(function (int $id): void {
|
|
||||||
unlink($this->pageDir() . 'index.html');
|
|
||||||
rmdir($this->pageDir());
|
|
||||||
})->run();
|
|
||||||
|
|
||||||
self::assertSame([], $problems);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testAnExceptionOfThePurgeIsAFixedTextALogLineAndEverythingIsCleanedUp(): void
|
|
||||||
{
|
|
||||||
$problems = [];
|
|
||||||
$logged = $this->logged(function () use (&$problems): void {
|
|
||||||
$problems = $this->probe(static function (int $id): never {
|
|
||||||
throw new RuntimeException('WPSC kaputt');
|
|
||||||
})->run();
|
|
||||||
});
|
|
||||||
|
|
||||||
self::assertSame(['Super-Cache-Probe abgebrochen (Details im PHP-Error-Log).'], $problems);
|
|
||||||
self::assertStringContainsString('WPSC kaputt', $logged);
|
|
||||||
self::assertDirectoryDoesNotExist($this->pageDir());
|
|
||||||
self::assertSame([77], $this->deleted);
|
|
||||||
self::assertSame(['77:private', '77:draft'], $this->statuses);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testNothingToProbeWhileTheCacheIsSwitchedOff(): void
|
|
||||||
{
|
|
||||||
self::assertNull($this->probe(null, null, false)->run());
|
|
||||||
self::assertNull($this->inserted);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testNothingToProbeWithoutACachePathForThePage(): void
|
|
||||||
{
|
|
||||||
self::assertNull($this->probe(null, static fn (int $id): ?string => null)->run());
|
|
||||||
self::assertSame([77], $this->deleted);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testACacheDirectoryThatCannotBeCreatedIsAFixedText(): void
|
|
||||||
{
|
|
||||||
$blocker = $this->cacheRoot . 'blocker';
|
|
||||||
file_put_contents($blocker, 'x');
|
|
||||||
$problems = [];
|
|
||||||
$logged = $this->logged(function () use (&$problems, $blocker): void {
|
|
||||||
$problems = $this->probe(null, static fn (int $id): string => $blocker . '/seite/')->run();
|
|
||||||
});
|
|
||||||
|
|
||||||
self::assertSame(['Die Testdatei konnte nicht im Cache-Verzeichnis angelegt werden (Details im PHP-Error-Log).'], $problems);
|
|
||||||
self::assertStringContainsString('seite', $logged);
|
|
||||||
self::assertSame([77], $this->deleted);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testTheDefaultPurgeIsWpCachePostEdit(): void
|
|
||||||
{
|
|
||||||
Functions\expect('wp_cache_post_edit')->once()->with(77)->andReturnUsing(function (int $id): int {
|
|
||||||
unlink($this->pageDir() . 'index.html');
|
|
||||||
|
|
||||||
return $id;
|
|
||||||
});
|
|
||||||
|
|
||||||
$probe = new SuperCacheProbe(
|
|
||||||
static fn (): bool => true,
|
|
||||||
fn (int $id): string => $this->pageDir(),
|
|
||||||
null,
|
|
||||||
static fn (): bool => true
|
|
||||||
);
|
|
||||||
|
|
||||||
self::assertSame([], $probe->run());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testTheDefaultStatusSwitchWritesStatusAndSlugStraightToTheDatabaseWithoutHooks(): void
|
|
||||||
{
|
|
||||||
$wpdb = Mockery::mock();
|
|
||||||
$wpdb->posts = 'wp_posts';
|
|
||||||
$wpdb->expects('update')->with('wp_posts', ['post_status' => 'private', 'post_name' => 'lit-mcp-check-77'], ['ID' => 77])->andReturn(1);
|
|
||||||
$wpdb->expects('update')->with('wp_posts', ['post_status' => 'draft', 'post_name' => 'lit-mcp-check-77'], ['ID' => 77])->andReturn(1);
|
|
||||||
$GLOBALS['wpdb'] = $wpdb;
|
|
||||||
Functions\expect('clean_post_cache')->never();
|
|
||||||
$dropped = [];
|
|
||||||
Functions\when('wp_cache_delete')->alias(static function (int $id, string $group) use (&$dropped): bool {
|
|
||||||
$dropped[] = $group . ':' . $id;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
$probe = new SuperCacheProbe(
|
|
||||||
static fn (): bool => true,
|
|
||||||
fn (int $id): string => $this->pageDir(),
|
|
||||||
fn (int $id) => unlink($this->pageDir() . 'index.html')
|
|
||||||
);
|
|
||||||
|
|
||||||
self::assertSame([], $probe->run());
|
|
||||||
self::assertSame(['posts:77', 'posts:77'], $dropped, 'the object cache must not keep the old status');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testTheDefaultEnabledCheckReadsTheWpscGlobal(): void
|
|
||||||
{
|
|
||||||
$GLOBALS['cache_enabled'] = false;
|
|
||||||
self::assertNull((new SuperCacheProbe())->run());
|
|
||||||
|
|
||||||
$GLOBALS['cache_enabled'] = null;
|
|
||||||
self::assertNull((new SuperCacheProbe())->run());
|
|
||||||
}
|
|
||||||
|
|
||||||
private function wpscDirs(string $home, string $current): void
|
|
||||||
{
|
|
||||||
Functions\when('get_supercache_dir')->justReturn($this->cacheRoot . 'supercache/' . $home . '/');
|
|
||||||
Functions\when('get_current_url_supercache_dir')->justReturn($this->cacheRoot . 'supercache/' . $current . '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testTheDefaultCachePathIsTheProbePageDirectoryBelowTheHomeHostDirectory(): void
|
|
||||||
{
|
|
||||||
$this->wpscDirs('example.test', 'example.test/lit-mcp-check-77');
|
|
||||||
|
|
||||||
self::assertSame($this->pageDir(), SuperCacheProbe::cacheDirFor(77));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** WPSC derives the host from the request's Host header; the probe must not follow it somewhere else. */
|
|
||||||
public function testTheDefaultCachePathIsNullWhenWpscWouldUseAnotherHostThanTheHomeUrl(): void
|
|
||||||
{
|
|
||||||
$this->wpscDirs('example.test', 'www.example.test/lit-mcp-check-77');
|
|
||||||
|
|
||||||
self::assertNull(SuperCacheProbe::cacheDirFor(77));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testTheDefaultCachePathIsNullForAnythingButTheProbePageOfThisSite(): void
|
|
||||||
{
|
|
||||||
// A plain "?page_id=" address or an empty slug: WPSC resolves it to the home page's directory.
|
|
||||||
$this->wpscDirs('example.test', 'example.test');
|
|
||||||
self::assertNull(SuperCacheProbe::cacheDirFor(77));
|
|
||||||
|
|
||||||
$this->wpscDirs('example.test', 'example.test/seite');
|
|
||||||
self::assertNull(SuperCacheProbe::cacheDirFor(77));
|
|
||||||
|
|
||||||
$this->wpscDirs('example.test', 'example.test/lit-mcp-check-77/../../x');
|
|
||||||
self::assertNull(SuperCacheProbe::cacheDirFor(77));
|
|
||||||
|
|
||||||
$this->wpscDirs('example.test', 'example.test/lit-mcp-check-78');
|
|
||||||
self::assertNull(SuperCacheProbe::cacheDirFor(77));
|
|
||||||
|
|
||||||
Functions\when('get_current_url_supercache_dir')->justReturn('/var/www/html/lit-mcp-check-77/');
|
|
||||||
self::assertNull(SuperCacheProbe::cacheDirFor(77));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testTheDefaultCachePathIsNullWhenWpscKnowsNoDirectory(): void
|
|
||||||
{
|
|
||||||
Functions\when('get_supercache_dir')->justReturn(false);
|
|
||||||
Functions\when('get_current_url_supercache_dir')->justReturn(false);
|
|
||||||
|
|
||||||
self::assertNull(SuperCacheProbe::cacheDirFor(77));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testOnlyTheKnownCacheFilesOfTheProbePageAreRemoved(): void
|
|
||||||
{
|
|
||||||
$problems = $this->probe(function (int $id): void {
|
|
||||||
// What WPSC leaves after a rebuild-style purge, plus a file that is none of its names.
|
|
||||||
rename($this->pageDir() . 'index.html', $this->pageDir() . 'index.html.needs-rebuild');
|
|
||||||
touch($this->pageDir() . 'index-https.html');
|
|
||||||
touch($this->pageDir() . 'index-https.html.needs-rebuild');
|
|
||||||
touch($this->pageDir() . 'fremd.txt');
|
|
||||||
})->run();
|
|
||||||
|
|
||||||
self::assertSame([], $problems);
|
|
||||||
self::assertSame(['fremd.txt'], array_values(array_diff(scandir($this->pageDir()), ['.', '..'])));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testTheCacheDirectoryOfThePageIsRemovedWhenEmpty(): void
|
|
||||||
{
|
|
||||||
$this->purgingProbe()->run();
|
|
||||||
|
|
||||||
self::assertDirectoryDoesNotExist($this->pageDir());
|
|
||||||
self::assertDirectoryExists($this->cacheRoot . 'supercache/example.test/');
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- P1: the purge must not empty the live cache ------------------------------------------------------------------
|
|
||||||
|
|
||||||
private function defaultPurgeProbe(): SuperCacheProbe
|
|
||||||
{
|
|
||||||
return new SuperCacheProbe(
|
|
||||||
static fn (): bool => true,
|
|
||||||
fn (int $id): string => $this->pageDir(),
|
|
||||||
null,
|
|
||||||
static fn (): bool => true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testTheDefaultPurgeRunsWithClearOnEditOffAndTheRelatedPagesFilterOffAndRestoresBoth(): void
|
|
||||||
{
|
|
||||||
$GLOBALS['wp_cache_clear_on_post_edit'] = 1;
|
|
||||||
$events = [];
|
|
||||||
$added = null;
|
|
||||||
Functions\expect('add_filter')->once()->with('wpsc_delete_related_pages_on_edit', Mockery::type(Closure::class), PHP_INT_MAX)
|
|
||||||
->andReturnUsing(static function (string $hook, Closure $callback) use (&$events, &$added): bool {
|
|
||||||
$events[] = 'add_filter returning ' . var_export($callback(true), true);
|
|
||||||
$added = $callback;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
Functions\expect('remove_filter')->once()->with('wpsc_delete_related_pages_on_edit', Mockery::type(Closure::class), PHP_INT_MAX)
|
|
||||||
->andReturnUsing(static function (string $hook, Closure $callback) use (&$events, &$added): bool {
|
|
||||||
$events[] = $callback === $added ? 'remove_filter of the same callback' : 'remove_filter of ANOTHER callback';
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
Functions\expect('wp_cache_post_edit')->once()->with(77)->andReturnUsing(function (int $id) use (&$events): int {
|
|
||||||
$events[] = 'purge with clear_on_post_edit=' . var_export($GLOBALS['wp_cache_clear_on_post_edit'], true);
|
|
||||||
unlink($this->pageDir() . 'index.html');
|
|
||||||
|
|
||||||
return $id;
|
|
||||||
});
|
|
||||||
|
|
||||||
self::assertSame([], $this->defaultPurgeProbe()->run());
|
|
||||||
|
|
||||||
self::assertSame(
|
|
||||||
['add_filter returning false', 'purge with clear_on_post_edit=0', 'remove_filter of the same callback'],
|
|
||||||
$events
|
|
||||||
);
|
|
||||||
self::assertSame(1, $GLOBALS['wp_cache_clear_on_post_edit']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testTheDefaultPurgeLeavesAnUnsetClearOnEditGlobalUnset(): void
|
|
||||||
{
|
|
||||||
unset($GLOBALS['wp_cache_clear_on_post_edit']);
|
|
||||||
Functions\when('add_filter')->justReturn(true);
|
|
||||||
Functions\when('remove_filter')->justReturn(true);
|
|
||||||
Functions\expect('wp_cache_post_edit')->once()->andReturnUsing(function (int $id): int {
|
|
||||||
self::assertSame(0, $GLOBALS['wp_cache_clear_on_post_edit']);
|
|
||||||
unlink($this->pageDir() . 'index.html');
|
|
||||||
|
|
||||||
return $id;
|
|
||||||
});
|
|
||||||
|
|
||||||
self::assertSame([], $this->defaultPurgeProbe()->run());
|
|
||||||
self::assertArrayNotHasKey('wp_cache_clear_on_post_edit', $GLOBALS);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testTheDefaultPurgeRestoresGlobalAndFilterWhenWpscThrows(): void
|
|
||||||
{
|
|
||||||
$GLOBALS['wp_cache_clear_on_post_edit'] = '1';
|
|
||||||
Functions\expect('add_filter')->once()->andReturn(true);
|
|
||||||
Functions\expect('remove_filter')->once()->with('wpsc_delete_related_pages_on_edit', Mockery::type(Closure::class), PHP_INT_MAX)->andReturn(true);
|
|
||||||
Functions\expect('wp_cache_post_edit')->once()->andThrow(new RuntimeException('WPSC kaputt'));
|
|
||||||
|
|
||||||
$problems = [];
|
|
||||||
$this->logged(function () use (&$problems): void {
|
|
||||||
$problems = $this->defaultPurgeProbe()->run();
|
|
||||||
});
|
|
||||||
|
|
||||||
self::assertSame(['Super-Cache-Probe abgebrochen (Details im PHP-Error-Log).'], $problems);
|
|
||||||
self::assertSame('1', $GLOBALS['wp_cache_clear_on_post_edit']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- P2: a status the database refused is no purge result ---------------------------------------------------------
|
|
||||||
|
|
||||||
public function testARefusedPrivateStatusIsItsOwnFixedProblemAndTheProbeNeverPurges(): void
|
|
||||||
{
|
|
||||||
$this->refusedStatus = 'private';
|
|
||||||
$purged = false;
|
|
||||||
$problems = [];
|
|
||||||
$logged = $this->logged(function () use (&$problems, &$purged): void {
|
|
||||||
$problems = $this->probe(static function (int $id) use (&$purged): void {
|
|
||||||
$purged = true;
|
|
||||||
})->run();
|
|
||||||
});
|
|
||||||
|
|
||||||
self::assertSame(['Der Status der Testseite konnte nicht gesetzt werden (Details im PHP-Error-Log).'], $problems);
|
|
||||||
self::assertStringContainsString('77', $logged);
|
|
||||||
self::assertStringContainsString('private', $logged);
|
|
||||||
self::assertFalse($purged);
|
|
||||||
self::assertSame([77], $this->deleted);
|
|
||||||
self::assertSame(['77:private', '77:draft'], $this->statuses);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return array<string, array{0: int|false}> */
|
|
||||||
public static function refusedUpdates(): array
|
|
||||||
{
|
|
||||||
return ['database error' => [false], 'no row changed' => [0]];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @dataProvider refusedUpdates */
|
|
||||||
public function testTheDefaultStatusSwitchTreatsAFailedOrEmptyUpdateAsRefused(int|false $result): void
|
|
||||||
{
|
|
||||||
$wpdb = Mockery::mock();
|
|
||||||
$wpdb->posts = 'wp_posts';
|
|
||||||
$wpdb->shouldReceive('update')->andReturn($result);
|
|
||||||
$GLOBALS['wpdb'] = $wpdb;
|
|
||||||
Functions\when('wp_cache_delete')->justReturn(true);
|
|
||||||
Functions\expect('wp_cache_post_edit')->never();
|
|
||||||
|
|
||||||
$problems = [];
|
|
||||||
$this->logged(function () use (&$problems): void {
|
|
||||||
$problems = (new SuperCacheProbe(
|
|
||||||
static fn (): bool => true,
|
|
||||||
fn (int $id): string => $this->pageDir(),
|
|
||||||
fn (int $id) => null
|
|
||||||
))->run();
|
|
||||||
});
|
|
||||||
|
|
||||||
self::assertSame(['Der Status der Testseite konnte nicht gesetzt werden (Details im PHP-Error-Log).'], $problems);
|
|
||||||
self::assertSame([77], $this->deleted);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testAFailingResetToDraftNeitherHidesTheResultNorEndsTheCheck(): void
|
|
||||||
{
|
|
||||||
$this->throwingStatus = 'draft';
|
|
||||||
$problems = [];
|
|
||||||
$logged = $this->logged(function () use (&$problems): void {
|
|
||||||
$problems = $this->purgingProbe()->run();
|
|
||||||
});
|
|
||||||
|
|
||||||
self::assertSame([], $problems);
|
|
||||||
self::assertStringContainsString('Status kaputt', $logged);
|
|
||||||
self::assertSame([77], $this->deleted);
|
|
||||||
self::assertDirectoryDoesNotExist($this->pageDir());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testAFailingResetToDraftDoesNotReplaceAnExceptionOfThePurge(): void
|
|
||||||
{
|
|
||||||
$this->throwingStatus = 'draft';
|
|
||||||
$problems = [];
|
|
||||||
$logged = $this->logged(function () use (&$problems): void {
|
|
||||||
$problems = $this->probe(static function (int $id): never {
|
|
||||||
throw new RuntimeException('WPSC kaputt');
|
|
||||||
})->run();
|
|
||||||
});
|
|
||||||
|
|
||||||
self::assertSame(['Super-Cache-Probe abgebrochen (Details im PHP-Error-Log).'], $problems);
|
|
||||||
self::assertStringContainsString('WPSC kaputt', $logged);
|
|
||||||
self::assertSame([77], $this->deleted);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testARefusedResetToDraftIsLoggedOnly(): void
|
|
||||||
{
|
|
||||||
$this->refusedStatus = 'draft';
|
|
||||||
$problems = [];
|
|
||||||
$logged = $this->logged(function () use (&$problems): void {
|
|
||||||
$problems = $this->purgingProbe()->run();
|
|
||||||
});
|
|
||||||
|
|
||||||
self::assertSame([], $problems);
|
|
||||||
self::assertStringContainsString('draft', $logged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+29
-28
@@ -50,18 +50,18 @@ final class PluginTest extends TestCase
|
|||||||
public function testIntegrationsInRegistryOrder(): void
|
public function testIntegrationsInRegistryOrder(): void
|
||||||
{
|
{
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
['core', 'wordpress', 'media', 'divi', 'divi-library', 'divi-theme-builder', 'pods', 'wpsc'],
|
['core', 'wordpress', 'media', 'divi', 'divi-library', 'divi-theme-builder', 'pods', 'wpfc'],
|
||||||
self::registry()->ids()
|
self::registry()->ids()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Divi, Pods and WP Super Cache boot (hooks, orphan cleanup); a second boot registers nothing twice. */
|
/** Divi, Pods and WP Fastest Cache boot (hooks, orphan cleanup); a second boot registers nothing twice. */
|
||||||
public function testTheBootableIntegrationsBootExactlyOnce(): void
|
public function testTheBootableIntegrationsBootExactlyOnce(): void
|
||||||
{
|
{
|
||||||
Actions\expectAdded('upgrader_process_complete')->once();
|
Actions\expectAdded('upgrader_process_complete')->once();
|
||||||
Actions\expectAdded('lit_mcp_compat_changed')->once();
|
Actions\expectAdded('lit_mcp_compat_changed')->once();
|
||||||
Filters\expectAdded(SettingsPage::TABS_FILTER)->never();
|
Filters\expectAdded(SettingsPage::TABS_FILTER)->never();
|
||||||
Actions\expectAdded(CompatibilityTab::LOAD_ACTION)->times(3); // Divi, Pods and WP Super Cache
|
Actions\expectAdded(CompatibilityTab::LOAD_ACTION)->times(3); // Divi, Pods and WP Fastest Cache
|
||||||
Actions\expectAdded(CompatibilityTab::EXTRA_ACTION)->once();
|
Actions\expectAdded(CompatibilityTab::EXTRA_ACTION)->once();
|
||||||
Filters\expectAdded(CompatibilityTab::COLUMNS_FILTER)->once();
|
Filters\expectAdded(CompatibilityTab::COLUMNS_FILTER)->once();
|
||||||
$registry = self::registry();
|
$registry = self::registry();
|
||||||
@@ -97,17 +97,17 @@ final class PluginTest extends TestCase
|
|||||||
self::assertArrayNotHasKey('pods', $registry->grouped());
|
self::assertArrayNotHasKey('pods', $registry->grouped());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** WP Super Cache is registered with its profile; its tool group appears only while the plugin is loaded. */
|
/** WP Fastest Cache is registered with its profile; its tool group appears only while the plugin is loaded. */
|
||||||
public function testSuperCacheIsRegisteredAndNoToolGroupWithoutTheCachePlugin(): void
|
public function testFastestCacheIsRegisteredAndNoToolGroupWithoutTheCachePlugin(): void
|
||||||
{
|
{
|
||||||
$registry = self::registry();
|
$registry = self::registry();
|
||||||
|
|
||||||
self::assertContains('wpsc', $registry->ids());
|
self::assertContains('wpfc', $registry->ids());
|
||||||
self::assertArrayNotHasKey('wpsc', $registry->grouped());
|
self::assertArrayNotHasKey('wpfc', $registry->grouped());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The purger of the real wiring asks the registry's Compatibility at call time: with WP Super Cache loaded and
|
* The purger of the real wiring asks the registry's Compatibility at call time: with WP Fastest Cache loaded and
|
||||||
* its manifest met, the whole-cache purge runs (a registry captured by value would still be null: always locked).
|
* its manifest met, the whole-cache purge runs (a registry captured by value would still be null: always locked).
|
||||||
*
|
*
|
||||||
* @runInSeparateProcess
|
* @runInSeparateProcess
|
||||||
@@ -115,33 +115,34 @@ final class PluginTest extends TestCase
|
|||||||
*/
|
*/
|
||||||
public function testTheWiredPurgerAsksTheCompatibilityOfTheRegistry(): void
|
public function testTheWiredPurgerAsksTheCompatibilityOfTheRegistry(): void
|
||||||
{
|
{
|
||||||
define('WPCACHEHOME', '/plugins/wp-super-cache/');
|
define('WPFC_MAIN_PATH', '/plugins/wp-fastest-cache/');
|
||||||
eval('function wp_cache_post_edit($id) { $GLOBALS["wpsc_calls"][] = "post:" . $id; }'
|
eval('function wpfc_clear_post_cache_by_id($id = false) { $GLOBALS["wpfc_calls"][] = "post:" . $id; }'
|
||||||
. 'function wp_cache_clear_cache($blog = 0) { $GLOBALS["wpsc_calls"][] = "all:" . $blog; }');
|
. 'function wpfc_clear_all_cache($minified = false) { $GLOBALS["wpfc_calls"][] = "all"; }'
|
||||||
$GLOBALS['wpsc_calls'] = [];
|
. 'class WpFastestCache { public function singleDeleteCache($c = false, $p = false, $parents = true) {}'
|
||||||
$GLOBALS['cache_path'] = '/cache/';
|
. ' public function getWpContentDir($path = false) { return ""; } }');
|
||||||
$GLOBALS['blog_cache_dir'] = '/cache/blogs/';
|
$GLOBALS['wpfc_calls'] = [];
|
||||||
$GLOBALS['cache_enabled'] = true;
|
$GLOBALS['wp_fastest_cache'] = new \WpFastestCache();
|
||||||
Functions\when('get_file_data')->justReturn(['Version' => '3.1.5']);
|
$GLOBALS['wp_fastest_cache_options'] = (object) ['wpFastestCacheStatus' => 'on'];
|
||||||
|
Functions\when('get_file_data')->justReturn(['Version' => '1.5.5']);
|
||||||
Functions\when('get_option')->justReturn(false);
|
Functions\when('get_option')->justReturn(false);
|
||||||
Functions\when('get_transient')->justReturn(false);
|
Functions\when('get_transient')->justReturn(false);
|
||||||
Functions\when('set_transient')->justReturn(true);
|
Functions\when('set_transient')->justReturn(true);
|
||||||
Functions\when('is_multisite')->justReturn(false);
|
Functions\when('is_multisite')->justReturn(false);
|
||||||
Functions\when('current_user_can')->justReturn(true);
|
Functions\when('current_user_can')->justReturn(true);
|
||||||
$integrations = (new ReflectionProperty(IntegrationRegistry::class, 'integrations'))->getValue(self::registry(log: new FakeChangeLog(1)));
|
$integrations = (new ReflectionProperty(IntegrationRegistry::class, 'integrations'))->getValue(self::registry(log: new FakeChangeLog(1)));
|
||||||
$wpsc = array_values(array_filter(
|
$wpfc = array_values(array_filter(
|
||||||
$integrations,
|
$integrations,
|
||||||
static fn ($integration): bool => $integration->id() === 'wpsc'
|
static fn ($integration): bool => $integration->id() === 'wpfc'
|
||||||
))[0];
|
))[0];
|
||||||
|
|
||||||
$result = $wpsc->abilities()[0]->execute(['scope' => 'all', 'confirm' => true]);
|
$result = $wpfc->abilities()[0]->execute(['scope' => 'all', 'confirm' => true]);
|
||||||
|
|
||||||
self::assertSame('purged', $result['outcome']);
|
self::assertSame('purged', $result['outcome']);
|
||||||
self::assertSame(['all:0'], $GLOBALS['wpsc_calls']);
|
self::assertSame(['all'], $GLOBALS['wpfc_calls']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** lit-site-status keeps its order: Divi, Pods, WP Super Cache, The Events Calendar. */
|
/** lit-site-status keeps its order: Divi, Pods, WP Fastest Cache, The Events Calendar. */
|
||||||
public function testSiteStatusListsDiviPodsSuperCacheAndTheEventsCalendarInThatOrder(): void
|
public function testSiteStatusListsDiviPodsFastestCacheAndTheEventsCalendarInThatOrder(): void
|
||||||
{
|
{
|
||||||
Functions\when('get_bloginfo')->justReturn('x');
|
Functions\when('get_bloginfo')->justReturn('x');
|
||||||
Functions\when('home_url')->justReturn('https://example.test');
|
Functions\when('home_url')->justReturn('https://example.test');
|
||||||
@@ -158,13 +159,13 @@ final class PluginTest extends TestCase
|
|||||||
|
|
||||||
$components = $status->execute([])['components'];
|
$components = $status->execute([])['components'];
|
||||||
|
|
||||||
self::assertSame(['divi', 'pods', 'wpsc', 'the-events-calendar'], array_column($components, 'id'));
|
self::assertSame(['divi', 'pods', 'wpfc', 'the-events-calendar'], array_column($components, 'id'));
|
||||||
$pods = array_column($components, null, 'id')['pods'];
|
$pods = array_column($components, null, 'id')['pods'];
|
||||||
self::assertFalse($pods['active'], 'no Pods in unit tests');
|
self::assertFalse($pods['active'], 'no Pods in unit tests');
|
||||||
self::assertSame([], $pods['problems']);
|
self::assertSame([], $pods['problems']);
|
||||||
$wpsc = array_column($components, null, 'id')['wpsc'];
|
$wpfc = array_column($components, null, 'id')['wpfc'];
|
||||||
self::assertFalse($wpsc['active'], 'no WP Super Cache in unit tests');
|
self::assertFalse($wpfc['active'], 'no WP Fastest Cache in unit tests');
|
||||||
self::assertSame([], $wpsc['problems']);
|
self::assertSame([], $wpfc['problems']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testTheCompatibilityCoversAllProfilesWithoutIdCollision(): void
|
public function testTheCompatibilityCoversAllProfilesWithoutIdCollision(): void
|
||||||
@@ -172,9 +173,9 @@ final class PluginTest extends TestCase
|
|||||||
$compat = self::registry()->compatibility();
|
$compat = self::registry()->compatibility();
|
||||||
|
|
||||||
$ids = array_map(static fn (Profile $p): string => $p->id, $compat->profiles());
|
$ids = array_map(static fn (Profile $p): string => $p->id, $compat->profiles());
|
||||||
self::assertSame(['divi', 'pods', 'wpsc'], $ids);
|
self::assertSame(['divi', 'pods', 'wpfc'], $ids);
|
||||||
self::assertSame($ids, array_values(array_unique($ids)));
|
self::assertSame($ids, array_values(array_unique($ids)));
|
||||||
self::assertSame('wpsc', $compat->owner($compat->capability('wpsc-purge-all'))?->id);
|
self::assertSame('wpfc', $compat->owner($compat->capability('wpfc-purge-all'))?->id);
|
||||||
self::assertNotNull($compat->capability('page-write'));
|
self::assertNotNull($compat->capability('page-write'));
|
||||||
self::assertNotNull($compat->capability('pods-field-read'));
|
self::assertNotNull($compat->capability('pods-field-read'));
|
||||||
self::assertSame('divi', $compat->owner($compat->capability('page-write'))?->id);
|
self::assertSame('divi', $compat->owner($compat->capability('page-write'))?->id);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use LIT\MCP\Admin\ConnectionStatus;
|
|||||||
use LIT\MCP\ChangeLog\ChangeLogCleanup;
|
use LIT\MCP\ChangeLog\ChangeLogCleanup;
|
||||||
use LIT\MCP\ChangeLog\ChangeLogInstaller;
|
use LIT\MCP\ChangeLog\ChangeLogInstaller;
|
||||||
use LIT\MCP\Integrations\Divi\Release\ReleaseResolver;
|
use LIT\MCP\Integrations\Divi\Release\ReleaseResolver;
|
||||||
use LIT\MCP\Integrations\SuperCache\SuperCacheListener;
|
use LIT\MCP\Integrations\FastestCache\FastestCacheListener;
|
||||||
use LIT\MCP\Limits;
|
use LIT\MCP\Limits;
|
||||||
use LIT\MCP\Updater\RecheckOnInstall;
|
use LIT\MCP\Updater\RecheckOnInstall;
|
||||||
use Mockery;
|
use Mockery;
|
||||||
@@ -60,7 +60,7 @@ final class UninstallTest extends TestCase
|
|||||||
Access::OPTION, ExposurePolicy::OPTION, Limits::OPTION, ChangeLogInstaller::VERSION_OPTION,
|
Access::OPTION, ExposurePolicy::OPTION, Limits::OPTION, ChangeLogInstaller::VERSION_OPTION,
|
||||||
'lit_mcp_divi_releases', 'lit_mcp_divi_check_report', 'lit_mcp_divi_check_lock',
|
'lit_mcp_divi_releases', 'lit_mcp_divi_check_report', 'lit_mcp_divi_check_lock',
|
||||||
'lit_mcp_pods_releases', 'lit_mcp_pods_check_report', 'lit_mcp_pods_check_lock',
|
'lit_mcp_pods_releases', 'lit_mcp_pods_check_report', 'lit_mcp_pods_check_lock',
|
||||||
'lit_mcp_wpsc_releases', 'lit_mcp_wpsc_check_report', 'lit_mcp_wpsc_check_lock',
|
'lit_mcp_wpfc_releases', 'lit_mcp_wpfc_check_report', 'lit_mcp_wpfc_check_lock',
|
||||||
'lit_mcp_snapshots_colors', 'lit_mcp_snapshots_variables', 'lit_mcp_snapshots_presets',
|
'lit_mcp_snapshots_colors', 'lit_mcp_snapshots_variables', 'lit_mcp_snapshots_presets',
|
||||||
'lit_mcp_snapshots_theme_options', 'lit_mcp_snapshots_custom_css',
|
'lit_mcp_snapshots_theme_options', 'lit_mcp_snapshots_custom_css',
|
||||||
DesignLock::option('colors'), DesignLock::option('variables'), DesignLock::option('presets'), DesignLock::option('custom_css'),
|
DesignLock::option('colors'), DesignLock::option('variables'), DesignLock::option('presets'), DesignLock::option('custom_css'),
|
||||||
@@ -73,15 +73,15 @@ final class UninstallTest extends TestCase
|
|||||||
self::assertNotContains('wpmedia_mcp_oauth_rewrite_version', $deleted);
|
self::assertNotContains('wpmedia_mcp_oauth_rewrite_version', $deleted);
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
[
|
[
|
||||||
ConnectionStatus::ENDPOINT_TRANSIENT, ReleaseResolver::CACHE, SuperCacheListener::PURGED_AT,
|
ConnectionStatus::ENDPOINT_TRANSIENT, ReleaseResolver::CACHE, FastestCacheListener::PURGED_AT,
|
||||||
ConnectionStatus::ENDPOINT_TRANSIENT, ReleaseResolver::CACHE, SuperCacheListener::PURGED_AT,
|
ConnectionStatus::ENDPOINT_TRANSIENT, ReleaseResolver::CACHE, FastestCacheListener::PURGED_AT,
|
||||||
],
|
],
|
||||||
$transients
|
$transients
|
||||||
);
|
);
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
[
|
[
|
||||||
ChangeLogCleanup::HOOK, SuperCacheListener::TRAILING_HOOK,
|
ChangeLogCleanup::HOOK, FastestCacheListener::TRAILING_HOOK,
|
||||||
ChangeLogCleanup::HOOK, SuperCacheListener::TRAILING_HOOK,
|
ChangeLogCleanup::HOOK, FastestCacheListener::TRAILING_HOOK,
|
||||||
],
|
],
|
||||||
$hooks
|
$hooks
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use LIT\MCP\Admin\LogTab;
|
|||||||
use LIT\MCP\ChangeLog\ChangeLogCleanup;
|
use LIT\MCP\ChangeLog\ChangeLogCleanup;
|
||||||
use LIT\MCP\ChangeLog\ChangeLogInstaller;
|
use LIT\MCP\ChangeLog\ChangeLogInstaller;
|
||||||
use LIT\MCP\Integrations\Divi\Release\ReleaseResolver;
|
use LIT\MCP\Integrations\Divi\Release\ReleaseResolver;
|
||||||
use LIT\MCP\Integrations\SuperCache\SuperCacheListener;
|
use LIT\MCP\Integrations\FastestCache\FastestCacheListener;
|
||||||
use LIT\MCP\Limits;
|
use LIT\MCP\Limits;
|
||||||
use LIT\MCP\Uninstaller;
|
use LIT\MCP\Uninstaller;
|
||||||
use LIT\MCP\Updater\RecheckOnInstall;
|
use LIT\MCP\Updater\RecheckOnInstall;
|
||||||
@@ -61,16 +61,16 @@ final class UninstallerTest extends TestCase
|
|||||||
Uninstaller::run();
|
Uninstaller::run();
|
||||||
|
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
[ConnectionStatus::ENDPOINT_TRANSIENT, ReleaseResolver::CACHE, SuperCacheListener::PURGED_AT],
|
[ConnectionStatus::ENDPOINT_TRANSIENT, ReleaseResolver::CACHE, FastestCacheListener::PURGED_AT],
|
||||||
$transients
|
$transients
|
||||||
);
|
);
|
||||||
self::assertSame([ChangeLogCleanup::HOOK, SuperCacheListener::TRAILING_HOOK], $hooks);
|
self::assertSame([ChangeLogCleanup::HOOK, FastestCacheListener::TRAILING_HOOK], $hooks);
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
[
|
[
|
||||||
Access::OPTION, ExposurePolicy::OPTION, Limits::OPTION, ChangeLogInstaller::VERSION_OPTION,
|
Access::OPTION, ExposurePolicy::OPTION, Limits::OPTION, ChangeLogInstaller::VERSION_OPTION,
|
||||||
'lit_mcp_divi_releases', 'lit_mcp_divi_check_report', 'lit_mcp_divi_check_lock',
|
'lit_mcp_divi_releases', 'lit_mcp_divi_check_report', 'lit_mcp_divi_check_lock',
|
||||||
'lit_mcp_pods_releases', 'lit_mcp_pods_check_report', 'lit_mcp_pods_check_lock',
|
'lit_mcp_pods_releases', 'lit_mcp_pods_check_report', 'lit_mcp_pods_check_lock',
|
||||||
'lit_mcp_wpsc_releases', 'lit_mcp_wpsc_check_report', 'lit_mcp_wpsc_check_lock',
|
'lit_mcp_wpfc_releases', 'lit_mcp_wpfc_check_report', 'lit_mcp_wpfc_check_lock',
|
||||||
'lit_mcp_snapshots_colors', 'lit_mcp_snapshots_variables', 'lit_mcp_snapshots_presets',
|
'lit_mcp_snapshots_colors', 'lit_mcp_snapshots_variables', 'lit_mcp_snapshots_presets',
|
||||||
'lit_mcp_snapshots_theme_options', 'lit_mcp_snapshots_custom_css',
|
'lit_mcp_snapshots_theme_options', 'lit_mcp_snapshots_custom_css',
|
||||||
DesignLock::option('colors'), DesignLock::option('variables'), DesignLock::option('presets'), DesignLock::option('custom_css'),
|
DesignLock::option('colors'), DesignLock::option('variables'), DesignLock::option('presets'), DesignLock::option('custom_css'),
|
||||||
@@ -108,15 +108,15 @@ final class UninstallerTest extends TestCase
|
|||||||
|
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
[
|
[
|
||||||
ConnectionStatus::ENDPOINT_TRANSIENT, ReleaseResolver::CACHE, SuperCacheListener::PURGED_AT,
|
ConnectionStatus::ENDPOINT_TRANSIENT, ReleaseResolver::CACHE, FastestCacheListener::PURGED_AT,
|
||||||
ConnectionStatus::ENDPOINT_TRANSIENT, ReleaseResolver::CACHE, SuperCacheListener::PURGED_AT,
|
ConnectionStatus::ENDPOINT_TRANSIENT, ReleaseResolver::CACHE, FastestCacheListener::PURGED_AT,
|
||||||
],
|
],
|
||||||
$transients
|
$transients
|
||||||
);
|
);
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
[
|
[
|
||||||
ChangeLogCleanup::HOOK, SuperCacheListener::TRAILING_HOOK,
|
ChangeLogCleanup::HOOK, FastestCacheListener::TRAILING_HOOK,
|
||||||
ChangeLogCleanup::HOOK, SuperCacheListener::TRAILING_HOOK,
|
ChangeLogCleanup::HOOK, FastestCacheListener::TRAILING_HOOK,
|
||||||
],
|
],
|
||||||
$hooks
|
$hooks
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user