on master help sign in
tests and robot tests framework, build overhaul
commite740c8a8ea7dc4bbda7dab9a63ec476167c209ef authorMatej Cotman <cotman.matej@gmail.com> dateSun 12 Jan 2014, 12:40 (13 years ago) parent348187c
17 files changed+732−5
.gitignore+10−1 Makefile+50 README.md+44 base.cfg+23 bootstrap.py+277 buildout.cfg+32 production.cfg+17 searx/settings_robot.py+16 searx/testing.py+59 searx/tests/__init__.py searx/tests/robot/__init__.py searx/tests/robot/test_basic.robot+11 searx/tests/test_robot.py+24 searx/tests/test_unit.py+10 searx/webapp.py+15−4 setup.py+51 versions.cfg+93
.gitignore+10−1
@@ -1,5 +1,14 @@
11env
22engines.cfg
3+.installed.cfg
4+setup.cfg
35
46*.pyc
5−*/*.pyc
7+*/*.pyc
8+
9+bin/
10+include/
11+lib/
12+build/
13+develop-eggs/
14+parts/
Makefilenew file+50
@@ -0,0 +1,50 @@
1+# convenience makefile to boostrap & run buildout
2+# use `make options=-v` to run buildout with extra options
3+
4+version = 2.7
5+python = bin/python
6+options =
7+
8+all: .installed.cfg
9+
10+.installed.cfg: bin/buildout buildout.cfg setup.py
11+ bin/buildout $(options)
12+
13+bin/buildout: $(python) buildout.cfg bootstrap.py
14+ $(python) bootstrap.py
15+ @touch $@
16+
17+$(python):
18+ virtualenv -p python$(version) --no-site-packages .
19+ @touch $@
20+
21+tests: .installed.cfg
22+ @bin/test
23+
24+enginescfg:
25+ @test -f ./engines.cfg || echo "Copying engines.cfg ..."
26+ @cp --no-clobber engines.cfg_sample engines.cfg
27+
28+robot: .installed.cfg enginescfg
29+ @bin/robot
30+
31+flake8: .installed.cfg
32+ @bin/flake8 setup.py
33+ @bin/flake8 ./searx/
34+
35+coverage: .installed.cfg
36+ @bin/coverage run --source=./searx/ --branch bin/test
37+ @bin/coverage report --show-missing
38+ @bin/coverage html --directory ./coverage
39+
40+minimal: bin/buildout production.cfg setup.py enginescfg
41+ bin/buildout -c production.cfg $(options)
42+ @echo "* Please modify `readlink --canonicalize-missing ./searx/settings.py`"
43+ @echo "* Hint 1: on production, disable debug mode and change secret_key"
44+ @echo "* Hint 2: to run server execute 'bin/searx-run'"
45+
46+clean:
47+ @rm -rf .installed.cfg .mr.developer.cfg bin parts develop-eggs \
48+ searx.egg-info lib include .coverage coverage
49+
50+.PHONY: all tests enginescfg robot flake8 coverage minimal clean
README.md+44
@@ -29,6 +29,48 @@ List of [running instances](https://github.com/asciimoo/searx/wiki/Searx-instanc
2929
3030For all the details, follow this [step by step installation](https://github.com/asciimoo/searx/wiki/Installation)
3131
32+### Alternative (Recommended) Installation
33+
34+* clone source: `git clone git@github.com:asciimoo/searx.git && cd searx`
35+* build in current folder: `make minimal`
36+* run `bin/searx-run` to start the application
37+
38+
39+### Development
40+
41+Just run `make`. Versions of dependencies are pinned down inside `versions.cfg` to produce most stable build.
42+
43+#### Command make
44+
45+##### `make`
46+
47+Builds development environment with testing support.
48+
49+##### `make tests`
50+
51+Runs tests. You can write tests [here](https://github.com/asciimoo/searx/tree/master/searx/tests) and remember 'untested code is broken code'.
52+
53+##### `make robot`
54+
55+Runs robot (Selenium) tests, you must have `firefox` installed because this functional tests actually run the browser and perform operations on it. Also searx is executed with [settings_robot](https://github.com/asciimoo/searx/blob/master/searx/settings_robot.py).
56+
57+##### `make flake8`
58+
59+'pep8 is a tool to check your Python code against some of the style conventions in [PEP 8](http://www.python.org/dev/peps/pep-0008/).'
60+
61+##### `make coverage`
62+
63+Checks coverage of tests, after running this, execute this: `firefox ./coverage/index.html`
64+
65+##### `make minimal`
66+
67+Used to make co-called production environment - without tests (you should ran tests before deploying searx on the server).
68+
69+##### `make clean`
70+
71+Deletes several folders and files (see `Makefile` for more), so that next time you run any other `make` command it will rebuild everithing.
72+
73+
3274### TODO
3375
3476* Moar engines
@@ -36,7 +78,9 @@ For all the details, follow this [step by step installation](https://github.com/
3678* Language support
3779* Documentation
3880* Pagination
81+* Fix `flake8` errors, `make flake8` will be merged into `make tests` when it does not fail anymore
3982* Tests
83+* When we have more tests, we can integrate Travis-CI
4084
4185
4286### Bugs
base.cfgnew file+23
@@ -0,0 +1,23 @@
1+[buildout]
2+extends = versions.cfg
3+versions = versions
4+unzip = true
5+newest = false
6+extends = versions.cfg
7+versions = versions
8+prefer-final = true
9+develop = .
10+
11+extensions =
12+ buildout_versions
13+
14+eggs =
15+ searx
16+
17+parts =
18+ omelette
19+
20+
21+[omelette]
22+recipe = collective.recipe.omelette
23+eggs = ${buildout:eggs}
bootstrap.pynew file+277
@@ -0,0 +1,277 @@
1+##############################################################################
2+#
3+# Copyright (c) 2006 Zope Foundation and Contributors.
4+# All Rights Reserved.
5+#
6+# This software is subject to the provisions of the Zope Public License,
7+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
8+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
9+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
10+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
11+# FOR A PARTICULAR PURPOSE.
12+#
13+##############################################################################
14+"""Bootstrap a buildout-based project
15+
16+Simply run this script in a directory containing a buildout.cfg.
17+The script accepts buildout command-line options, so you can
18+use the -c option to specify an alternate configuration file.
19+"""
20+
21+import os, shutil, sys, tempfile, urllib, urllib2, subprocess
22+from optparse import OptionParser
23+
24+if sys.platform == 'win32':
25+ def quote(c):
26+ if ' ' in c:
27+ return '"%s"' % c # work around spawn lamosity on windows
28+ else:
29+ return c
30+else:
31+ quote = str
32+
33+# See zc.buildout.easy_install._has_broken_dash_S for motivation and comments.
34+stdout, stderr = subprocess.Popen(
35+ [sys.executable, '-Sc',
36+ 'try:\n'
37+ ' import ConfigParser\n'
38+ 'except ImportError:\n'
39+ ' print 1\n'
40+ 'else:\n'
41+ ' print 0\n'],
42+ stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
43+has_broken_dash_S = bool(int(stdout.strip()))
44+
45+# In order to be more robust in the face of system Pythons, we want to
46+# run without site-packages loaded. This is somewhat tricky, in
47+# particular because Python 2.6's distutils imports site, so starting
48+# with the -S flag is not sufficient. However, we'll start with that:
49+if not has_broken_dash_S and 'site' in sys.modules:
50+ # We will restart with python -S.
51+ args = sys.argv[:]
52+ args[0:0] = [sys.executable, '-S']
53+ args = map(quote, args)
54+ os.execv(sys.executable, args)
55+# Now we are running with -S. We'll get the clean sys.path, import site
56+# because distutils will do it later, and then reset the path and clean
57+# out any namespace packages from site-packages that might have been
58+# loaded by .pth files.
59+clean_path = sys.path[:]
60+import site # imported because of its side effects
61+sys.path[:] = clean_path
62+for k, v in sys.modules.items():
63+ if k in ('setuptools', 'pkg_resources') or (
64+ hasattr(v, '__path__') and
65+ len(v.__path__) == 1 and
66+ not os.path.exists(os.path.join(v.__path__[0], '__init__.py'))):
67+ # This is a namespace package. Remove it.
68+ sys.modules.pop(k)
69+
70+is_jython = sys.platform.startswith('java')
71+
72+setuptools_source = 'http://peak.telecommunity.com/dist/ez_setup.py'
73+distribute_source = 'http://python-distribute.org/distribute_setup.py'
74+distribute_source = 'https://bitbucket.org/pypa/setuptools/raw/f657df1f1ed46596d236376649c99a470662b4ba/distribute_setup.py'
75+
76+# parsing arguments
77+def normalize_to_url(option, opt_str, value, parser):
78+ if value:
79+ if '://' not in value: # It doesn't smell like a URL.
80+ value = 'file://%s' % (
81+ urllib.pathname2url(
82+ os.path.abspath(os.path.expanduser(value))),)
83+ if opt_str == '--download-base' and not value.endswith('/'):
84+ # Download base needs a trailing slash to make the world happy.
85+ value += '/'
86+ else:
87+ value = None
88+ name = opt_str[2:].replace('-', '_')
89+ setattr(parser.values, name, value)
90+
91+usage = '''\
92+[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options]
93+
94+Bootstraps a buildout-based project.
95+
96+Simply run this script in a directory containing a buildout.cfg, using the
97+Python that you want bin/buildout to use.
98+
99+Note that by using --setup-source and --download-base to point to
100+local resources, you can keep this script from going over the network.
101+'''
102+
103+parser = OptionParser(usage=usage)
104+parser.add_option("-v", "--version", dest="version",
105+ help="use a specific zc.buildout version")
106+parser.add_option("-d", "--distribute",
107+ action="store_true", dest="use_distribute", default=False,
108+ help="Use Distribute rather than Setuptools.")
109+parser.add_option("--setup-source", action="callback", dest="setup_source",
110+ callback=normalize_to_url, nargs=1, type="string",
111+ help=("Specify a URL or file location for the setup file. "
112+ "If you use Setuptools, this will default to " +
113+ setuptools_source + "; if you use Distribute, this "
114+ "will default to " + distribute_source + "."))
115+parser.add_option("--download-base", action="callback", dest="download_base",
116+ callback=normalize_to_url, nargs=1, type="string",
117+ help=("Specify a URL or directory for downloading "
118+ "zc.buildout and either Setuptools or Distribute. "
119+ "Defaults to PyPI."))
120+parser.add_option("--eggs",
121+ help=("Specify a directory for storing eggs. Defaults to "
122+ "a temporary directory that is deleted when the "
123+ "bootstrap script completes."))
124+parser.add_option("-t", "--accept-buildout-test-releases",
125+ dest='accept_buildout_test_releases',
126+ action="store_true", default=False,
127+ help=("Normally, if you do not specify a --version, the "
128+ "bootstrap script and buildout gets the newest "
129+ "*final* versions of zc.buildout and its recipes and "
130+ "extensions for you. If you use this flag, "
131+ "bootstrap and buildout will get the newest releases "
132+ "even if they are alphas or betas."))
133+parser.add_option("-c", None, action="store", dest="config_file",
134+ help=("Specify the path to the buildout configuration "
135+ "file to be used."))
136+
137+options, args = parser.parse_args()
138+
139+if options.eggs:
140+ eggs_dir = os.path.abspath(os.path.expanduser(options.eggs))
141+else:
142+ eggs_dir = tempfile.mkdtemp()
143+
144+if options.setup_source is None:
145+ if options.use_distribute:
146+ options.setup_source = distribute_source
147+ else:
148+ options.setup_source = setuptools_source
149+
150+if options.accept_buildout_test_releases:
151+ args.insert(0, 'buildout:accept-buildout-test-releases=true')
152+
153+try:
154+ import pkg_resources
155+ import setuptools # A flag. Sometimes pkg_resources is installed alone.
156+ if not hasattr(pkg_resources, '_distribute'):
157+ raise ImportError
158+except ImportError:
159+ ez_code = urllib2.urlopen(
160+ options.setup_source).read().replace('\r\n', '\n')
161+ ez = {}
162+ exec ez_code in ez
163+ setup_args = dict(to_dir=eggs_dir, download_delay=0)
164+ if options.download_base:
165+ setup_args['download_base'] = options.download_base
166+ if options.use_distribute:
167+ setup_args['no_fake'] = True
168+ if sys.version_info[:2] == (2, 4):
169+ setup_args['version'] = '0.6.32'
170+ ez['use_setuptools'](**setup_args)
171+ if 'pkg_resources' in sys.modules:
172+ reload(sys.modules['pkg_resources'])
173+ import pkg_resources
174+ # This does not (always?) update the default working set. We will
175+ # do it.
176+ for path in sys.path:
177+ if path not in pkg_resources.working_set.entries:
178+ pkg_resources.working_set.add_entry(path)
179+
180+cmd = [quote(sys.executable),
181+ '-c',
182+ quote('from setuptools.command.easy_install import main; main()'),
183+ '-mqNxd',
184+ quote(eggs_dir)]
185+
186+if not has_broken_dash_S:
187+ cmd.insert(1, '-S')
188+
189+find_links = options.download_base
190+if not find_links:
191+ find_links = os.environ.get('bootstrap-testing-find-links')
192+if not find_links and options.accept_buildout_test_releases:
193+ find_links = 'http://downloads.buildout.org/'
194+if find_links:
195+ cmd.extend(['-f', quote(find_links)])
196+
197+if options.use_distribute:
198+ setup_requirement = 'distribute'
199+else:
200+ setup_requirement = 'setuptools'
201+ws = pkg_resources.working_set
202+setup_requirement_path = ws.find(
203+ pkg_resources.Requirement.parse(setup_requirement)).location
204+env = dict(
205+ os.environ,
206+ PYTHONPATH=setup_requirement_path)
207+
208+requirement = 'zc.buildout'
209+version = options.version
210+if version is None and not options.accept_buildout_test_releases:
211+ # Figure out the most recent final version of zc.buildout.
212+ import setuptools.package_index
213+ _final_parts = '*final-', '*final'
214+
215+ def _final_version(parsed_version):
216+ for part in parsed_version:
217+ if (part[:1] == '*') and (part not in _final_parts):
218+ return False
219+ return True
220+ index = setuptools.package_index.PackageIndex(
221+ search_path=[setup_requirement_path])
222+ if find_links:
223+ index.add_find_links((find_links,))
224+ req = pkg_resources.Requirement.parse(requirement)
225+ if index.obtain(req) is not None:
226+ best = []
227+ bestv = None
228+ for dist in index[req.project_name]:
229+ distv = dist.parsed_version
230+ if distv >= pkg_resources.parse_version('2dev'):
231+ continue
232+ if _final_version(distv):
233+ if bestv is None or distv > bestv:
234+ best = [dist]
235+ bestv = distv
236+ elif distv == bestv:
237+ best.append(dist)
238+ if best:
239+ best.sort()
240+ version = best[-1].version
241+
242+if version:
243+ requirement += '=='+version
244+else:
245+ requirement += '<2dev'
246+
247+cmd.append(requirement)
248+
249+if is_jython:
250+ import subprocess
251+ exitcode = subprocess.Popen(cmd, env=env).wait()
252+else: # Windows prefers this, apparently; otherwise we would prefer subprocess
253+ exitcode = os.spawnle(*([os.P_WAIT, sys.executable] + cmd + [env]))
254+if exitcode != 0:
255+ sys.stdout.flush()
256+ sys.stderr.flush()
257+ print ("An error occurred when trying to install zc.buildout. "
258+ "Look above this message for any errors that "
259+ "were output by easy_install.")
260+ sys.exit(exitcode)
261+
262+ws.add_entry(eggs_dir)
263+ws.require(requirement)
264+import zc.buildout.buildout
265+
266+# If there isn't already a command in the args, add bootstrap
267+if not [a for a in args if '=' not in a]:
268+ args.append('bootstrap')
269+
270+
271+# if -c was provided, we push it back into args for buildout's main function
272+if options.config_file is not None:
273+ args[0:0] = ['-c', options.config_file]
274+
275+zc.buildout.buildout.main(args)
276+if not options.eggs: # clean up temporary egg directory
277+ shutil.rmtree(eggs_dir)
buildout.cfgnew file+32
@@ -0,0 +1,32 @@
1+[buildout]
2+extends = base.cfg
3+develop = .
4+
5+eggs =
6+ searx [test]
7+
8+parts +=
9+ pyscripts
10+ robot
11+ test
12+
13+
14+[pyscripts]
15+recipe = zc.recipe.egg:script
16+eggs = ${buildout:eggs}
17+interpreter = py
18+dependent-scripts = true
19+entry-points =
20+ searx-run=searx.webapp:run
21+
22+
23+[robot]
24+recipe = zc.recipe.testrunner
25+eggs = ${buildout:eggs}
26+defaults = ['--color', '--auto-progress', '--layer', 'SearxRobotLayer']
27+
28+
29+[test]
30+recipe = zc.recipe.testrunner
31+eggs = ${buildout:eggs}
32+defaults = ['--color', '--auto-progress', '--layer', 'SearxTestLayer', '--layer', '!SearxRobotLayer']
production.cfgnew file+17
@@ -0,0 +1,17 @@
1+[buildout]
2+extends = base.cfg
3+develop = .
4+
5+eggs =
6+ searx
7+
8+parts +=
9+ pyscripts
10+
11+
12+[pyscripts]
13+recipe = zc.recipe.egg:script
14+eggs = ${buildout:eggs}
15+interpreter = py
16+entry-points =
17+ searx-run=searx.webapp:run
searx/settings_robot.pynew file+16
@@ -0,0 +1,16 @@
1+
2+port = 11111
3+
4+secret_key = "ultrasecretkey" # change this!
5+
6+debug = False
7+
8+request_timeout = 5.0 # seconds
9+
10+weights = {} # 'search_engine_name': float(weight) | default is 1.0
11+
12+blacklist = [] # search engine blacklist
13+
14+categories = {} # custom search engine categories
15+
16+base_url = None # "https://your.domain.tld/" or None (to use request parameters)
searx/testing.pynew file+59
@@ -0,0 +1,59 @@
1+# -*- coding: utf-8 -*-
2+"""Shared testing code."""
3+
4+from plone.testing import Layer
5+from unittest2 import TestCase
6+
7+
8+import os
9+import subprocess
10+import sys
11+
12+
13+class SearxTestLayer:
14+
15+ __name__ = u'SearxTestLayer'
16+
17+ def setUp(cls):
18+ pass
19+ setUp = classmethod(setUp)
20+
21+ def tearDown(cls):
22+ pass
23+ tearDown = classmethod(tearDown)
24+
25+ def testSetUp(cls):
26+ pass
27+ testSetUp = classmethod(testSetUp)
28+
29+ def testTearDown(cls):
30+ pass
31+ testTearDown = classmethod(testTearDown)
32+
33+
34+class SearxRobotLayer(Layer):
35+ """Searx Robot Test Layer"""
36+
37+ def setUp(self):
38+ os.setpgrp() # create new process group, become its leader
39+ webapp = os.path.join(
40+ os.path.abspath(os.path.dirname(os.path.realpath(__file__))),
41+ 'webapp.py'
42+ )
43+ exe = os.path.abspath(os.path.dirname(__file__) + '/../bin/py')
44+ self.server = subprocess.Popen(
45+ [exe, webapp, 'settings_robot'],
46+ stdout=subprocess.PIPE,
47+ stderr=subprocess.STDOUT
48+ )
49+
50+ def tearDown(self):
51+ # TERM all processes in my group
52+ os.killpg(os.getpgid(self.server.pid), 15)
53+
54+
55+SEARXROBOTLAYER = SearxRobotLayer()
56+
57+
58+class SearxTestCase(TestCase):
59+ layer = SearxTestLayer
searx/tests/__init__.pynew file

No changes to the content.

searx/tests/robot/__init__.pynew file

No changes to the content.

searx/tests/robot/test_basic.robotnew file+11
@@ -0,0 +1,11 @@
1+*** Settings ***
2+Library Selenium2Library timeout=10 implicit_wait=0.5
3+Test Setup Open Browser http://localhost:11111/
4+Test Teardown Close All Browsers
5+
6+
7+*** Test Cases ***
8+Front page
9+ Page Should Contain about
10+ Page Should Contain preferences
11+
searx/tests/test_robot.pynew file+24
@@ -0,0 +1,24 @@
1+# -*- coding: utf-8 -*-
2+
3+from plone.testing import layered
4+from robotsuite import RobotTestSuite
5+from searx.testing import SEARXROBOTLAYER
6+
7+import os
8+import unittest2 as unittest
9+
10+
11+def test_suite():
12+ suite = unittest.TestSuite()
13+ current_dir = os.path.abspath(os.path.dirname(__file__))
14+ robot_dir = os.path.join(current_dir, 'robot')
15+ tests = [
16+ os.path.join('robot', f) for f in
17+ os.listdir(robot_dir) if f.endswith('.robot') and
18+ f.startswith('test_')
19+ ]
20+ for test in tests:
21+ suite.addTests([
22+ layered(RobotTestSuite(test), layer=SEARXROBOTLAYER),
23+ ])
24+ return suite
searx/tests/test_unit.pynew file+10
@@ -0,0 +1,10 @@
1+# -*- coding: utf-8 -*-
2+
3+from searx.testing import SearxTestCase
4+
5+
6+class UnitTestCase(SearxTestCase):
7+
8+ def test_flask(self):
9+ import flask
10+ self.assertIn('Flask', dir(flask))
searx/webapp.py+15−4
@@ -18,13 +18,20 @@ along with searx. If not, see < http://www.gnu.org/licenses/ >.
1818'''
1919
2020import os
21+import sys
2122if __name__ == "__main__":
22− from sys import path
23− path.append(os.path.realpath(os.path.dirname(os.path.realpath(__file__))+'/../'))
23+ sys.path.append(os.path.realpath(os.path.dirname(os.path.realpath(__file__))+'/../'))
24+
25+# first argument is for specifying settings module, used mostly by robot tests
26+from sys import argv
27+if len(argv) == 2:
28+ from importlib import import_module
29+ settings = import_module('searx.' + argv[1])
30+else:
31+ from searx import settings
2432
2533from flask import Flask, request, render_template, url_for, Response, make_response, redirect
2634from searx.engines import search, categories, engines, get_engines_stats
27−from searx import settings
2835import json
2936import cStringIO
3037from searx.utils import UnicodeWriter
@@ -226,7 +233,7 @@ def favicon():
226233 'favicon.png', mimetype='image/vnd.microsoft.icon')
227234
228235
229−if __name__ == "__main__":
236+def run():
230237 from gevent import monkey
231238 monkey.patch_all()
232239
@@ -234,3 +241,7 @@ if __name__ == "__main__":
234241 ,use_debugger = settings.debug
235242 ,port = settings.port
236243 )
244+
245+
246+if __name__ == "__main__":
247+ run()
setup.pynew file+51
@@ -0,0 +1,51 @@
1+# -*- coding: utf-8 -*-
2+"""Installer for Searx package."""
3+
4+from setuptools import setup
5+from setuptools import find_packages
6+
7+import os
8+
9+
10+def read(*rnames):
11+ return open(os.path.join(os.path.dirname(__file__), *rnames)).read()
12+
13+
14+long_description = read('README.md')
15+
16+setup(
17+ name='searx',
18+ version="0.1",
19+ description="",
20+ long_description=long_description,
21+ classifiers=[
22+ "Programming Language :: Python",
23+ ],
24+ keywords='meta search engine',
25+ author='Adam Tauber',
26+ author_email='asciimoo@gmail.com',
27+ url='https://github.com/asciimoo/searx',
28+ license='GNU Affero General Public License',
29+ packages=find_packages('.'),
30+ zip_safe=False,
31+ install_requires=[
32+ 'flask',
33+ 'grequests',
34+ 'lxml',
35+ 'setuptools',
36+ ],
37+ extras_require={
38+ 'test': [
39+ 'coverage',
40+ 'flake8',
41+ 'plone.testing',
42+ 'robotframework',
43+ 'robotframework-debuglibrary',
44+ 'robotframework-httplibrary',
45+ 'robotframework-selenium2library',
46+ 'robotsuite',
47+ 'unittest2',
48+ 'zope.testrunner',
49+ ]
50+ },
51+)
versions.cfgnew file+93
@@ -0,0 +1,93 @@
1+[versions]
2+Flask = 0.10.1
3+Jinja2 = 2.7.2
4+MarkupSafe = 0.18
5+WebOb = 1.3.1
6+WebTest = 2.0.11
7+Werkzeug = 0.9.4
8+buildout-versions = 1.7
9+collective.recipe.omelette = 0.16
10+coverage = 3.7.1
11+decorator = 3.4.0
12+docutils = 0.11
13+flake8 = 2.1.0
14+itsdangerous = 0.23
15+mccabe = 0.2.1
16+pep8 = 1.4.6
17+plone.testing = 4.0.8
18+pyflakes = 0.7.3
19+requests = 2.2.0
20+robotframework-debuglibrary = 0.3
21+robotframework-httplibrary = 0.4.2
22+robotframework-selenium2library = 1.5.0
23+robotsuite = 1.4.2
24+selenium = 2.39.0
25+unittest2 = 0.5.1
26+waitress = 0.8.8
27+zc.recipe.testrunner = 2.0.0
28+
29+# Required by:
30+# WebTest==2.0.11
31+beautifulsoup4 = 4.3.2
32+
33+# Required by:
34+# grequests==0.2.0
35+gevent = 1.0
36+
37+# Required by:
38+# gevent==1.0
39+greenlet = 0.4.2
40+
41+# Required by:
42+# searx==0.1
43+grequests = 0.2.0
44+
45+# Required by:
46+# robotframework-httplibrary==0.4.2
47+jsonpatch = 1.3
48+
49+# Required by:
50+# robotframework-httplibrary==0.4.2
51+jsonpointer = 1.1
52+
53+# Required by:
54+# robotsuite==1.4.2
55+# searx==0.1
56+lxml = 3.2.5
57+
58+# Required by:
59+# robotframework-httplibrary==0.4.2
60+robotframework = 2.8.3
61+
62+# Required by:
63+# plone.testing==4.0.8
64+# robotsuite==1.4.2
65+# searx==0.1
66+# zope.exceptions==4.0.6
67+# zope.interface==4.0.5
68+# zope.testrunner==4.4.1
69+setuptools = 2.1
70+
71+# Required by:
72+# zope.testrunner==4.4.1
73+six = 1.5.2
74+
75+# Required by:
76+# collective.recipe.omelette==0.16
77+zc.recipe.egg = 2.0.1
78+
79+# Required by:
80+# zope.testrunner==4.4.1
81+zope.exceptions = 4.0.6
82+
83+# Required by:
84+# zope.testrunner==4.4.1
85+zope.interface = 4.0.5
86+
87+# Required by:
88+# plone.testing==4.0.8
89+zope.testing = 4.1.2
90+
91+# Required by:
92+# zc.recipe.testrunner==2.0.0
93+zope.testrunner = 4.4.1