Mailing List Archive

SVN: zope.tutorial/trunk/ - Reimplemented tutorial runner. It's much saner now.
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]

_______________________________________________
Zope-CVS maillist - Zope-CVS@zope.org
http://mail.zope.org/mailman/listinfo/zope-cvs

Zope CVS instructions: http://dev.zope.org/CVS