Skip to content

Instantly share code, notes, and snippets.

@olleharstedt
Created March 2, 2022 23:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save olleharstedt/e18004ad82e57e18047690596781a05a to your computer and use it in GitHub Desktop.
Save olleharstedt/e18004ad82e57e18047690596781a05a to your computer and use it in GitHub Desktop.
One universal dry-run mock-spy AST evaluator to rule them all
<?php
namespace Tmp6;
use InvalidArgumentException;
interface Node
{
}
class If_ implements Node
{
public $if;
public $then;
public function __construct(Node|Callable $if)
{
$this->if = $if;
}
public function setThen(Node $if)
{
$this->then = $if;
}
}
class FileExists implements Node
{
public $file;
public function __construct($file)
{
$this->file = $file;
}
}
function fileExists(string $file)
{
return new FileExists($file);
}
class FileGetContents implements Node
{
public $file;
public function __construct($file)
{
$this->file = $file;
}
}
function fileGetContents(string $file)
{
return new FileGetContents($file);
}
class Set implements Node
{
public $var;
public $val;
public function __construct(mixed &$var, mixed $val)
{
$this->var = &$var;
$this->val = $val;
}
}
function set(&$var, $val)
{
return new Set($var, $val);
}
interface EvaluatorInterface
{
public function evalNode(Node $node);
}
class NodeEvaluator implements EvaluatorInterface
{
/**
* @return mixed
*/
public function evalNode(Node $node)
{
$className = get_class_name($node::class);
switch ($className) {
case "If_":
if ($this->evalNode($node->if)) {
$this->evalNode($node->then);
} elseif (!empty($node->else)) {
$this->evalNode($node->else);
}
break;
case "FileExists":
return file_exists($node->file);
case "FileGetContents":
return file_get_contents($node->file);
case "Set":
if ($node->val instanceof Node) {
$node->var = $this->evalNode($node->val);
} elseif (gettype($node->val) === 'string') {
$node->var = $node->val;
} else {
throw new InvalidArgumentException('Unknown type of val in set: ' . gettype($node->val));
}
break;
default:
throw new InvalidArgumentException('Unsupported node type: ' . $className);
}
}
}
class DryRunEvaluator implements EvaluatorInterface
{
public $log = [];
public $returnValues = [];
public function __construct(array $returnValues)
{
$this->returnValues = $returnValues;
}
/**
* @return mixed
*/
public function evalNode($node)
{
$className = get_class_name($node::class);
switch ($className) {
case "If_":
$this->log[] = "Evaluating if";
if ($this->evalNode($node->if)) {
$this->log[] = "Evaluating then";
$this->evalNode($node->then);
} elseif (!empty($node->else)) {
$this->log[] = "Evaluating else";
$this->evalNode($node->else);
}
break;
case "FileExists":
$this->log[] = "File exists: arg1 = " . $node->file;
return array_pop($this->returnValues);
case "FileGetContents":
$this->log[] = "File get contents: arg1 = " . $node->file;
return array_pop($this->returnValues);
case "Set":
if ($node->val instanceof Node) {
$val = $this->evalNode($node->val);
$node->var = $val;
$this->log[] = "Set var to: " . $val;
} elseif (gettype($node->val) === 'string') {
$node->var = $node->val;
$this->log[] = "Set var to: " . $node->val;
} else {
throw new InvalidArgumentException('Unknown type of val in set: ' . gettype($node->val));
}
break;
default:
throw new InvalidArgumentException('Unsupported node type: ' . $className);
}
}
}
class St
{
public $queue = [];
public $ev;
public function __construct(EvaluatorInterface $ev)
{
$this->ev = $ev;
}
public function if($if)
{
$this->queue[] = new If_($if);
return $this;
}
public function then($if)
{
$i = \count($this->queue) - 1;
if ($this->queue[$i] instanceof If_) {
$this->queue[$i]->setThen($if);
} else {
throw new InvalidArgumentException('then must come after if');
}
return $this;
}
public function run($ev)
{
foreach ($this->queue as $node) {
$ev->evalNode($node);
}
}
public function set($var, $value)
{
return $this;
}
public function __invoke()
{
foreach ($this->queue as $node) {
$this->ev->evalNode($node);
}
}
}
function get_class_name($classname)
{
if ($pos = strrpos($classname, '\\'))
return substr($classname, $pos + 1);
return $pos;
}
/*
Use-case from: https://blog.ploeh.dk/2016/09/26/decoupling-decisions-from-effects/
public static string GetUpperText(string path)
{
if (!File.Exists(path)) return "DEFAULT";
var text = File.ReadAllText(path);
return text.ToUpperInvariant();
}
*/
// Using an expression builder
function getUpperText(string $file, St $st)
{
$result = 'DEFAULT';
$st
->if(fileExists($file))
->then(set($result, fileGetContents($file)))
();
return strtoupper($result);
}
// Using a mock
function getUpperTextMock(string $file, IO $io)
{
$result = 'DEFAULT';
if ($io->fileExists($file)) {
$result = $io->fileGetContents($file);
}
return strtoupper($result);
}
// Instead of mocking return types, set the return values
$returnValues = array_reverse(
[
true,
'Some example file content, bla bla bla'
]
);
$ev = new DryRunEvaluator($returnValues);
$st = new St($ev);
$text = getUpperText('moo.txt', $st);
// Output: string(38) "SOME EXAMPLE FILE CONTENT, BLA BLA BLA"
var_dump($text);
// Instead of a spy, you can inspect the dry-run log
var_dump($ev->log);
/* Output:
array(5) {
[0] =>
string(13) "Evaluating if"
[1] =>
string(27) "File exists: arg1 = moo.txt"
[2] =>
string(15) "Evaluating then"
[3] =>
string(33) "File get contents: arg1 = moo.txt"
[4] =>
string(50) "Set var to: Some example file content, bla bla bla"
}
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment