[Zope-CVS] SVN: zope.tutorial/trunk/ - Reimplemented tutorial runner. It's much saner now.

Stephan Richter srichter at cosmos.phy.tufts.edu
Fri Nov 25 21:29:55 EST 2005


Log message for revision 40376:
  - Reimplemented tutorial runner. It's much saner now.
  
  - Made sessions non-persistent, since it stores a bunch of information 
    that is or might not be pickable. This is okay, since sessions don't 
    have to survive server restarts.
  
  - Removed some silly, early-design code and its tests.
  
  - Added a bunch of tests for the new code.
  
  

Changed:
  U   zope.tutorial/trunk/browser/configure.zcml
  U   zope.tutorial/trunk/browser/tutorial.py
  U   zope.tutorial/trunk/browser/tutorials-runner.js
  D   zope.tutorial/trunk/cli.py
  U   zope.tutorial/trunk/configure.zcml
  U   zope.tutorial/trunk/interfaces.py
  D   zope.tutorial/trunk/runner.py
  U   zope.tutorial/trunk/sample_tutorial.txt
  A   zope.tutorial/trunk/session.py
  U   zope.tutorial/trunk/session.txt
  U   zope.tutorial/trunk/testbrowser.py
  A   zope.tutorial/trunk/testbrowser.txt
  U   zope.tutorial/trunk/tests.py
  U   zope.tutorial/trunk/tutorial.py

-=-
Modified: zope.tutorial/trunk/browser/configure.zcml
===================================================================
--- zope.tutorial/trunk/browser/configure.zcml	2005-11-25 21:30:39 UTC (rev 40375)
+++ zope.tutorial/trunk/browser/configure.zcml	2005-11-26 02:29:54 UTC (rev 40376)
@@ -53,7 +53,7 @@
 
   <jsonrpc:view
       for="..interfaces.ITutorialSession"
-      methods="getNextStep setCommandResult keepGoing"
+      methods="getCommand addResult keepGoing"
       class=".tutorial.TutorialSession"
       permission="zope.View"
       />

Modified: zope.tutorial/trunk/browser/tutorial.py
===================================================================
--- zope.tutorial/trunk/browser/tutorial.py	2005-11-25 21:30:39 UTC (rev 40375)
+++ zope.tutorial/trunk/browser/tutorial.py	2005-11-26 02:29:54 UTC (rev 40376)
@@ -16,18 +16,11 @@
 $Id$
 """
 __docformat__ = "reStructuredText"
-import thread
-import types
-import time
 import zope.proxy
 
 from jsonserver.jsonrpc import MethodPublisher
 from zope.app.basicskin.standardmacros import StandardMacros
-from zope.app.apidoc.utilities import renderText
 
-from zope.tutorial import testbrowser
-import zope.testbrowser
-
 class TutorialMacros(StandardMacros):
     """Page Template METAL macros for Tutorial"""
     macro_pages = ('runner_macros',)
@@ -42,45 +35,21 @@
 
     def createSession(self):
         name = self.context.createSession()
-        self.context[name].initialize()
+        import zope.proxy
+        zope.proxy.removeAllProxies(self.context[name]).initialize()
         return name
 
     def deleteSession(self, id):
         self.context.deleteSession(id)
 
 
-def run(tutorial, example):
-    OldBrowser = zope.testbrowser.Browser
-    zope.testbrowser.Browser = testbrowser.Browser
-    exec compile(example.source, '<string>', "single") in tutorial.globs
-    # Eek, gotta remove the __builtins__
-    del tutorial.globs['__builtins__']
-    tutorial.locked = False
-    zope.testbrowser.Browser = OldBrowser
-
-
 class TutorialSession(MethodPublisher):
 
-    def getNextStep(self):
-        tutorial = zope.proxy.removeAllProxies(self.context)
-        step = tutorial.getNextStep()
-        if isinstance(step, types.StringTypes):
-            text = renderText(step, format='zope.source.rest')
-            return {'action': 'displayText',
-                    'params': (text,)}
-        elif step is None:
-            return {'action': 'finishTutorial',
-                    'params': ()}
-        else:
-            tutorial.locked = True
-            testbrowser.State.reset()
-            thread.start_new_thread(run, (tutorial, step))
-            while tutorial.locked and not testbrowser.State.hasAction():
-                time.sleep(0.1)
-            return testbrowser.State.action
+    def getCommand(self):
+        return self.context.getCommand()
 
-    def setCommandResult(self, result):
-        testbrowser.State.result = result
+    def addResult(self, id, result):
+        self.context.addResult(id, result)
         return True
 
     def keepGoing(self):

Modified: zope.tutorial/trunk/browser/tutorials-runner.js
===================================================================
--- zope.tutorial/trunk/browser/tutorials-runner.js	2005-11-25 21:30:39 UTC (rev 40375)
+++ zope.tutorial/trunk/browser/tutorials-runner.js	2005-11-26 02:29:54 UTC (rev 40376)
@@ -53,7 +53,7 @@
     /* Create a new server connection to the session */
     var addr = document.URL + CurrentTutorial + '/++sessions++' + SessionId
         ServerConnection = new jsonrpc.ServiceProxy(
-        addr, ['getNextStep', 'setCommandResult', 'keepGoing']);
+        addr, ['getCommand', 'addResult', 'keepGoing']);
 }
 
 function stopTutorial() {
@@ -74,9 +74,13 @@
 function runNextStep() {
     var keepGoing = true;
     while (keepGoing) {
-        command = ServerConnection.getNextStep();
+        answer = ServerConnection.getCommand();
+        id = answer[0];
+        command = answer[1];
         result = commands[command.action].apply(null, command.params);
-        answer = ServerConnection.setCommandResult(result);
-        keepGoing = ServerConnection.keepGoing();
+        if (result) {
+            ServerConnection.addResult(id, result);
+        }
+        keepGoing = ServerConnection.keepGoing()
     }
 }

Deleted: zope.tutorial/trunk/cli.py
===================================================================
--- zope.tutorial/trunk/cli.py	2005-11-25 21:30:39 UTC (rev 40375)
+++ zope.tutorial/trunk/cli.py	2005-11-26 02:29:54 UTC (rev 40376)
@@ -1,91 +0,0 @@
-##############################################################################
-#
-# Copyright (c) 2005 Zope Corporation and Contributors.
-# All Rights Reserved.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
-# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
-# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE.
-#
-##############################################################################
-"""Simple Text Controller implementation.
-
-$Id$
-"""
-__docformat__ = "reStructuredText"
-import types
-import zope.interface
-
-from zope.tutorial import interfaces, runner
-
-class SimpleCLIController(object):
-    """A dummy Command-line based controller.
-
-    Instead of running the tests, this controller simply displays the text and
-    examples. This makes this controller well-suited for testing.
-    """
-    #zope.interface.implements(interfaces.ITutorialController)
-
-    PYTHON_PROMPT = '>>> '
-    PYTHON_CONTINUE = '... '
-
-    def __init__(self, session):
-        self.session = session
-        self.running = False
-
-    def start(self):
-        """See interfaces.ITutorialController"""
-        self.running = True
-        print 'Starting Tutorial: ' + self.session.tutorial.title
-
-    def end(self):
-        """See interfaces.ITutorialController"""
-        print '---------- The End ----------'
-        self.running = False
-
-    def display(self, text):
-        """See interfaces.ITutorialController"""
-        print
-        print text.strip()
-        print
-
-    def run(self, example):
-        """See interfaces.ITutorialController"""
-        # Prepare the source and print it
-        source = example.source.strip()
-        source = ' '*example.indent + self.PYTHON_PROMPT + source
-        source = source.replace(
-            '\n', '\n' + ' '*example.indent + self.PYTHON_CONTINUE)
-        print source
-
-        # Prepare the expected output and print it
-        if example.want:
-            want = example.want.strip()
-            want = ' '*example.indent + want
-            want = want.replace('\n', '\n' + ' '*example.indent)
-            print want
-
-    def doNextStep(self):
-        """See interfaces.ITutorialController"""
-        step = self.session.getNextStep()
-        if isinstance(step, types.StringTypes):
-            self.display(step)
-        elif step is None:
-            self.end()
-        else:
-            self.run(step)
-
-
-class ExecutingCLIController(SimpleCLIController):
-
-    def __init__(self, session):
-        super(ExecutingCLIController, self).__init__(session)
-        self.erunner = runner.ExampleRunner(session.globs)
-
-    def run(self, example):
-        """See interfaces.ITutorialController"""
-        super(ExecutingCLIController, self).run(example)
-        self.erunner.run(example)

Modified: zope.tutorial/trunk/configure.zcml
===================================================================
--- zope.tutorial/trunk/configure.zcml	2005-11-25 21:30:39 UTC (rev 40375)
+++ zope.tutorial/trunk/configure.zcml	2005-11-26 02:29:54 UTC (rev 40376)
@@ -39,7 +39,7 @@
 
   <!-- Tutorial Session Configuration -->
 
-  <content class=".tutorial.TutorialSession">
+  <content class=".session.TutorialSession">
     <require
         permission="zope.View"
         interface=".interfaces.ITutorialSession"
@@ -48,7 +48,7 @@
 
   <!-- Tutorial Session Manager Configuration -->
 
-  <content class=".tutorial.TutorialSessionManager">
+  <content class=".session.TutorialSessionManager">
     <require
         permission="zope.View"
         interface=".interfaces.ITutorialSessionManager"
@@ -61,14 +61,14 @@
       name="sessions" type="*"
       for=".interfaces.ITutorial"
       provides="zope.app.traversing.interfaces.ITraversable"
-      factory=".tutorial.sessionsNamespace"
+      factory=".session.sessionsNamespace"
       />
 
   <adapter
       name="sessions"
       for=".interfaces.ITutorial"
       provides="zope.app.traversing.interfaces.ITraversable"
-      factory=".tutorial.sessionsNamespace"
+      factory=".session.sessionsNamespace"
       />
 
   <!-- Setup of initial tutorials -->

Modified: zope.tutorial/trunk/interfaces.py
===================================================================
--- zope.tutorial/trunk/interfaces.py	2005-11-25 21:30:39 UTC (rev 40375)
+++ zope.tutorial/trunk/interfaces.py	2005-11-26 02:29:54 UTC (rev 40376)
@@ -70,14 +70,46 @@
     """
     constraints.containers(ITutorialSessionManager)
 
+    locked = zope.schema.Bool(
+        title=u'Locked',
+        description=u'Specifies whether the session is locked.',
+        default=False)
+
     def initialize():
         """Initialize the session."""
 
-    def getNextStep():
-        """Return the next step in the tutorial.
+    def addCommand(command):
+        """Add a command to the commands queue.
 
-        Can be text or an example.
+        This method should also create and return a unique command id that is
+        used to associate the result with.
         """
 
+    def getCommand():
+        """Return the next command in the queue.
+
+        This method returns the command id and the command itself. The
+        returned command must be removed from the queue. ``(None, None)`` is
+        returned, if no command is in the queue.
+        """
+
+    def addResult(id, result):
+        """Add a result for a command.
+
+        The id identifies the command this result is for.
+        """
+
+    def getResult(id):
+        """Get result for a given command id.
+        """
+
     def keepGoing():
-        """ """
+        """Return whether the system should keep going processing events.
+
+        The method should return False, when the parts switch from a string to
+        an example and vice versa.
+        """
+
+    def setTimeout(seconds):
+        """Set a timeout for a result to be returned or the next command to be
+        retrieved."""

Deleted: zope.tutorial/trunk/runner.py
===================================================================
--- zope.tutorial/trunk/runner.py	2005-11-25 21:30:39 UTC (rev 40375)
+++ zope.tutorial/trunk/runner.py	2005-11-26 02:29:54 UTC (rev 40376)
@@ -1,40 +0,0 @@
-##############################################################################
-#
-# Copyright (c) 2005 Zope Corporation and Contributors.
-# All Rights Reserved.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
-# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
-# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE.
-#
-##############################################################################
-"""Simple Text Controller implementation.
-
-$Id$
-"""
-__docformat__ = "reStructuredText"
-from zope.testing import doctest
-
-
-class PermissiveOutputChecker(object):
-
-    def check_output(self, want, got, optionflags):
-        return True
-
-
-class ExampleRunner(doctest.DocTestRunner):
-    """Example Runner"""
-
-    def __init__(self, globs, checker=None, verbose=None, optionflags=0):
-        if checker is None:
-            checker = PermissiveOutputChecker()
-        doctest.DocTestRunner.__init__(self, checker, verbose, optionflags)
-        self.globs = globs
-
-    def run(self, example, compileflags=None, out=None):
-        """ """
-        test = doctest.DocTest([example], self.globs, '', '', 0, '')
-        return doctest.DocTestRunner.run(self, test, clear_globs=False)

Modified: zope.tutorial/trunk/sample_tutorial.txt
===================================================================
--- zope.tutorial/trunk/sample_tutorial.txt	2005-11-25 21:30:39 UTC (rev 40375)
+++ zope.tutorial/trunk/sample_tutorial.txt	2005-11-26 02:29:54 UTC (rev 40376)
@@ -6,11 +6,15 @@
   >>> from zope.testbrowser import Browser
   >>> browser = Browser()
   >>> browser.open('http://localhost:8080/manage')
+
   >>> browser.url
   >>> browser.title
   >>> browser.contents
+
   >>> browser.reload()
+
   >>> browser.getLink('Buddy Folder').click()
-  >>> browser.goBack()
 
+  #>>> browser.goBack()
+
 That's it!
\ No newline at end of file

Added: zope.tutorial/trunk/session.py
===================================================================
--- zope.tutorial/trunk/session.py	2005-11-25 21:30:39 UTC (rev 40375)
+++ zope.tutorial/trunk/session.py	2005-11-26 02:29:54 UTC (rev 40376)
@@ -0,0 +1,195 @@
+##############################################################################
+#
+# Copyright (c) 2005 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Tutorial Implementation
+
+$Id$
+"""
+__docformat__ = "reStructuredText"
+import doctest
+import thread
+import types
+import time
+import types
+import zope.proxy
+import zope.testbrowser
+from zope.app import annotation
+from zope.app import zapi
+from zope.app import container
+from zope.app.apidoc.utilities import renderText
+from zope.app.component import hooks
+from zope.app.location import location
+
+from zope.tutorial import interfaces
+from zope.tutorial import testbrowser
+
+NORESULT = object()
+NOACTION = {'action': 'nullAction', 'params': ()}
+
+SESSIONMANAGER_CACHE = {}
+
+
+class BrowserBroker(object):
+
+    def __init__(self, session):
+        self.session = session
+
+    def executeAction(self, action, *args):
+        id = self.session.addCommand({'action': action, 'params': args})
+        # wait for the answer to come in
+        result = NORESULT
+        while result is NORESULT:
+            time.sleep(0.1)
+            result = self.session.getResult(id)
+
+        return result
+
+    def __getattr__(self, name):
+        def action(*args):
+            return self.executeAction(name, *args)
+        return action
+
+
+def run(session, example):
+    """Run a doctest example."""
+
+    def BrowserFactory(url=None):
+        return testbrowser.Browser(BrowserBroker(session), url)
+
+    OldBrowser = zope.testbrowser.Browser
+    zope.testbrowser.Browser = BrowserFactory
+    exec compile(example.source, '<string>', "single") in session.globals
+    session.locked = False
+    zope.testbrowser.Browser = OldBrowser
+
+
+class TutorialSession(location.Location):
+    """Tutorial Session"""
+    zope.component.adapts(interfaces.ITutorial)
+    zope.interface.implements(interfaces.ITutorialSession)
+
+    locked = False
+
+    def __init__(self, tutorialName):
+        self.tutorialName = tutorialName
+        self._commandCounter = 0
+
+    def initialize(self):
+        """See interfaces.ITutorialSession"""
+        # Create a parts stack
+        tutorial = zapi.getUtility(interfaces.ITutorial, name=self.tutorialName)
+        text = open(tutorial.path, 'r').read()
+        parser = doctest.DocTestParser()
+        parts = parser.parse(text)
+
+        # Clean up the parts by removing empty strings
+        self._parts = [
+            part for part in parts
+            if not isinstance(part, types.StringTypes) or part.strip()]
+        self._parts.reverse()
+        self._current = None
+
+        # Setup actions
+        self._commands = []
+        self._results = {}
+
+        # The global variables of the execution environment
+        self.globals = {}
+
+    def addCommand(self, command):
+        """See interfaces.ITutorialSession"""
+        name = u'command-' + unicode(self._commandCounter)
+        self._commandCounter += 1
+        self._commands.append((name, command))
+        return name
+
+    def getCommand(self):
+        """See interfaces.ITutorialSession"""
+        if len(self._commands):
+            return self._commands.pop()
+
+        if not len(self._parts):
+            self.addCommand({'action': 'finishTutorial',
+                             'params': ()})
+            return self._commands.pop()
+
+        part = self._current = self._parts.pop()
+        if isinstance(part, types.StringTypes):
+            text = renderText(part, format='zope.source.rest')
+            self.addCommand({'action': 'displayText',
+                             'params': (text,)})
+            return self._commands.pop()
+        else:
+            self.locked = True
+            thread.start_new_thread(run, (self, part))
+            while self.locked and not len(self._commands):
+                time.sleep(0.1)
+
+            # The part was executed without creating any command
+            if not self.locked:
+                return (None, NOACTION)
+
+            return self._commands.pop()
+
+    def addResult(self, id, result):
+        """See interfaces.ITutorialSession"""
+        self._results[id] = result
+
+    def getResult(self, name):
+        """See interfaces.ITutorialSession"""
+        return self._results.pop(name, NORESULT)
+
+    def keepGoing(self):
+        """See interfaces.ITutorialSession"""
+        if len(self._parts) == 0:
+            return False
+        return type(self._current) == type(self._parts[-1])
+
+
+class TutorialSessionManager(container.btree.BTreeContainer):
+    """A session manager based on BTrees."""
+    zope.component.adapter(interfaces.ITutorial)
+    zope.interface.implements(interfaces.ITutorialSessionManager)
+
+    def __init__(self):
+        super(TutorialSessionManager, self).__init__()
+
+    def createSession(self):
+        session = TutorialSession(zapi.getName(self))
+        chooser = container.interfaces.INameChooser(self)
+        name = chooser.chooseName(u'session', session)
+        self[name] = session
+        return name
+
+    def deleteSession(self, name):
+        del self[name]
+
+
+class sessionsNamespace(object):
+    """Used to traverse the `++sessions++` namespace"""
+
+    def __init__(self, ob=None, request=None):
+        tutorialName = zapi.name(ob)
+        manager = SESSIONMANAGER_CACHE.get(tutorialName)
+        if manager is None:
+            manager = TutorialSessionManager()
+            location.locate(manager, ob, tutorialName)
+            SESSIONMANAGER_CACHE[tutorialName] = manager
+
+        self.sessionManager = manager
+
+    def traverse(self, name, ignore=None):
+        if name == '':
+            return self.sessionManager
+        else:
+            return self.sessionManager[name]


Property changes on: zope.tutorial/trunk/session.py
___________________________________________________________________
Name: svn:eol-style
   + native

Modified: zope.tutorial/trunk/session.txt
===================================================================
--- zope.tutorial/trunk/session.txt	2005-11-25 21:30:39 UTC (rev 40375)
+++ zope.tutorial/trunk/session.txt	2005-11-26 02:29:54 UTC (rev 40376)
@@ -1,6 +1,6 @@
-====================
-The Tutorial Session
-====================
+=================
+Tutorial Sessions
+=================
 
 A tutorial session is created whenever a user watches or takes a tutorial. A
 session is an adapter to a tutorial, so we have to create that first:
@@ -18,11 +18,12 @@
   ...   >>> print 'sample'
   ...   sample
   ...
-  ... And now a variable assignment with a return value:
+  ... Let's now create a testbrowser instance and open a ficticious URL:
   ...
-  ...   >>> num = 5
-  ...   >>> num
-  ...   5
+  ...   >>> from zope.testbrowser import Browser
+  ...   >>> browser = Browser('www.zope.org')
+  ...   >>> browser.url
+  ...   'http://www.zope.org'
   ...
   ... That's it!
   ... ''')
@@ -30,83 +31,150 @@
   >>> from zope.tutorial import tutorial
   >>> sample = tutorial.Tutorial('Sample Documentation', sample_txt)
 
-Now that we have the tutorial, we can create a session:
+  >>> import zope.component
+  >>> zope.component.provideUtility(sample, name=u'sample')
 
-  >>> session = tutorial.TutorialSession(sample)
-  >>> session.initialize()
+  >>> from zope.app.location import location
+  >>> location.locate(sample, None, u'sample')
 
-Nothing is setup until ``initialize()`` is called. Once the session is
-prepared, we can choose a controller that knows about the input and output
-interfaces. The simplest controller is the `SimpleCLIController`, which simply
-displays the text and examples:
+Now that we have the tutorial, we can access the session manager using the
+'sessions' namespace:
 
-  >>> from zope.tutorial import cli
-  >>> controller = cli.SimpleCLIController(session)
+  >>> from zope.tutorial import session
+  >>> ns = session.sessionsNamespace(sample)
+  >>> manager = ns.traverse('')
+  >>> manager
+  <zope.tutorial.session.TutorialSessionManager object at ...>
 
-Since we are in a unit test file already, the Python prompt needs to be
-changed, so that the test does not get confused:
+Since the session contains a lot of non-pickable data and uses threads in
+unorthodox ways, they are stored in a global variable cache:
 
-  >>> controller.PYTHON_PROMPT = 'Py: '
+  >>> session.SESSIONMANAGER_CACHE[u'sample']
+  <zope.tutorial.session.TutorialSessionManager object at ...>
 
-We can now write a simple function that runs the tutorial for us:
+You can now use the session manager to create a new session:
 
-  >>> def run():
-  ...     controller.start()
-  ...     while controller.running:
-  ...         controller.doNextStep()
+  >>> name = manager.createSession()
+  >>> name
+  u'session'
 
-  >>> run()
-  Starting Tutorial: Sample Documentation
-  <BLANKLINE>
-  Sample Documentation
-  ====================
-  <BLANKLINE>
-  Here is a simple print statement:
-  <BLANKLINE>
-    Py: print 'sample'
-    sample
-  <BLANKLINE>
-  And now a variable assignment with a return value:
-  <BLANKLINE>
-    Py: num = 5
-    Py: num
-    5
-  <BLANKLINE>
-  That's it!
-  <BLANKLINE>
-  ---------- The End ----------
+  >>> mysession = manager[name]
 
-Next let's try a little bit more interesting. There is also a CLI controller
-that actually executes the examples:
+Nothing is setup until ``initialize()`` is called.
 
-  >>> session = tutorial.TutorialSession(sample)
-  >>> session.initialize()
+  >>> mysession.initialize()
 
-  >>> controller = cli.ExecutingCLIController(session)
-  >>> controller.PYTHON_PROMPT = 'Py: '
+Since the session runs other interactive browser code that in turn drives an
+external Web browser, the session provides a fairly sophisticated command
+distribution and result retrieval system. Let's first look at a simple sample
+run. The executing code first adds a command to be executed in the external
+browser:
 
-  >>> def run():
-  ...     controller.start()
-  ...     while controller.running:
-  ...         controller.doNextStep()
+  >>> name = mysession.addCommand(
+  ...     {'action': 'sampleAction', 'params': ('value',)})
+  >>> name
+  u'command-0'
 
-  >>> run()
-  Starting Tutorial: Sample Documentation
-  <BLANKLINE>
-  Sample Documentation
-  ====================
-  <BLANKLINE>
-  Here is a simple print statement:
-  <BLANKLINE>
-    Py: print 'sample'
-    sample
-  <BLANKLINE>
-  And now a variable assignment with a return value:
-  <BLANKLINE>
-    Py: num = 5
-    Py: num
-    5
-  <BLANKLINE>
-  That's it!
-  <BLANKLINE>
-  ---------- The End ----------
+The command can be really anything. But commands that are supposed to work
+with this package's Web browser driver must have the command form demonstrated
+above. The returned ``name`` variable of the command is later used to identify
+the result. The Web server now gets the command for processing ...
+
+  >>> mysession.getCommand()
+  (u'command-0', {'action': 'sampleAction', 'params': ('value',)})
+
+and then sends the result:
+
+  >>> mysession.addResult(name, {'data': 42})
+
+Once the result is available, the executable code will pick it up:
+
+  >>> mysession.getResult(name)
+  {'data': 42}
+
+
+A Complete Run
+--------------
+
+However, the command generation is often not that easy. When the session was
+initialized, it generated a list of parts. Parts are either a simple string
+representing the text snippets in a test file or an ``Example`` instance that
+can be executed. If no command is available when calling ``getCommand()`` then
+the next part is retrieved and commands are generated. In our example above,
+the first generated command is a text display:
+
+  >>> mysession.getCommand()
+  (u'command-1', {'action': 'displayText', 'params': (u'...Sample Doc...',)})
+
+Next there is a simple example to execute. Since it does not generate a
+command itself, the null-action is returned:
+
+  >>> mysession.getCommand()
+  sample
+  (None, {'action': 'nullAction', 'params': ()})
+
+The next command is again some text:
+
+  >>> mysession.getCommand()
+  (u'command-2', {'action': 'displayText', 'params': (u"...testbrowser...",)})
+
+Finally we are at the last code example. The first statement is just an
+import, so we get a null action:
+
+  >>> mysession.getCommand()
+  (None, {'action': 'nullAction', 'params': ()})
+
+Then we create a test browser instance and open the URL at the same time:
+
+  >>> pprint(mysession.globals)
+  {'Browser': <function BrowserFactory at ...>,
+   '__builtins__': {...}}
+
+  >>> mysession.getCommand()
+  (u'command-3', {'action': 'openUrl', 'params': ('www.zope.org', None)})
+
+Now that a command has been sent, the real browser has to provide a response:
+
+  >>> mysession.addResult(u'command-3', 'www.zope.org')
+
+  # Wait a little bit so that the result can be picked up:
+  >>> import time
+  >>> time.sleep(0.5)
+
+The session should now have testbrowser instance:
+
+  >>> pprint(mysession.globals)
+  {'Browser': <function BrowserFactory at ...>,
+   '__builtins__': {...},
+   'browser': <zope.tutorial.testbrowser.Browser object at ...>}
+
+Once a site is opened, we can ask for the URL, which creates a respective
+command:
+
+  >>> mysession.getCommand()
+  (u'command-4', {'action': 'getUrl', 'params': ()})
+
+The real browser sends back the answer:
+
+  >>> mysession.addResult(u'command-4', 'http://www.zope.org')
+
+  # Wait a little bit so that the result can be picked up:
+  >>> time.sleep(0.5)
+  'http://www.zope.org'
+
+The final command is just a closing text remark.
+
+  >>> mysession.getCommand()
+  (u'command-5', {'action': 'displayText', 'params': (u"<p>That's it!</p>\n",)})
+
+Now that the tutorial is over, asking for the next command will always return
+the finish command:
+
+  >>> mysession.getCommand()
+  (u'command-6', {'action': 'finishTutorial', 'params': ()})
+
+  >>> mysession.getCommand()
+  (u'command-7', {'action': 'finishTutorial', 'params': ()})
+
+This completes a session and the Web tutorial driver should allow the user to
+select another tutorial.
\ No newline at end of file

Modified: zope.tutorial/trunk/testbrowser.py
===================================================================
--- zope.tutorial/trunk/testbrowser.py	2005-11-25 21:30:39 UTC (rev 40375)
+++ zope.tutorial/trunk/testbrowser.py	2005-11-26 02:29:54 UTC (rev 40376)
@@ -22,43 +22,6 @@
 from zope import testbrowser
 
 
-NORESULT = object()
-NOACTION = {'action': 'nullAction', 'params': ()}
-
-# TODO: Make this user specific later; this should be really stored in the
-# session, but the test browser does not know about the session :-(
-class State(object):
-    __slots__ = ('result', 'action')
-
-    def __init__(self):
-        self.reset()
-
-    def reset(self):
-        self.result = NORESULT
-        self.action = NOACTION
-
-    def hasAction(self):
-        return self.action is not NOACTION
-
-    def hasResult(self):
-        return self.result is not NORESULT
-
-    def executeAction(self, action, *args):
-        self.result = None
-        self.action = {'action': action, 'params': args}
-        # wait for the answer to come in
-        while self.result is NORESULT:
-            time.sleep(0.5)
-        return self.result
-
-    def __getattr__(self, name):
-        def action(*args):
-            return self.executeAction(name, *args)
-        return action
-
-State = State()
-
-
 class Browser(testbrowser.browser.SetattrErrorsMixin):
     """ """
     zope.interface.implements(testbrowser.interfaces.IBrowser)
@@ -66,7 +29,8 @@
     _contents = None
     _counter = 0
 
-    def __init__(self, url=None):
+    def __init__(self, broker, url=None):
+        self.broker = broker
         self.timer = testbrowser.browser.PystoneTimer()
         if url:
             self.open(url)
@@ -74,24 +38,23 @@
     @property
     def url(self):
         """See zope.testbrowser.interfaces.IBrowser"""
-        return State.getUrl()
+        return self.broker.getUrl()
 
     @property
     def isHtml(self):
         """See zope.testbrowser.interfaces.IBrowser"""
-        # TODO: It is always HTML for now ;-)
-        return True
+        return self.broker.isHtml()
 
     @property
     def title(self):
         """See zope.testbrowser.interfaces.IBrowser"""
-        return State.getTitle()
+        return self.broker.getTitle()
 
     @property
     def contents(self):
         """See zope.testbrowser.interfaces.IBrowser"""
         if self._contents is None:
-            self._contents = State.getContent()
+            self._contents = self.broker.getContent()
         return self._contents
 
     @property
@@ -107,7 +70,7 @@
     def open(self, url, data=None):
         """See zope.testbrowser.interfaces.IBrowser"""
         self._start_timer()
-        State.openUrl(url, data)
+        self.broker.openUrl(url, data)
         self._stop_timer()
         self._changed()
 
@@ -128,14 +91,14 @@
     def reload(self):
         """See zope.testbrowser.interfaces.IBrowser"""
         self._start_timer()
-        State.reload()
+        self.broker.reload()
         self._stop_timer()
         self._changed()
 
     def goBack(self, count=1):
         """See zope.testbrowser.interfaces.IBrowser"""
         self._start_timer()
-        State.goBack(count)
+        self.broker.goBack(count)
         self._stop_timer()
         self._changed()
 
@@ -186,11 +149,12 @@
         self._info = None
 
     def click(self):
-        return State.executeAction('clickLink', self._text, self._url, self._id)
+        return self.browser.broker.executeAction(
+            'clickLink', self._text, self._url, self._id)
 
     def getInfo(self):
         if self._info is None:
-            self._info = State.executeAction(
+            self._info = self.browser.broker.executeAction(
                 'getLinkInfo', self._text, self._url, self._id)
         return self._info
 
@@ -224,4 +188,4 @@
         self._index = index
 
     def click(self):
-        return State.executeAction('clickControl', self._text, self._url, self._id)
+        return self.browser.broker.executeAction('clickControl', self._text, self._url, self._id)

Added: zope.tutorial/trunk/testbrowser.txt
===================================================================
--- zope.tutorial/trunk/testbrowser.txt	2005-11-25 21:30:39 UTC (rev 40375)
+++ zope.tutorial/trunk/testbrowser.txt	2005-11-26 02:29:54 UTC (rev 40376)
@@ -0,0 +1,151 @@
+================
+The Test Browser
+================
+
+Since the tutorial uses ``zope.testbrowser``-based tests to generate its
+content, it is necessary to create an alternative implementation of the
+``zope.testbrowser.interfaces.IBrowser`` interface. This implementation uses a
+broker object to communicate with the real browser to execute the
+commands. For the purposes of this demonstration, let's implement a dummy
+broker:
+
+  >>> class Broker(object):
+  ...     pass
+  >>> broker = Broker()
+
+We will complete the API of the broker as the test progresses. As you might
+know from reading the documentation of the ``zope.testbrowser`` package, the
+test browser implements several classes. The content of this document is
+organized by classes and methods.
+
+Furthermore, this document implicitely also documents the broker API, since
+all broker methods must be correctly implemented in order to document the
+testbrowser.
+
+The ``Browser`` class
+---------------------
+
+The constructor
+~~~~~~~~~~~~~~~
+
+Let's now create a browser instance that uses the broker:
+
+  >>> from zope.tutorial import testbrowser
+  >>> browser = testbrowser.Browser(broker)
+  >>> browser.broker
+  <Broker object at ...>
+  >>> browser.timer
+  <zope.testbrowser.browser.PystoneTimer object at ...>
+
+Additionally you can instantiate the object by also providing a URL that is
+immediately opened:
+
+  >>> def openUrl(self, url, data=None):
+  ...     self.url, self.data = url, data
+  ...     return url
+  >>> Broker.openUrl = openUrl
+
+  >>> browser = testbrowser.Browser(broker, 'http://www.zope.org')
+  >>> broker.url
+  'http://www.zope.org'
+  >>> broker.data
+
+
+The ``url`` property
+~~~~~~~~~~~~~~~~~~~~
+
+Once a page is opened, you can always ask for the URL.
+
+  >>> def getUrl(self):
+  ...     return self.url
+  >>> Broker.getUrl = getUrl
+
+  >>> browser.url
+  'http://www.zope.org'
+
+Initially you might think this is obvious, but you often deal with redirects
+and form clicks and the URL might not be easily guessable.
+
+
+The ``isHtml`` property
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This property tests whether the current URL's content is HTML:
+
+  >>> def isHtml(self):
+  ...     return getattr(self, 'html', True)
+  >>> Broker.isHtml = isHtml
+
+  >>> browser.isHtml
+  True
+
+The ``title`` property
+~~~~~~~~~~~~~~~~~~~~~~
+
+Report the HTML title of the current page.
+
+  >>> def getTitle(self):
+  ...     return self.title
+  >>> Broker.getTitle = getTitle
+  >>> broker.title = 'Zope 3'
+
+  >>> browser.title
+  'Zope 3'
+
+
+The ``contents`` property
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Return the full page source.
+
+  >>> def getContent(self):
+  ...     return self.content
+  >>> Broker.getContent = getContent
+  >>> broker.content = '<html>...</html>'
+
+  >>> browser.contents
+  '<html>...</html>'
+
+
+The ``headers`` property
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+Return a list of all response headers.
+
+XXX: TO BE DONE!!!
+
+
+The ``handleErrors`` property
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When set to true, errors should not be converted to error pages.
+
+XXX: TO BE DONE!!!
+
+
+The ``open(url, data=None)`` method
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+At any time, you can simply open a new page by specifying a URL.
+
+  >>> browser.open('http://localhost:8080', 'some data')
+  >>> broker.url
+  'http://localhost:8080'
+  >>> broker.data
+  'some data'
+
+
+The ``getLink(text=None, url=None, id=None)`` method
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+One of the more advanced features of the test browser is the retrieval of a
+link using either the link text, url or id. Here we simply return a ``Link``
+object instance:
+
+  >>> link = browser.getLink('Folder')
+  >>> link
+  <Link text='Folder' url=None id=None>
+
+The ``Link`` API is documented in the next section.
+
+


Property changes on: zope.tutorial/trunk/testbrowser.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Modified: zope.tutorial/trunk/tests.py
===================================================================
--- zope.tutorial/trunk/tests.py	2005-11-25 21:30:39 UTC (rev 40375)
+++ zope.tutorial/trunk/tests.py	2005-11-26 02:29:54 UTC (rev 40376)
@@ -18,24 +18,47 @@
 __docformat__ = 'restructuredtext'
 
 import unittest
-from zope.testing import doctest
-from zope.testing.doctestunit import DocFileSuite
-from zope.app.testing import placelesssetup
+import zope.component.interfaces
+import zope.interface
+from zope.testing import doctest, doctestunit
 
+from zope.app.container import contained
+from zope.app.renderer import rest
+from zope.app.testing import placelesssetup, setup, ztapi
 
+
+def setUp(test):
+    setup.placefulSetUp(True)
+    zope.component.provideAdapter(contained.NameChooser,
+                                  (zope.interface.Interface,))
+    # Register Renderer Components
+    ztapi.provideUtility(zope.component.interfaces.IFactory,
+                         rest.ReStructuredTextSourceFactory,
+                         'zope.source.rest')
+    ztapi.browserView(rest.IReStructuredTextSource, '',
+                      rest.ReStructuredTextToHTMLRenderer)
+
+def tearDown(test):
+    setup.placefulTearDown()
+
 def test_suite():
     return unittest.TestSuite((
-        DocFileSuite('README.txt',
+        doctestunit.DocFileSuite('README.txt',
                      setUp=placelesssetup.setUp,
                      tearDown=placelesssetup.tearDown,
                      optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
                      ),
-        DocFileSuite('session.txt',
-                     setUp=placelesssetup.setUp,
-                     tearDown=placelesssetup.tearDown,
+        doctestunit.DocFileSuite('session.txt',
+                     setUp=setUp, tearDown=tearDown,
+                     globs={'pprint': doctestunit.pprint},
                      optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
                      ),
-        DocFileSuite('directives.txt',
+        doctestunit.DocFileSuite('testbrowser.txt',
+                     setUp=setUp, tearDown=tearDown,
+                     globs={'pprint': doctestunit.pprint},
+                     optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
+                     ),
+        doctestunit.DocFileSuite('directives.txt',
                      setUp=placelesssetup.setUp,
                      tearDown=placelesssetup.tearDown,
                      optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,

Modified: zope.tutorial/trunk/tutorial.py
===================================================================
--- zope.tutorial/trunk/tutorial.py	2005-11-25 21:30:39 UTC (rev 40375)
+++ zope.tutorial/trunk/tutorial.py	2005-11-26 02:29:54 UTC (rev 40376)
@@ -16,25 +16,12 @@
 $Id$
 """
 __docformat__ = "reStructuredText"
-import doctest
 import os
-import persistent
-import types
 import zope.component
 import zope.interface
-import zope.proxy
-from zope.app import annotation
-from zope.app import zapi
-from zope.app.component import hooks
-from zope.app.container import btree
-from zope.app.location import location
 
 from zope.tutorial import interfaces
 
-
-SessionManagerKey = 'zope.tutorial.SessionManager'
-
-
 class Tutorial(object):
     """Tutorial"""
     zope.interface.implements(interfaces.ITutorial)
@@ -46,89 +33,3 @@
     def __repr__(self):
         return '<%s title=%r, file=%r>' %(
             self.__class__.__name__, self.title, os.path.split(self.path)[-1])
-
-
-class TutorialSession(persistent.Persistent, location.Location):
-    """Tutorial Session"""
-
-    zope.component.adapts(interfaces.ITutorial)
-    zope.interface.implements(interfaces.ITutorialSession)
-
-    locked = False
-
-    def __init__(self, tutorial):
-        self.tutorial = tutorial
-
-    def initialize(self):
-        """See interfaces.ITutorialSession"""
-        text = open(self.tutorial.path, 'r').read()
-        parser = doctest.DocTestParser()
-        self.parts = parser.parse(text)
-        # Clean up the parts by removing empty strings
-        self.parts = [part for part in self.parts
-                      if (not isinstance(part, types.StringTypes) or
-                          part.strip())]
-        # Create a parts stack
-        self.parts.reverse()
-        self.current = None
-
-        # Set some runtime variables
-        self.globs = {}
-
-    def getNextStep(self):
-        """See interfaces.ITutorialSession"""
-        if self.locked:
-            return None
-        try:
-            self.current = self.parts.pop()
-        except IndexError:
-            self.current = None
-            return None
-
-        return self.current
-
-    def keepGoing(self):
-        return type(self.parts[-1]) == type(self.current)
-
-
-class TutorialSessionManager(btree.BTreeContainer):
-    """A session manager based on BTrees."""
-    zope.component.adapter(interfaces.ITutorial)
-    zope.interface.implements(interfaces.ITutorialSessionManager)
-
-    def __init__(self):
-        super(TutorialSessionManager, self).__init__()
-        self.__counter = 0
-
-    def createSession(self):
-        name = unicode(self.__counter)
-        self[name] = TutorialSession(zapi.getParent(self))
-        self.__counter += 1;
-        return name
-
-    def deleteSession(self, name):
-        del self[name]
-
-
-class sessionsNamespace(object):
-    """Used to traverse the `++sessions++` namespace"""
-
-    def __init__(self, ob=None, request=None):
-        site = hooks.getSite()
-        annotations = annotation.interfaces.IAnnotations(site)
-        manager = annotations.get(SessionManagerKey)
-
-        if manager is None:
-            manager = TutorialSessionManager()
-            tutorial = zope.proxy.removeAllProxies(ob)
-            location.locate(manager, tutorial, '++sessions++')
-            annotations[SessionManagerKey] = manager
-
-        self.sessionManager = manager
-
-
-    def traverse(self, name, ignore=None):
-        if name == '':
-            return self.sessionManager
-        else:
-            return self.sessionManager[name]



More information about the Zope-CVS mailing list