Add prototype
Signed-off-by: Jacob Kiers <jacob@jacobkiers.net>
This commit is contained in:
parent
bc53dd3b2e
commit
054634b2c4
105
content/posts/2015-01-24-multiple-return-values-with-mockery.md
Normal file
105
content/posts/2015-01-24-multiple-return-values-with-mockery.md
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
+++
|
||||||
|
title = Mockery: returning values and throwing exceptions
|
||||||
|
date = 2015-04-24
|
||||||
|
author = Jacob Kiers
|
||||||
|
+++
|
||||||
|
|
||||||
|
Last week, I had to write a piece of code that contains retry logic. Naturally, I want to test it. That proved trickier than expected.
|
||||||
|
|
||||||
|
The application code looks like this:
|
||||||
|
|
||||||
|
```php
|
||||||
|
class Sender
|
||||||
|
{
|
||||||
|
protected $connection;
|
||||||
|
|
||||||
|
public function send()
|
||||||
|
{
|
||||||
|
$success = false;
|
||||||
|
|
||||||
|
$i = 0;
|
||||||
|
do {
|
||||||
|
$i++;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$success = $this->doSend($i);
|
||||||
|
} catch (SenderException $e) {
|
||||||
|
$success = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} while (!$success && $i < 3);
|
||||||
|
|
||||||
|
return $success;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doSend($data)
|
||||||
|
{
|
||||||
|
// Can throw SenderException
|
||||||
|
$response = $this->connection->send($data);
|
||||||
|
|
||||||
|
if ('OK' === $response) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
I specifically want to test the retry logic, so I have to mock the ::doSend() method. Then I can simulate the different outcomes (returning true or false, or throwing a SenderException).
|
||||||
|
|
||||||
|
I use [Mockery] to do the real work. It is a great library. If you don't know it yet, please check it out. I will wait right here...
|
||||||
|
|
||||||
|
Now, since ::doSend() is a protected method, Mockery must be instructed to allow that.
|
||||||
|
|
||||||
|
So after the first try, I ended up with:
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function testItWillRetrySending()
|
||||||
|
{
|
||||||
|
$sender = M::mock('Sender');
|
||||||
|
$sender->shouldAllowMockingProtectedMethods()
|
||||||
|
|
||||||
|
$sender->shouldReceive('doSend')
|
||||||
|
->andReturn(false, new Exception());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To my surprise, this did not work. Instead of throwing the exception, Mockery returns it. So my next try was this:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$sender->shouldReceive('doSend')
|
||||||
|
->andReturn(false)
|
||||||
|
->andThrow(new Exception());
|
||||||
|
```
|
||||||
|
|
||||||
|
Another surprise: with this code, Mockery will always throw the exception, and ignore the first return value (false). After some debugging, I found out that Mockery just overwrites the return values in this case.
|
||||||
|
|
||||||
|
Fortunately, there is another way to return multiple return values: the ::andReturnUsing() method. It gives full control over the return values.
|
||||||
|
|
||||||
|
So I ended up with this testing code:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$return_value_generator = function () {
|
||||||
|
static $counter = 0;
|
||||||
|
|
||||||
|
$counter++;
|
||||||
|
|
||||||
|
switch ($counter) {
|
||||||
|
case 1: return false;
|
||||||
|
case 2: throw new SenderException();
|
||||||
|
case 3: return true;
|
||||||
|
default: throw new Exception("Should never reach this.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$sender = M::mock('Sender');
|
||||||
|
|
||||||
|
$sender->shouldAllowMockingProtectedMethods()
|
||||||
|
->shouldReceive('doSend')
|
||||||
|
->andReturnUsing($return_value_generator);
|
||||||
|
```
|
||||||
|
|
||||||
|
This works perfectly. It feels a bit like a hack though. So if you know a better way or have any other remarks, please let me know.
|
||||||
|
|
||||||
|
[Mockery]: https://github.com/mockery/mockery
|
140
index.html
Normal file
140
index.html
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
<!DOCTYPE html charset="UTF-8">
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.2.0/build/styles/default.min.css">
|
||||||
|
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.2.0/build/highlight.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
|
<script>
|
||||||
|
class Content {
|
||||||
|
content = "";
|
||||||
|
type = "";
|
||||||
|
metaData = {}
|
||||||
|
|
||||||
|
constructor(content, type, metaData = {})
|
||||||
|
{
|
||||||
|
this.content = content;
|
||||||
|
this.type = type;
|
||||||
|
this.metaData = metaData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Index
|
||||||
|
{
|
||||||
|
mimeType = "";
|
||||||
|
chunks = 0;
|
||||||
|
hash = "";
|
||||||
|
metaData = {};
|
||||||
|
|
||||||
|
constructor(mimeType, chunks, hash, metaData = {})
|
||||||
|
{
|
||||||
|
this.mimeType = mimeType;
|
||||||
|
this.chunks = chunks;
|
||||||
|
this.hash = hash;
|
||||||
|
this.metaData = metaData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dohServer = "https://cloudflare-dns.com/dns-query?ct=application/dns-json&type=TXT&name=";
|
||||||
|
const baseDomain = "hod.experiments.jacobkiers.net";
|
||||||
|
|
||||||
|
async function readUrl(domain) {
|
||||||
|
var index = await fetchIndex(`${dohServer}${domain}.${baseDomain}`);
|
||||||
|
|
||||||
|
var chunk_promises = [];
|
||||||
|
for(i = 0; i < index.chunks; i++)
|
||||||
|
{
|
||||||
|
chunk_promises[i] = fetchChunk(i, index.hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
var chunks = await Promise.all(chunk_promises);
|
||||||
|
var content = chunks.reduce((built, current) => built += current);
|
||||||
|
|
||||||
|
return handleContent(new Content(atob(content), index.mimeType, index.metaData));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchChunk(id, hash)
|
||||||
|
{
|
||||||
|
var domain = `${id}.${hash}.${baseDomain}`;
|
||||||
|
const json = await fetch(`${dohServer}${domain}`)
|
||||||
|
.then(response => response.json());
|
||||||
|
|
||||||
|
const data = json.Answer[0].data.slice(1, -1);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchIndex(domain)
|
||||||
|
{
|
||||||
|
const response = await fetch(`${dohServer}.${domain}`);
|
||||||
|
const json = await response.json();
|
||||||
|
const index = json.Answer[0].data.slice(1, -1);
|
||||||
|
|
||||||
|
let ret = {};
|
||||||
|
let items = index.split(';');
|
||||||
|
items.forEach(item => {
|
||||||
|
let md = item.split('=');
|
||||||
|
let key = md[0];
|
||||||
|
let value = md[1];
|
||||||
|
|
||||||
|
ret[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const metadata = JSON.parse(atob(ret["m"]));
|
||||||
|
return new Index(ret["t"], ret["c"], ret["h"], metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleContent(content)
|
||||||
|
{
|
||||||
|
if (!content instanceof Content) {
|
||||||
|
console.log("Not valid content in handleContent.")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(content.type)
|
||||||
|
{
|
||||||
|
case "text/javascript":
|
||||||
|
return handleJavascript(content);
|
||||||
|
|
||||||
|
case "text/markdown":
|
||||||
|
return handleMarkdown(content);
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log(`handleContent() does not know how to parse ${content.type}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleJavascript(content)
|
||||||
|
{
|
||||||
|
console.log("Got some javascript!", content.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleMarkdown(content)
|
||||||
|
{
|
||||||
|
console.log("Got me some markdown!");
|
||||||
|
marked.setOptions({
|
||||||
|
highlight: function(code, lang) {
|
||||||
|
return hljs.highlight(lang, code).value;
|
||||||
|
},
|
||||||
|
// langPrefix: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
if (content.metaData.title != undefined) document.title = content.metaData.title;
|
||||||
|
document.getElementById("post").innerHTML = marked(content.content);
|
||||||
|
let title = document.createElement("h1");
|
||||||
|
title.innerHTML = content.metaData.title;
|
||||||
|
document.getElementById("post").prepend(title)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
readUrl("posts-2015-01-24-multiple-return-values-with-mockery-md");
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<div id="post"></div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
172
publish-zones.php
Normal file
172
publish-zones.php
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
class Content
|
||||||
|
{
|
||||||
|
public string $data;
|
||||||
|
public string $mimeType;
|
||||||
|
public string $fileName;
|
||||||
|
public string $hash;
|
||||||
|
private array $metadata = [];
|
||||||
|
|
||||||
|
const TTL = 60;
|
||||||
|
|
||||||
|
public function __construct(string $file)
|
||||||
|
{
|
||||||
|
$this->fileName = basename(dirname($file)).'/'.basename($file);
|
||||||
|
$this->data = file_get_contents($file);
|
||||||
|
$this->hash = md5($this->data);
|
||||||
|
|
||||||
|
$this->parseMetadata();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toRecords(): string
|
||||||
|
{
|
||||||
|
$chunks = str_split(base64_encode($this->data), 250);
|
||||||
|
|
||||||
|
$records = [];
|
||||||
|
$records[] = "; {$this->getDnsName()}";
|
||||||
|
$records[] = $this->getDnsName().$this->buildMiddle().'"'.$this->buildIndexString(count($chunks)).'"';
|
||||||
|
|
||||||
|
foreach($chunks as $id => $chunk)
|
||||||
|
{
|
||||||
|
$records[] = $this->buildRecord($id, $chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = "";
|
||||||
|
foreach ($records as $record)
|
||||||
|
{
|
||||||
|
$result .= $record . PHP_EOL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getDnsName(): string
|
||||||
|
{
|
||||||
|
return str_replace(['/', '\\', '.'], '-', $this->fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildIndexString(int $chunkCount): string
|
||||||
|
{
|
||||||
|
$metadata = base64_encode(json_encode($this->metadata));
|
||||||
|
return "t={$this->mimeType()};c={$chunkCount};h={$this->hash};m={$metadata}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildMiddle(): string
|
||||||
|
{
|
||||||
|
return "\t".self::TTL."\tIN\tTXT\t";
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildRecord(int $id, string $data): string
|
||||||
|
{
|
||||||
|
$name = "$id.{$this->hash}";
|
||||||
|
|
||||||
|
return "$name".$this->buildMiddle().'"'.$data.'"';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mimeType()
|
||||||
|
{
|
||||||
|
$extension = pathinfo($this->fileName, PATHINFO_EXTENSION);
|
||||||
|
|
||||||
|
switch($extension)
|
||||||
|
{
|
||||||
|
case "js":
|
||||||
|
return "text/javascript";
|
||||||
|
case "md":
|
||||||
|
return "text/markdown";
|
||||||
|
|
||||||
|
default:
|
||||||
|
return mime_content_type($this->file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseMetadata()
|
||||||
|
{
|
||||||
|
if ($this->mimeType() != "text/markdown") return [];
|
||||||
|
|
||||||
|
$frontmatter_chars = "+++";
|
||||||
|
|
||||||
|
$end_of_frontmatter = strpos($this->data, $frontmatter_chars, 2);
|
||||||
|
if (false === $end_of_frontmatter) {
|
||||||
|
var_dump($end_of_frontmatter);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$frontmatter = substr(
|
||||||
|
$this->data,
|
||||||
|
strlen($frontmatter_chars) + 1,
|
||||||
|
$end_of_frontmatter - strlen($frontmatter_chars) - 2
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->data = trim(substr($this->data, $end_of_frontmatter + strlen($frontmatter_chars))).PHP_EOL;
|
||||||
|
|
||||||
|
$lines = explode(PHP_EOL, $frontmatter);
|
||||||
|
foreach($lines as $line) {
|
||||||
|
$eq_pos = strpos($line, "=");
|
||||||
|
$key = trim(substr($line, 0, $eq_pos - 1));
|
||||||
|
$value = trim(substr($line, $eq_pos + 1));
|
||||||
|
$this->metadata[$key] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function update_serial_line(string $line)
|
||||||
|
{
|
||||||
|
$matches = [];
|
||||||
|
preg_match("#(\s+)(\d{10})(.*)#", $line, $matches);
|
||||||
|
list($_, $start, $serial, $last) = $matches;
|
||||||
|
|
||||||
|
$today = date("Ymd");
|
||||||
|
$day = substr($serial, 0, 8);
|
||||||
|
$nr = (int) substr($serial, -2);
|
||||||
|
|
||||||
|
if ($day === $today) {
|
||||||
|
$nr++;
|
||||||
|
} else {
|
||||||
|
$day = $today;
|
||||||
|
$nr = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$new_serial = sprintf('%1$s%2$02d', $day, $nr);
|
||||||
|
|
||||||
|
return $start.$new_serial.$last;
|
||||||
|
}
|
||||||
|
|
||||||
|
$zone_file = "zones/db.hod.experiments.jacobkiers.net";
|
||||||
|
if (!file_exists($zone_file)) {
|
||||||
|
fwrite(STDERR, "The zone file {$zone_file} does not exist!");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = file($zone_file);
|
||||||
|
|
||||||
|
foreach($lines as $index => &$line)
|
||||||
|
{
|
||||||
|
if (str_contains($line, "; serial")) {
|
||||||
|
$matches = [];
|
||||||
|
preg_match("#(\s+)(\d{10})(.*)#", $line, $matches);
|
||||||
|
list($_, $start, $serial, $last) = $matches;
|
||||||
|
$line = update_serial_line($line).PHP_EOL;
|
||||||
|
}
|
||||||
|
if (str_starts_with($line, ";; START BLOG RECORDS")) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$zone_file_contents = implode("", array_slice($lines, 0, $index+1));
|
||||||
|
|
||||||
|
echo $zone_file_contents;
|
||||||
|
|
||||||
|
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(__DIR__."/content"));
|
||||||
|
foreach ($it as $file)
|
||||||
|
{
|
||||||
|
if ($file->isDir()) continue;
|
||||||
|
if (str_contains($file->getPathname(), "ignore")) continue;
|
||||||
|
|
||||||
|
$bootstrap = new Content($file->getPathname());
|
||||||
|
$zone_file_contents .= PHP_EOL.$bootstrap->toRecords().PHP_EOL;
|
||||||
|
}
|
||||||
|
|
||||||
|
$zone_file_contents .= PHP_EOL;
|
||||||
|
|
||||||
|
#echo $zone_file_contents;
|
||||||
|
|
||||||
|
file_put_contents($zone_file, $zone_file_contents);
|
19
zones/db.hod.experiments.jacobkiers.net
Normal file
19
zones/db.hod.experiments.jacobkiers.net
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
;
|
||||||
|
$TTL 5m ; Default TTL
|
||||||
|
@ IN SOA home.kie.rs. postmaster.kie.rs. (
|
||||||
|
2021081611 ; serial
|
||||||
|
1h ; slave refresh interval
|
||||||
|
15m ; slave retry interval
|
||||||
|
1w ; slave copy expire time
|
||||||
|
1h ; NXDOMAIN cache time
|
||||||
|
)
|
||||||
|
|
||||||
|
$ORIGIN hod.experiments.jacobkiers.net.
|
||||||
|
|
||||||
|
;
|
||||||
|
; domain name servers
|
||||||
|
;
|
||||||
|
@ IN NS home.kie.rs.
|
||||||
|
|
||||||
|
|
||||||
|
;; START BLOG RECORDS
|
Loading…
Reference in New Issue
Block a user