summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.html261
-rw-r--r--README.pdfbin0 -> 310354 bytes
-rw-r--r--test.py1513
3 files changed, 1774 insertions, 0 deletions
diff --git a/README.html b/README.html
new file mode 100644
index 0000000..b5511b2
--- /dev/null
+++ b/README.html
@@ -0,0 +1,261 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">
+<head>
+ <meta charset="utf-8" />
+ <meta name="generator" content="pandoc" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
+ <meta name="author" content="Lars Wirzenius" />
+ <title>OSO work sample—MAX</title>
+ <style type="text/css">
+ code{white-space: pre-wrap;}
+ span.smallcaps{font-variant: small-caps;}
+ span.underline{text-decoration: underline;}
+ div.column{display: inline-block; vertical-align: top; width: 50%;}
+ </style>
+ <style type="text/css">
+a.sourceLine { display: inline-block; line-height: 1.25; }
+a.sourceLine { pointer-events: none; color: inherit; text-decoration: inherit; }
+a.sourceLine:empty { height: 1.2em; }
+.sourceCode { overflow: visible; }
+code.sourceCode { white-space: pre; position: relative; }
+div.sourceCode { margin: 1em 0; }
+pre.sourceCode { margin: 0; }
+@media screen {
+div.sourceCode { overflow: auto; }
+}
+@media print {
+code.sourceCode { white-space: pre-wrap; }
+a.sourceLine { text-indent: -1em; padding-left: 1em; }
+}
+pre.numberSource a.sourceLine
+ { position: relative; left: -4em; }
+pre.numberSource a.sourceLine::before
+ { content: attr(title);
+ position: relative; left: -1em; text-align: right; vertical-align: baseline;
+ border: none; pointer-events: all; display: inline-block;
+ -webkit-touch-callout: none; -webkit-user-select: none;
+ -khtml-user-select: none; -moz-user-select: none;
+ -ms-user-select: none; user-select: none;
+ padding: 0 4px; width: 4em;
+ color: #aaaaaa;
+ }
+pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa; padding-left: 4px; }
+div.sourceCode
+ { }
+@media screen {
+a.sourceLine::before { text-decoration: underline; }
+}
+code span.al { color: #ff0000; font-weight: bold; } /* Alert */
+code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
+code span.at { color: #7d9029; } /* Attribute */
+code span.bn { color: #40a070; } /* BaseN */
+code span.bu { } /* BuiltIn */
+code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
+code span.ch { color: #4070a0; } /* Char */
+code span.cn { color: #880000; } /* Constant */
+code span.co { color: #60a0b0; font-style: italic; } /* Comment */
+code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
+code span.do { color: #ba2121; font-style: italic; } /* Documentation */
+code span.dt { color: #902000; } /* DataType */
+code span.dv { color: #40a070; } /* DecVal */
+code span.er { color: #ff0000; font-weight: bold; } /* Error */
+code span.ex { } /* Extension */
+code span.fl { color: #40a070; } /* Float */
+code span.fu { color: #06287e; } /* Function */
+code span.im { } /* Import */
+code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
+code span.kw { color: #007020; font-weight: bold; } /* Keyword */
+code span.op { color: #666666; } /* Operator */
+code span.ot { color: #007020; } /* Other */
+code span.pp { color: #bc7a00; } /* Preprocessor */
+code span.sc { color: #4070a0; } /* SpecialChar */
+code span.ss { color: #bb6688; } /* SpecialString */
+code span.st { color: #4070a0; } /* String */
+code span.va { color: #19177c; } /* Variable */
+code span.vs { color: #4070a0; } /* VerbatimString */
+code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
+ </style>
+</head>
+<body>
+<header>
+<h1 class="title">OSO work sample—MAX</h1>
+<p class="author">Lars Wirzenius</p>
+<p class="date">2021-05-29 22:26</p>
+</header>
+<nav id="TOC">
+<ul>
+<li><a href="#work-sample-for-oso"><span class="toc-section-number">1</span> Work sample for OSO</a><ul>
+<li><a href="#re-statement-of-problem"><span class="toc-section-number">1.1</span> Re-statement of problem</a></li>
+<li><a href="#client-request-start-computation"><span class="toc-section-number">1.2</span> Client request: start computation</a></li>
+<li><a href="#server-response-request-computation"><span class="toc-section-number">1.3</span> Server response: request computation</a></li>
+<li><a href="#client-request-computation-result"><span class="toc-section-number">1.4</span> Client request: computation result</a></li>
+<li><a href="#server-response-result"><span class="toc-section-number">1.5</span> Server response: result</a></li>
+</ul></li>
+<li><a href="#example"><span class="toc-section-number">2</span> Example</a></li>
+<li><a href="#assumptions"><span class="toc-section-number">3</span> Assumptions</a></li>
+<li><a href="#remarks"><span class="toc-section-number">4</span> Remarks</a></li>
+<li><a href="#acceptnace"><span class="toc-section-number">5</span> Acceptance criteria</a><ul>
+<li><a href="#find-max-of-a-list-of-one"><span class="toc-section-number">5.1</span> Find max of a list of one</a></li>
+<li><a href="#find-max-of-a-list-of-two"><span class="toc-section-number">5.2</span> Find max of a list of two</a></li>
+<li><a href="#find-max-of-a-list-of-three"><span class="toc-section-number">5.3</span> Find max of a list of three</a></li>
+</ul></li>
+<li><a href="#what-i-did"><span class="toc-section-number">6</span> What I did</a><ul>
+<li><a href="#the-client"><span class="toc-section-number">6.1</span> The client</a></li>
+<li><a href="#the-server"><span class="toc-section-number">6.2</span> The server</a></li>
+<li><a href="#the-testing"><span class="toc-section-number">6.3</span> The testing</a></li>
+</ul></li>
+</ul>
+</nav>
+<h1 id="work-sample-for-oso"><span class="header-section-number">1</span> Work sample for OSO</h1>
+<p>This is the work sample for my job application for a developer position for OSO.</p>
+<p>This document explains the work I’ve done and verifies that the code I wrote works together with the client.</p>
+<h2 id="re-statement-of-problem"><span class="header-section-number">1.1</span> Re-statement of problem</h2>
+<p>To clarify the problem for myself, I am re-stating it. This will also work to make sure I’ve understood it in the intended way when we discuss this.</p>
+<p>The goal is to write a server, which communicates with a client using messages over HTTP. The client has a list of integers, and the asks the server to figure out what is the largest integer in the list. The crux is that the client does not send the server the whole list, but only small messages and the server needs to, effectively, ask the client to do pairwise comparisons of integers.</p>
+<p>The possible messages sent by the client and the server are listed below, as examples. Communication is started by client by a “start computation” message. Server responds with a suitable message, which causes the client to make a new HTTP request with the response to the server’s message. This continues until the server sends a message with result it has come up with.</p>
+<h2 id="client-request-start-computation"><span class="header-section-number">1.2</span> Client request: start computation</h2>
+<p>Client starts the protocol by asking the server to compute something about a list of numbers the client holds. It tells the server what computation is requested, and how long the list is.</p>
+<p>The supported computations are, for now, <code>compute_max</code> (find index of the largest integer) and <code>compute_min</code> (similar, but smallest integer).</p>
+<div class="sourceCode" id="cb1"><pre class="sourceCode json"><code class="sourceCode json"><a class="sourceLine" id="cb1-1" title="1"><span class="fu">{</span></a>
+<a class="sourceLine" id="cb1-2" title="2"> <span class="dt">&quot;type&quot;</span><span class="fu">:</span> <span class="st">&quot;compute_max&quot;</span><span class="fu">,</span></a>
+<a class="sourceLine" id="cb1-3" title="3"> <span class="dt">&quot;length&quot;</span><span class="fu">:</span> <span class="dv">2</span></a>
+<a class="sourceLine" id="cb1-4" title="4"><span class="fu">}</span></a></code></pre></div>
+<h2 id="server-response-request-computation"><span class="header-section-number">1.3</span> Server response: request computation</h2>
+<p>Server ask the client to report the result of a less-than comparison operation between arbitrary list items.</p>
+<div class="sourceCode" id="cb2"><pre class="sourceCode json"><code class="sourceCode json"><a class="sourceLine" id="cb2-1" title="1"><span class="fu">{</span></a>
+<a class="sourceLine" id="cb2-2" title="2"> <span class="dt">&quot;type&quot;</span><span class="fu">:</span> <span class="st">&quot;compare&quot;</span><span class="fu">,</span></a>
+<a class="sourceLine" id="cb2-3" title="3"> <span class="dt">&quot;left&quot;</span><span class="fu">:</span> <span class="dv">0</span><span class="fu">,</span></a>
+<a class="sourceLine" id="cb2-4" title="4"> <span class="dt">&quot;right&quot;</span><span class="fu">:</span> <span class="dv">1</span><span class="fu">,</span></a>
+<a class="sourceLine" id="cb2-5" title="5"> <span class="dt">&quot;request_id&quot;</span><span class="fu">:</span> <span class="dv">7</span></a>
+<a class="sourceLine" id="cb2-6" title="6"><span class="fu">}</span></a></code></pre></div>
+<h2 id="client-request-computation-result"><span class="header-section-number">1.4</span> Client request: computation result</h2>
+<p>The client reports the result of a comparison. The server should respond by another comparison request, or the result of the computation.</p>
+<div class="sourceCode" id="cb3"><pre class="sourceCode json"><code class="sourceCode json"><a class="sourceLine" id="cb3-1" title="1"><span class="fu">{</span></a>
+<a class="sourceLine" id="cb3-2" title="2"> <span class="dt">&quot;type&quot;</span><span class="fu">:</span> <span class="st">&quot;comp_result&quot;</span><span class="fu">,</span></a>
+<a class="sourceLine" id="cb3-3" title="3"> <span class="dt">&quot;answer&quot;</span><span class="fu">:</span> <span class="kw">true</span><span class="fu">,</span></a>
+<a class="sourceLine" id="cb3-4" title="4"> <span class="dt">&quot;request_id&quot;</span><span class="fu">:</span> <span class="dv">7</span></a>
+<a class="sourceLine" id="cb3-5" title="5"><span class="fu">}</span></a></code></pre></div>
+<h2 id="server-response-result"><span class="header-section-number">1.5</span> Server response: result</h2>
+<div class="sourceCode" id="cb4"><pre class="sourceCode json"><code class="sourceCode json"><a class="sourceLine" id="cb4-1" title="1"><span class="fu">{</span></a>
+<a class="sourceLine" id="cb4-2" title="2"> <span class="dt">&quot;type&quot;</span><span class="fu">:</span> <span class="st">&quot;done&quot;</span><span class="fu">,</span></a>
+<a class="sourceLine" id="cb4-3" title="3"> <span class="dt">&quot;result&quot;</span><span class="fu">:</span> <span class="dv">2</span></a>
+<a class="sourceLine" id="cb4-4" title="4"><span class="fu">}</span></a></code></pre></div>
+<h1 id="example"><span class="header-section-number">2</span> Example</h1>
+<p>In this example, assume the client has the following list:</p>
+<blockquote>
+<p>5, 6, 7, 5</p>
+</blockquote>
+<p>Note that the list is not in order and numbers aren’t unique. The sequence diagram below shows the messages the go between the client and the server.</p>
+<p>For simplicity, request ids are not shown.</p>
+<p><img src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBjb250ZW50U2NyaXB0VHlwZT0iYXBwbGljYXRpb24vZWNtYXNjcmlwdCIgY29udGVudFN0eWxlVHlwZT0idGV4dC9jc3MiIGhlaWdodD0iNTkzcHgiIHByZXNlcnZlQXNwZWN0UmF0aW89Im5vbmUiIHN0eWxlPSJ3aWR0aDo2MzBweDtoZWlnaHQ6NTkzcHg7IiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCA2MzAgNTkzIiB3aWR0aD0iNjMwcHgiIHpvb21BbmRQYW49Im1hZ25pZnkiPjxkZWZzPjxmaWx0ZXIgaGVpZ2h0PSIzMDAlIiBpZD0iZml6ZmF5ZXk4c2EyaSIgd2lkdGg9IjMwMCUiIHg9Ii0xIiB5PSItMSI+PGZlR2F1c3NpYW5CbHVyIHJlc3VsdD0iYmx1ck91dCIgc3RkRGV2aWF0aW9uPSIyLjAiLz48ZmVDb2xvck1hdHJpeCBpbj0iYmx1ck91dCIgcmVzdWx0PSJibHVyT3V0MiIgdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIC40IDAiLz48ZmVPZmZzZXQgZHg9IjQuMCIgZHk9IjQuMCIgaW49ImJsdXJPdXQyIiByZXN1bHQ9ImJsdXJPdXQzIi8+PGZlQmxlbmQgaW49IlNvdXJjZUdyYXBoaWMiIGluMj0iYmx1ck91dDMiIG1vZGU9Im5vcm1hbCIvPjwvZmlsdGVyPjwvZGVmcz48Zz48bGluZSBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDEuMDsgc3Ryb2tlLWRhc2hhcnJheTogNS4wLDUuMDsiIHgxPSIyMDQiIHgyPSIyMDQiIHkxPSIzOC4yOTY5IiB5Mj0iNTUyLjk1MzEiLz48bGluZSBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDEuMDsgc3Ryb2tlLWRhc2hhcnJheTogNS4wLDUuMDsiIHgxPSI0MDEiIHgyPSI0MDEiIHkxPSIzOC4yOTY5IiB5Mj0iNTUyLjk1MzEiLz48cmVjdCBmaWxsPSIjRkVGRUNFIiBmaWx0ZXI9InVybCgjZml6ZmF5ZXk4c2EyaSkiIGhlaWdodD0iMzAuMjk2OSIgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjU7IiB3aWR0aD0iNTEiIHg9IjE3NyIgeT0iMyIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmdBbmRHbHlwaHMiIHRleHRMZW5ndGg9IjM3IiB4PSIxODQiIHk9IjIyLjk5NTEiPmNsaWVudDwvdGV4dD48cmVjdCBmaWxsPSIjRkVGRUNFIiBmaWx0ZXI9InVybCgjZml6ZmF5ZXk4c2EyaSkiIGhlaWdodD0iMzAuMjk2OSIgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjU7IiB3aWR0aD0iNTEiIHg9IjE3NyIgeT0iNTUxLjk1MzEiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nQW5kR2x5cGhzIiB0ZXh0TGVuZ3RoPSIzNyIgeD0iMTg0IiB5PSI1NzEuOTQ4MiI+Y2xpZW50PC90ZXh0PjxyZWN0IGZpbGw9IiNGRUZFQ0UiIGZpbHRlcj0idXJsKCNmaXpmYXlleThzYTJpKSIgaGVpZ2h0PSIzMC4yOTY5IiBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDEuNTsiIHdpZHRoPSI1NyIgeD0iMzcxIiB5PSIzIi8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTQiIGxlbmd0aEFkanVzdD0ic3BhY2luZ0FuZEdseXBocyIgdGV4dExlbmd0aD0iNDMiIHg9IjM3OCIgeT0iMjIuOTk1MSI+c2VydmVyPC90ZXh0PjxyZWN0IGZpbGw9IiNGRUZFQ0UiIGZpbHRlcj0idXJsKCNmaXpmYXlleThzYTJpKSIgaGVpZ2h0PSIzMC4yOTY5IiBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDEuNTsiIHdpZHRoPSI1NyIgeD0iMzcxIiB5PSI1NTEuOTUzMSIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmdBbmRHbHlwaHMiIHRleHRMZW5ndGg9IjQzIiB4PSIzNzgiIHk9IjU3MS45NDgyIj5zZXJ2ZXI8L3RleHQ+PHBvbHlnb24gZmlsbD0iI0E4MDAzNiIgcG9pbnRzPSIzODkuNSw5My4xMjg5LDM5OS41LDk3LjEyODksMzg5LjUsMTAxLjEyODksMzkzLjUsOTcuMTI4OSIgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7Ii8+PGxpbmUgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7IiB4MT0iMjA0LjUiIHgyPSIzOTUuNSIgeTE9Ijk3LjEyODkiIHkyPSI5Ny4xMjg5Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTMiIGxlbmd0aEFkanVzdD0ic3BhY2luZ0FuZEdseXBocyIgdGV4dExlbmd0aD0iMTU5IiB4PSIyMTEuNSIgeT0iOTIuMDYzIj5jb21wdXRlX21heCwgbGVuZ3RoPTQ8L3RleHQ+PHBhdGggZD0iTTgsNTMuMjk2OSBMOCwxMjMuMjk2OSBMMTk2LDEyMy4yOTY5IEwxOTYsNjMuMjk2OSBMMTg2LDUzLjI5NjkgTDgsNTMuMjk2OSAiIGZpbGw9IiNGQkZCNzciIGZpbHRlcj0idXJsKCNmaXpmYXlleThzYTJpKSIgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7Ii8+PHBhdGggZD0iTTE4Niw1My4yOTY5IEwxODYsNjMuMjk2OSBMMTk2LDYzLjI5NjkgTDE4Niw1My4yOTY5ICIgZmlsbD0iI0ZCRkI3NyIgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTMiIGxlbmd0aEFkanVzdD0ic3BhY2luZ0FuZEdseXBocyIgdGV4dExlbmd0aD0iMTQ2IiB4PSIxNCIgeT0iNzAuMzYzOCI+Y2xpZW50IHRlbGxzIHNlcnZlciB0aGVyZTwvdGV4dD48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMyIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nQW5kR2x5cGhzIiB0ZXh0TGVuZ3RoPSIxNjciIHg9IjE0IiB5PSI4NS40OTY2Ij5hcmUgNCBpbnRlZ2VzIGFuZCB0aGF0IHRoZTwvdGV4dD48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMyIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nQW5kR2x5cGhzIiB0ZXh0TGVuZ3RoPSIxNTEiIHg9IjE0IiB5PSIxMDAuNjI5NCI+c2VydmVyIHNob3VsZCBmaWd1cmUgb3V0PC90ZXh0Pjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjEzIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmdBbmRHbHlwaHMiIHRleHRMZW5ndGg9Ijk3IiB4PSIxNCIgeT0iMTE1Ljc2MjIiPnRoZSBsYXJnZXN0IG9uZTwvdGV4dD48cG9seWdvbiBmaWxsPSIjQTgwMDM2IiBwb2ludHM9IjIxNS41LDE3MC4wOTM4LDIwNS41LDE3NC4wOTM4LDIxNS41LDE3OC4wOTM4LDIxMS41LDE3NC4wOTM4IiBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDEuMDsiLz48bGluZSBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDEuMDsiIHgxPSIyMDkuNSIgeDI9IjQwMC41IiB5MT0iMTc0LjA5MzgiIHkyPSIxNzQuMDkzOCIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjEzIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmdBbmRHbHlwaHMiIHRleHRMZW5ndGg9Ijg3IiB4PSIyMjEuNSIgeT0iMTY5LjAyNzgiPmNvbXBhcmUoMCwxKTwvdGV4dD48cGF0aCBkPSJNNDA2LDEzNy44MjgxIEw0MDYsMTkyLjgyODEgTDU2MywxOTIuODI4MSBMNTYzLDE0Ny44MjgxIEw1NTMsMTM3LjgyODEgTDQwNiwxMzcuODI4MSAiIGZpbGw9IiNGQkZCNzciIGZpbHRlcj0idXJsKCNmaXpmYXlleThzYTJpKSIgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7Ii8+PHBhdGggZD0iTTU1MywxMzcuODI4MSBMNTUzLDE0Ny44MjgxIEw1NjMsMTQ3LjgyODEgTDU1MywxMzcuODI4MSAiIGZpbGw9IiNGQkZCNzciIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMS4wOyIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjEzIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmdBbmRHbHlwaHMiIHRleHRMZW5ndGg9IjEzNiIgeD0iNDEyIiB5PSIxNTQuODk1Ij5zZXJ2ZXIgd2FudHMgY2xpZW50IHRvPC90ZXh0Pjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjEzIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmdBbmRHbHlwaHMiIHRleHRMZW5ndGg9IjEzMCIgeD0iNDEyIiB5PSIxNzAuMDI3OCI+Y29tcGFyZSBpbnRlZ2VycyBhdDwvdGV4dD48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMyIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nQW5kR2x5cGhzIiB0ZXh0TGVuZ3RoPSIxMDEiIHg9IjQxMiIgeT0iMTg1LjE2MDYiPmluZGV4ZXMgMCBhbmQgMTwvdGV4dD48cG9seWdvbiBmaWxsPSIjQTgwMDM2IiBwb2ludHM9IjM4OS41LDI0Ny4wNTg2LDM5OS41LDI1MS4wNTg2LDM4OS41LDI1NS4wNTg2LDM5My41LDI1MS4wNTg2IiBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDEuMDsiLz48bGluZSBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDEuMDsiIHgxPSIyMDQuNSIgeDI9IjM5NS41IiB5MT0iMjUxLjA1ODYiIHkyPSIyNTEuMDU4NiIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjEzIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmdBbmRHbHlwaHMiIHRleHRMZW5ndGg9IjE2OSIgeD0iMjExLjUiIHk9IjI0NS45OTI3Ij5jb21wX3Jlc3VsdCwgYW5zd2VyPXRydWU8L3RleHQ+PHBhdGggZD0iTTM3LDIwNy4yMjY2IEwzNywyNzcuMjI2NiBMMTk1LDI3Ny4yMjY2IEwxOTUsMjE3LjIyNjYgTDE4NSwyMDcuMjI2NiBMMzcsMjA3LjIyNjYgIiBmaWxsPSIjRkJGQjc3IiBmaWx0ZXI9InVybCgjZml6ZmF5ZXk4c2EyaSkiIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMS4wOyIvPjxwYXRoIGQ9Ik0xODUsMjA3LjIyNjYgTDE4NSwyMTcuMjI2NiBMMTk1LDIxNy4yMjY2IEwxODUsMjA3LjIyNjYgIiBmaWxsPSIjRkJGQjc3IiBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDEuMDsiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMyIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nQW5kR2x5cGhzIiB0ZXh0TGVuZ3RoPSIxMTQiIHg9IjQzIiB5PSIyMjQuMjkzNSI+Y2xpZW50IHJlcG9ydHMgdGhhdDwvdGV4dD48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMyIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nQW5kR2x5cGhzIiB0ZXh0TGVuZ3RoPSIxMjUiIHg9IjQzIiB5PSIyMzkuNDI2MyI+dGhlIGludGVnZXIgYXQgaW5kZXg8L3RleHQ+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTMiIGxlbmd0aEFkanVzdD0ic3BhY2luZ0FuZEdseXBocyIgdGV4dExlbmd0aD0iMTM3IiB4PSI0MyIgeT0iMjU0LjU1OTEiPjAgaXMgbGVzcyB0aGFuIHRoZSBvbmU8L3RleHQ+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTMiIGxlbmd0aEFkanVzdD0ic3BhY2luZ0FuZEdseXBocyIgdGV4dExlbmd0aD0iNjMiIHg9IjQzIiB5PSIyNjkuNjkxOSI+YXQgaW5kZXggMTwvdGV4dD48cG9seWdvbiBmaWxsPSIjQTgwMDM2IiBwb2ludHM9IjIxNS41LDMxNi40NTcsMjA1LjUsMzIwLjQ1NywyMTUuNSwzMjQuNDU3LDIxMS41LDMyMC40NTciIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMS4wOyIvPjxsaW5lIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMS4wOyIgeDE9IjIwOS41IiB4Mj0iNDAwLjUiIHkxPSIzMjAuNDU3IiB5Mj0iMzIwLjQ1NyIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjEzIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmdBbmRHbHlwaHMiIHRleHRMZW5ndGg9Ijg3IiB4PSIyMjEuNSIgeT0iMzE1LjM5MTEiPmNvbXBhcmUoMSwyKTwvdGV4dD48cGF0aCBkPSJNNDA2LDI5MS43NTc4IEw0MDYsMzMxLjc1NzggTDYwOSwzMzEuNzU3OCBMNjA5LDMwMS43NTc4IEw1OTksMjkxLjc1NzggTDQwNiwyOTEuNzU3OCAiIGZpbGw9IiNGQkZCNzciIGZpbHRlcj0idXJsKCNmaXpmYXlleThzYTJpKSIgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7Ii8+PHBhdGggZD0iTTU5OSwyOTEuNzU3OCBMNTk5LDMwMS43NTc4IEw2MDksMzAxLjc1NzggTDU5OSwyOTEuNzU3OCAiIGZpbGw9IiNGQkZCNzciIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMS4wOyIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjEzIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmdBbmRHbHlwaHMiIHRleHRMZW5ndGg9IjE1MyIgeD0iNDEyIiB5PSIzMDguODI0NyI+bWF4IHNvIGZhciBpcyBhdCBpbmRleCAxOzwvdGV4dD48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMyIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nQW5kR2x5cGhzIiB0ZXh0TGVuZ3RoPSIxODIiIHg9IjQxMiIgeT0iMzIzLjk1NzUiPnNlcnZlciBhc2tzIGlzIGxpc3RbMV0gJmx0OyBsaXN0WzJdPC90ZXh0Pjxwb2x5Z29uIGZpbGw9IiNBODAwMzYiIHBvaW50cz0iMzg5LjUsMzYzLjE1NjMsMzk5LjUsMzY3LjE1NjMsMzg5LjUsMzcxLjE1NjMsMzkzLjUsMzY3LjE1NjMiIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMS4wOyIvPjxsaW5lIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMS4wOyIgeDE9IjIwNC41IiB4Mj0iMzk1LjUiIHkxPSIzNjcuMTU2MyIgeTI9IjM2Ny4xNTYzIi8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTMiIGxlbmd0aEFkanVzdD0ic3BhY2luZ0FuZEdseXBocyIgdGV4dExlbmd0aD0iMTY5IiB4PSIyMTEuNSIgeT0iMzYyLjA5MDMiPmNvbXBfcmVzdWx0LCBhbnN3ZXI9dHJ1ZTwvdGV4dD48cGF0aCBkPSJNNjQsMzQ2LjAyMzQgTDY0LDM3MS4wMjM0IEwxOTUsMzcxLjAyMzQgTDE5NSwzNTYuMDIzNCBMMTg1LDM0Ni4wMjM0IEw2NCwzNDYuMDIzNCAiIGZpbGw9IiNGQkZCNzciIGZpbHRlcj0idXJsKCNmaXpmYXlleThzYTJpKSIgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7Ii8+PHBhdGggZD0iTTE4NSwzNDYuMDIzNCBMMTg1LDM1Ni4wMjM0IEwxOTUsMzU2LjAyMzQgTDE4NSwzNDYuMDIzNCAiIGZpbGw9IiNGQkZCNzciIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMS4wOyIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjEzIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmdBbmRHbHlwaHMiIHRleHRMZW5ndGg9IjExMCIgeD0iNzAiIHk9IjM2My4wOTAzIj5jbGllbnQgcmVwb3J0cyB5ZXM8L3RleHQ+PHBvbHlnb24gZmlsbD0iI0E4MDAzNiIgcG9pbnRzPSIyMTUuNSw0MDkuODU1NSwyMDUuNSw0MTMuODU1NSwyMTUuNSw0MTcuODU1NSwyMTEuNSw0MTMuODU1NSIgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7Ii8+PGxpbmUgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7IiB4MT0iMjA5LjUiIHgyPSI0MDAuNSIgeTE9IjQxMy44NTU1IiB5Mj0iNDEzLjg1NTUiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMyIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nQW5kR2x5cGhzIiB0ZXh0TGVuZ3RoPSI4NyIgeD0iMjIxLjUiIHk9IjQwOC43ODk2Ij5jb21wYXJlKDIsMyk8L3RleHQ+PHBhdGggZD0iTTQwNiwzODUuMTU2MyBMNDA2LDQyNS4xNTYzIEw2MDksNDI1LjE1NjMgTDYwOSwzOTUuMTU2MyBMNTk5LDM4NS4xNTYzIEw0MDYsMzg1LjE1NjMgIiBmaWxsPSIjRkJGQjc3IiBmaWx0ZXI9InVybCgjZml6ZmF5ZXk4c2EyaSkiIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMS4wOyIvPjxwYXRoIGQ9Ik01OTksMzg1LjE1NjMgTDU5OSwzOTUuMTU2MyBMNjA5LDM5NS4xNTYzIEw1OTksMzg1LjE1NjMgIiBmaWxsPSIjRkJGQjc3IiBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDEuMDsiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMyIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nQW5kR2x5cGhzIiB0ZXh0TGVuZ3RoPSIxNTMiIHg9IjQxMiIgeT0iNDAyLjIyMzEiPm1heCBzbyBmYXIgaXMgYXQgaW5kZXggMjs8L3RleHQ+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTMiIGxlbmd0aEFkanVzdD0ic3BhY2luZ0FuZEdseXBocyIgdGV4dExlbmd0aD0iMTgyIiB4PSI0MTIiIHk9IjQxNy4zNTYiPnNlcnZlciBhc2tzIGlzIGxpc3RbMl0gJmx0OyBsaXN0WzNdPC90ZXh0Pjxwb2x5Z29uIGZpbGw9IiNBODAwMzYiIHBvaW50cz0iMzg5LjUsNDU2LjU1NDcsMzk5LjUsNDYwLjU1NDcsMzg5LjUsNDY0LjU1NDcsMzkzLjUsNDYwLjU1NDciIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMS4wOyIvPjxsaW5lIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMS4wOyIgeDE9IjIwNC41IiB4Mj0iMzk1LjUiIHkxPSI0NjAuNTU0NyIgeTI9IjQ2MC41NTQ3Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTMiIGxlbmd0aEFkanVzdD0ic3BhY2luZ0FuZEdseXBocyIgdGV4dExlbmd0aD0iMTczIiB4PSIyMTEuNSIgeT0iNDU1LjQ4ODgiPmNvbXBfcmVzdWx0LCBhbnN3ZXI9ZmFsc2U8L3RleHQ+PHBhdGggZD0iTTcwLDQzOS40MjE5IEw3MCw0NjQuNDIxOSBMMTk1LDQ2NC40MjE5IEwxOTUsNDQ5LjQyMTkgTDE4NSw0MzkuNDIxOSBMNzAsNDM5LjQyMTkgIiBmaWxsPSIjRkJGQjc3IiBmaWx0ZXI9InVybCgjZml6ZmF5ZXk4c2EyaSkiIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMS4wOyIvPjxwYXRoIGQ9Ik0xODUsNDM5LjQyMTkgTDE4NSw0NDkuNDIxOSBMMTk1LDQ0OS40MjE5IEwxODUsNDM5LjQyMTkgIiBmaWxsPSIjRkJGQjc3IiBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDEuMDsiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMyIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nQW5kR2x5cGhzIiB0ZXh0TGVuZ3RoPSIxMDQiIHg9Ijc2IiB5PSI0NTYuNDg4OCI+Y2xpZW50IHJlcG9ydHMgbm88L3RleHQ+PHBvbHlnb24gZmlsbD0iI0E4MDAzNiIgcG9pbnRzPSIyMTUuNSw1MTAuODIwMywyMDUuNSw1MTQuODIwMywyMTUuNSw1MTguODIwMywyMTEuNSw1MTQuODIwMyIgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7Ii8+PGxpbmUgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7IiB4MT0iMjA5LjUiIHgyPSI0MDAuNSIgeTE9IjUxNC44MjAzIiB5Mj0iNTE0LjgyMDMiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMyIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nQW5kR2x5cGhzIiB0ZXh0TGVuZ3RoPSIxMDQiIHg9IjIyMS41IiB5PSI1MDkuNzU0NCI+ZG9uZSwgYW5zd2VyPTI8L3RleHQ+PHBhdGggZD0iTTQwNiw0NzguNTU0NyBMNDA2LDUzMy41NTQ3IEw2MTgsNTMzLjU1NDcgTDYxOCw0ODguNTU0NyBMNjA4LDQ3OC41NTQ3IEw0MDYsNDc4LjU1NDcgIiBmaWxsPSIjRkJGQjc3IiBmaWx0ZXI9InVybCgjZml6ZmF5ZXk4c2EyaSkiIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMS4wOyIvPjxwYXRoIGQ9Ik02MDgsNDc4LjU1NDcgTDYwOCw0ODguNTU0NyBMNjE4LDQ4OC41NTQ3IEw2MDgsNDc4LjU1NDcgIiBmaWxsPSIjRkJGQjc3IiBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDEuMDsiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMyIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nQW5kR2x5cGhzIiB0ZXh0TGVuZ3RoPSIxOTEiIHg9IjQxMiIgeT0iNDk1LjYyMTYiPnNlcnZlciBrbm93cyBpdCBoYXMgY29tcGFyZWQ8L3RleHQ+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTMiIGxlbmd0aEFkanVzdD0ic3BhY2luZ0FuZEdseXBocyIgdGV4dExlbmd0aD0iMTgwIiB4PSI0MTIiIHk9IjUxMC43NTQ0Ij5hbGwgbGlzdCBpdGVtcyBhbmQgdGhhdCBtYXggaXM8L3RleHQ+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTMiIGxlbmd0aEFkanVzdD0ic3BhY2luZ0FuZEdseXBocyIgdGV4dExlbmd0aD0iNjMiIHg9IjQxMiIgeT0iNTI1Ljg4NzIiPmF0IGluZGV4IDI8L3RleHQ+PCEtLQpAc3RhcnR1bWwNCg0KcGFydGljaXBhbnQgImNsaWVudCIgYXMgYw0KcGFydGljaXBhbnQgInNlcnZlciIgYXMgcw0KDQpjIC0+IHMgOiBjb21wdXRlX21heCwgbGVuZ3RoPTQNCm5vdGUgbGVmdA0KY2xpZW50IHRlbGxzIHNlcnZlciB0aGVyZSANCmFyZSA0IGludGVnZXMgYW5kIHRoYXQgdGhlDQpzZXJ2ZXIgc2hvdWxkIGZpZ3VyZSBvdXQNCnRoZSBsYXJnZXN0IG9uZQ0KZW5kIG5vdGUNCg0KYyA8LSBzIDogY29tcGFyZSgwLDEpDQpub3RlIHJpZ2h0DQpzZXJ2ZXIgd2FudHMgY2xpZW50IHRvDQpjb21wYXJlIGludGVnZXJzIGF0DQppbmRleGVzIDAgYW5kIDENCmVuZCBub3RlDQoNCmMgLT4gcyA6IGNvbXBfcmVzdWx0LCBhbnN3ZXI9dHJ1ZQ0Kbm90ZSBsZWZ0DQpjbGllbnQgcmVwb3J0cyB0aGF0DQp0aGUgaW50ZWdlciBhdCBpbmRleA0KMCBpcyBsZXNzIHRoYW4gdGhlIG9uZQ0KYXQgaW5kZXggMQ0KZW5kIG5vdGUNCg0KYyA8LSBzIDogY29tcGFyZSgxLDIpDQpub3RlIHJpZ2h0DQptYXggc28gZmFyIGlzIGF0IGluZGV4IDE7DQpzZXJ2ZXIgYXNrcyBpcyBsaXN0WzFdIDwgbGlzdFsyXQ0KZW5kIG5vdGUNCg0KYyAtPiBzIDogY29tcF9yZXN1bHQsIGFuc3dlcj10cnVlDQpub3RlIGxlZnQNCmNsaWVudCByZXBvcnRzIHllcw0KZW5kIG5vdGUNCg0KYyA8LSBzIDogY29tcGFyZSgyLDMpDQpub3RlIHJpZ2h0DQptYXggc28gZmFyIGlzIGF0IGluZGV4IDI7DQpzZXJ2ZXIgYXNrcyBpcyBsaXN0WzJdIDwgbGlzdFszXQ0KZW5kIG5vdGUNCg0KYyAtPiBzIDogY29tcF9yZXN1bHQsIGFuc3dlcj1mYWxzZQ0Kbm90ZSBsZWZ0DQpjbGllbnQgcmVwb3J0cyBubw0KZW5kIG5vdGUNCg0KYyA8LSBzIDogZG9uZSwgYW5zd2VyPTINCm5vdGUgcmlnaHQNCnNlcnZlciBrbm93cyBpdCBoYXMgY29tcGFyZWQNCmFsbCBsaXN0IGl0ZW1zIGFuZCB0aGF0IG1heCBpcw0KYXQgaW5kZXggMg0KZW5kIG5vdGUNCg0KQGVuZHVtbA0KClBsYW50VU1MIHZlcnNpb24gMS4yMDE4LjEzKE1vbiBOb3YgMjYgMTk6MTE6NTEgRUVUIDIwMTgpCihHUEwgc291cmNlIGRpc3RyaWJ1dGlvbikKSmF2YSBSdW50aW1lOiBPcGVuSkRLIFJ1bnRpbWUgRW52aXJvbm1lbnQKSlZNOiBPcGVuSkRLIDY0LUJpdCBTZXJ2ZXIgVk0KSmF2YSBWZXJzaW9uOiAxMS4wLjExKzktcG9zdC1EZWJpYW4tMWRlYjEwdTEKT3BlcmF0aW5nIFN5c3RlbTogTGludXgKT1MgVmVyc2lvbjogNC4xOS4wLTE2LWFtZDY0CkRlZmF1bHQgRW5jb2Rpbmc6IFVURi04Ckxhbmd1YWdlOiBlbgpDb3VudHJ5OiBVUwotLT48L2c+PC9zdmc+" /></p>
+<p>This is very simple computation. Given the server can’t assume the list is ordered, it has to compare all list elements to the largest one it has found so far.</p>
+<h1 id="assumptions"><span class="header-section-number">3</span> Assumptions</h1>
+<ul>
+<li><p>The server will abort if the client doesn’t use the request id from the latest server message. That is, the client and server do not need to handle multiple outstanding comparison requests.</p></li>
+<li><p>The server does not need to handle the case of the client having an empty list of integers, because there messages as given do no indicate a way to signal a result of “no result”.</p></li>
+<li><p>The list in the client doesn’t change during the computation: items stay in the same order, and are not added, deleted, or changed.</p></li>
+<li><p>The client responds truthfully.</p></li>
+</ul>
+<h1 id="remarks"><span class="header-section-number">4</span> Remarks</h1>
+<ul>
+<li>Given the list the client has is unordered, O(n) comparisons is the best we can do. If the client can make some guarantees, faster algorithms are possible. If list can be ordered, a binary search would be optimal.</li>
+</ul>
+<h1 id="acceptnace"><span class="header-section-number">5</span> Acceptance criteria</h1>
+<h2 id="find-max-of-a-list-of-one"><span class="header-section-number">5.1</span> Find max of a list of one</h2>
+<p>This scenario verifies that the server finds the maximum integer in a list of one.</p>
+<div class="line-block"><em>given</em> server<br />
+<em>when</em> I run <strong>max-client.py</strong><strong> 1</strong><br />
+<em>then</em> answer is <strong>1</strong></div>
+<h2 id="find-max-of-a-list-of-two"><span class="header-section-number">5.2</span> Find max of a list of two</h2>
+<p>These scenarios verify that the server finds the maximum integer in a list of two. There is a separate scenario for every possible list of two elements.</p>
+<div class="line-block"><em>given</em> server<br />
+<em>when</em> I run <strong>max-client.py</strong><strong> 5 5</strong><br />
+<em>then</em> answer is <strong>5</strong><br />
+<em>when</em> I run <strong>max-client.py</strong><strong> 5 6</strong><br />
+<em>then</em> answer is <strong>6</strong><br />
+<em>when</em> I run <strong>max-client.py</strong><strong> 6 5</strong><br />
+<em>then</em> answer is <strong>6</strong></div>
+<h2 id="find-max-of-a-list-of-three"><span class="header-section-number">5.3</span> Find max of a list of three</h2>
+<p>These scenarios verify that the server finds the maximum integer in a list of two. There is a separate scenario for every possible list of two elements.</p>
+<div class="line-block"><em>given</em> server<br />
+<em>when</em> I run <strong>max-client.py</strong><strong> 5 5 5</strong><br />
+<em>then</em> answer is <strong>5</strong><br />
+<em>when</em> I run <strong>max-client.py</strong><strong> 5 5 6</strong><br />
+<em>then</em> answer is <strong>6</strong><br />
+<em>when</em> I run <strong>max-client.py</strong><strong> 5 6 5</strong><br />
+<em>then</em> answer is <strong>6</strong><br />
+<em>when</em> I run <strong>max-client.py</strong><strong> 6 5 5</strong><br />
+<em>then</em> answer is <strong>6</strong><br />
+<em>when</em> I run <strong>max-client.py</strong><strong> 5 6 7</strong><br />
+<em>then</em> answer is <strong>7</strong><br />
+<em>when</em> I run <strong>max-client.py</strong><strong> 5 7 6</strong><br />
+<em>then</em> answer is <strong>7</strong><br />
+<em>when</em> I run <strong>max-client.py</strong><strong> 6 5 7</strong><br />
+<em>then</em> answer is <strong>7</strong><br />
+<em>when</em> I run <strong>max-client.py</strong><strong> 6 7 5</strong><br />
+<em>then</em> answer is <strong>7</strong><br />
+<em>when</em> I run <strong>max-client.py</strong><strong> 7 5 6</strong><br />
+<em>then</em> answer is <strong>7</strong><br />
+<em>when</em> I run <strong>max-client.py</strong><strong> 7 6 5</strong><br />
+<em>then</em> answer is <strong>7</strong></div>
+<h1 id="what-i-did"><span class="header-section-number">6</span> What I did</h1>
+<h2 id="the-client"><span class="header-section-number">6.1</span> The client</h2>
+<p>I modified slightly the <code>max-client.py</code> file:</p>
+<ul>
+<li>it is now an executable Python script, and formatted with Black</li>
+<li>the user can invoke it with a list of numbers, and tell it whether to ask the server to find the min or the max number</li>
+</ul>
+<p>I made these changes so that I could use it when verifying the acceptance criteria defined in this document. The original code tested only two cases, which I found to be inadequate for my purposes.</p>
+<p>The Python code is not entirely to current Python best practices. For example, it doesn’t use type annotations. I have not started using those yet: at work I’ve only used old versions of Python that don’t support type annotations, and in my free time, I don’t write anything of significant size in Python anymore.</p>
+<h2 id="the-server"><span class="header-section-number">6.2</span> The server</h2>
+<p>The server is in <code>server.py</code>, and is a Python program using <code>bottle.py</code>. I chose Python, because for something small and simple like this it’s easy. I chose <code>bottle.py</code> because it’s familiar for me.</p>
+<p>I could have chosen Rust, and probably the <code>warp</code> crate for the HTTP API, but it would have required much more implementation work, and probably more than is warranted for this exercise.</p>
+<p>The code is a little simplistic in that it doesn’t do much in terms of error handling, logging, or such. At the same time it’s overly complicated, because I wanted to make sure it allows for more than just the “max” algorithm. “min” is implemented, and the same structure should be usable for, say, finding the second largest element. More interesting algorithms would require changes to the messages: if, for example, one wanted the server to find out if the list is ordered, the “done” message would need to be able to express the result.</p>
+<h2 id="the-testing"><span class="header-section-number">6.3</span> The testing</h2>
+<p>I have used the <a href="https://subplot.liw.fi/">Subplot</a> program to verify that my server works. Subplot documents the acceptance criteria and how they are verified. That is this document. The section <a href="#acceptance">Acceptance criteria</a> documents the acceptance criteria using <em>scenarios</em> consisting of given/when/then steps.</p>
+<p>Subplot produces two typeset documents (one in HTML, one in PDF), and a self-standing test program, which can be run to verify the system under test fulfills the acceptance criteria. To avoid requiring you to have Subplot installed, the test program is included in the git repository as <code>test.py</code>. You can run it like this:</p>
+<div class="sourceCode" id="cb5"><pre class="sourceCode sh"><code class="sourceCode bash"><a class="sourceLine" id="cb5-1" title="1">$ <span class="ex">python3</span> test.py --log test.log</a>
+<a class="sourceLine" id="cb5-2" title="2"><span class="ex">srcdir</span> /home/liw/pers/oso/work-sample</a>
+<a class="sourceLine" id="cb5-3" title="3"><span class="ex">datadir</span> /tmp/tmpcom39rm7</a>
+<a class="sourceLine" id="cb5-4" title="4"><span class="ex">scenario</span>: Find max of a list of one</a>
+<a class="sourceLine" id="cb5-5" title="5"> <span class="ex">step</span>: given server</a>
+<a class="sourceLine" id="cb5-6" title="6"> <span class="ex">step</span>: when I run max-client.py 1</a>
+<a class="sourceLine" id="cb5-7" title="7"> <span class="ex">step</span>: then answer is 1</a>
+<a class="sourceLine" id="cb5-8" title="8"> <span class="ex">cleanup</span>: given server</a>
+<a class="sourceLine" id="cb5-9" title="9"><span class="ex">scenario</span>: Find max of a list of two</a>
+<a class="sourceLine" id="cb5-10" title="10"> <span class="ex">step</span>: given server</a>
+<a class="sourceLine" id="cb5-11" title="11"> <span class="ex">step</span>: when I run max-client.py 5 5</a>
+<a class="sourceLine" id="cb5-12" title="12"> <span class="ex">step</span>: then answer is 5</a>
+<a class="sourceLine" id="cb5-13" title="13"> <span class="ex">step</span>: when I run max-client.py 5 6</a>
+<a class="sourceLine" id="cb5-14" title="14"> <span class="ex">step</span>: then answer is 6</a>
+<a class="sourceLine" id="cb5-15" title="15"> <span class="ex">step</span>: when I run max-client.py 6 5</a>
+<a class="sourceLine" id="cb5-16" title="16"> <span class="ex">step</span>: then answer is 6</a>
+<a class="sourceLine" id="cb5-17" title="17"> <span class="ex">cleanup</span>: given server</a>
+<a class="sourceLine" id="cb5-18" title="18"><span class="ex">scenario</span>: Find max of a list of three</a>
+<a class="sourceLine" id="cb5-19" title="19"> <span class="ex">step</span>: given server</a>
+<a class="sourceLine" id="cb5-20" title="20"> <span class="ex">step</span>: when I run max-client.py 5 5 5</a>
+<a class="sourceLine" id="cb5-21" title="21"> <span class="ex">step</span>: then answer is 5</a>
+<a class="sourceLine" id="cb5-22" title="22"> <span class="ex">step</span>: when I run max-client.py 5 5 6</a>
+<a class="sourceLine" id="cb5-23" title="23"> <span class="ex">step</span>: then answer is 6</a>
+<a class="sourceLine" id="cb5-24" title="24"> <span class="ex">step</span>: when I run max-client.py 5 6 5</a>
+<a class="sourceLine" id="cb5-25" title="25"> <span class="ex">step</span>: then answer is 6</a>
+<a class="sourceLine" id="cb5-26" title="26"> <span class="ex">step</span>: when I run max-client.py 6 5 5</a>
+<a class="sourceLine" id="cb5-27" title="27"> <span class="ex">step</span>: then answer is 6</a>
+<a class="sourceLine" id="cb5-28" title="28"> <span class="ex">step</span>: when I run max-client.py 5 6 7</a>
+<a class="sourceLine" id="cb5-29" title="29"> <span class="ex">step</span>: then answer is 7</a>
+<a class="sourceLine" id="cb5-30" title="30"> <span class="ex">step</span>: when I run max-client.py 5 7 6</a>
+<a class="sourceLine" id="cb5-31" title="31"> <span class="ex">step</span>: then answer is 7</a>
+<a class="sourceLine" id="cb5-32" title="32"> <span class="ex">step</span>: when I run max-client.py 6 5 7</a>
+<a class="sourceLine" id="cb5-33" title="33"> <span class="ex">step</span>: then answer is 7</a>
+<a class="sourceLine" id="cb5-34" title="34"> <span class="ex">step</span>: when I run max-client.py 6 7 5</a>
+<a class="sourceLine" id="cb5-35" title="35"> <span class="ex">step</span>: then answer is 7</a>
+<a class="sourceLine" id="cb5-36" title="36"> <span class="ex">step</span>: when I run max-client.py 7 5 6</a>
+<a class="sourceLine" id="cb5-37" title="37"> <span class="ex">step</span>: then answer is 7</a>
+<a class="sourceLine" id="cb5-38" title="38"> <span class="ex">step</span>: when I run max-client.py 7 6 5</a>
+<a class="sourceLine" id="cb5-39" title="39"> <span class="ex">step</span>: then answer is 7</a>
+<a class="sourceLine" id="cb5-40" title="40"> <span class="ex">cleanup</span>: given server</a>
+<a class="sourceLine" id="cb5-41" title="41"><span class="ex">OK</span>, all scenarios finished successfully</a>
+<a class="sourceLine" id="cb5-42" title="42">$</a></code></pre></div>
+<p>I hope that is satisfactory.</p>
+</body>
+</html>
diff --git a/README.pdf b/README.pdf
new file mode 100644
index 0000000..3c86cb3
--- /dev/null
+++ b/README.pdf
Binary files differ
diff --git a/test.py b/test.py
new file mode 100644
index 0000000..698cefd
--- /dev/null
+++ b/test.py
@@ -0,0 +1,1513 @@
+#############################################################################
+# Functions that implement steps.
+
+
+#----------------------------------------------------------------------------
+# This code comes from: oso.py
+
+import os
+
+
+def start_server(ctx):
+ # Declare Subplot library names. In the generated Python program, the
+ # libraries will be included and can just be used via names, but to placate
+ # automated checks that only see this file, get the names from globals() at
+ # runtime.
+ daemon_start_on_port = globals()["daemon_start_on_port"]
+ runcmd_helper_srcdir_path = globals()["runcmd_helper_srcdir_path"]
+ srcdir = globals()["srcdir"]
+
+ # This installs srcdir in $PATH so that we can run the client and server
+ # easily.
+ runcmd_helper_srcdir_path(ctx)
+
+ # Start server.
+ server = os.path.join(srcdir, "server.py")
+ daemon_start_on_port(ctx, path=server, args="", name="server", port=5000)
+
+
+def stop_server(ctx):
+ daemon_stop = globals()["daemon_stop"]
+ daemon_stop(ctx, name="server")
+
+
+def answer_is(ctx, index):
+ assert_eq = globals()["assert_eq"]
+ runcmd_get_stdout = globals()["runcmd_get_stdout"]
+ stdout = runcmd_get_stdout(ctx)
+ assert_eq(stdout.strip(), index)
+
+
+#----------------------------------------------------------------------------
+# This code comes from: lib/daemon.py
+
+import logging
+import os
+import signal
+import socket
+import subprocess
+import time
+
+
+# A helper function for testing lib/daemon itself.
+def _daemon_shell_script(ctx, filename=None):
+ get_file = globals()["get_file"]
+ data = get_file(filename)
+ with open(filename, "wb") as f:
+ f.write(data)
+ os.chmod(filename, 0o755)
+
+
+# Start a daemon that will open a port on localhost.
+def daemon_start_on_port(ctx, path=None, args=None, name=None, port=None):
+ _daemon_start(ctx, path=path, args=args, name=name)
+ daemon_wait_for_port("localhost", port)
+
+
+# Start a daemon after a little wait. This is used only for testing the
+# port-waiting code.
+def _daemon_start_soonish(ctx, path=None, args=None, name=None, port=None):
+ _daemon_start(ctx, path=os.path.abspath(path), args=args, name=name)
+ daemon = ctx.declare("_daemon")
+
+ # Store the PID of the process we just started so that _daemon_stop_soonish
+ # can kill it during the cleanup phase. This works around the Subplot
+ # Python template not giving the step captures to cleanup functions. Note
+ # that this code assume at most one _soonish function is called.
+ daemon["_soonish"] = daemon[name]["pid"]
+
+ try:
+ daemon_wait_for_port("localhost", port)
+ except Exception as e:
+ daemon["_start_error"] = repr(e)
+
+ logging.info("pgrep: %r", _daemon_pgrep(path))
+
+
+def _daemon_stop_soonish(ctx, path=None, args=None, name=None, port=None):
+ ns = ctx.declare("_daemon")
+ pid = ns["_soonish"]
+ logging.debug(f"Stopping soonishly-started daemon, {pid}")
+ signo = signal.SIGKILL
+ try:
+ os.kill(pid, signo)
+ except ProcessLookupError:
+ logging.warning("Process did not actually exist (anymore?)")
+
+
+# Start a daeamon, get its PID. Don't wait for a port or anything. This is
+# meant for background processes that don't have port. Useful for testing the
+# lib/daemon library of Subplot, but not much else.
+def _daemon_start(ctx, path=None, args=None, name=None):
+ runcmd_run = globals()["runcmd_run"]
+ runcmd_exit_code_is = globals()["runcmd_exit_code_is"]
+ runcmd_get_exit_code = globals()["runcmd_get_exit_code"]
+ runcmd_get_stderr = globals()["runcmd_get_stderr"]
+ runcmd_prepend_to_path = globals()["runcmd_prepend_to_path"]
+
+ path = os.path.abspath(path)
+ argv = [path] + args.split()
+
+ logging.debug(f"Starting daemon {name}")
+ logging.debug(f" ctx={ctx.as_dict()}")
+ logging.debug(f" name={name}")
+ logging.debug(f" path={path}")
+ logging.debug(f" args={args}")
+ logging.debug(f" argv={argv}")
+
+ ns = ctx.declare("_daemon")
+
+ this = ns[name] = {
+ "pid-file": f"{name}.pid",
+ "stderr": f"{name}.stderr",
+ "stdout": f"{name}.stdout",
+ }
+
+ # Debian installs `daemonize` to /usr/sbin, which isn't part of the minimal
+ # environment that Subplot sets up. So we add /usr/sbin to the PATH.
+ runcmd_prepend_to_path(ctx, "/usr/sbin")
+ runcmd_run(
+ ctx,
+ [
+ "daemonize",
+ "-c",
+ os.getcwd(),
+ "-p",
+ this["pid-file"],
+ "-e",
+ this["stderr"],
+ "-o",
+ this["stdout"],
+ ]
+ + argv,
+ )
+
+ # Check that daemonize has exited OK. If it hasn't, it didn't start the
+ # background process at all. If so, log the stderr in case there was
+ # something useful there for debugging.
+ exit = runcmd_get_exit_code(ctx)
+ if exit != 0:
+ stderr = runcmd_get_stderr(ctx)
+ logging.error(f"daemon {name} stderr: {stderr}")
+ runcmd_exit_code_is(ctx, 0)
+
+ # Get the pid of the background process, from the pid file created by
+ # daemonize. We don't need to wait for it, since we know daemonize already
+ # exited. If it isn't there now, it's won't appear later.
+ if not os.path.exists(this["pid-file"]):
+ raise Exception("daemonize didn't create a PID file")
+
+ this["pid"] = _daemon_wait_for_pid(this["pid-file"], 10.0)
+
+ logging.debug(f"Started daemon {name}")
+ logging.debug(f" pid={this['pid']}")
+ logging.debug(f" ctx={ctx.as_dict()}")
+
+
+def _daemon_wait_for_pid(filename, timeout):
+ start = time.time()
+ while time.time() < start + timeout:
+ with open(filename) as f:
+ data = f.read().strip()
+ if data:
+ return int(data)
+ raise Exception("daemonize created a PID file without a PID")
+
+
+def daemon_wait_for_port(host, port, timeout=5.0):
+ addr = (host, port)
+ until = time.time() + timeout
+ while True:
+ try:
+ s = socket.create_connection(addr, timeout=timeout)
+ s.close()
+ return
+ except socket.timeout:
+ logging.error(
+ f"daemon did not respond at port {port} within {timeout} seconds"
+ )
+ raise
+ except socket.error as e:
+ logging.info(f"could not connect to daemon at {port}: {e}")
+ pass
+ if time.time() >= until:
+ logging.error(
+ f"could not connect to daemon at {port} within {timeout} seconds"
+ )
+ raise ConnectionRefusedError()
+ # Sleep a bit to avoid consuming too much CPU while busy-waiting.
+ time.sleep(0.1)
+
+
+# Stop a daemon.
+def daemon_stop(ctx, path=None, args=None, name=None):
+ logging.debug(f"Stopping daemon {name}")
+
+ ns = ctx.declare("_daemon")
+ logging.debug(f" ns={ns}")
+ pid = ns[name]["pid"]
+ signo = signal.SIGTERM
+
+ this = ns[name]
+ data = open(this["stdout"]).read()
+ logging.debug(f"{name} stdout, before: {data!r}")
+ data = open(this["stderr"]).read()
+ logging.debug(f"{name} stderr, before: {data!r}")
+
+ logging.debug(f"Terminating process {pid} with signal {signo}")
+ try:
+ os.kill(pid, signo)
+ except ProcessLookupError:
+ logging.warning("Process did not actually exist (anymore?)")
+
+ while True:
+ try:
+ os.kill(pid, 0)
+ logging.debug(f"Daemon {name}, pid {pid} still exists")
+ time.sleep(1)
+ except ProcessLookupError:
+ break
+ logging.debug(f"Daemon {name} is gone")
+
+ data = open(this["stdout"]).read()
+ logging.debug(f"{name} stdout, after: {data!r}")
+ data = open(this["stderr"]).read()
+ logging.debug(f"{name} stderr, after: {data!r}")
+
+
+def daemon_no_such_process(ctx, args=None):
+ assert not _daemon_pgrep(args)
+
+
+def daemon_process_exists(ctx, args=None):
+ assert _daemon_pgrep(args)
+
+
+def _daemon_pgrep(pattern):
+ logging.info(f"checking if process exists: pattern={pattern}")
+ exit = subprocess.call(
+ ["pgrep", "-laf", pattern], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
+ )
+ logging.info(f"exit code: {exit}")
+ return exit == 0
+
+
+def daemon_start_fails_with(ctx, message=None):
+ daemon = ctx.declare("_daemon")
+ error = daemon["_start_error"]
+ logging.debug(f"daemon_start_fails_with: error={error!r}")
+ logging.debug(f"daemon_start_fails_with: message={message!r}")
+ assert message.lower() in error.lower()
+
+
+def daemon_get_stdout(ctx, name):
+ return _daemon_get_output(ctx, name, "stdout")
+
+
+def daemon_get_stderr(ctx, name):
+ return _daemon_get_output(ctx, name, "stderr")
+
+
+def _daemon_get_output(ctx, name, which):
+ ns = ctx.declare("_daemon")
+ this = ns[name]
+ filename = this[which]
+ data = open(filename).read()
+ logging.debug(f"Read {which} of daemon {name} from {filename}: {data!r}")
+ return data
+
+
+def daemon_has_produced_output(ctx, name=None):
+ started = time.time()
+ timeout = 5.0
+ while time.time() < started + timeout:
+ stdout = daemon_get_stdout(ctx, name)
+ stderr = daemon_get_stderr(ctx, name)
+ if stdout and stderr:
+ break
+ time.sleep(0.1)
+
+
+def daemon_stdout_is(ctx, name=None, text=None):
+ daemon_get_stdout = globals()["daemon_get_stdout"]
+ _daemon_output_is(ctx, name, text, daemon_get_stdout)
+
+
+def daemon_stderr_is(ctx, name=None, text=None):
+ daemon_get_stderr = globals()["daemon_get_stderr"]
+ _daemon_output_is(ctx, name, text, daemon_get_stderr)
+
+
+def _daemon_output_is(ctx, name, text, getter):
+ assert_eq = globals()["assert_eq"]
+ text = bytes(text, "UTF-8").decode("unicode_escape")
+ output = getter(ctx, name)
+ assert_eq(text, output)
+
+
+#----------------------------------------------------------------------------
+# This code comes from: lib/runcmd.py
+
+import logging
+import os
+import re
+import shlex
+import subprocess
+
+
+#
+# Helper functions.
+#
+
+# Get exit code or other stored data about the latest command run by
+# runcmd_run.
+
+
+def _runcmd_get(ctx, name):
+ ns = ctx.declare("_runcmd")
+ return ns[name]
+
+
+def runcmd_get_exit_code(ctx):
+ return _runcmd_get(ctx, "exit")
+
+
+def runcmd_get_stdout(ctx):
+ return _runcmd_get(ctx, "stdout")
+
+
+def runcmd_get_stdout_raw(ctx):
+ return _runcmd_get(ctx, "stdout.raw")
+
+
+def runcmd_get_stderr(ctx):
+ return _runcmd_get(ctx, "stderr")
+
+
+def runcmd_get_stderr_raw(ctx):
+ return _runcmd_get(ctx, "stderr.raw")
+
+
+def runcmd_get_argv(ctx):
+ return _runcmd_get(ctx, "argv")
+
+
+# Run a command, given an argv and other arguments for subprocess.Popen.
+#
+# This is meant to be a helper function, not bound directly to a step. The
+# stdout, stderr, and exit code are stored in the "_runcmd" namespace in the
+# ctx context.
+def runcmd_run(ctx, argv, **kwargs):
+ log_value = globals()["log_value"]
+
+ ns = ctx.declare("_runcmd")
+
+ # The Subplot Python template empties os.environ at startup, modulo a small
+ # number of variables with carefully chosen values. Here, we don't need to
+ # care about what those variables are, but we do need to not overwrite
+ # them, so we just add anything in the env keyword argument, if any, to
+ # os.environ.
+ env = dict(os.environ)
+ for key, arg in kwargs.pop("env", {}).items():
+ env[key] = arg
+
+ pp = ns.get("path-prefix")
+ if pp:
+ env["PATH"] = pp + ":" + env["PATH"]
+
+ kwargs["stdout"] = subprocess.PIPE
+ kwargs["stderr"] = subprocess.PIPE
+
+ logging.debug(f"runcmd_run: running command")
+ log_value("argv", 1, dict(enumerate(argv)))
+ log_value("env", 1, env)
+ log_value("kwargs:", 1, kwargs)
+
+ p = subprocess.Popen(argv, env=env, **kwargs)
+ stdout, stderr = p.communicate("")
+
+ ns["argv"] = argv
+ ns["stdout.raw"] = stdout
+ ns["stderr.raw"] = stderr
+ ns["stdout"] = stdout.decode("utf-8")
+ ns["stderr"] = stderr.decode("utf-8")
+ ns["exit"] = p.returncode
+
+ log_value("ns", 1, ns.as_dict())
+
+
+# Step: prepend srcdir to PATH whenever runcmd runs a command.
+def runcmd_helper_srcdir_path(ctx):
+ srcdir = globals()["srcdir"]
+ runcmd_prepend_to_path(ctx, srcdir)
+
+
+# Step: This creates a helper script.
+def runcmd_helper_script(ctx, filename=None):
+ get_file = globals()["get_file"]
+ with open(filename, "wb") as f:
+ f.write(get_file(filename))
+
+
+#
+# Step functions for running commands.
+#
+
+
+def runcmd_prepend_to_path(ctx, dirname=None):
+ ns = ctx.declare("_runcmd")
+ pp = ns.get("path-prefix", "")
+ if pp:
+ pp = f"{pp}:{dirname}"
+ else:
+ pp = dirname
+ ns["path-prefix"] = pp
+
+
+def runcmd_step(ctx, argv0=None, args=None):
+ runcmd_try_to_run(ctx, argv0=argv0, args=args)
+ runcmd_exit_code_is_zero(ctx)
+
+
+def runcmd_step_in(ctx, dirname=None, argv0=None, args=None):
+ runcmd_try_to_run_in(ctx, dirname=dirname, argv0=argv0, args=args)
+ runcmd_exit_code_is_zero(ctx)
+
+
+def runcmd_try_to_run(ctx, argv0=None, args=None):
+ runcmd_try_to_run_in(ctx, dirname=None, argv0=argv0, args=args)
+
+
+def runcmd_try_to_run_in(ctx, dirname=None, argv0=None, args=None):
+ argv = [shlex.quote(argv0)] + shlex.split(args)
+ runcmd_run(ctx, argv, cwd=dirname)
+
+
+#
+# Step functions for examining exit codes.
+#
+
+
+def runcmd_exit_code_is_zero(ctx):
+ runcmd_exit_code_is(ctx, exit=0)
+
+
+def runcmd_exit_code_is(ctx, exit=None):
+ assert_eq = globals()["assert_eq"]
+ assert_eq(runcmd_get_exit_code(ctx), int(exit))
+
+
+def runcmd_exit_code_is_nonzero(ctx):
+ runcmd_exit_code_is_not(ctx, exit=0)
+
+
+def runcmd_exit_code_is_not(ctx, exit=None):
+ assert_ne = globals()["assert_ne"]
+ assert_ne(runcmd_get_exit_code(ctx), int(exit))
+
+
+#
+# Step functions and helpers for examining output in various ways.
+#
+
+
+def runcmd_stdout_is(ctx, text=None):
+ _runcmd_output_is(runcmd_get_stdout(ctx), text)
+
+
+def runcmd_stdout_isnt(ctx, text=None):
+ _runcmd_output_isnt(runcmd_get_stdout(ctx), text)
+
+
+def runcmd_stderr_is(ctx, text=None):
+ _runcmd_output_is(runcmd_get_stderr(ctx), text)
+
+
+def runcmd_stderr_isnt(ctx, text=None):
+ _runcmd_output_isnt(runcmd_get_stderr(ctx), text)
+
+
+def _runcmd_output_is(actual, wanted):
+ assert_eq = globals()["assert_eq"]
+ log_lines = globals()["log_lines"]
+ indent = " " * 4
+
+ wanted = bytes(wanted, "utf8").decode("unicode_escape")
+ logging.debug("_runcmd_output_is:")
+
+ logging.debug(f" actual:")
+ log_lines(indent, actual)
+
+ logging.debug(f" wanted:")
+ log_lines(indent, wanted)
+
+ assert_eq(actual, wanted)
+
+
+def _runcmd_output_isnt(actual, wanted):
+ assert_ne = globals()["assert_ne"]
+ log_lines = globals()["log_lines"]
+ indent = " " * 4
+
+ wanted = bytes(wanted, "utf8").decode("unicode_escape")
+ logging.debug("_runcmd_output_isnt:")
+
+ logging.debug(f" actual:")
+ log_lines(indent, actual)
+
+ logging.debug(f" wanted:")
+ log_lines(indent, wanted)
+
+ assert_ne(actual, wanted)
+
+
+def runcmd_stdout_contains(ctx, text=None):
+ _runcmd_output_contains(runcmd_get_stdout(ctx), text)
+
+
+def runcmd_stdout_doesnt_contain(ctx, text=None):
+ _runcmd_output_doesnt_contain(runcmd_get_stdout(ctx), text)
+
+
+def runcmd_stderr_contains(ctx, text=None):
+ _runcmd_output_contains(runcmd_get_stderr(ctx), text)
+
+
+def runcmd_stderr_doesnt_contain(ctx, text=None):
+ _runcmd_output_doesnt_contain(runcmd_get_stderr(ctx), text)
+
+
+def _runcmd_output_contains(actual, wanted):
+ assert_eq = globals()["assert_eq"]
+ log_lines = globals()["log_lines"]
+ indent = " " * 4
+
+ wanted = bytes(wanted, "utf8").decode("unicode_escape")
+ logging.debug("_runcmd_output_contains:")
+
+ logging.debug(f" actual:")
+ log_lines(indent, actual)
+
+ logging.debug(f" wanted:")
+ log_lines(indent, wanted)
+
+ assert_eq(wanted in actual, True)
+
+
+def _runcmd_output_doesnt_contain(actual, wanted):
+ assert_ne = globals()["assert_ne"]
+ log_lines = globals()["log_lines"]
+ indent = " " * 4
+
+ wanted = bytes(wanted, "utf8").decode("unicode_escape")
+ logging.debug("_runcmd_output_doesnt_contain:")
+
+ logging.debug(f" actual:")
+ log_lines(indent, actual)
+
+ logging.debug(f" wanted:")
+ log_lines(indent, wanted)
+
+ assert_ne(wanted in actual, True)
+
+
+def runcmd_stdout_matches_regex(ctx, regex=None):
+ _runcmd_output_matches_regex(runcmd_get_stdout(ctx), regex)
+
+
+def runcmd_stdout_doesnt_match_regex(ctx, regex=None):
+ _runcmd_output_doesnt_match_regex(runcmd_get_stdout(ctx), regex)
+
+
+def runcmd_stderr_matches_regex(ctx, regex=None):
+ _runcmd_output_matches_regex(runcmd_get_stderr(ctx), regex)
+
+
+def runcmd_stderr_doesnt_match_regex(ctx, regex=None):
+ _runcmd_output_doesnt_match_regex(runcmd_get_stderr(ctx), regex)
+
+
+def _runcmd_output_matches_regex(actual, regex):
+ assert_ne = globals()["assert_ne"]
+ log_lines = globals()["log_lines"]
+ indent = " " * 4
+
+ r = re.compile(regex)
+ m = r.search(actual)
+
+ logging.debug("_runcmd_output_matches_regex:")
+ logging.debug(f" actual: {actual!r}")
+ log_lines(indent, actual)
+
+ logging.debug(f" regex: {regex!r}")
+ logging.debug(f" match: {m}")
+
+ assert_ne(m, None)
+
+
+def _runcmd_output_doesnt_match_regex(actual, regex):
+ assert_eq = globals()["assert_eq"]
+ log_lines = globals()["log_lines"]
+ indent = " " * 4
+
+ r = re.compile(regex)
+ m = r.search(actual)
+
+ logging.debug("_runcmd_output_doesnt_match_regex:")
+ logging.debug(f" actual: {actual!r}")
+ log_lines(indent, actual)
+
+ logging.debug(f" regex: {regex!r}")
+ logging.debug(f" match: {m}")
+
+ assert_eq(m, None)
+
+
+
+
+#############################################################################
+# Scaffolding for generated test program.
+
+# import logging
+import re
+
+
+# Store context between steps.
+class Context:
+ def __init__(self):
+ self._vars = {}
+ self._ns = {}
+
+ def as_dict(self):
+ return dict(self._vars)
+
+ def get(self, key, default=None):
+ return self._vars.get(key, default)
+
+ def __getitem__(self, key):
+ return self._vars[key]
+
+ def __setitem__(self, key, value):
+ # logging.debug("Context: key {!r} set to {!r}".format(key, value))
+ self._vars[key] = value
+
+ def keys(self):
+ return self._vars.keys()
+
+ def __contains__(self, key):
+ return key in self._vars
+
+ def __delitem__(self, key):
+ del self._vars[key]
+
+ def __repr__(self):
+ return repr({"vars": self._vars, "namespaces": self._ns})
+
+ def declare(self, name):
+ if name not in self._ns:
+ self._ns[name] = NameSpace(name)
+ return self._ns[name]
+
+ def remember_value(self, name, value):
+ ns = self.declare("_values")
+ if name in ns:
+ raise KeyError(name)
+ ns[name] = value
+
+ def recall_value(self, name):
+ ns = self.declare("_values")
+ if name not in ns:
+ raise KeyError(name)
+ return ns[name]
+
+ def expand_values(self, pattern):
+ parts = []
+ while pattern:
+ m = re.search(r"(?<!\$)\$\{(?P<name>\S*)\}", pattern)
+ if not m:
+ parts.append(pattern)
+ break
+ name = m.group("name")
+ if not name:
+ raise KeyError("empty name in expansion")
+ value = self.recall_value(name)
+ parts.append(value)
+ pattern = pattern[m.end() :]
+ return "".join(parts)
+
+
+class NameSpace:
+ def __init__(self, name):
+ self.name = name
+ self._dict = {}
+
+ def as_dict(self):
+ return dict(self._dict)
+
+ def get(self, key, default=None):
+ if key not in self._dict:
+ if default is None:
+ return None
+ self._dict[key] = default
+ return self._dict[key]
+
+ def __setitem__(self, key, value):
+ self._dict[key] = value
+
+ def __getitem__(self, key):
+ return self._dict[key]
+
+ def keys(self):
+ return self._dict.keys()
+
+ def __contains__(self, key):
+ return key in self._dict
+
+ def __delitem__(self, key):
+ del self._dict[key]
+
+ def __repr__(self):
+ return repr(self._dict)
+
+# Decode a base64 encoded string. Result is binary or unicode string.
+
+
+import base64
+
+
+def decode_bytes(s):
+ return base64.b64decode(s)
+
+
+def decode_str(s):
+ return base64.b64decode(s).decode()
+
+# Retrieve an embedded test data file using filename.
+
+
+class Files:
+ def __init__(self):
+ self._files = {}
+
+ def set(self, filename, content):
+ self._files[filename] = content
+
+ def get(self, filename):
+ return self._files[filename]
+
+
+_files = Files()
+
+
+def store_file(filename, content):
+ _files.set(filename, content)
+
+
+def get_file(filename):
+ return _files.get(filename)
+
+# Check two values for equality and give error if they are not equal
+def assert_eq(a, b):
+ assert a == b, "expected %r == %r" % (a, b)
+
+
+# Check two values for inequality and give error if they are equal
+def assert_ne(a, b):
+ assert a != b, "expected %r != %r" % (a, b)
+
+
+# Check that two dict values are equal.
+def assert_dict_eq(a, b):
+ assert isinstance(a, dict)
+ assert isinstance(b, dict)
+ for key in a:
+ assert key in b, f"exected {key} in both dicts"
+ av = a[key]
+ bv = b[key]
+ assert_eq(type(av), type(bv))
+ if isinstance(av, list):
+ assert_eq(list(sorted(av)), list(sorted(bv)))
+ for key in b:
+ assert key in a, f"exected {key} in both dicts"
+
+import logging
+import os
+import tempfile
+
+
+#############################################################################
+# Code to implement the scenarios.
+
+
+class Step:
+ def __init__(self):
+ self._kind = None
+ self._text = None
+ self._args = {}
+ self._function = None
+ self._cleanup = None
+
+ def set_kind(self, kind):
+ self._kind = kind
+
+ def set_text(self, text):
+ self._text = text
+
+ def set_arg(self, name, value):
+ self._args[name] = value
+
+ def set_function(self, function):
+ self._function = function
+
+ def set_cleanup(self, cleanup):
+ self._cleanup = cleanup
+
+ def do(self, ctx):
+ print(" step: {} {}".format(self._kind, self._text))
+ logging.info("step: {} {}".format(self._kind, self._text))
+ self._function(ctx, **self._args)
+
+ def cleanup(self, ctx):
+ if self._cleanup:
+ print(" cleanup: {} {}".format(self._kind, self._text))
+ logging.info("cleanup: {} {}".format(self._kind, self._text))
+ self._cleanup(ctx, **self._args)
+
+
+class Scenario:
+ def __init__(self, ctx):
+ self._title = None
+ self._steps = []
+ self._ctx = ctx
+ self._logged_env = False
+
+ def get_title(self):
+ return self._title
+
+ def set_title(self, title):
+ self._title = title
+
+ def append_step(self, step):
+ self._steps.append(step)
+
+ def run(self, datadir, extra_env):
+ print("scenario: {}".format(self._title))
+ logging.info("Scenario: {}".format(self._title))
+
+ scendir = tempfile.mkdtemp(dir=datadir)
+ os.chdir(scendir)
+ self._set_environment_variables_to(scendir, extra_env)
+
+ done = []
+ ctx = self._ctx
+ try:
+ for step in self._steps:
+ step.do(ctx)
+ done.append(step)
+ except Exception as e:
+ logging.error(str(e), exc_info=True)
+ for step in reversed(done):
+ step.cleanup(ctx)
+ raise
+ for step in reversed(done):
+ step.cleanup(ctx)
+
+ def _set_environment_variables_to(self, scendir, extra_env):
+ log_value = globals()["log_value"]
+
+ minimal = {
+ "PATH": "/bin:/usr/bin",
+ "SHELL": "/bin/sh",
+ "HOME": scendir,
+ "TMPDIR": scendir,
+ }
+
+ os.environ.clear()
+ os.environ.update(minimal)
+ os.environ.update(extra_env)
+ if not self._logged_env:
+ self._logged_env = True
+ log_value("extra_env", 0, dict(extra_env))
+ log_value("os.environ", 0, dict(os.environ))
+
+import argparse
+import logging
+import os
+import random
+import shutil
+import tarfile
+import tempfile
+
+
+class MultilineFormatter(logging.Formatter):
+ def format(self, record):
+ s = super().format(record)
+ lines = list(s.splitlines())
+ return lines.pop(0) + "\n".join(" %s" % line for line in lines)
+
+
+def indent(n):
+ return " " * n
+
+
+def log_value(msg, level, v):
+ if is_multiline_string(v):
+ logging.debug(f"{indent(level)}{msg}:")
+ log_lines(indent(level + 1), v)
+ elif isinstance(v, dict) and v:
+ # Only non-empty dictionaries
+ logging.debug(f"{indent(level)}{msg}:")
+ for k in sorted(v.keys()):
+ log_value(f"{k!r}", level + 1, v[k])
+ elif isinstance(v, list) and v:
+ # Only non-empty lists
+ logging.debug(f"{indent(level)}{msg}:")
+ for i, x in enumerate(v):
+ log_value(f"{i}", level + 1, x)
+ else:
+ logging.debug(f"{indent(level)}{msg}: {v!r}")
+
+
+def is_multiline_string(v):
+ if isinstance(v, str) and "\n" in v:
+ return True
+ elif isinstance(v, bytes) and b"\n" in v:
+ return True
+ else:
+ return False
+
+
+def log_lines(prefix, v):
+ if isinstance(v, str):
+ nl = "\n"
+ else:
+ nl = b"\n"
+ if nl in v:
+ for line in v.splitlines(keepends=True):
+ logging.debug(f"{prefix}{line!r}")
+ else:
+ logging.debug(f"{prefix}{v!r}")
+
+
+# Remember where we started from. The step functions may need to refer
+# to files there.
+srcdir = os.getcwd()
+print("srcdir", srcdir)
+
+# Create a new temporary directory and chdir there. This allows step
+# functions to create new files in the current working directory
+# without having to be so careful.
+_datadir = tempfile.mkdtemp()
+print("datadir", _datadir)
+os.chdir(_datadir)
+
+
+def parse_command_line():
+ p = argparse.ArgumentParser()
+ p.add_argument("--log")
+ p.add_argument("--env", action="append", default=[])
+ p.add_argument("--save-on-failure")
+ p.add_argument("patterns", nargs="*")
+ return p.parse_args()
+
+
+def setup_logging(args):
+ if args.log:
+ fmt = "%(asctime)s %(levelname)s %(message)s"
+ datefmt = "%Y-%m-%d %H:%M:%S"
+ formatter = MultilineFormatter(fmt, datefmt)
+
+ filename = os.path.abspath(os.path.join(srcdir, args.log))
+ handler = logging.FileHandler(filename)
+ handler.setFormatter(formatter)
+ else:
+ handler = logging.NullHandler()
+
+ logger = logging.getLogger()
+ logger.addHandler(handler)
+ logger.setLevel(logging.DEBUG)
+
+
+def save_directory(dirname, tarname):
+ print("tarname", tarname)
+ logging.info("Saving {} to {}".format(dirname, tarname))
+ tar = tarfile.open(tarname, "w")
+ tar.add(dirname, arcname="datadir")
+ tar.close()
+
+
+def main(scenarios):
+ args = parse_command_line()
+ setup_logging(args)
+ logging.info("Test program starts")
+
+ logging.info("patterns: {}".format(args.patterns))
+ if len(args.patterns) == 0:
+ logging.info("Executing all scenarios")
+ todo = list(scenarios)
+ random.shuffle(todo)
+ else:
+ logging.info("Executing requested scenarios only: {}".format(args.patterns))
+ patterns = [arg.lower() for arg in args.patterns]
+ todo = [
+ scen
+ for scen in scenarios
+ if any(pattern in scen.get_title().lower() for pattern in patterns)
+ ]
+
+ extra_env = {}
+ for env in args.env:
+ (name, value) = env.split("=", 1)
+ extra_env[name] = value
+
+ try:
+ for scen in todo:
+ scen.run(_datadir, extra_env)
+ except Exception as e:
+ logging.error(str(e), exc_info=True)
+ if args.save_on_failure:
+ print(args.save_on_failure)
+ filename = os.path.abspath(os.path.join(srcdir, args.save_on_failure))
+ print(filename)
+ save_directory(_datadir, filename)
+ raise
+
+ shutil.rmtree(_datadir)
+ print("OK, all scenarios finished successfully")
+ logging.info("OK, all scenarios finished successfully")
+
+
+
+#############################################################################
+# Test data files that were embedded in the source document. Base64
+# encoding is used to allow arbitrary data.
+
+
+
+
+
+#############################################################################
+# Classes for individual scenarios.
+
+
+#----------------------------------------------------------------------------
+# Scenario: Find max of a list of one
+class Scenario_1():
+ def __init__(self):
+ ctx = Context()
+ self._scenario = Scenario(ctx)
+ self._scenario.set_title(decode_str('RmluZCBtYXggb2YgYSBsaXN0IG9mIG9uZQ=='))
+
+ # Step: server
+ step = Step()
+ step.set_kind('given')
+ step.set_text(decode_str('c2VydmVy'))
+ step.set_function(start_server)
+ if 'stop_server':
+ step.set_cleanup(stop_server)
+ self._scenario.append_step(step)
+
+ # Step: I run max-client.py 1
+ step = Step()
+ step.set_kind('when')
+ step.set_text(decode_str('SSBydW4gbWF4LWNsaWVudC5weSAx'))
+ step.set_function(runcmd_step)
+ if '':
+ step.set_cleanup()
+ self._scenario.append_step(step)
+ name = decode_str('YXJndjA=')
+ text = decode_str('bWF4LWNsaWVudC5weQ==')
+ step.set_arg(name, text)
+ name = decode_str('YXJncw==')
+ text = decode_str('IDE=')
+ step.set_arg(name, text)
+
+ # Step: answer is 1
+ step = Step()
+ step.set_kind('then')
+ step.set_text(decode_str('YW5zd2VyIGlzIDE='))
+ step.set_function(answer_is)
+ if '':
+ step.set_cleanup()
+ self._scenario.append_step(step)
+ name = decode_str('aW5kZXg=')
+ text = decode_str('MQ==')
+ step.set_arg(name, text)
+
+
+ def get_title(self):
+ return self._scenario.get_title()
+
+ def run(self, datadir, extra_env):
+ self._scenario.run(datadir, extra_env)
+
+#----------------------------------------------------------------------------
+# Scenario: Find max of a list of two
+class Scenario_2():
+ def __init__(self):
+ ctx = Context()
+ self._scenario = Scenario(ctx)
+ self._scenario.set_title(decode_str('RmluZCBtYXggb2YgYSBsaXN0IG9mIHR3bw=='))
+
+ # Step: server
+ step = Step()
+ step.set_kind('given')
+ step.set_text(decode_str('c2VydmVy'))
+ step.set_function(start_server)
+ if 'stop_server':
+ step.set_cleanup(stop_server)
+ self._scenario.append_step(step)
+
+ # Step: I run max-client.py 5 5
+ step = Step()
+ step.set_kind('when')
+ step.set_text(decode_str('SSBydW4gbWF4LWNsaWVudC5weSA1IDU='))
+ step.set_function(runcmd_step)
+ if '':
+ step.set_cleanup()
+ self._scenario.append_step(step)
+ name = decode_str('YXJndjA=')
+ text = decode_str('bWF4LWNsaWVudC5weQ==')
+ step.set_arg(name, text)
+ name = decode_str('YXJncw==')
+ text = decode_str('IDUgNQ==')
+ step.set_arg(name, text)
+
+ # Step: answer is 5
+ step = Step()
+ step.set_kind('then')
+ step.set_text(decode_str('YW5zd2VyIGlzIDU='))
+ step.set_function(answer_is)
+ if '':
+ step.set_cleanup()
+ self._scenario.append_step(step)
+ name = decode_str('aW5kZXg=')
+ text = decode_str('NQ==')
+ step.set_arg(name, text)
+
+ # Step: I run max-client.py 5 6
+ step = Step()
+ step.set_kind('when')
+ step.set_text(decode_str('SSBydW4gbWF4LWNsaWVudC5weSA1IDY='))
+ step.set_function(runcmd_step)
+ if '':
+ step.set_cleanup()
+ self._scenario.append_step(step)
+ name = decode_str('YXJndjA=')
+ text = decode_str('bWF4LWNsaWVudC5weQ==')
+ step.set_arg(name, text)
+ name = decode_str('YXJncw==')
+ text = decode_str('IDUgNg==')
+ step.set_arg(name, text)
+
+ # Step: answer is 6
+ step = Step()
+ step.set_kind('then')
+ step.set_text(decode_str('YW5zd2VyIGlzIDY='))
+ step.set_function(answer_is)
+ if '':
+ step.set_cleanup()
+ self._scenario.append_step(step)
+ name = decode_str('aW5kZXg=')
+ text = decode_str('Ng==')
+ step.set_arg(name, text)
+
+ # Step: I run max-client.py 6 5
+ step = Step()
+ step.set_kind('when')
+ step.set_text(decode_str('SSBydW4gbWF4LWNsaWVudC5weSA2IDU='))
+ step.set_function(runcmd_step)
+ if '':
+ step.set_cleanup()
+ self._scenario.append_step(step)
+ name = decode_str('YXJndjA=')
+ text = decode_str('bWF4LWNsaWVudC5weQ==')
+ step.set_arg(name, text)
+ name = decode_str('YXJncw==')
+ text = decode_str('IDYgNQ==')
+ step.set_arg(name, text)
+
+ # Step: answer is 6
+ step = Step()
+ step.set_kind('then')
+ step.set_text(decode_str('YW5zd2VyIGlzIDY='))
+ step.set_function(answer_is)
+ if '':
+ step.set_cleanup()
+ self._scenario.append_step(step)
+ name = decode_str('aW5kZXg=')
+ text = decode_str('Ng==')
+ step.set_arg(name, text)
+
+
+ def get_title(self):
+ return self._scenario.get_title()
+
+ def run(self, datadir, extra_env):
+ self._scenario.run(datadir, extra_env)
+
+#----------------------------------------------------------------------------
+# Scenario: Find max of a list of three
+class Scenario_3():
+ def __init__(self):
+ ctx = Context()
+ self._scenario = Scenario(ctx)
+ self._scenario.set_title(decode_str('RmluZCBtYXggb2YgYSBsaXN0IG9mIHRocmVl'))
+
+ # Step: server
+ step = Step()
+ step.set_kind('given')
+ step.set_text(decode_str('c2VydmVy'))
+ step.set_function(start_server)
+ if 'stop_server':
+ step.set_cleanup(stop_server)
+ self._scenario.append_step(step)
+
+ # Step: I run max-client.py 5 5 5
+ step = Step()
+ step.set_kind('when')
+ step.set_text(decode_str('SSBydW4gbWF4LWNsaWVudC5weSA1IDUgNQ=='))
+ step.set_function(runcmd_step)
+ if '':
+ step.set_cleanup()
+ self._scenario.append_step(step)
+ name = decode_str('YXJndjA=')
+ text = decode_str('bWF4LWNsaWVudC5weQ==')
+ step.set_arg(name, text)
+ name = decode_str('YXJncw==')
+ text = decode_str('IDUgNSA1')
+ step.set_arg(name, text)
+
+ # Step: answer is 5
+ step = Step()
+ step.set_kind('then')
+ step.set_text(decode_str('YW5zd2VyIGlzIDU='))
+ step.set_function(answer_is)
+ if '':
+ step.set_cleanup()
+ self._scenario.append_step(step)
+ name = decode_str('aW5kZXg=')
+ text = decode_str('NQ==')
+ step.set_arg(name, text)
+
+ # Step: I run max-client.py 5 5 6
+ step = Step()
+ step.set_kind('when')
+ step.set_text(decode_str('SSBydW4gbWF4LWNsaWVudC5weSA1IDUgNg=='))
+ step.set_function(runcmd_step)
+ if '':
+ step.set_cleanup()
+ self._scenario.append_step(step)
+ name = decode_str('YXJndjA=')
+ text = decode_str('bWF4LWNsaWVudC5weQ==')
+ step.set_arg(name, text)
+ name = decode_str('YXJncw==')
+ text = decode_str('IDUgNSA2')
+ step.set_arg(name, text)
+
+ # Step: answer is 6
+ step = Step()
+ step.set_kind('then')
+ step.set_text(decode_str('YW5zd2VyIGlzIDY='))
+ step.set_function(answer_is)
+ if '':
+ step.set_cleanup()
+ self._scenario.append_step(step)
+ name = decode_str('aW5kZXg=')
+ text = decode_str('Ng==')
+ step.set_arg(name, text)
+
+ # Step: I run max-client.py 5 6 5
+ step = Step()
+ step.set_kind('when')
+ step.set_text(decode_str('SSBydW4gbWF4LWNsaWVudC5weSA1IDYgNQ=='))
+ step.set_function(runcmd_step)
+ if '':
+ step.set_cleanup()
+ self._scenario.append_step(step)
+ name = decode_str('YXJndjA=')
+ text = decode_str('bWF4LWNsaWVudC5weQ==')
+ step.set_arg(name, text)
+ name = decode_str('YXJncw==')
+ text = decode_str('IDUgNiA1')
+ step.set_arg(name, text)
+
+ # Step: answer is 6
+ step = Step()
+ step.set_kind('then')
+ step.set_text(decode_str('YW5zd2VyIGlzIDY='))
+ step.set_function(answer_is)
+ if '':
+ step.set_cleanup()
+ self._scenario.append_step(step)
+ name = decode_str('aW5kZXg=')
+ text = decode_str('Ng==')
+ step.set_arg(name, text)
+
+ # Step: I run max-client.py 6 5 5
+ step = Step()
+ step.set_kind('when')
+ step.set_text(decode_str('SSBydW4gbWF4LWNsaWVudC5weSA2IDUgNQ=='))
+ step.set_function(runcmd_step)
+ if '':
+ step.set_cleanup()
+ self._scenario.append_step(step)
+ name = decode_str('YXJndjA=')
+ text = decode_str('bWF4LWNsaWVudC5weQ==')
+ step.set_arg(name, text)
+ name = decode_str('YXJncw==')
+ text = decode_str('IDYgNSA1')
+ step.set_arg(name, text)
+
+ # Step: answer is 6
+ step = Step()
+ step.set_kind('then')
+ step.set_text(decode_str('YW5zd2VyIGlzIDY='))
+ step.set_function(answer_is)
+ if '':
+ step.set_cleanup()
+ self._scenario.append_step(step)
+ name = decode_str('aW5kZXg=')
+ text = decode_str('Ng==')
+ step.set_arg(name, text)
+
+ # Step: I run max-client.py 5 6 7
+ step = Step()
+ step.set_kind('when')
+ step.set_text(decode_str('SSBydW4gbWF4LWNsaWVudC5weSA1IDYgNw=='))
+ step.set_function(runcmd_step)
+ if '':
+ step.set_cleanup()
+ self._scenario.append_step(step)
+ name = decode_str('YXJndjA=')
+ text = decode_str('bWF4LWNsaWVudC5weQ==')
+ step.set_arg(name, text)
+ name = decode_str('YXJncw==')
+ text = decode_str('IDUgNiA3')
+ step.set_arg(name, text)
+
+ # Step: answer is 7
+ step = Step()
+ step.set_kind('then')
+ step.set_text(decode_str('YW5zd2VyIGlzIDc='))
+ step.set_function(answer_is)
+ if '':
+ step.set_cleanup()
+ self._scenario.append_step(step)
+ name = decode_str('aW5kZXg=')
+ text = decode_str('Nw==')
+ step.set_arg(name, text)
+
+ # Step: I run max-client.py 5 7 6
+ step = Step()
+ step.set_kind('when')
+ step.set_text(decode_str('SSBydW4gbWF4LWNsaWVudC5weSA1IDcgNg=='))
+ step.set_function(runcmd_step)
+ if '':
+ step.set_cleanup()
+ self._scenario.append_step(step)
+ name = decode_str('YXJndjA=')
+ text = decode_str('bWF4LWNsaWVudC5weQ==')
+ step.set_arg(name, text)
+ name = decode_str('YXJncw==')
+ text = decode_str('IDUgNyA2')
+ step.set_arg(name, text)
+
+ # Step: answer is 7
+ step = Step()
+ step.set_kind('then')
+ step.set_text(decode_str('YW5zd2VyIGlzIDc='))
+ step.set_function(answer_is)
+ if '':
+ step.set_cleanup()
+ self._scenario.append_step(step)
+ name = decode_str('aW5kZXg=')
+ text = decode_str('Nw==')
+ step.set_arg(name, text)
+
+ # Step: I run max-client.py 6 5 7
+ step = Step()
+ step.set_kind('when')
+ step.set_text(decode_str('SSBydW4gbWF4LWNsaWVudC5weSA2IDUgNw=='))
+ step.set_function(runcmd_step)
+ if '':
+ step.set_cleanup()
+ self._scenario.append_step(step)
+ name = decode_str('YXJndjA=')
+ text = decode_str('bWF4LWNsaWVudC5weQ==')
+ step.set_arg(name, text)
+ name = decode_str('YXJncw==')
+ text = decode_str('IDYgNSA3')
+ step.set_arg(name, text)
+
+ # Step: answer is 7
+ step = Step()
+ step.set_kind('then')
+ step.set_text(decode_str('YW5zd2VyIGlzIDc='))
+ step.set_function(answer_is)
+ if '':
+ step.set_cleanup()
+ self._scenario.append_step(step)
+ name = decode_str('aW5kZXg=')
+ text = decode_str('Nw==')
+ step.set_arg(name, text)
+
+ # Step: I run max-client.py 6 7 5
+ step = Step()
+ step.set_kind('when')
+ step.set_text(decode_str('SSBydW4gbWF4LWNsaWVudC5weSA2IDcgNQ=='))
+ step.set_function(runcmd_step)
+ if '':
+ step.set_cleanup()
+ self._scenario.append_step(step)
+ name = decode_str('YXJndjA=')
+ text = decode_str('bWF4LWNsaWVudC5weQ==')
+ step.set_arg(name, text)
+ name = decode_str('YXJncw==')
+ text = decode_str('IDYgNyA1')
+ step.set_arg(name, text)
+
+ # Step: answer is 7
+ step = Step()
+ step.set_kind('then')
+ step.set_text(decode_str('YW5zd2VyIGlzIDc='))
+ step.set_function(answer_is)
+ if '':
+ step.set_cleanup()
+ self._scenario.append_step(step)
+ name = decode_str('aW5kZXg=')
+ text = decode_str('Nw==')
+ step.set_arg(name, text)
+
+ # Step: I run max-client.py 7 5 6
+ step = Step()
+ step.set_kind('when')
+ step.set_text(decode_str('SSBydW4gbWF4LWNsaWVudC5weSA3IDUgNg=='))
+ step.set_function(runcmd_step)
+ if '':
+ step.set_cleanup()
+ self._scenario.append_step(step)
+ name = decode_str('YXJndjA=')
+ text = decode_str('bWF4LWNsaWVudC5weQ==')
+ step.set_arg(name, text)
+ name = decode_str('YXJncw==')
+ text = decode_str('IDcgNSA2')
+ step.set_arg(name, text)
+
+ # Step: answer is 7
+ step = Step()
+ step.set_kind('then')
+ step.set_text(decode_str('YW5zd2VyIGlzIDc='))
+ step.set_function(answer_is)
+ if '':
+ step.set_cleanup()
+ self._scenario.append_step(step)
+ name = decode_str('aW5kZXg=')
+ text = decode_str('Nw==')
+ step.set_arg(name, text)
+
+ # Step: I run max-client.py 7 6 5
+ step = Step()
+ step.set_kind('when')
+ step.set_text(decode_str('SSBydW4gbWF4LWNsaWVudC5weSA3IDYgNQ=='))
+ step.set_function(runcmd_step)
+ if '':
+ step.set_cleanup()
+ self._scenario.append_step(step)
+ name = decode_str('YXJndjA=')
+ text = decode_str('bWF4LWNsaWVudC5weQ==')
+ step.set_arg(name, text)
+ name = decode_str('YXJncw==')
+ text = decode_str('IDcgNiA1')
+ step.set_arg(name, text)
+
+ # Step: answer is 7
+ step = Step()
+ step.set_kind('then')
+ step.set_text(decode_str('YW5zd2VyIGlzIDc='))
+ step.set_function(answer_is)
+ if '':
+ step.set_cleanup()
+ self._scenario.append_step(step)
+ name = decode_str('aW5kZXg=')
+ text = decode_str('Nw==')
+ step.set_arg(name, text)
+
+
+ def get_title(self):
+ return self._scenario.get_title()
+
+ def run(self, datadir, extra_env):
+ self._scenario.run(datadir, extra_env)
+
+
+_scenarios = {
+ Scenario_1(),
+ Scenario_2(),
+ Scenario_3(),
+}
+
+
+#############################################################################
+# Call main function and clean up.
+main(_scenarios)