From 3873f55d64b9ba41547acf4e3257baec721a5990 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 19 Mar 2017 17:35:05 +0100 Subject: [PATCH] make Input tests pass, clarify error cases, cleanups --- docs/api.md | 53 ++--- docs/state-machines/input.dot | 2 +- src/wormhole/_code.py | 7 +- src/wormhole/_input.py | 165 +++++++++++++--- src/wormhole/_lister.py | 22 ++- src/wormhole/_rendezvous.py | 10 +- src/wormhole/_wordlist.py | 4 +- src/wormhole/errors.py | 10 + src/wormhole/test/test_machines.py | 301 +++++++++++++++++++++++++---- 9 files changed, 461 insertions(+), 113 deletions(-) diff --git a/docs/api.md b/docs/api.md index 6417406..656a94f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -181,42 +181,49 @@ The code-entry Helper object has the following API: "4" in "4-purple-sausages"). Note that they are unicode strings (so "4", not 4). The Helper will get the response in the background, and calls to `get_nameplate_completions()` after the response will use the new list. + Calling this after `h.choose_nameplate` will raise + `AlreadyChoseNameplateError`. * `completions = h.get_nameplate_completions(prefix)`: returns - (synchronously) a list of suffixes for the given nameplate prefix. For + (synchronously) a set of suffixes for the given nameplate prefix. For example, if the server reports nameplates 1, 12, 13, 24, and 170 are in - use, `get_nameplate_completions("1")` will return `["", "2", "3", "70"]`. - Raises `AlreadyClaimedNameplateError` if called after `h.choose_nameplate`. -* `h.choose_nameplate(nameplate)`: accepts a string with the chosen nameplate. - May only be called once, after which `OnlyOneNameplateError` is raised. (in - this future, this might return a Deferred that fires (with None) when the - nameplate's wordlist is known (which happens after the nameplate is - claimed, requiring a roundtrip to the server)). + use, `get_nameplate_completions("1")` will return `{"", "2", "3", "70"}`. + You may want to sort these before displaying them to the user. Raises + `AlreadyChoseNameplateError` if called after `h.choose_nameplate`. +* `h.choose_nameplate(nameplate)`: accepts a string with the chosen + nameplate. May only be called once, after which + `AlreadyChoseNameplateError` is raised. (in this future, this might + return a Deferred that fires (with None) when the nameplate's wordlist is + known (which happens after the nameplate is claimed, requiring a roundtrip + to the server)). * `completions = h.get_word_completions(prefix)`: return (synchronously) a - list of suffixes for the given words prefix. The possible completions - depend upon the wordlist in use for the previously-claimed nameplate, so - calling this before `choose_nameplate` will raise - `MustClaimNameplateFirstError`. Given a prefix like "su", this returns a - list of strings which are appropriate to append to the prefix (e.g. - `["pportive", "rrender", "spicious"]`, for expansion into "supportive", - "surrender", and "suspicious". The prefix should not include the nameplate, - but *should* include whatever words and hyphens have been typed so far (the - default wordlist uses alternate lists, where even numbered words have three + set of suffixes for the given words prefix. The possible completions depend + upon the wordlist in use for the previously-claimed nameplate, so calling + this before `choose_nameplate` will raise `MustChooseNameplateFirstError`. + Calling this after `h.choose_words()` will raise `AlreadyChoseWordsError`. + Given a prefix like "su", this returns a set of strings which are + appropriate to append to the prefix (e.g. `{"pportive", "rrender", + "spicious"}`, for expansion into "supportive", "surrender", and + "suspicious". The prefix should not include the nameplate, but *should* + include whatever words and hyphens have been typed so far (the default + wordlist uses alternate lists, where even numbered words have three syllables, and odd numbered words have two, so the completions depend upon how many words are present, not just the partial last word). E.g. - `get_word_completions("pr")` will return `["ocessor", "ovincial", - "oximate"]`, while `get_word_completions("opulent-pr")` will return - `["eclude", "efer", "eshrunk", "inter", "owler"]`. If the wordlist is not - yet known, this returns an empty list. It will also return an empty list if + `get_word_completions("pr")` will return `{"ocessor", "ovincial", + "oximate"}`, while `get_word_completions("opulent-pr")` will return + `{"eclude", "efer", "eshrunk", "inter", "owler"}`. If the wordlist is not + yet known, this returns an empty set. It will also return an empty set if the prefix is complete (the last word matches something in the completion list, and there are no longer extension words), although the code may not yet be complete if there are additional words. The completions will never - include a hyphen: the UI frontend must supply these if desired. + include a hyphen: the UI frontend must supply these if desired. The + frontend is also responsible for sorting the results before display. * `h.choose_words(words)`: call this when the user is finished typing in the code. It does not return anything, but will cause the Wormhole's `w.when_code()` (or corresponding delegate) to fire, and triggers the wormhole connection process. This accepts a string like "purple-sausages", without the nameplate. It must be called after `h.choose_nameplate()` or - `MustClaimNameplateFirstError` will be raised. + `MustChooseNameplateFirstError` will be raised. May only be called once, + after which `AlreadyChoseWordsError` is raised. The `rlcompleter` wrapper is a function that knows how to use the code-entry helper to do tab completion of wormhole codes: diff --git a/docs/state-machines/input.dot b/docs/state-machines/input.dot index 0902988..580d2b9 100644 --- a/docs/state-machines/input.dot +++ b/docs/state-machines/input.dot @@ -36,7 +36,7 @@ digraph { S4 [label="S4: done" color="green"] S4 -> S4 [label="got_nameplates\ngot_wordlist"] - other [shape="box" style="dotted" + other [shape="box" style="dotted" color="orange" fontcolor="orange" label="h.refresh_nameplates()\nh.get_nameplate_completions(prefix)\nh.choose_nameplate(nameplate)\nh.get_word_completions(prefix)\nh.choose_words(words)" ] {rank=same; S4 other} diff --git a/src/wormhole/_code.py b/src/wormhole/_code.py index 2787e12..d19a9ee 100644 --- a/src/wormhole/_code.py +++ b/src/wormhole/_code.py @@ -35,7 +35,7 @@ class Code(object): @m.input() def allocate_code(self, length, wordlist): pass @m.input() - def input_code(self, input_helper): pass + def input_code(self): pass @m.input() def set_code(self, code): pass @@ -57,8 +57,8 @@ class Code(object): self._B.got_code(code) @m.output() - def do_start_input(self, input_helper): - self._I.start(input_helper) + def do_start_input(self): + return self._I.start() @m.output() def do_middle_input(self, nameplate): self._N.set_nameplate(nameplate) @@ -72,6 +72,7 @@ class Code(object): self._A.allocate(length, wordlist) @m.output() def do_finish_allocate(self, nameplate, code): + assert code.startswith(nameplate+"-"), (nameplate, code) self._N.set_nameplate(nameplate) self._K.got_code(code) self._B.got_code(code) diff --git a/src/wormhole/_input.py b/src/wormhole/_input.py index 858ad9a..bd16a43 100644 --- a/src/wormhole/_input.py +++ b/src/wormhole/_input.py @@ -3,7 +3,10 @@ from zope.interface import implementer from attr import attrs, attrib from attr.validators import provides from automat import MethodicalMachine -from . import _interfaces +from . import _interfaces, errors + +def first(outputs): + return list(outputs)[0] @attrs @implementer(_interfaces.IInput) @@ -14,6 +17,7 @@ class Input(object): def set_trace(): pass # pragma: no cover def __attrs_post_init__(self): + self._all_nameplates = set() self._nameplate = None self._wordlist = None @@ -34,11 +38,11 @@ class Input(object): # from Code @m.input() - def start(self, input_helper): pass + def start(self): pass # from Lister @m.input() - def got_nameplates(self, nameplates): pass + def got_nameplates(self, all_nameplates): pass # from Nameplate @m.input() @@ -48,62 +52,163 @@ class Input(object): @m.input() def refresh_nameplates(self): pass @m.input() + def get_nameplate_completions(self, prefix): pass + @m.input() def choose_nameplate(self, nameplate): pass @m.input() + def get_word_completions(self, prefix): pass + @m.input() def choose_words(self, words): pass @m.output() - def do_start(self, input_helper): - self._input_helper = input_helper - self._L.refresh_nameplates() + def do_start(self): + self._L.refresh() + return Helper(self) @m.output() def do_refresh(self): - self._L.refresh_nameplates() + self._L.refresh() @m.output() - def do_nameplate(self, nameplate): + def record_nameplates(self, all_nameplates): + # we get a set of nameplate id strings + self._all_nameplates = all_nameplates + @m.output() + def _get_nameplate_completions(self, prefix): + lp = len(prefix) + completions = set() + for nameplate in self._all_nameplates: + if nameplate.startswith(prefix): + completions.add(nameplate[lp:]) + return completions + @m.output() + def record_all_nameplates(self, nameplate): self._nameplate = nameplate self._C.got_nameplate(nameplate) @m.output() - def do_wordlist(self, wordlist): + def record_wordlist(self, wordlist): self._wordlist = wordlist + @m.output() + def no_word_completions(self, prefix): + return set() + @m.output() + def _get_word_completions(self, prefix): + assert self._wordlist + return self._wordlist.get_completions(prefix) + + @m.output() + def raise_must_choose_nameplate1(self, prefix): + raise errors.MustChooseNameplateFirstError() + @m.output() + def raise_must_choose_nameplate2(self, words): + raise errors.MustChooseNameplateFirstError() + @m.output() + def raise_already_chose_nameplate1(self): + raise errors.AlreadyChoseNameplateError() + @m.output() + def raise_already_chose_nameplate2(self, prefix): + raise errors.AlreadyChoseNameplateError() + @m.output() + def raise_already_chose_nameplate3(self, nameplate): + raise errors.AlreadyChoseNameplateError() + @m.output() + def raise_already_chose_words1(self, prefix): + raise errors.AlreadyChoseWordsError() + @m.output() + def raise_already_chose_words2(self, words): + raise errors.AlreadyChoseWordsError() + @m.output() def do_words(self, words): code = self._nameplate + "-" + words self._C.finished_input(code) - S0_idle.upon(start, enter=S1_typing_nameplate, outputs=[do_start]) + S0_idle.upon(start, enter=S1_typing_nameplate, + outputs=[do_start], collector=first) + S1_typing_nameplate.upon(got_nameplates, enter=S1_typing_nameplate, + outputs=[record_nameplates]) + # too early for got_wordlist, should never happen S1_typing_nameplate.upon(refresh_nameplates, enter=S1_typing_nameplate, outputs=[do_refresh]) + S1_typing_nameplate.upon(get_nameplate_completions, + enter=S1_typing_nameplate, + outputs=[_get_nameplate_completions], + collector=first) S1_typing_nameplate.upon(choose_nameplate, enter=S2_typing_code_no_wordlist, - outputs=[do_nameplate]) - S2_typing_code_no_wordlist.upon(got_wordlist, - enter=S3_typing_code_yes_wordlist, - outputs=[do_wordlist]) - S2_typing_code_no_wordlist.upon(choose_words, enter=S4_done, - outputs=[do_words]) + outputs=[record_all_nameplates]) + S1_typing_nameplate.upon(get_word_completions, + enter=S1_typing_nameplate, + outputs=[raise_must_choose_nameplate1]) + S1_typing_nameplate.upon(choose_words, enter=S1_typing_nameplate, + outputs=[raise_must_choose_nameplate2]) + S2_typing_code_no_wordlist.upon(got_nameplates, enter=S2_typing_code_no_wordlist, outputs=[]) - S3_typing_code_yes_wordlist.upon(choose_words, enter=S4_done, - outputs=[do_words]) + S2_typing_code_no_wordlist.upon(got_wordlist, + enter=S3_typing_code_yes_wordlist, + outputs=[record_wordlist]) + S2_typing_code_no_wordlist.upon(refresh_nameplates, + enter=S2_typing_code_no_wordlist, + outputs=[raise_already_chose_nameplate1]) + S2_typing_code_no_wordlist.upon(get_nameplate_completions, + enter=S2_typing_code_no_wordlist, + outputs=[raise_already_chose_nameplate2]) + S2_typing_code_no_wordlist.upon(choose_nameplate, + enter=S2_typing_code_no_wordlist, + outputs=[raise_already_chose_nameplate3]) + S2_typing_code_no_wordlist.upon(get_word_completions, + enter=S2_typing_code_no_wordlist, + outputs=[no_word_completions], + collector=first) + S2_typing_code_no_wordlist.upon(choose_words, enter=S4_done, + outputs=[do_words]) + S3_typing_code_yes_wordlist.upon(got_nameplates, enter=S3_typing_code_yes_wordlist, outputs=[]) + # got_wordlist: should never happen + S3_typing_code_yes_wordlist.upon(refresh_nameplates, + enter=S3_typing_code_yes_wordlist, + outputs=[raise_already_chose_nameplate1]) + S3_typing_code_yes_wordlist.upon(get_nameplate_completions, + enter=S3_typing_code_yes_wordlist, + outputs=[raise_already_chose_nameplate2]) + S3_typing_code_yes_wordlist.upon(choose_nameplate, + enter=S3_typing_code_yes_wordlist, + outputs=[raise_already_chose_nameplate3]) + S3_typing_code_yes_wordlist.upon(get_word_completions, + enter=S3_typing_code_yes_wordlist, + outputs=[_get_word_completions], + collector=first) + S3_typing_code_yes_wordlist.upon(choose_words, enter=S4_done, + outputs=[do_words]) + S4_done.upon(got_nameplates, enter=S4_done, outputs=[]) S4_done.upon(got_wordlist, enter=S4_done, outputs=[]) + S4_done.upon(refresh_nameplates, + enter=S4_done, + outputs=[raise_already_chose_nameplate1]) + S4_done.upon(get_nameplate_completions, + enter=S4_done, + outputs=[raise_already_chose_nameplate2]) + S4_done.upon(choose_nameplate, enter=S4_done, + outputs=[raise_already_chose_nameplate3]) + S4_done.upon(get_word_completions, enter=S4_done, + outputs=[raise_already_chose_words1]) + S4_done.upon(choose_words, enter=S4_done, + outputs=[raise_already_chose_words2]) - # methods for the CodeInputHelper to use - #refresh_nameplates/_choose_nameplate/choose_words: @m.input methods +# we only expose the Helper to application code, not _Input +@attrs +class Helper(object): + _input = attrib() + def refresh_nameplates(self): + self._input.refresh_nameplates() def get_nameplate_completions(self, prefix): - lp = len(prefix) - completions = [] - for nameplate in self._nameplates: - if nameplate.startswith(prefix): - completions.append(nameplate[lp:]) - return completions - + return self._input.get_nameplate_completions(prefix) + def choose_nameplate(self, nameplate): + self._input.choose_nameplate(nameplate) def get_word_completions(self, prefix): - if self._wordlist: - return self._wordlist.get_completions(prefix) - return [] + return self._input.get_word_completions(prefix) + def choose_words(self, words): + self._input.choose_words(words) diff --git a/src/wormhole/_lister.py b/src/wormhole/_lister.py index 8b49d91..a58ecf6 100644 --- a/src/wormhole/_lister.py +++ b/src/wormhole/_lister.py @@ -1,10 +1,11 @@ from __future__ import print_function, absolute_import, unicode_literals from zope.interface import implementer -from attr import attrib +from attr import attrs, attrib from attr.validators import provides from automat import MethodicalMachine from . import _interfaces +@attrs @implementer(_interfaces.ILister) class Lister(object): _timing = attrib(validator=provides(_interfaces.ITiming)) @@ -39,32 +40,35 @@ class Lister(object): @m.input() def lost(self): pass @m.input() - def refresh_nameplates(self): pass + def refresh(self): pass @m.input() - def rx_nameplates(self, message): pass + def rx_nameplates(self, all_nameplates): pass @m.output() def RC_tx_list(self): self._RC.tx_list() @m.output() - def I_got_nameplates(self, message): - self._I.got_nameplates(message["nameplates"]) + def I_got_nameplates(self, all_nameplates): + # We get a set of nameplate ids. There may be more attributes in the + # future: change RendezvousConnector._response_handle_nameplates to + # get them + self._I.got_nameplates(all_nameplates) S0A_idle_disconnected.upon(connected, enter=S0B_idle_connected, outputs=[]) S0B_idle_connected.upon(lost, enter=S0A_idle_disconnected, outputs=[]) - S0A_idle_disconnected.upon(refresh_nameplates, + S0A_idle_disconnected.upon(refresh, enter=S1A_wanting_disconnected, outputs=[]) - S1A_wanting_disconnected.upon(refresh_nameplates, + S1A_wanting_disconnected.upon(refresh, enter=S1A_wanting_disconnected, outputs=[]) S1A_wanting_disconnected.upon(connected, enter=S1B_wanting_connected, outputs=[RC_tx_list]) - S0B_idle_connected.upon(refresh_nameplates, enter=S1B_wanting_connected, + S0B_idle_connected.upon(refresh, enter=S1B_wanting_connected, outputs=[RC_tx_list]) S0B_idle_connected.upon(rx_nameplates, enter=S0B_idle_connected, outputs=[I_got_nameplates]) S1B_wanting_connected.upon(lost, enter=S1A_wanting_disconnected, outputs=[]) - S1B_wanting_connected.upon(refresh_nameplates, enter=S1B_wanting_connected, + S1B_wanting_connected.upon(refresh, enter=S1B_wanting_connected, outputs=[RC_tx_list]) S1B_wanting_connected.upon(rx_nameplates, enter=S0B_idle_connected, outputs=[I_got_nameplates]) diff --git a/src/wormhole/_rendezvous.py b/src/wormhole/_rendezvous.py index 0568fac..2ed6192 100644 --- a/src/wormhole/_rendezvous.py +++ b/src/wormhole/_rendezvous.py @@ -147,6 +147,7 @@ class RendezvousConnector(object): self._N.connected() self._M.connected() self._L.connected() + self._A.connected() except Exception as e: self._B.error(e) raise @@ -186,6 +187,7 @@ class RendezvousConnector(object): self._N.lost() self._M.lost() self._L.lost() + self._A.lost() # internal def _stopped(self, res): @@ -207,17 +209,19 @@ class RendezvousConnector(object): def _response_handle_allocated(self, msg): nameplate = msg["nameplate"] assert isinstance(nameplate, type("")), type(nameplate) - self._C.rx_allocated(nameplate) + self._A.rx_allocated(nameplate) def _response_handle_nameplates(self, msg): + # we get list of {id: ID}, with maybe more attributes in the future nameplates = msg["nameplates"] assert isinstance(nameplates, list), type(nameplates) - nids = [] + nids = set() for n in nameplates: assert isinstance(n, dict), type(n) nameplate_id = n["id"] assert isinstance(nameplate_id, type("")), type(nameplate_id) - nids.append(nameplate_id) + nids.add(nameplate_id) + # deliver a set of nameplate ids self._L.rx_nameplates(nids) def _response_handle_ack(self, msg): diff --git a/src/wormhole/_wordlist.py b/src/wormhole/_wordlist.py index 6c266a6..71d3f1a 100644 --- a/src/wormhole/_wordlist.py +++ b/src/wormhole/_wordlist.py @@ -165,10 +165,10 @@ class PGPWordList(object): words = even_words_lowercase last_partial_word = prefix.split("-")[-1] lp = len(last_partial_word) - completions = [] + completions = set() for word in words: if word.startswith(prefix): - completions.append(word[lp:]) + completions.add(word[lp:]) return completions def choose_words(self, length): diff --git a/src/wormhole/errors.py b/src/wormhole/errors.py index d8f8fee..419605f 100644 --- a/src/wormhole/errors.py +++ b/src/wormhole/errors.py @@ -57,6 +57,16 @@ class NoKeyError(WormholeError): class OnlyOneCodeError(WormholeError): """Only one w.generate_code/w.set_code/w.input_code may be called""" +class MustChooseNameplateFirstError(WormholeError): + """The InputHelper was asked to do get_word_completions() or + choose_words() before the nameplate was chosen.""" +class AlreadyChoseNameplateError(WormholeError): + """The InputHelper was asked to do get_nameplate_completions() after + choose_nameplate() was called, or choose_nameplate() was called a second + time.""" +class AlreadyChoseWordsError(WormholeError): + """The InputHelper was asked to do get_word_completions() after + choose_words() was called, or choose_words() was called a second time.""" class WormholeClosed(Exception): """Deferred-returning API calls errback with WormholeClosed if the wormhole was already closed, or if it closes before a real result can be diff --git a/src/wormhole/test/test_machines.py b/src/wormhole/test/test_machines.py index b412344..3380b86 100644 --- a/src/wormhole/test/test_machines.py +++ b/src/wormhole/test/test_machines.py @@ -1,14 +1,23 @@ from __future__ import print_function, unicode_literals import json -from zope.interface import directlyProvides +from zope.interface import directlyProvides, implementer from twisted.trial import unittest -from .. import timing, _order, _receive, _key, _code +from .. import errors, timing, _order, _receive, _key, _code, _lister, _input, _allocator from .._interfaces import (IKey, IReceive, IBoss, ISend, IMailbox, - IRendezvousConnector, ILister) + IRendezvousConnector, ILister, IInput, IAllocator, + INameplate, ICode, IWordlist) from .._key import derive_key, derive_phase_key, encrypt_data from ..util import dict_to_bytes, hexstr_to_bytes, bytes_to_hexstr, to_bytes from spake2 import SPAKE2_Symmetric +@implementer(IWordlist) +class FakeWordList(object): + def choose_words(self, length): + return "-".join(["word"] * length) + def get_completions(self, prefix): + self._get_completions_prefix = prefix + return self._completions + class Dummy: def __init__(self, name, events, iface, *meths): self.name = name @@ -194,52 +203,260 @@ class Code(unittest.TestCase): events = [] c = _code.Code(timing.DebugTiming()) b = Dummy("b", events, IBoss, "got_code") + a = Dummy("a", events, IAllocator, "allocate") + n = Dummy("n", events, INameplate, "set_nameplate") + k = Dummy("k", events, IKey, "got_code") + i = Dummy("i", events, IInput, "start") + c.wire(b, a, n, k, i) + return c, b, a, n, k, i, events + + def test_set_code(self): + c, b, a, n, k, i, events = self.build() + c.set_code(u"1-code") + self.assertEqual(events, [("n.set_nameplate", u"1"), + ("k.got_code", u"1-code"), + ("b.got_code", u"1-code"), + ]) + + def test_allocate_code(self): + c, b, a, n, k, i, events = self.build() + wl = FakeWordList() + c.allocate_code(2, wl) + self.assertEqual(events, [("a.allocate", 2, wl)]) + events[:] = [] + c.allocated("1", "1-code") + self.assertEqual(events, [("n.set_nameplate", u"1"), + ("k.got_code", u"1-code"), + ("b.got_code", u"1-code"), + ]) + + def test_input_code(self): + c, b, a, n, k, i, events = self.build() + c.input_code() + self.assertEqual(events, [("i.start",)]) + events[:] = [] + c.got_nameplate("1") + self.assertEqual(events, [("n.set_nameplate", u"1"), + ]) + events[:] = [] + c.finished_input("1-code") + self.assertEqual(events, [("k.got_code", u"1-code"), + ("b.got_code", u"1-code"), + ]) + +class Input(unittest.TestCase): + def build(self): + events = [] + i = _input.Input(timing.DebugTiming()) + c = Dummy("c", events, ICode, "got_nameplate", "finished_input") + l = Dummy("l", events, ILister, "refresh") + i.wire(c, l) + return i, c, l, events + + def test_ignore_completion(self): + i, c, l, events = self.build() + helper = i.start() + self.assertIsInstance(helper, _input.Helper) + self.assertEqual(events, [("l.refresh",)]) + events[:] = [] + with self.assertRaises(errors.MustChooseNameplateFirstError): + helper.choose_words("word-word") + helper.choose_nameplate("1") + self.assertEqual(events, [("c.got_nameplate", "1")]) + events[:] = [] + with self.assertRaises(errors.AlreadyChoseNameplateError): + helper.choose_nameplate("2") + helper.choose_words("word-word") + with self.assertRaises(errors.AlreadyChoseWordsError): + helper.choose_words("word-word") + self.assertEqual(events, [("c.finished_input", "1-word-word")]) + + def test_with_completion(self): + i, c, l, events = self.build() + helper = i.start() + self.assertIsInstance(helper, _input.Helper) + self.assertEqual(events, [("l.refresh",)]) + events[:] = [] + helper.refresh_nameplates() + self.assertEqual(events, [("l.refresh",)]) + events[:] = [] + with self.assertRaises(errors.MustChooseNameplateFirstError): + helper.get_word_completions("prefix") + i.got_nameplates({"1", "12", "34", "35", "367"}) + self.assertEqual(helper.get_nameplate_completions(""), + {"1", "12", "34", "35", "367"}) + self.assertEqual(helper.get_nameplate_completions("1"), + {"", "2"}) + self.assertEqual(helper.get_nameplate_completions("2"), set()) + self.assertEqual(helper.get_nameplate_completions("3"), + {"4", "5", "67"}) + helper.choose_nameplate("34") + with self.assertRaises(errors.AlreadyChoseNameplateError): + helper.refresh_nameplates() + with self.assertRaises(errors.AlreadyChoseNameplateError): + helper.get_nameplate_completions("1") + self.assertEqual(events, [("c.got_nameplate", "34")]) + events[:] = [] + # no wordlist yet + self.assertEqual(helper.get_word_completions(""), set()) + wl = FakeWordList() + i.got_wordlist(wl) + wl._completions = {"bc", "bcd", "e"} + self.assertEqual(helper.get_word_completions("a"), wl._completions) + self.assertEqual(wl._get_completions_prefix, "a") + with self.assertRaises(errors.AlreadyChoseNameplateError): + helper.refresh_nameplates() + with self.assertRaises(errors.AlreadyChoseNameplateError): + helper.get_nameplate_completions("1") + helper.choose_words("word-word") + with self.assertRaises(errors.AlreadyChoseWordsError): + helper.get_word_completions("prefix") + with self.assertRaises(errors.AlreadyChoseWordsError): + helper.choose_words("word-word") + self.assertEqual(events, [("c.finished_input", "34-word-word")]) + + + +class Lister(unittest.TestCase): + def build(self): + events = [] + l = _lister.Lister(timing.DebugTiming()) + rc = Dummy("rc", events, IRendezvousConnector, "tx_list") + i = Dummy("i", events, IInput, "got_nameplates") + l.wire(rc, i) + return l, rc, i, events + + def test_connect_first(self): + l, rc, i, events = self.build() + l.connected() + l.lost() + l.connected() + self.assertEqual(events, []) + l.refresh() + self.assertEqual(events, [("rc.tx_list",), + ]) + events[:] = [] + l.rx_nameplates({"1", "2", "3"}) + self.assertEqual(events, [("i.got_nameplates", {"1", "2", "3"}), + ]) + events[:] = [] + # now we're satisfied: disconnecting and reconnecting won't ask again + l.lost() + l.connected() + self.assertEqual(events, []) + + # but if we're told to refresh, we'll do so + l.refresh() + self.assertEqual(events, [("rc.tx_list",), + ]) + + def test_connect_first_ask_twice(self): + l, rc, i, events = self.build() + l.connected() + self.assertEqual(events, []) + l.refresh() + l.refresh() + self.assertEqual(events, [("rc.tx_list",), + ("rc.tx_list",), + ]) + l.rx_nameplates({"1", "2", "3"}) + self.assertEqual(events, [("rc.tx_list",), + ("rc.tx_list",), + ("i.got_nameplates", {"1", "2", "3"}), + ]) + l.rx_nameplates({"1" ,"2", "3", "4"}) + self.assertEqual(events, [("rc.tx_list",), + ("rc.tx_list",), + ("i.got_nameplates", {"1", "2", "3"}), + ("i.got_nameplates", {"1", "2", "3", "4"}), + ]) + + def test_reconnect(self): + l, rc, i, events = self.build() + l.refresh() + l.connected() + self.assertEqual(events, [("rc.tx_list",), + ]) + events[:] = [] + l.lost() + l.connected() + self.assertEqual(events, [("rc.tx_list",), + ]) + + def test_refresh_first(self): + l, rc, i, events = self.build() + l.refresh() + self.assertEqual(events, []) + l.connected() + self.assertEqual(events, [("rc.tx_list",), + ]) + l.rx_nameplates({"1", "2", "3"}) + self.assertEqual(events, [("rc.tx_list",), + ("i.got_nameplates", {"1", "2", "3"}), + ]) + + def test_unrefreshed(self): + l, rc, i, events = self.build() + self.assertEqual(events, []) + # we receive a spontaneous rx_nameplates, without asking + l.connected() + self.assertEqual(events, []) + l.rx_nameplates({"1", "2", "3"}) + self.assertEqual(events, [("i.got_nameplates", {"1", "2", "3"}), + ]) + +class Allocator(unittest.TestCase): + def build(self): + events = [] + a = _allocator.Allocator(timing.DebugTiming()) rc = Dummy("rc", events, IRendezvousConnector, "tx_allocate") - l = Dummy("l", events, ILister, "refresh_nameplates") - c.wire(b, rc, l) - return c, b, rc, l, events + c = Dummy("c", events, ICode, "allocated") + a.wire(rc, c) + return a, rc, c, events - def test_set_disconnected(self): - c, b, rc, l, events = self.build() - c.set_code(u"code") - self.assertEqual(events, [("b.got_code", u"code")]) - - def test_set_connected(self): - c, b, rc, l, events = self.build() - c.connected() - c.set_code(u"code") - self.assertEqual(events, [("b.got_code", u"code")]) - - def test_allocate_disconnected(self): - c, b, rc, l, events = self.build() - c.allocate_code(2) + def test_no_allocation(self): + a, rc, c, events = self.build() + a.connected() self.assertEqual(events, []) - c.connected() - self.assertEqual(events, [("rc.tx_allocate",)]) - events[:] = [] - c.lost() + + def test_allocate_first(self): + a, rc, c, events = self.build() + a.allocate(2, FakeWordList()) self.assertEqual(events, []) - c.connected() + a.connected() self.assertEqual(events, [("rc.tx_allocate",)]) events[:] = [] - c.rx_allocated("4") - self.assertEqual(len(events), 1, events) - self.assertEqual(events[0][0], "b.got_code") - code = events[0][1] - self.assert_(code.startswith("4-"), code) + a.lost() + a.connected() + self.assertEqual(events, [("rc.tx_allocate",), + ]) + events[:] = [] + a.rx_allocated("1") + self.assertEqual(events, [("c.allocated", "1", "1-word-word"), + ]) - def test_allocate_connected(self): - c, b, rc, l, events = self.build() - c.connected() - c.allocate_code(2) + def test_connect_first(self): + a, rc, c, events = self.build() + a.connected() + self.assertEqual(events, []) + a.allocate(2, FakeWordList()) self.assertEqual(events, [("rc.tx_allocate",)]) events[:] = [] - c.rx_allocated("4") - self.assertEqual(len(events), 1, events) - self.assertEqual(events[0][0], "b.got_code") - code = events[0][1] - self.assert_(code.startswith("4-"), code) - - # TODO: input_code - + a.lost() + a.connected() + self.assertEqual(events, [("rc.tx_allocate",), + ]) + events[:] = [] + a.rx_allocated("1") + self.assertEqual(events, [("c.allocated", "1", "1-word-word"), + ]) +# TODO +# Send +# Mailbox +# Nameplate +# Terminator +# Boss +# RendezvousConnector (not a state machine) +# #Input: exercise helper methods +# wordlist