make Input tests pass, clarify error cases, cleanups
This commit is contained in:
parent
175fef2ab4
commit
3873f55d64
53
docs/api.md
53
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:
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user