diff --git a/composer.json b/composer.json index 61f5643..f17024c 100644 --- a/composer.json +++ b/composer.json @@ -48,6 +48,7 @@ } }, "bin": [ + "php/bin/cachectl.php", "php/bin/dumpser.php", "php/bin/json2yml.php", "php/bin/yml2json.php", diff --git a/php/bin/cachectl.php b/php/bin/cachectl.php new file mode 100755 index 0000000..48de2bc --- /dev/null +++ b/php/bin/cachectl.php @@ -0,0 +1,7 @@ +#!/usr/bin/php + parent::ARGS, + "purpose" => "gestion de fichiers cache", + ["-r", "--read", "name" => "action", "value" => self::ACTION_READ, + "help" => "Afficher le contenu d'un fichier cache", + ], + ["-i", "--infos", "name" => "action", "value" => self::ACTION_INFOS, + "help" => "Afficher des informations sur le fichier cache", + ], + ["-k", "--clean", "name" => "action", "value" => self::ACTION_CLEAN, + "help" => "Supprimer le fichier cache s'il a expiré", + ], + ["-a", "--add-duration", "args" => 1, + "action" => [null, "->setActionUpdate", self::ACTION_UPDATE_ADD], + "help" => "Ajouter le nombre de secondes spécifié à la durée du cache", + ], + ["-b", "--sub-duration", "args" => 1, + "action" => [null, "->setActionUpdate", self::ACTION_UPDATE_SUB], + "help" => "Enlever le nombre de secondes spécifié à la durée du cache", + ], + #XXX pas encore implémenté + //["-s", "--set-duration", "args" => 1, + // "action" => [null, "->setActionUpdate", self::ACTION_UPDATE_SET], + // "help" => "Mettre à jour la durée du cache à la valeur spécifiée", + //], + ]; + + protected $action = self::ACTION_READ; + + protected $updateAction, $updateDuration; + + function setActionUpdate(int $action, $updateDuration): void { + $this->action = self::ACTION_UPDATE; + switch ($action) { + case self::ACTION_UPDATE_SUB: + $this->updateAction = CacheFile::UPDATE_SUB; + break; + case self::ACTION_UPDATE_SET: + $this->updateAction = CacheFile::UPDATE_SET; + break; + case self::ACTION_UPDATE_ADD: + $this->updateAction = CacheFile::UPDATE_ADD; + break; + } + $this->updateDuration = $updateDuration; + } + + protected function findCaches(string $dir, ?array &$files): void { + foreach (glob("$dir/*") as $file) { + if (is_dir($file)) { + $this->findCaches($file, $files); + } elseif (is_file($file) && fnmatch("*.cache", $file)) { + $files[] = $file; + } + } + } + + function main() { + $files = []; + foreach ($this->args as $arg) { + if (is_dir($arg)) { + $this->findCaches($arg, $files); + } elseif (is_file($arg)) { + $files[] = $arg; + } else { + msg::warning("$arg: fichier introuvable"); + } + } + $showSection = count($files) > 1; + foreach ($files as $file) { + switch ($this->action) { + case self::ACTION_READ: + if ($showSection) msg::section($file); + $cache = new CacheFile($file, null, [ + "readonly" => true, + "duration" => "INF", + "override_duration" => true, + ]); + yaml::dump($cache->get()); + break; + case self::ACTION_INFOS: + if ($showSection) msg::section($file); + $cache = new CacheFile($file, null, [ + "readonly" => true, + ]); + yaml::dump($cache->getInfos()); + break; + case self::ACTION_CLEAN: + msg::action(path::ppath($file)); + $cache = new CacheFile($file); + try { + if ($cache->deleteExpired()) msg::asuccess("fichier supprimé"); + else msg::adone("fichier non expiré"); + } catch (Exception $e) { + msg::afailure($e); + } + break; + case self::ACTION_UPDATE: + msg::action(path::ppath($file)); + $cache = new CacheFile($file); + try { + $cache->updateDuration($this->updateDuration, $this->updateAction); + msg::asuccess("fichier mis à jour"); + } catch (Exception $e) { + msg::afailure($e); + } + break; + default: + self::die("$this->action: action non implémentée"); + } + } + } +} diff --git a/php/src/cache/CacheData.php b/php/src/cache/CacheData.php new file mode 100644 index 0000000..5ce5b83 --- /dev/null +++ b/php/src/cache/CacheData.php @@ -0,0 +1,46 @@ +name = $name ?? ""; + $this->compute = func::withn($compute ?? static::COMPUTE); + } + + protected string $name; + + function getName() : string { + return $this->name; + } + + protected ?func $compute; + + /** calculer la donnée */ + function compute() { + $compute = $this->compute; + $data = $compute !== null? $compute->invoke(): null; + return $data; + } + + /** spécifier le chemin du cache à partir du fichier de base */ + abstract function setDatafile(?string $basefile): void; + + /** indiquer si le cache existe */ + abstract function exists(): bool; + + /** charger la donnée depuis le cache */ + abstract function load(); + + /** sauvegarder la donnée dans le cache et la retourner */ + abstract function save($data); + + /** supprimer le cache */ + abstract function delete(); +} diff --git a/php/src/cache/CacheFile.php b/php/src/cache/CacheFile.php new file mode 100644 index 0000000..f40ed80 --- /dev/null +++ b/php/src/cache/CacheFile.php @@ -0,0 +1,360 @@ +initialDuration = Delay::with($params["duration"] ?? static::DURATION); + $this->overrideDuration = $params["override_duration"] ?? false; + $this->readonly = $params["readonly"] ?? false; + $this->cacheNull = $params["cache_null"] ?? false; + $data ??= $params["data"] ?? null; + $this->sources = null; + if (self::ensure_source($data, $source)) { + if ($source !== null) $source->setDatafile($basefile); + $this->sources = ["" => $source]; + } else { + $sources = []; + $index = 0; + foreach ($data as $key => $source) { + self::ensure_source($source, $source, false); + if ($source !== null) { + $source->setDatafile($basefile); + if ($key === $index) { + $index++; + $key = $source->getName(); + } + } elseif ($key === $index) { + $index++; + } + $sources[$key] = $source; + } + $this->sources = $sources; + } + parent::__construct($file); + } + + protected Delay $initialDuration; + + protected bool $overrideDuration; + + protected bool $readonly; + + protected bool $cacheNull; + + /** @var ?CacheData[] */ + protected ?array $sources; + + /** + * vérifier si le fichier est valide. s'il est invalide, il faut le recréer. + * + * on assume que le fichier existe, vu qu'il a été ouvert en c+b + */ + function isValid(): bool { + # considèrer que le fichier est invalide s'il est de taille nulle + return $this->getSize() > 0; + } + + protected ?DateTime $start; + + protected ?Delay $duration; + + protected $data; + + /** charger les données. le fichier a déjà été verrouillé en lecture */ + protected function loadMetadata(): void { + if ($this->isValid()) { + $this->rewind(); + [ + "start" => $start, + "duration" => $duration, + "data" => $data, + ] = $this->unserialize(null, false, true); + if ($this->overrideDuration) { + $duration = Delay::with($this->initialDuration, $start); + } + } else { + $start = null; + $duration = null; + $data = null; + } + $this->start = $start; + $this->duration = $duration; + $this->data = $data; + } + + /** + * tester s'il faut mettre les données à jour. le fichier a déjà été + * verrouillé en lecture + */ + protected function shouldUpdate(bool $noCache=false): bool { + if ($this->isValid()) { + $expired = $this->duration->isElapsed(); + } else { + $expired = false; + $noCache = true; + } + return $noCache || $expired; + } + + /** sauvegarder les données. le fichier a déjà été verrouillé en écriture */ + protected function saveMetadata(): void { + $this->duration ??= $this->initialDuration; + if ($this->start === null) { + $this->start = new DateTime(); + $this->duration = Delay::with($this->duration, $this->start); + } + $this->ftruncate(); + $this->serialize([ + "start" => $this->start, + "duration" => $this->duration, + "data" => $this->data, + ], false, true); + } + + protected function unlinkFiles(bool $datafilesOnly=false): void { + foreach ($this->sources as $source) { + if ($source !== null) $source->delete(); + } + if (!$datafilesOnly) @unlink($this->file); + } + + /** tester si $value peut être mis en cache */ + protected function shouldCache($value): bool { + return $this->cacheNull || $value !== null; + } + + protected ?DateTime $ostart; + + protected ?Delay $oduration; + + protected $odata; + + protected function beforeAction() { + $this->loadMetadata(); + $this->ostart = cv::clone($this->start); + $this->oduration = cv::clone($this->duration); + $this->odata = cv::clone($this->data); + } + + protected function afterAction() { + $modified = false; + if ($this->start != $this->ostart) $modified = true; + $duration = $this->duration; + $oduration = $this->oduration; + if ($duration === null || $oduration === null) $modified = true; + elseif ($duration->getDest() != $oduration->getDest()) $modified = true; + # égalité stricte uniquement pour $data et $datafiles + if ($this->data !== $this->odata) $modified = true; + if ($modified && !$this->readonly) { + $this->lockWrite(); + $this->saveMetadata(); + } + } + + protected function action(callable $callback, bool $willWrite=false) { + if ($willWrite && !$this->readonly) $this->lockWrite(); + else $this->lockRead(); + try { + $this->beforeAction(); + $result = $callback(); + $this->afterAction(); + return $result; + } finally { + $this->ostart = null; + $this->oduration = null; + $this->odata = null; + $this->start = null; + $this->duration = null; + $this->data = null; + $this->unlock(true); + } + } + + protected function compute() { + return null; + } + + protected function refreshData($data, bool $noCache) { + $source = $this->sources[$data] ?? null; + $updateMetadata = $this->shouldUpdate($noCache); + if ($source === null) $updateData = $this->data === null; + else $updateData = !$source->exists(); + if (!$this->readonly && ($updateMetadata || $updateData)) { + $this->lockWrite(); + if ($updateMetadata) { + # il faut refaire tout le cache + $this->unlinkFiles(true); + $this->start = null; + $this->duration = null; + $this->data = null; + $updateData = true; + } + if ($source === null) { + if ($updateData) { + # calculer la valeur + try { + $data = $this->compute(); + } catch (Exception $e) { + # le fichier n'est pas mis à jour, mais ce n'est pas gênant: lors + # des futurs appels, l'exception continuera d'être lancée ou la + # valeur sera finalement mise à jour + throw $e; + } + } else { + $data = $this->data; + } + if ($this->shouldCache($data)) $this->data = $data; + else $this->data = $data = null; + } else { + if ($updateData) { + # calculer la valeur + try { + $data = $source->compute(); + } catch (Exception $e) { + # le fichier n'est pas mis à jour, mais ce n'est pas gênant: lors + # des futurs appels, l'exception continuera d'être lancée ou la + # valeur sera finalement mise à jour + throw $e; + } + } else { + $data = $source->load(); + } + if ($this->shouldCache($data)) { + $data = $source->save($data); + } else { + # ne pas garder le fichier s'il ne faut pas mettre en cache + $source->delete(); + $data = null; + } + } + } elseif ($source === null) { + $data = $this->data; + } elseif ($source->exists()) { + $data = $source->load(); + } else { + $data = null; + } + return $data; + } + + /** + * s'assurer que le cache est à jour avec les données les plus récentes. si + * les données sont déjà présentes dans le cache et n'ont pas encore expirées + * cette méthode est un NOP + */ + function refresh(bool $noCache=false): self { + $this->action(function() use ($noCache) { + foreach (array_keys($this->sources) as $data) { + $this->refreshData($data, $noCache); + } + }); + return $this; + } + + function get($data=null, bool $noCache=false) { + return $this->action(function () use ($data, $noCache) { + return $this->refreshData($data, $noCache); + }); + } + + function all($data=null, bool $noCache=false): ?iterable { + $data = $this->get($data, $noCache); + if ($data !== null && !is_iterable($data)) $data = [$data]; + return $data; + } + + function delete($data=null): void { + $source = $this->sources[$data] ?? null; + if ($source !== null) $source->delete(); + } + + /** obtenir les informations sur le fichier */ + function getInfos(): array { + return $this->action(function () { + if (!$this->isValid()) { + return ["valid" => false]; + } + $start = $this->start; + $duration = $this->duration; + return [ + "valid" => true, + "start" => $start, + "duration" => strval($duration), + "date_start" => $start->format(), + "date_end" => $duration->getDest()->format(), + ]; + }); + } + + const UPDATE_SUB = -1, UPDATE_SET = 0, UPDATE_ADD = 1; + + /** + * mettre à jour la durée de validité du fichier + * + * XXX UPDATE_SET n'est pas implémenté + */ + function updateDuration($nduration, int $action=self::UPDATE_ADD): void { + if ($this->readonly) return; + $this->action(function () use ($nduration, $action) { + if (!$this->isValid()) return; + $duration = $this->duration; + if ($action < 0) $duration->subDuration($nduration); + elseif ($action > 0) $duration->addDuration($nduration); + }, true); + } + + /** supprimer les fichiers s'ils ont expiré */ + function deleteExpired(bool $force=false): bool { + if ($this->readonly) return false; + return $this->action(function () use ($force) { + if ($force || $this->shouldUpdate()) { + $this->unlinkFiles(); + return true; + } + return false; + }, true); + } +} diff --git a/php/src/cache/CacheManager.php b/php/src/cache/CacheManager.php new file mode 100644 index 0000000..2b134c9 --- /dev/null +++ b/php/src/cache/CacheManager.php @@ -0,0 +1,68 @@ +shouldCaches = []; + $this->defaultCache = true; + $this->includes = $includes; + $this->excludes = $excludes; + } + + /** + * @var array tableau {id => shouldCache} indiquant si l'élément id doit être + * mis en cache + */ + protected array $shouldCaches; + + /** + * @var bool valeur par défaut de shouldCache si la valeur n'est pas trouvée + * dans $shouldCache + */ + protected bool $defaultCache; + + /** + * @var array|null groupes à toujours inclure dans le cache. pour les + * identifiants de ces groupe, {@link self::shouldCache()} retourne toujours + * true. + * + * $excludes est prioritaire par rapport à $includes + */ + protected ?array $includes; + + /** + * @var array|null groupes à exclure de la mise en cache. la mise en cache est + * toujours calculée pour les identifiants de ces groupes. + */ + protected ?array $excludes; + + function setNoCache(bool $noCache=true, bool $reset=true): self { + if ($reset) $this->shouldCaches = []; + $this->defaultCache = !$noCache; + return $this; + } + + function shouldCache(string $id, ?string $groupId=null, bool $reset=true): bool { + if ($groupId !== null) { + $includes = $this->includes; + $shouldInclude = $includes !== null && in_array($groupId, $includes); + $excludes = $this->excludes; + $shouldExclude = $excludes !== null && in_array($groupId, $excludes); + if ($shouldInclude && !$shouldExclude) return true; + } + $cacheId = "$groupId-$id"; + $shouldCache = cl::get($this->shouldCaches, $cacheId, $this->defaultCache); + $this->shouldCaches[$cacheId] = $reset?: $shouldCache; + return $shouldCache; + } +} diff --git a/php/src/cache/CursorCacheData.php b/php/src/cache/CursorCacheData.php new file mode 100644 index 0000000..bcfdd33 --- /dev/null +++ b/php/src/cache/CursorCacheData.php @@ -0,0 +1,36 @@ +initStorage(cache::storage()); + $this->channel = $channel; + } + + function setDatafile(?string $basefile): void { + } + + protected CursorChannel $channel; + + function exists(): bool { + return $this->channel->count() > 0; + } + + function load() { + return $this->channel; + } + + function save($data) { + if (!is_iterable($data)) $data = [$data]; + $this->channel->rechargeAll($data); + return $this->channel; + } + + function delete() { + $this->channel->delete(null); + } +} diff --git a/php/src/cache/CursorChannel.php b/php/src/cache/CursorChannel.php new file mode 100644 index 0000000..1af52b1 --- /dev/null +++ b/php/src/cache/CursorChannel.php @@ -0,0 +1,127 @@ +initStorage($storage); + if ($rows !== null) $channel->rechargeAll($rows); + return $channel; + } + + const NAME = "cursor"; + const TABLE_NAME = "cursor"; + + const COLUMN_DEFINITIONS = [ + "group_id_" => "varchar(32) not null", // groupe de curseur + "id_" => "varchar(128) not null", // nom du curseur + "key_index_" => "integer not null", + "key_" => "varchar(128) not null", + "search_" => "varchar(255)", + + "primary key (group_id_, id_, key_index_)", + ]; + + const ADD_COLUMNS = null; + + protected function COLUMN_DEFINITIONS(): ?array { + return cl::merge(self::COLUMN_DEFINITIONS, static::ADD_COLUMNS); + } + + /** + * @param array|string $cursorId + */ + function __construct($cursorId) { + parent::__construct(); + cache::verifix_id($cursorId); + [ + "group_id" => $this->groupId, + "id" => $this->id, + ] = $cursorId; + } + + protected string $groupId; + + protected string $id; + + function getCursorId(): array { + return [ + "group_id" => $this->groupId, + "id" => $this->id, + ]; + } + + function getBaseFilter(): ?array { + return [ + "group_id_" => $this->groupId, + "id_" => $this->id, + ]; + } + + protected int $index = 0; + + protected function getSearch($item): ?string { + $search = cl::filter_n(cl::with($item)); + $search = implode(" ", $search); + return substr($search, 0, 255); + } + + function getItemValues($item, $key=null): ?array { + $index = $this->index++; + $key = $key ?? $index; + $key = substr(strval($key), 0, 128); + $addColumns = static::ADD_COLUMNS ?? []; + $addColumns = cl::select($item, + array_filter(array_keys($addColumns), function ($key) { + return is_string($key); + })); + return cl::merge($addColumns, [ + "group_id_" => $this->groupId, + "id_" => $this->id, + "key_index_" => $index, + "key_" => $key, + "search_" => $this->getSearch($item), + ]); + } + + function reset(bool $recreate=false): void { + $this->index = 0; + parent::reset($recreate); + } + + function chargeAll(?iterable $items, $func=null, ?array $args=null): int { + if ($items === null) return 0; + $count = 0; + if ($func !== null) $func = func::with($func, $args)->bind($this); + foreach ($items as $key => $item) { + $count += $this->charge($item, $func, [$key]); + } + return $count; + } + + function rechargeAll(?iterable $items): self { + $this->delete(null); + $this->index = 0; + $this->chargeAll($items); + return $this; + } + + function getIterator(): Traversable { + $rows = $this->dbAll([ + "cols" => ["key_", "item__"], + "where" => $this->getBaseFilter(), + ]); + foreach ($rows as $row) { + $key = $row["key_"]; + $item = $this->unserialize($row["item__"]); + yield $key => $item; + } + } +} diff --git a/php/src/cache/DataCacheData.php b/php/src/cache/DataCacheData.php new file mode 100644 index 0000000..7ada552 --- /dev/null +++ b/php/src/cache/DataCacheData.php @@ -0,0 +1,59 @@ +setDatafile($basefile); + } + + function compute() { + $data = parent::compute(); + if ($data instanceof Traversable) $data = cl::all($data); + return $data; + } + + protected string $datafile; + + function setDatafile(?string $basefile): void { + if ($basefile === null) { + $basedir = "."; + $basename = ""; + } else { + $basedir = path::dirname($basefile); + $basename = path::filename($basefile); + } + $this->datafile = "$basedir/.$basename.{$this->name}".cache::EXT; + } + + function exists(): bool { + return file_exists($this->datafile); + } + + function load() { + return file::reader($this->datafile)->unserialize(); + } + + function save($data) { + file::writer($this->datafile)->serialize($data); + return $data; + } + + function delete(): void { + @unlink($this->datafile); + } +} diff --git a/php/src/cache/TODO.md b/php/src/cache/TODO.md new file mode 100644 index 0000000..e8c41c0 --- /dev/null +++ b/php/src/cache/TODO.md @@ -0,0 +1,6 @@ +# nulib\cache + +* [ ] CacheChannel: stocker aussi la clé primaire, ce qui permet de récupérer + la donnée correspondante dans la source? + +-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary \ No newline at end of file diff --git a/php/src/cache/cache.php b/php/src/cache/cache.php new file mode 100644 index 0000000..ed599fa --- /dev/null +++ b/php/src/cache/cache.php @@ -0,0 +1,93 @@ +getVarfile("cache.db"); + } + + protected static ?CapacitorStorage $storage = null; + + static function storage(): CapacitorStorage { + return self::$storage ??= new SqliteStorage(self::dbfile()); + } + + static function set_storage(CapacitorStorage $storage): CapacitorStorage { + return self::$storage = $storage; + } + + protected static ?CacheManager $manager = null; + + static function manager(): CacheManager { + return self::$manager ??= new CacheManager(); + } + + static function set_manager(CacheManager $manager): CacheManager { + return self::$manager = $manager; + } + + static function nc(bool $noCache=true, bool $reset=false): void { + self::manager()->setNoCache($noCache, $reset); + } + + protected static function should_cache(string $id, ?string $groupId=null, bool $reset=true): bool { + return self::manager()->shouldCache($id, $groupId, $reset); + } + + static function verifix_id(&$cacheId): void { + $cacheId ??= utils::uuidgen(); + if (is_array($cacheId)) { + $keys = array_keys($cacheId); + if (array_key_exists("id", $cacheId)) $idKey = "id"; + else $idKey = $keys[0] ?? null; + $id = strval($cacheId[$idKey] ?? ""); + if (array_key_exists("group_id", $cacheId)) $groupIdKey = "group_id"; + else $groupIdKey = $keys[1] ?? null; + $groupId = strval($cacheId[$groupIdKey] ?? ""); + } else { + $id = strval($cacheId); + $groupId = ""; + } + # si le groupe ou le nom sont trop grand, en faire un hash + if (strlen($groupId) > 32) $groupId = md5($groupId); + if (strlen($id) > 128) $id = substr($id, 0, 128 - 32).md5($id); + $cacheId = ["group_id" => $groupId, "id" => $id]; + } + + private static function new(array $cacheId, ?string $suffix, $data, ?array $params=null): CacheFile { + $file = $cacheId["group_id"]; + if ($file) $file .= "_"; + $file .= $cacheId["id"]; + $file .= $suffix; + return new CacheFile($file, $data, $params); + } + + static function cache($dataId, $data, ?array $params=null): CacheFile { + self::verifix_id($dataId); + return self::new($dataId, null, $data, $params); + } + + static function get($dataId, $data, ?array $params=null) { + self::verifix_id($dataId); + $noCache = !self::should_cache($dataId["id"], $dataId["group_id"]); + $cache = self::new($dataId, null, $data, $params); + return $cache->get(null, $noCache); + } + + static function all($cursorId, $rows, ?array $params=null): ?iterable { + self::verifix_id($cursorId); + $noCache = !self::should_cache($cursorId["id"], $cursorId["group_id"]); + $cache = self::new($cursorId, "_rows", new CursorCacheData($cursorId, $rows), $params); + return $cache->get(null, $noCache); + } +} diff --git a/php/tbin/.gitignore b/php/tbin/.gitignore index f3b938a..c44eb09 100644 --- a/php/tbin/.gitignore +++ b/php/tbin/.gitignore @@ -1 +1,2 @@ /*.db +/*.cache diff --git a/php/tbin/test-cache.php b/php/tbin/test-cache.php new file mode 100755 index 0000000..f3590c0 --- /dev/null +++ b/php/tbin/test-cache.php @@ -0,0 +1,73 @@ +#!/usr/bin/php +get()); + if ($dumpInfos) { + yaml::dump($cache->getInfos()); + } +} + +//system("rm -f *.cache .*.cache"); + +$what = [ + "null", + "one", + "two", + "three", +]; +$duration = 10; + +if (in_array("null", $what)) { + $null = new CacheFile("null", null, [ + "duration" => $duration, + ]); + show("null", $null); +} + +if (in_array("one", $what)) { + $one = new class("one", null, [ + "duration" => $duration, + ]) extends CacheFile { + protected function compute() { + return 1; + } + }; + show("one", $one); +} + +if (in_array("two", $what)) { + $two = new CacheFile("two", new DataCacheData(null, function () { + return 2; + }), [ + "duration" => $duration, + ]); + show("two", $two); +} + +if (in_array("three", $what)) { + $data31 = new DataCacheData("data31name", function () { + return 31; + }); + + $data32 = new DataCacheData(null, function () { + return 32; + }); + + $three = new CacheFile("three", [ + "data31" => $data31, + $data31, # name=data31name + "data32" => $data32, + $data32, # name="" + ]); + Txx("three.0=", $three->get("data31")); + Txx("three.1=", $three->get("data31name")); + Txx("three.2=", $three->get("data32")); + Txx("three.3=", $three->get("")); +} diff --git a/php/tests/cache/CursorChannelTest.php b/php/tests/cache/CursorChannelTest.php new file mode 100644 index 0000000..70e7343 --- /dev/null +++ b/php/tests/cache/CursorChannelTest.php @@ -0,0 +1,38 @@ + ["a" => "un", "b" => "deux"], + "eng" => ["a" => "one", "b" => "two"], + ["a" => 1, "b" => 2], + ]; + + function testUsage() { + $channel = CursorChannel::with("numbers", self::DATA, self::$storage); + $count = 0; + foreach ($channel as $key => $item) { + msg::info("one: $key => {$item["a"]}"); + $count++; + } + self::assertSame(3, $count); + } + + function testAddColumns() { + $channel = (new class("numbers") extends CursorChannel { + const NAME = "numbersac"; + const TABLE_NAME = self::NAME; + const ADD_COLUMNS = [ + "a" => "varchar(30)", + ]; + })->initStorage(self::$storage)->rechargeAll(self::DATA); + $count = 0; + foreach ($channel as $key => $item) { + msg::info("one: $key => {$item["a"]}"); + $count++; + } + self::assertSame(3, $count); + } +} diff --git a/php/tests/cache/SourceDb.php b/php/tests/cache/SourceDb.php new file mode 100644 index 0000000..31dc119 --- /dev/null +++ b/php/tests/cache/SourceDb.php @@ -0,0 +1,22 @@ +exec("insert into source (s, i, b) values (null, null, null)"); + $db->exec("insert into source (s, i, b) values ('false', 0, 0)"); + $db->exec("insert into source (s, i, b) values ('first', 1, 1)"); + $db->exec("insert into source (s, i, b) values ('second', 2, 1)"); + } + + public function __construct() { + parent::__construct(__DIR__."/source.db"); + } +} diff --git a/php/tests/cache/_TestCase.php b/php/tests/cache/_TestCase.php new file mode 100644 index 0000000..f05875c --- /dev/null +++ b/php/tests/cache/_TestCase.php @@ -0,0 +1,23 @@ +close(); + } +} diff --git a/php/tests/cache/cacheTest.php b/php/tests/cache/cacheTest.php new file mode 100644 index 0000000..3a16cb0 --- /dev/null +++ b/php/tests/cache/cacheTest.php @@ -0,0 +1,108 @@ + ["a" => "un", "b" => "deux"], + "eng" => ["a" => "one", "b" => "two"], + ["a" => 1, "b" => 2], + ]; + + function gendata() { + msg::note("gendata"); + foreach (self::DATA as $key => $item) { + msg::info("yield $key"); + yield $key => $item; + sleep(2); + } + msg::note("fin gendata"); + } + + function _testRows(iterable $rows, int $expectedCount) { + $count = 0; + foreach ($rows as $key => $row) { + $parts = ["got $key => {"]; + $i = 0; + foreach ($row as $k => $v) { + if ($i++ > 0) $parts[] = ", "; + $parts[] = "$k=$v"; + } + $parts[] = "}"; + msg::info(implode("", $parts)); + $count++; + } + self::assertSame($expectedCount, $count); + } + + function _testGet(string $dataId, int $expectedCount, callable $gencompute) { + msg::section($dataId); + cache::nc(true, true); + + msg::step("premier"); + $rows = cache::get($dataId, $gencompute()); + $this->_testRows($rows, $expectedCount); + msg::step("deuxième"); + $rows = cache::get($dataId, $gencompute()); + $this->_testRows($rows, $expectedCount); + + msg::step("vider le cache"); + cache::nc(true, true); + + msg::step("premier"); + $rows = cache::get($dataId, $gencompute()); + $this->_testRows($rows, $expectedCount); + msg::step("deuxième"); + $rows = cache::get($dataId, $gencompute()); + $this->_testRows($rows, $expectedCount); + } + + function testGetStatic() { + $this->_testGet("getStatic", 3, function () { + return static function () { + msg::note("getdata"); + return self::DATA; + }; + }); + } + + function testGetGenerator() { + $this->_testGet("getGenerator", 3, function () { + return $this->gendata(); + }); + } + + function _testAll(string $cursorId, int $expectedCount, callable $gencompute) { + msg::section($cursorId); + cache::nc(true, true); + + msg::step("premier"); + $rows = cache::all($cursorId, $gencompute()); + $this->_testRows($rows, $expectedCount); + msg::step("deuxième"); + $rows = cache::all($cursorId, $gencompute()); + $this->_testRows($rows, $expectedCount); + + msg::step("vider le cache"); + cache::nc(true, true); + + msg::step("premier"); + $rows = cache::all($cursorId, $gencompute()); + $this->_testRows($rows, $expectedCount); + msg::step("deuxième"); + $rows = cache::all($cursorId, $gencompute()); + $this->_testRows($rows, $expectedCount); + } + + function testAllGenerator() { + $this->_testAll("allGenerator", 4, function() { + return static function() { + $db = new SourceDb(); + msg::note("query source"); + yield from $db->all("select * from source"); + }; + }); + } +}