Grammars with Sapi5SharedEngine containing rules with <n> do not load.
This was initially discussed on Gitter, summarized here and discovered during testing synkarius/caster#305.

Mapping like '[use] function <n\> [<text\>]' with rules containing `<n>` don't load on windows speech recognition.  Dragonfly's `get_engine` method uses `Sapi5SharedEngine`. WSR module loader uses `Sapi5InProcEngine` which loads `sample.py` loads properly.  

[t4ngo/dragonfly#3](https://github.com/t4ngo/dragonfly/issues/3) appears to be the rational behind having two engine classes for it. Sapi5InProcEngine doesn't seem to need you to start WSR at all, but doesn't have any UI components.

To replicate `E:\NatLink\NatLink\MacroSystem> python .\sample.py`
[sample.zip](https://github.com/Danesprite/dragonfly/files/2531662/sample.zip)
```
 Traceback (most recent call last):
  File ".\sample.py", line 81, in <module>
    grammar.load()
  File "C:\Python27\lib\site-packages\dragonfly\grammar\grammar_base.py", line 338, in load
    self._engine.load_grammar(self)
  File "C:\Python27\lib\site-packages\dragonfly\engines\base\engine.py", line 136, in load_grammar
    wrapper = self._load_grammar(grammar)
  File "C:\Python27\lib\site-packages\dragonfly\engines\backend_sapi5\engine.py", line 124, in _load_grammar
    handle.CmdSetRuleState(rule.name, constants.SGDSActive)
  File "C:\Python27\lib\site-packages\win32com\gen_py\C866CA3A-32F7-11D2-9602-00C04F8EE628x0x5x4\ISpeechRecoGrammar.py", line 67, in CmdSetRuleState
    , State)
pywintypes.com_error: (-2147352567, 'Exception occurred.', (0, None, None, None, 0, -2147200940), None)
```

diff --git a/documentation/engines.txt b/documentation/engines.txt
index e65eeb2..649733e 100644
--- a/documentation/engines.txt
+++ b/documentation/engines.txt
@@ -53,3 +53,14 @@ Dictation container classes
 
 .. automodule:: dragonfly.engines.backend_natlink.dictation
    :members:
+
+
+Engine timer classes
+----------------------------------------------------------------------------
+
+.. automodule:: dragonfly.engines.base.timer
+   :members: Timer, TimerManagerBase, ThreadedTimerManager
+   :private-members:
+
+.. automodule:: dragonfly.engines.backend_natlink.timer
+   :members:
diff --git a/documentation/test_recobs_doctest.txt b/documentation/test_recobs_doctest.txt
index 584094d..f37cff3 100644
--- a/documentation/test_recobs_doctest.txt
+++ b/documentation/test_recobs_doctest.txt
@@ -3,10 +3,8 @@ RecognitionObserver base class
 ==========================================================================
 
 .. note:: :class:`RecognitionObserver` instances can be used for both
-   the DNS and the WSR backend engines.  However, WSR does not offer
-   access to the words recognized by a different context, and therefore
-   the :meth:`RecognitionObservers.on_recognition` will always
-   be called with ``words = False``.
+   the DNS and the WSR backend engines.  However, WSR will sometimes call
+   observer methods multiple times, so be careful using observers with it.
 
 Test fixture initialization::
 
diff --git a/documentation/tests.txt b/documentation/tests.txt
index e4284c0..8b16a0a 100644
--- a/documentation/tests.txt
+++ b/documentation/tests.txt
@@ -75,6 +75,13 @@ Follow the following steps to run the test suite for the WSR backend:
         cd <dir>
         python setup.py test --test-suite=dragonfly.test.suites.sapi5_suite
 
+You can also run the test suite for the in process engine class. It has no
+GUI for the moment. Run the tests with the following commands:
+
+    .. code:: shell
+
+        cd <dir>
+        python setup.py test --test-suite=dragonfly.test.suites.sapi5inproc_suite
 
 Using the text-input engine
 ----------------------------------------------------------------------------
diff --git a/dragonfly/engines/__init__.py b/dragonfly/engines/__init__.py
index 6db8eba..ea35f8c 100644
--- a/dragonfly/engines/__init__.py
+++ b/dragonfly/engines/__init__.py
@@ -78,13 +78,13 @@ def get_engine(name=None):
             if name:
                 raise EngineError(message)
 
-    if not name or name == "sapi5":
+    if not name or name in ["sapi5shared", "sapi5inproc", "sapi5"]:
         # Attempt to retrieve the sapi5 back-end.
         try:
             from .backend_sapi5 import is_engine_available
             from .backend_sapi5 import get_engine as get_specific_engine
-            if is_engine_available():
-                _default_engine = get_specific_engine()
+            if is_engine_available(name):
+                _default_engine = get_specific_engine(name)
                 _engines_by_name["sapi5"] = _default_engine
                 return _default_engine
         except Exception as e:
diff --git a/dragonfly/engines/backend_natlink/timer.py b/dragonfly/engines/backend_natlink/timer.py
index abe272d..d4d378c 100644
--- a/dragonfly/engines/backend_natlink/timer.py
+++ b/dragonfly/engines/backend_natlink/timer.py
@@ -31,6 +31,22 @@ from ..base import TimerManagerBase
 
 class NatlinkTimerManager(TimerManagerBase):
     """
+    Timer manager for the Natlink engine.
+
+    This class utilises :meth:`natlink.setTimerCallback` to ensure that
+    timer functions are called on-time regardless of Dragon's current
+    status.
+
+    Python code run outside of timer functions will be blocked when
+    ``natlink`` functions are executing. This is a
+    `limitation with Python threads
+    <https://docs.python.org/3/c-api/init.html#thread-state-and-the-global-interpreter-lock>`__.
+
+    :meth:`engine.connect` must be called before using timers with this
+    manager.
+
+    **Note**: long-running timers will block dragonfly from processing
+    what was said, so be careful with how you use them!
     """
 
     def __init__(self, interval, engine):
diff --git a/dragonfly/engines/backend_sapi5/__init__.py b/dragonfly/engines/backend_sapi5/__init__.py
index 8e2190a..ebf8c97 100644
--- a/dragonfly/engines/backend_sapi5/__init__.py
+++ b/dragonfly/engines/backend_sapi5/__init__.py
@@ -19,9 +19,24 @@
 #
 
 """
-SR back-end package for SAPI 5
+SR back-end package for WSR and SAPI 5
 ============================================================================
 
+The WSR / SAPI 5 back-end has two engine classes:
+
+* `sapi5inproc` - engine class for SAPI 5 in process recognizer. This is the
+  default implementation and has no GUI (yet). :meth:`get_engine` will
+  return an instance of this class if the ``name`` parameter is ``None``
+  (default) or ``"sapi5inproc"``. It is recommended that you run this from
+  command-line.
+
+* `sapi5shared` - engine class for SAPI 5 shared recognizer. This
+  implementation uses the Windows Speech Recognition GUI. This
+  implementation's behaviour can be inconsistent and a little buggy at
+  times, which is why it is no longer the default. To use it anyway
+  pass ``"sapi5"`` or ``"sapi5shared"`` to :meth:`get_engine`.
+
+
 """
 
 import logging
@@ -34,8 +49,13 @@ _log = logging.getLogger("engine.sapi5")
 _engine = None
 
 
-def is_engine_available():
-    """ Check whether SAPI is available. """
+def is_engine_available(name):
+    """
+        Check whether SAPI is available.
+
+        :param name: optional human-readable name of the engine to return.
+        :type name: str
+    """
     global _engine
     if _engine:
         return True
@@ -55,9 +75,16 @@ def is_engine_available():
                    "installed?" % e)
         return False
 
-    # Attempt to connect to SAPI.
+    # Attempt to connect to SAPI using the correct dispatch name.
+    from .engine import Sapi5Engine, Sapi5InProcEngine
+
+    # Use the in-process engine by default, otherwise use the shared engine.
+    if not name or name == "sapi5inproc":
+        dispatch_name = Sapi5InProcEngine.recognizer_dispatch_name
+    else:
+        dispatch_name = Sapi5Engine.recognizer_dispatch_name
     try:
-        Dispatch("SAPI.SpSharedRecognizer")
+        Dispatch(dispatch_name)
     except com_error as e:
         _log.exception("COM error during dispatch: %s" % e)
         return False
@@ -67,10 +94,19 @@ def is_engine_available():
     return True
 
 
-def get_engine():
-    """ Retrieve the Sapi5 back-end engine object. """
+def get_engine(name):
+    """
+        Retrieve the Sapi5 back-end engine object.
+
+        :param name: optional human-readable name of the engine to return.
+        :type name: str
+    """
     global _engine
     if not _engine:
-        from .engine import Sapi5Engine
-        _engine = Sapi5Engine()
+        from .engine import Sapi5Engine, Sapi5InProcEngine
+        # Use the in-process engine by default.
+        if not name or name == "sapi5inproc":
+            _engine = Sapi5InProcEngine()
+        else:
+            _engine = Sapi5Engine()
     return _engine
diff --git a/dragonfly/engines/backend_sapi5/compiler.py b/dragonfly/engines/backend_sapi5/compiler.py
index 2220b4a..35c24ee 100644
--- a/dragonfly/engines/backend_sapi5/compiler.py
+++ b/dragonfly/engines/backend_sapi5/compiler.py
@@ -77,6 +77,14 @@ class Sapi5Compiler(CompilerBase):
 
         return grammar_handle
 
+    def _get_next_rule_id(self, grammar):
+        # Generate a new unique rule ID within this grammar.
+        if not hasattr(grammar, "_sapi5_next_rule_id"):
+            grammar._sapi5_next_rule_id = 1
+        rule_id = grammar._sapi5_next_rule_id
+        grammar._sapi5_next_rule_id += 1
+        return rule_id
+
     def _compile_rule(self, rule, grammar, grammar_handle):
         self._log.debug("%s: Compiling rule %s." % (self, rule.name))
 
@@ -86,18 +94,13 @@ class Sapi5Compiler(CompilerBase):
             self._log.debug("%s: Already compiled rule %s." % (self, rule.name))
             return
 
-        # Generate a new unique rule ID within this grammar.
-        if not hasattr(grammar, "_sapi5_next_rule_id"):
-            grammar._sapi5_next_rule_id = 1
-        rule_id = grammar._sapi5_next_rule_id
-        grammar._sapi5_next_rule_id += 1
-
         # Determine the flags to set when adding this rule.
         flags = 0
         if rule.exported:
             flags |= constants.SRATopLevel
 
         # Add this rule, and compile its root element.
+        rule_id = self._get_next_rule_id(grammar)
         rule_handle = grammar_handle.Rules.Add(rule.name, flags, rule_id)
         self._log.debug("%s: Compiling rule %r (id %r)." % (self, rule_handle.Name, rule_handle.Id))
         self.compile_element(rule.element, rule_handle.InitialState, None, grammar, grammar_handle)
@@ -175,7 +178,9 @@ class Sapi5Compiler(CompilerBase):
         if not rule_handle:
             grammar.add_list(element.list)
             flags = constants.SRADynamic
-            rule_handle = grammar_handle.Rules.Add(list_rule_name, flags, 0)
+            rule_id = self._get_next_rule_id(grammar)
+            rule_handle = grammar_handle.Rules.Add(list_rule_name, flags,
+                                                   rule_id)
         src_state.AddRuleTransition(dst_state, rule_handle)
 
     @trace_compile
@@ -192,14 +197,13 @@ class Sapi5Compiler(CompilerBase):
         """
         rule_handle = grammar_handle.Rules.FindRule("dgndictation")
         if rule_handle:
-#            self._log.error("%s: dictation rule already present." % self)
             return rule_handle
-#        self._log.error("%s: building dictation rule ." % self)
 
         flags = 0
-#        flags = constants.SRADynamic
-        rule_handle = grammar_handle.Rules.Add("dgndictation", flags, 0)
-#        grammar.add_rule(
+        # flags = constants.SRADynamic
+        rule_id = self._get_next_rule_id(grammar)
+        rule_handle = grammar_handle.Rules.Add("dgndictation", flags,
+                                               rule_id)
 
         src_state = rule_handle.InitialState
         dst_state = None
diff --git a/dragonfly/engines/backend_sapi5/engine.py b/dragonfly/engines/backend_sapi5/engine.py
index c327f04..e615cba 100644
--- a/dragonfly/engines/backend_sapi5/engine.py
+++ b/dragonfly/engines/backend_sapi5/engine.py
@@ -27,22 +27,23 @@ SAPI 5 engine classes
 
 #---------------------------------------------------------------------------
 
+import logging
 import time
+
 from six import string_types, integer_types
 
 import win32con
 from ctypes import *
-
 from win32com.client           import Dispatch, getevents, constants
 from win32com.client.gencache  import EnsureDispatch
-from pywintypes                import com_error
 
-from ..base                    import EngineBase, EngineError
+from ..base                    import (EngineBase, EngineError,
+                                       MimicFailure, ThreadedTimerManager)
 from .compiler                 import Sapi5Compiler
 from .dictation                import Sapi5DictationContainer
 from .recobs                   import Sapi5RecObsManager
-#from .timer                    import NatlinkTimerManager
 from ...grammar.state          import State
+from ...grammar.recobs         import RecognitionObserver
 from ...windows.window         import Window
 
 
@@ -61,13 +62,28 @@ class MSG(Structure):
                 ('pt', POINT)]
 
 
+class MimicObserver(RecognitionObserver):
+    _log = logging.getLogger("SAPI5 RecObs")
+
+    def __init__(self):
+        self.status = "none"
+
+    def on_recognition(self, words):
+        self._log.debug("SAPI5 RecObs on_recognition(): %r" % (words,))
+        self.status = "recognition: %r" % (words,)
+
+    def on_failure(self):
+        self._log.debug("SAPI5 RecObs on_failure()")
+        self.status = "failure"
+
+
 #===========================================================================
 
 class Sapi5SharedEngine(EngineBase):
     """ Speech recognition engine back-end for SAPI 5 shared recognizer. """
 
     _name = "sapi5shared"
-    _recognizer_dispatch_name = "SAPI.SpSharedRecognizer"
+    recognizer_dispatch_name = "SAPI.SpSharedRecognizer"
     DictationContainer = Sapi5DictationContainer
 
     #-----------------------------------------------------------------------
@@ -75,18 +91,18 @@ class Sapi5SharedEngine(EngineBase):
     def __init__(self):
         EngineBase.__init__(self)
 
-        EnsureDispatch(self._recognizer_dispatch_name)
+        EnsureDispatch(self.recognizer_dispatch_name)
         EnsureDispatch("SAPI.SpVoice")
         self._recognizer  = None
         self._speaker     = None
         self._compiler    = None
 
         self._recognition_observer_manager = Sapi5RecObsManager(self)
-#        self._timer_manager = NatlinkTimerManager(0.02, self)
+        self._timer_manager = ThreadedTimerManager(0.02, self)
 
     def connect(self):
         """ Connect to back-end SR engine. """
-        self._recognizer  = Dispatch(self._recognizer_dispatch_name)
+        self._recognizer  = Dispatch(self.recognizer_dispatch_name)
         self._speaker     = Dispatch("SAPI.SpVoice")
         self._compiler    = Sapi5Compiler()
 
@@ -117,7 +133,8 @@ class Sapi5SharedEngine(EngineBase):
         #  the grammar wrapper object for managing this grammar.
         context = self._recognizer.CreateRecoContext()
         handle = self._compiler.compile_grammar(grammar, context)
-        wrapper = GrammarWrapper(grammar, handle, context, self)
+        wrapper = GrammarWrapper(grammar, handle, context, self,
+                                 self._recognition_observer_manager)
 
         handle.State = constants.SGSEnabled
         for rule in grammar.rules:
@@ -189,48 +206,69 @@ class Sapi5SharedEngine(EngineBase):
 
     def mimic(self, words):
         """ Mimic a recognition of the given *words*. """
+        self._log.debug("SAPI5 mimic: %r" % (words,))
         if isinstance(words, string_types):
             phrase = words
         else:
             phrase = " ".join(words)
-        self._recognizer.EmulateRecognition(phrase)
 
-    def speak(self, text):
-        """ Speak the given *text* using text-to-speech. """
-        self._speaker.Speak(text)
+        # Register a recognition observer for checking the success of this
+        # mimic.
+        observer = MimicObserver()
+        observer.register()
 
-    def _get_language(self):
-        return "en"
-
-    def wait_for_recognition(self, timeout=None):
+        # Emulate recognition of the phrase and wait for recognition to
+        # finish, timing out after 2 seconds.
+        self._recognizer.EmulateRecognition(phrase)
+        timeout = 2
         NULL = c_int(win32con.NULL)
         if timeout != None:
             begin_time = time.time()
-            timed_out = False
             windll.user32.SetTimer(NULL, NULL, int(timeout * 1000), NULL)
     
         message = MSG()
         message_pointer = pointer(message)
-
         while (not timeout) or (time.time() - begin_time < timeout):
-            self._log.error("loop")
+            if timeout:
+                self._log.debug("SAPI5 message loop: %s sec left"
+                                % (timeout + begin_time - time.time()))
+            else:
+                self._log.debug("SAPI5 message loop: no timeout")
+
             if windll.user32.GetMessageW(message_pointer, NULL, 0, 0) == 0:
                 msg = str(WinError())
                 self._log.error("GetMessageW() failed: %s" % msg)
                 raise EngineError("GetMessageW() failed: %s" % msg)
 
+            self._log.debug("SAPI5 message: %r" % (message.message,))
             if message.message == win32con.WM_TIMER:
-                self._log.error("loop, timeout")
                 # A timer message means this loop has timed out.
-                timed_out = True
+                self._log.debug("SAPI5 message loop timed out: %s sec left"
+                                % (timeout + begin_time - time.time()))
                 break
             else:
-                self._log.error("loop, dispatch")
                 # Process other messages as normal.
+                self._log.debug("SAPI5 message translating and dispatching.")
                 windll.user32.TranslateMessage(message_pointer)
                 windll.user32.DispatchMessageW(message_pointer)
 
-        return not timed_out
+            if observer.status.startswith("recognition:"):
+                # The previous message was a recognition which matched.
+                self._log.debug("SAPI5 message caused recognition.")
+
+        # Unregister the observer and check its status.
+        observer.unregister()
+        if observer.status == "failure":
+            raise MimicFailure("Mimic failed.")
+        elif observer.status == "none":
+            raise MimicFailure("Mimic failed, nothing happened.")
+
+    def speak(self, text):
+        """ Speak the given *text* using text-to-speech. """
+        self._speaker.Speak(text)
+
+    def _get_language(self):
+        return "en"
 
 
 #---------------------------------------------------------------------------
@@ -250,7 +288,7 @@ class Sapi5InProcEngine(Sapi5SharedEngine):
     """
 
     _name = "sapi5inproc"
-    _recognizer_dispatch_name = "SAPI.SpInProcRecognizer"
+    recognizer_dispatch_name = "SAPI.SpInProcRecognizer"
 
     def connect(self, audio_source=0):
         """
@@ -342,11 +380,12 @@ def collection_iter(collection):
 
 class GrammarWrapper(object):
 
-    def __init__(self, grammar, handle, context, engine):
+    def __init__(self, grammar, handle, context, engine, recobs_manager):
         self.grammar = grammar
         self.handle = handle
         self.engine = engine
         self.context = context
+        self.recobs_manager = recobs_manager
 
         # Register callback functions which will handle recognizer events.
         base = getevents("SAPI.SpSharedRecoContext")
@@ -368,7 +407,6 @@ class GrammarWrapper(object):
         try:
             newResult = Dispatch(Result)
             phrase_info = newResult.PhraseInfo
-            rule_name = phrase_info.Rule.Name
 
             #---------------------------------------------------------------
             # Build a list of rule names for each element.
@@ -406,10 +444,17 @@ class GrammarWrapper(object):
 
             results = []
             rule_set = list(set(rule_names))
+
             elements = phrase_info.Elements
             for index in range(len(rule_names)):
                 element = elements.Item(index)
                 rule_id = rule_set.index(rule_names[index])
+
+                # Map dictation rule IDs to 1M so that dragonfly recognizes
+                # the words as dictation.
+                if rule_names[index] == "dgndictation":
+                    rule_id = 1000000
+
                 replacement = replacements[index]
                 info = [element.LexicalForm, rule_id,
                         element.DisplayText, element.DisplayAttributes,
@@ -420,18 +465,22 @@ class GrammarWrapper(object):
             # Attempt to parse the recognition.
 
             func = getattr(self.grammar, "process_recognition", None)
+            words = tuple([r[0] for r in results])
             if func:
-                words = [r[2] for r in results]
                 if not func(words):
                     return
 
             s = State(results, rule_set, self.engine)
-            for r in self.grammar._rules:
-                if r.name != rule_name:
+            for r in self.grammar.rules:
+                if not r.active:
                     continue
+
                 s.initialize_decoding()
                 for result in r.decode(s):
                     if s.finished():
+                        # Notify recognition observers, then process the
+                        # rule.
+                        self.recobs_manager.notify_recognition(words)
                         root = s.build_parse_tree()
                         r.process_recognition(root)
                         return
@@ -450,16 +499,16 @@ class GrammarWrapper(object):
                                   [r[0] for r in results]))
 
     def recognition_other_callback(self, StreamNumber, StreamPosition):
-            func = getattr(self.grammar, "process_recognition_other", None)
-            if func:
-                # Note that SAPI 5.3 doesn't offer access to the actual
-                #  recognition contents during a
-                #  OnRecognitionForOtherContext event.
-                func(words=False)
-            return
+        func = getattr(self.grammar, "process_recognition_other", None)
+        if func:
+            # Note that SAPI 5.3 doesn't offer access to the actual
+            #  recognition contents during a
+            #  OnRecognitionForOtherContext event.
+            func(words=False)
+        return
 
     def recognition_failure_callback(self, StreamNumber, StreamPosition, Result):
-            func = getattr(self.grammar, "process_recognition_failure", None)
-            if func:
-                func()
-            return
+        func = getattr(self.grammar, "process_recognition_failure", None)
+        if func:
+            func()
+        return
diff --git a/dragonfly/engines/backend_sapi5/recobs.py b/dragonfly/engines/backend_sapi5/recobs.py
index 5deccbf..3026d08 100644
--- a/dragonfly/engines/backend_sapi5/recobs.py
+++ b/dragonfly/engines/backend_sapi5/recobs.py
@@ -36,17 +36,13 @@ class Sapi5RecObsManager(RecObsManagerBase):
 
     def __init__(self, engine):
         RecObsManagerBase.__init__(self, engine)
-        self._grammar = None
+        self._grammar = Sapi5RecObsGrammar(self)
 
     def _activate(self):
-        if not self._grammar:
-            self._grammar = Sapi5RecObsGrammar(self)
         self._grammar.load()
 
     def _deactivate(self):
-        if self._grammar:
-            self._grammar.unload()
-        self._grammar = None
+        self._grammar.unload()
 
 
 #---------------------------------------------------------------------------
@@ -63,16 +59,17 @@ class Sapi5RecObsGrammar(Grammar):
 
     #-----------------------------------------------------------------------
     # Callback methods for handling utterances and recognitions.
+    # This grammar does not receive recognized words due to a limitation
+    # with WSR. The engine instead notifies observers by using the grammar
+    # that successfully recognized the words, if any words were recognized.
 
     def process_begin(self, executable, title, handle):
         self._manager.notify_begin()
 
     def process_recognition(self, words):
+        # Successfully recognising this grammar should be impossible.
         raise RuntimeError("Recognition observer received an unexpected"
                            " recognition: %s" % (words,))
 
-    def process_recognition_other(self, words):
-        self._manager.notify_recognition(words)
-
     def process_recognition_failure(self):
         self._manager.notify_failure()
diff --git a/dragonfly/engines/backend_text/engine.py b/dragonfly/engines/backend_text/engine.py
index 59ae50d..1c684c8 100644
--- a/dragonfly/engines/backend_text/engine.py
+++ b/dragonfly/engines/backend_text/engine.py
@@ -25,8 +25,8 @@ from dragonfly import Window
 
 from .dictation import TextDictationContainer
 from .recobs import TextRecobsManager
-from .timer import TextTimerManager
-from ..base import EngineBase, EngineError, MimicFailure
+from ..base import (EngineBase, EngineError, MimicFailure,
+                    ThreadedTimerManager)
 
 
 def _map_word(word):
@@ -51,21 +51,14 @@ class TextInputEngine(EngineBase):
         EngineBase.__init__(self)
         self._language = "en"
         self._recognition_observer_manager = TextRecobsManager(self)
-        self._timer_manager = TextTimerManager(0.02, self)
-        self._connected = False
+        self._timer_manager = ThreadedTimerManager(0.02, self)
 
     def connect(self):
-        self._connected = True
+        pass
 
     def disconnect(self):
         # Clear grammar wrappers on disconnect()
         self._grammar_wrappers.clear()
-        self._connected = False
-
-    @property
-    def connected(self):
-        """ Whether :meth:`connect` has been called. """
-        return self._connected
 
     # -----------------------------------------------------------------------
     # Methods for administrating timers.
@@ -75,12 +68,9 @@ class TextInputEngine(EngineBase):
         Create and return a timer using the specified callback and repeat
         interval.
 
-        Timers created using this engine will only be run if
-        :meth:`engine.connect` is called beforehand. :meth:`threading.Timer`
-        may be used instead with no blocking issues.
-
         Timers created using this engine will be run in a separate daemon
         thread, meaning that their callbacks will **not** be thread safe.
+        :meth:`threading.Timer` may be used instead with no blocking issues.
         """
         return EngineBase.create_timer(self, callback, interval)
 
diff --git a/dragonfly/engines/backend_text/timer.py b/dragonfly/engines/backend_text/timer.py
deleted file mode 100644
index 32695b3..0000000
--- a/dragonfly/engines/backend_text/timer.py
+++ /dev/null
@@ -1,66 +0,0 @@
-#
-# This file is part of Dragonfly.
-# (c) Copyright 2018 by Dane Finlay
-# Licensed under the LGPL.
-#
-#   Dragonfly is free software: you can redistribute it and/or modify it
-#   under the terms of the GNU Lesser General Public License as published
-#   by the Free Software Foundation, either version 3 of the License, or
-#   (at your option) any later version.
-#
-#   Dragonfly is distributed in the hope that it will be useful, but
-#   WITHOUT ANY WARRANTY; without even the implied warranty of
-#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-#   Lesser General Public License for more details.
-#
-#   You should have received a copy of the GNU Lesser General Public
-#   License along with Dragonfly.  If not, see
-#   <http://www.gnu.org/licenses/>.
-#
-
-"""
-Multiplexing interface for the text input engine
-=============================================================================
-
-"""
-
-import time
-
-from threading import Thread
-
-from ..base import TimerManagerBase
-
-
-# ---------------------------------------------------------------------------
-
-
-class TextTimerManager(TimerManagerBase):
-    """
-    Timer manager for the text input engine.
-    """
-
-    def __init__(self, interval, engine):
-        TimerManagerBase.__init__(self, interval, engine)
-        self._running = False
-        self._thread = None
-
-    def _activate_main_callback(self, callback, sec):
-        # Do nothing if the thread is already running or if the engine is not
-        # connected.
-        if self._running or not self.engine.connected:
-            return
-
-        def run():
-            while self._running:
-                time.sleep(sec)
-                callback()
-
-        self._running = True
-        self._thread = Thread(target=run)
-        self._thread.setDaemon(True)
-        self._thread.start()
-
-    def _deactivate_main_callback(self):
-        self._running = False
-        self._thread.join()
-        self._thread = None
diff --git a/dragonfly/engines/base/__init__.py b/dragonfly/engines/base/__init__.py
index b15f8a0..a69914b 100644
--- a/dragonfly/engines/base/__init__.py
+++ b/dragonfly/engines/base/__init__.py
@@ -23,4 +23,4 @@ from .engine       import EngineBase, EngineError, MimicFailure
 from .compiler     import CompilerBase, CompilerError
 from .dictation    import DictationContainerBase
 from .recobs       import RecObsManagerBase
-from .timer        import TimerManagerBase
+from .timer        import TimerManagerBase, ThreadedTimerManager
diff --git a/dragonfly/engines/base/timer.py b/dragonfly/engines/base/timer.py
index 1184a2a..fd822bb 100644
--- a/dragonfly/engines/base/timer.py
+++ b/dragonfly/engines/base/timer.py
@@ -27,10 +27,25 @@ Multiplexing interface to a timer
 import time
 import logging
 
+from threading import Thread
 
 #---------------------------------------------------------------------------
 
 class Timer(object):
+    """
+    Timer class for calling a function every N seconds.
+
+    Constructor arguments:
+
+     - *function* (*callable*) -- the function to call every N seconds. Must
+       have no required arguments.
+     - *interval* (*float*) -- number of seconds between calls to the
+       function.
+     - *manager* (:class:`TimerManagerBase`) -- engine timer manager instance.
+
+    Instances of this class are normally initialised from
+    :meth:`engine.create_timer`.
+    """
 
     _log = logging.getLogger("engine.timer")
 
@@ -43,6 +58,11 @@ class Timer(object):
         self.start()
 
     def start(self):
+        """
+        Start calling the timer's function on an interval.
+
+        This method is called on initialisation.
+        """
         if self.active:
             return
         self.manager.add_timer(self)
@@ -50,6 +70,7 @@ class Timer(object):
         self.next_time = time.time() + self.interval
 
     def stop(self):
+        """ Stop calling the timer's function on an interval. """
         if not self.active:
             return
         self.manager.remove_timer(self)
@@ -57,6 +78,11 @@ class Timer(object):
         self.next_time = None
 
     def call(self):
+        """
+        Call the timer's function.
+
+        This method is normally called by the timer manager.
+        """
         self.next_time += self.interval
         try:
             self.function()
@@ -65,25 +91,24 @@ class Timer(object):
 
 
 class TimerManagerBase(object):
-    """
-    """
+    """ Base timer manager class. """
 
     _log = logging.getLogger("engine.timer")
 
     def __init__(self, interval, engine):
-        """
-        """
         self.interval = interval
         self.engine = engine
         self.timers = []
 
     def add_timer(self, timer):
+        """ Add a timer and activate the main callback if required. """
         self.timers.append(timer)
         if len(self.timers) == 1:
             self._activate_main_callback(self.main_callback,
                                          self.interval)
 
     def remove_timer(self, timer):
+        """ Remove a timer and deactivate the main callback if required. """
         try:
             self.timers.remove(timer)
         except Exception as e:
@@ -93,6 +118,7 @@ class TimerManagerBase(object):
             self._deactivate_main_callback()
 
     def main_callback(self):
+        """ Method to call each timer's function when required. """
         now = time.time()
         for c in self.timers:
             if c.next_time < now:
@@ -103,9 +129,58 @@ class TimerManagerBase(object):
                                         " timer callback: %s" % (e,))
 
     def _activate_main_callback(self, callback, msec):
+        """
+        Virtual method to implement to start calling :meth:`main_callback`
+        on an interval.
+        """
         raise NotImplementedError("_activate_main_callback() not implemented for"
                                   " %s class." % self.__class__.__name__)
 
     def _deactivate_main_callback(self):
+        """
+        Virtual method to implement to stop calling :meth:`main_callback` on
+        an interval.
+        """
         raise NotImplementedError("_deactivate_main_callback() not implemented for"
                                   " %s class." % self.__class__.__name__)
+
+
+class ThreadedTimerManager(TimerManagerBase):
+    """
+    Timer manager class using a daemon thread.
+
+    This class is used by the "text" and SAPI 5 engines.
+
+    It may be used by any SR engine that doesn't have to contend with
+    Python's multi-threading limitations.
+    """
+
+    def __init__(self, interval, engine):
+        TimerManagerBase.__init__(self, interval, engine)
+        self._running = False
+        self._thread = None
+
+    def _activate_main_callback(self, callback, sec):
+        """"""
+        # Do nothing if the thread is already running.
+        if self._running:
+            return
+
+        def run():
+            while self._running:
+                time.sleep(sec)
+                callback()
+
+        self._running = True
+        self._thread = Thread(target=run)
+        self._thread.setDaemon(True)
+        self._thread.start()
+
+    def _deactivate_main_callback(self):
+        """"""
+        # Stop the thread's main loop and wait until it finishes, timing out
+        # after 5 seconds.
+        self._running = False
+        if self._thread:
+            self._thread.join(timeout=5)
+            self._thread = None
diff --git a/dragonfly/examples/dfly-loader-wsr.py b/dragonfly/examples/dfly-loader-wsr.py
index 4a77205..5340b77 100644
--- a/dragonfly/examples/dfly-loader-wsr.py
+++ b/dragonfly/examples/dfly-loader-wsr.py
@@ -20,7 +20,7 @@ import os.path
 import logging
 import pythoncom
 
-from dragonfly.engines.backend_sapi5.engine import Sapi5InProcEngine
+from dragonfly import RecognitionObserver, get_engine
 from dragonfly.loader import CommandModuleDirectory
 
 
@@ -30,6 +30,22 @@ from dragonfly.loader import CommandModuleDirectory
 logging.basicConfig(level=logging.DEBUG)
 logging.getLogger("compound.parse").setLevel(logging.INFO)
 
+# --------------------------------------------------------------------------
+# Simple recognition observer class.
+
+class Observer(RecognitionObserver):
+    def __init__(self):
+        super(Observer, self).__init__()
+
+    def on_begin(self):
+        print("Speech start detected.")
+
+    def on_recognition(self, words):
+        print(" ".join(words))
+
+    def on_failure(self):
+        print("Sorry, what was that?")
+
 
 #---------------------------------------------------------------------------
 # Main event driving loop.
@@ -46,9 +62,13 @@ def main():
         path = os.getcwd()
         __file__ = os.path.join(path, "dfly-loader-wsr.py")
 
-    engine = Sapi5InProcEngine()
+    engine = get_engine("sapi5inproc")
     engine.connect()
 
+    # Register a recognition observer
+    observer = Observer()
+    observer.register()
+
     directory = CommandModuleDirectory(path, excludes=[__file__])
     directory.load()
 
diff --git a/dragonfly/test/element_tester.py b/dragonfly/test/element_tester.py
index f61f387..d9d8cf3 100644
--- a/dragonfly/test/element_tester.py
+++ b/dragonfly/test/element_tester.py
@@ -71,8 +71,7 @@ class ElementTester(Grammar):
 
             # Mimic recognition.
             try:
-                mimic_method = self._mimic_methods[self.engine.name]
-                mimic_method(self, words)
+                self.engine.mimic(words)
             except MimicFailure as e:
                 self._recognized_value = RecognitionFailure
             except Exception as e:
@@ -103,98 +102,3 @@ class ElementTester(Grammar):
     def _process_recognition(self, node):
         element_node = node.children[0]
         self._recognized_value = element_node.value()
-
-
-    #-----------------------------------------------------------------------
-    # Engine-specific logic.
-
-    _mimic_methods = {}
-
-    def _mimic_words(self, words):
-        self.engine.mimic(words)
-
-    _mimic_methods["natlink"] = _mimic_words
-    _mimic_methods["sphinx"] = _mimic_words
-    _mimic_methods["text"] = _mimic_words
-
-    def _mimic_sapi5(self, words):
-        import time
-        import win32con
-        from ctypes import (windll, pointer, WinError, Structure,
-                            c_int, c_uint, c_long)
-
-        class POINT(Structure):
-            _fields_ = [('x', c_long),
-                        ('y', c_long)]
-
-        class MSG(Structure):
-            _fields_ = [('hwnd', c_int),
-                        ('message', c_uint),
-                        ('wParam', c_int),
-                        ('lParam', c_int),
-                        ('time', c_int),
-                        ('pt', POINT)]
-
-        class Obs(RecognitionObserver):
-            _log = logging.getLogger("SAPI5 RecObs")
-            status = "none"
-            def on_recognition(self, words):
-                self._log.debug("SAPI5 RecObs on_recognition(): %r" % (words,))
-                self.status = "recognition: %r" % (words,)
-            def on_failure(self):
-                self._log.debug("SAPI5 RecObs on_failure()")
-                self.status = "failure"
-        observer = Obs()
-        observer.register()
-
-        self._log.debug("SAPI5 mimic: %r" % (words,))
-        self.engine.mimic(words)
-
-        timeout = 10
-        NULL = c_int(win32con.NULL)
-        if timeout != None:
-            begin_time = time.time()
-            timed_out = False
-            windll.user32.SetTimer(NULL, NULL, int(timeout * 1000), NULL)
-    
-        message = MSG()
-        message_pointer = pointer(message)
-
-        while (not timeout) or (time.time() - begin_time < timeout):
-            if timeout:
-                self._log.debug("SAPI5 message loop: %s sec left" % (timeout + begin_time - time.time()))
-            else:
-                self._log.debug("SAPI5 message loop: no timeout")
-
-            if windll.user32.GetMessageW(message_pointer, NULL, 0, 0) == 0:
-                msg = str(WinError())
-                self._log.error("GetMessageW() failed: %s" % msg)
-                raise EngineError("GetMessageW() failed: %s" % msg)
-
-            self._log.debug("SAPI5 message: %r" % (message.message,))
-            if message.message == win32con.WM_TIMER:
-                # A timer message means this loop has timed out.
-                self._log.debug("SAPI5 message loop timed out: %s sec left" % (timeout + begin_time - time.time()))
-                timed_out = True
-                break
-            else:
-                # Process other messages as normal.
-                self._log.debug("SAPI5 message translating and dispatching.")
-                windll.user32.TranslateMessage(message_pointer)
-                windll.user32.DispatchMessageW(message_pointer)
-
-            if self._recognized_value != self._NotSet:
-                # The previous message was a recognition which matched.
-                self._log.debug("SAPI5 message caused recognition.")
-                break
-
-        observer.unregister()
-
-        if self._recognized_value == self._NotSet:
-            if observer.status == "failure":
-                raise MimicFailure("Mimic failed.")
-            elif observer.status == "none":
-                raise MimicFailure("Mimic failed, nothing happened.")
-
-    _mimic_methods["sapi5"] = _mimic_sapi5
-    _mimic_methods["sapi5shared"] = _mimic_sapi5
diff --git a/dragonfly/test/rule_test_grammar.py b/dragonfly/test/rule_test_grammar.py
index 36e4825..3432e77 100644
--- a/dragonfly/test/rule_test_grammar.py
+++ b/dragonfly/test/rule_test_grammar.py
@@ -66,18 +66,9 @@ class RuleTestGrammar(Grammar):
                 self._log.exception(msg)
                 raise TestError(msg)
 
-            # Retrieve method appropriate for active engine.
-            try:
-                mimic_method = self._mimic_methods[self.engine.name]
-            except KeyError:
-                msg = ("Test infrastructure does not support"
-                       " back-end engine: %r" % (self.engine.name,))
-                self._log.exception(msg)
-                raise TestError(msg)
-
             # Mimic recognition.
             try:
-                mimic_method(self, words)
+                self.engine.mimic(words)
             except MimicFailure as e:
                 msg = "Recognition failed. (Words: %s)" % (words,)
                 self._log.error(msg)
@@ -183,16 +174,3 @@ class RuleTestGrammar(Grammar):
             # Restore original process recognition methods.
             for rule, original_method in patched_rules:
                 rule._process_recognition = original_method
-
-    #-----------------------------------------------------------------------
-    # Engine-specific logic.
-
-    _mimic_methods = {}
-
-    def _mimic_words(self, words):
-        self.engine.mimic(words)
-
-    _mimic_methods["natlink"] = _mimic_words
-    _mimic_methods["sphinx"] = _mimic_words
-    _mimic_methods["text"] = _mimic_words
-
diff --git a/dragonfly/test/suites.py b/dragonfly/test/suites.py
index c42d4a1..f3dd962 100644
--- a/dragonfly/test/suites.py
+++ b/dragonfly/test/suites.py
@@ -71,30 +71,33 @@ natlink_10_names = [
     "test_dictation",
     "test_engine_natlink",
     "doc:documentation/test_word_formatting_v10_doctest.txt",
-] + language_names
+] + common_names + language_names
 
 # Define the tests to run for DNS versions 11 and above.
 natlink_11_names = [
     "test_dictation",
     "test_engine_natlink",
     "doc:documentation/test_word_formatting_v11_doctest.txt",
-] + language_names
+] + common_names + language_names
 
 sapi5_names = [
-    "test_dictation",
     "test_engine_sapi5",
     "test_language_en_number",
-]
+] + common_names
+
+# Don't include recognition observer tests for sapi5 because its quirky
+# behaviour requires separate tests. These are in test_engine_sapi5.
+sapi5_names.remove("doc:documentation/test_recobs_doctest.txt")
 
 sphinx_names = [
     "test_engine_sphinx",
     "test_language_en_number",
-]
+] + common_names
 
 text_names = [
     "test_engine_text",
     "test_engine_text_dictation",
-] + language_names
+] + common_names + language_names
 
 # ==========================================================================
 
@@ -121,12 +124,11 @@ def build_suite(suite, names):
             raise Exception("Invalid test name: %r." % (name,))
     return suite
 
-sapi5_suite      = build_suite(EngineTestSuite("sapi5"),
-                               sapi5_names + common_names)
-sphinx_suite     = build_suite(EngineTestSuite("sphinx"),
-                               sphinx_names + common_names)
-text_suite       = build_suite(EngineTestSuite("text"),
-                               text_names + common_names)
+
+sapi5_suite       = build_suite(EngineTestSuite("sapi5"), sapi5_names)
+sapi5inproc_suite = build_suite(EngineTestSuite("sapi5inproc"), sapi5_names)
+sphinx_suite      = build_suite(EngineTestSuite("sphinx"), sphinx_names)
+text_suite        = build_suite(EngineTestSuite("text"), text_names)
 
 
 # Build the natlink test suite for the current version of DNS.
@@ -150,12 +152,11 @@ else:
 # bug with natlink/Dragon.
 if dns_version and dns_version >= 15:
     lists_doctest = "doc:documentation/test_grammar_list_doctest.txt"
-    common_names.remove(lists_doctest)
+    natlink_names.remove(lists_doctest)
     _log.warning("DNS version %d detected! Excluding test file: %s"
                  % (dns_version, lists_doctest[4:]))
     _log.warning("List functionality doesn't work if used in modules not "
                  "loaded by natlinkmain.")
     _log.warning("Please see: https://github.com/dictation-toolbox/dragonfly/pull/55")
 
-natlink_suite    = build_suite(EngineTestSuite("natlink"),
-                               natlink_names + common_names)
+natlink_suite    = build_suite(EngineTestSuite("natlink"), natlink_names)
diff --git a/dragonfly/test/test_engine_sapi5.py b/dragonfly/test/test_engine_sapi5.py
index ee55b0e..d1d93e0 100644
--- a/dragonfly/test/test_engine_sapi5.py
+++ b/dragonfly/test/test_engine_sapi5.py
@@ -19,17 +19,22 @@
 #
 
 import unittest
-from dragonfly.engines import get_engine, EngineBase, EngineError
+
+from six import text_type, string_types
+
+from dragonfly.engines import get_engine, EngineBase
+from dragonfly.engines.base.dictation import DictationContainerBase
 
 #---------------------------------------------------------------------------
 
+
 class TestEngineSapi5(unittest.TestCase):
 
     def test_get_engine_sapi5_is_usable(self):
         """ Verify that the sapi5 engine is usable. """
         engine = get_engine()
         self.assertTrue(isinstance(engine, EngineBase))
-        self.assertEqual("sapi5", engine.name)
+        self.assertTrue(engine.name.startswith("sapi5"))
 
         engine.speak("testing WSR")
         from dragonfly import Literal, Sequence
@@ -38,3 +43,118 @@ class TestEngineSapi5(unittest.TestCase):
         tester = ElementTester(seq, engine=engine)
         results = tester.recognize("hello world")
         self.assertEqual([u"hello", u"world"], results)
+
+    def test_dictation(self):
+        # Test dictation separately for SAPI5 because test_dictation.py
+        # won't work with it.
+        from dragonfly import Dictation, Literal, Sequence
+        from dragonfly.test import ElementTester, RecognitionFailure
+        seq = Sequence([Literal("hello"), Dictation("text")])
+        tester = ElementTester(seq)
+
+        # Test one word.
+        results = tester.recognize("hello world")
+        assert results[0] == "hello"
+
+        # Verify recognition returned dictation result.
+        dictation = results[1]
+        if not isinstance(dictation, DictationContainerBase):
+            message = (u"Expected recognition result to be a dictation"
+                       u" container, but received %r"
+                       % (repr(dictation).decode("windows-1252"),))
+            self.fail(message.encode("windows-1252"))
+
+        # Verifying dictation converts/encode successfully.
+        self.assertEqual(str(dictation), "world")
+        self.assertEqual(text_type(dictation), "world")
+        self.assertTrue(isinstance(repr(dictation), string_types))
+
+        # Test incomplete.
+        results = tester.recognize("hello")
+        assert results is RecognitionFailure
+
+    def test_recognition_observers(self):
+        # RecognitionObservers are a bit quirky for the sapi5 engines,
+        # so the tests for them are repeated here to handle that.
+        from dragonfly import (Integer, Literal, RecognitionHistory,
+                               RecognitionObserver)
+        from dragonfly.test import ElementTester, RecognitionFailure
+
+        class RecognitionObserverTester(RecognitionObserver):
+            """ RecognitionObserver class from the recobs doctests. """
+
+            def __init__(self):
+                RecognitionObserver.__init__(self)
+                self.waiting = False
+                self.words = None
+
+            def on_begin(self):
+                self.waiting = True
+
+            def on_recognition(self, words):
+                self.waiting = False
+                self.words = words
+
+            def on_failure(self):
+                self.waiting = False
+                self.words = False
+
+        test_recobs = RecognitionObserverTester()
+        test_recobs.register()
+        results = test_recobs.waiting, test_recobs.words
+        assert results == (False, None)
+
+        # Test simple literal element recognitions.
+        test_lit = ElementTester(Literal("hello world"))
+        assert test_lit.recognize("hello world") == "hello world"
+        results = test_recobs.waiting, test_recobs.words
+        assert results == (False, (u'hello', u'world'))
+        assert test_lit.recognize("hello universe") is RecognitionFailure
+        results = test_recobs.waiting, test_recobs.words
+        assert results == (False, False)
+
+        # Test Integer element recognitions
+        test_int = ElementTester(Integer(min=1, max=100))
+        assert test_int.recognize("seven") == 7
+        results = test_recobs.waiting, test_recobs.words
+        assert results == (False, (u'seven',))
+        assert test_int.recognize("forty seven") == 47
+        results = test_recobs.waiting, test_recobs.words
+        assert results == (False, (u'forty', u'seven'))
+        assert test_int.recognize("one hundred") is RecognitionFailure
+        results = test_recobs.waiting, test_recobs.words
+        assert results == (False, False)
+        assert test_lit.recognize("hello world") == u'hello world'
+
+        # Now test RecognitionHistory.
+        history = RecognitionHistory()
+        assert test_lit.recognize("hello world") == u'hello world'
+
+        # Not yet registered, so didn't receive previous recognition.
+        assert history == []
+        history.register()
+        assert test_lit.recognize("hello world") == u'hello world'
+
+        # Now registered, so should have received previous recognition.
+        assert history == [(u'hello', u'world')]
+        assert test_lit.recognize("hello universe") is RecognitionFailure
+
+        # Failed recognitions are ignored, so history is unchanged.
+        assert history == [(u'hello', u'world')]
+        assert test_int.recognize("eighty six") == 86
+        assert history == [(u'hello', u'world'), (u'eighty', u'six')]
+
+        # The RecognitionHistory class allows its maximum length to be set.
+        history = RecognitionHistory(3)
+        history.register()
+        assert history == []
+        for i, word in enumerate(["one", "two", "three", "four", "five"]):
+            assert test_int.recognize(word) == i + 1
+        assert history == [(u'three',), (u'four',), (u'five',)]
+
+        history = RecognitionHistory(1)
+        history.register()
+        assert history == []
+        for i, word in enumerate(["one", "two", "three", "four", "five"]):
+            assert test_int.recognize(word) == i + 1
+        assert history == [(u'five',)]
diff --git a/dragonfly/test/test_timer.py b/dragonfly/test/test_timer.py
index ccfce7f..09b08c9 100644
--- a/dragonfly/test/test_timer.py
+++ b/dragonfly/test/test_timer.py
@@ -65,10 +65,14 @@ class TestTimer(unittest.TestCase):
         # Callback was called one or more times. The engine may or may not
         # have called it already by the time we get here, but getting an
         # exact call count is not required.
-        self.assertTrue(callback_called[0] >= 1)
-        self.assertTrue(len(self.log_capture.records) >= 1)
-        log_message = self.log_capture.records[0].msg
-        self.assertTrue("Exception from timer callback" in log_message)
+        try:
+            self.assertTrue(callback_called[0] >= 1)
+            self.assertTrue(len(self.log_capture.records) >= 1)
+            log_message = self.log_capture.records[0].msg
+            self.assertTrue("Exception from timer callback" in log_message)
+        finally:
+            # Stop the timer at the end regardless of the result.
+            timer.stop()
 
 #===========================================================================
 
['dragonfly/test/rule_test_grammar.py', 'dragonfly/engines/__init__.py', 'documentation/engines.txt', 'dragonfly/engines/backend_natlink/timer.py', 'dragonfly/test/test_engine_sapi5.py', 'dragonfly/test/test_timer.py', 'dragonfly/engines/backend_sapi5/compiler.py', 'dragonfly/examples/dfly-loader-wsr.py', 'dragonfly/engines/base/timer.py', 'dragonfly/engines/backend_text/engine.py', 'dragonfly/engines/backend_sapi5/__init__.py', 'dragonfly/test/suites.py', 'dragonfly/engines/backend_sapi5/engine.py', 'dragonfly/test/element_tester.py', 'dragonfly/engines/base/__init__.py', 'dragonfly/engines/backend_sapi5/recobs.py', 'documentation/test_recobs_doctest.txt', 'documentation/tests.txt']