funkload-1.17.1/000077500000000000000000000000001302537724200134115ustar00rootroot00000000000000funkload-1.17.1/.gitignore000066400000000000000000000001151302537724200153760ustar00rootroot00000000000000syntax:glob dist build doc/*.html *.pyc *.swp .#* *~ src/funkload.egg-info/ funkload-1.17.1/BUILDOUT.txt000066400000000000000000000011301302537724200153740ustar00rootroot00000000000000We provide buildouts for funkload in two flavours: default and minimal. The default ``buildout.cfg`` also installs TCPWatch, which is needed to *record* tests. The ``minimial.cfg`` installs funkload only, without any dependencies. It is useful for installations where you only want to *run* the tests. If you also want to generate reports (i.e. use ``fl-build-report``) you need to install gnuplot. See the `installation instructions`_ for details. gnuplot has quite a few platform specific dependencies and is thus outside the scope of a buildout. funkload-1.17.1/CHANGES.txt000066400000000000000000001007041302537724200152240ustar00rootroot00000000000000================= FunkLoad_ CHANGES ================= :author: Benoit Delbosc :address: bdelbosc _at_ nuxeo.com :abstract: This document describes changes between FunkLoad_ versions. .. contents:: Table of Contents FunkLoad GIT master -------------------- :git: https://github.com/nuxeo/FunkLoad :Target: 1.17.1 FunkLoad 1.17.0 ------------------ :Package: http://pypi.python.org/packages/source/f/funkload/funkload-1.17.0.tar.gz :github: https://github.com/nuxeo/FunkLoad/tree/1.17.0 :Released date: 2015-05-06 New Features ~~~~~~~~~~~~~~ * Add a discover mode to the bench runner, so fl-run-bench can be invoked without specifying the particular test case. This implementation attempts to behave like unittest's 'discover' argument, and there is even a similar --failfast option. (Shandy (Andy) Brown) * Support https proxy (http proxy was already supported) * Add a midCycle hook to execute an action in the middle of a cycle * Add -L options to fl-record to enable tcpwatch forwarded mode which is usefull if you have an http_proxy to pass. * Add --quiet option to fl-build-report to remove warning in rst to html convertion * GH-71: zeromq-based publisher that will let each node publish in realtime the tests results (Tarek Ziade) * GH-68: Use lightweight threads (greenlets) if available. (Tarek Ziade) * Added one more RPS chart in Section 5, where the X-axis represents time(in second) and the Y-axis represents RPS. (Seong-Kook Shin) * Added support for providing the test configuration file with --config (Juha Mustonen) * Display the description in the URL when rendering the page in Firefox (-V option) * GH-41: Allow fl-run-bench to take modules in addition to filenames (joeshwaw):: fl-run-bench project.submodule.loadtests MyTestCase.test_something * Support for gzip compression, you need to add the standard header:: self.setHeader('Accept-encoding', 'gzip') response = self.get(...) Then the `response.body` will be uncompressed when needed. * Experimental comet request support, handle stream request in a separate thread, the server input is send to a consumer method:: # async request thread = self.comet(server_url + "/comet?t=1234&c=1", self.consumer, description="Comet connection") # do some requests ... # stop comet request self.stop_consumer = True thread.join() The consumer get the server input character by character and can stop the the connection by returning 0 value:: def consumer(self, string): trace(string) if self.stop_consumer: self.logd('Stop consumer') return 0 * Ability to configure multiple benchers per server in distribute mode (Dylan Jay) * Ability to configure multiple monitors per server (useful if you have to use ssh tunnels instead of read hostnames). (Dylan Jay) Bug Fixes ~~~~~~~~~~ * GH-69: Cleanly abort on SIGTERM (Tarek Ziade) * GH-32: Fix Funkload WF_fetch patch to handle cookies correctly. Submitted by jorgebastida. * GH-33: Cookies with a leading '.' in the domain are being ignored. Submitted by jorgebastida. * GH-50: Fix monitoring to support kernel 3.0. FunkLoad 1.16.1 ------------------ :Package: http://pypi.python.org/packages/source/f/funkload/funkload-1.16.1.tar.gz :github: https://github.com/nuxeo/FunkLoad/tree/1.16.1 :Released date: 2011-07-28 Bug Fixes ~~~~~~~~~~ * GH-2[345]: Fixing distributed mode regression on 1.6.0 FunkLoad 1.16.0 ------------------ :Package: http://pypi.python.org/packages/source/f/funkload/funkload-1.16.0.tar.gz :github: https://github.com/nuxeo/FunkLoad/tree/1.16.0 :Released date: 2011-07-21 New Features ~~~~~~~~~~~~~~ * New monitoring plugins contribution from Krzysztof A. Adamski. You can extend the monitoring by adding new plugins checkout the `Munin `_ and `Nagios `_ examples. * Extends HTTP methods with HEAD and OPTIONS, enable to send any methods using the new ``method`` api, adding PROPFIND DAV exemple :: # requests for options self.options(server_url, description="options") self.propfind(server_url + '/dav/foo', depth=1, description="propfind") # put a lock using the new method api data = Data('text/xml', """ http://www.webdav.org/webdav_fs/ """) self.method("LOCK", server_url + "/somedocpath", params=data, description="lock") * GH-9: New options for distributed mode: you can specify the Python binary to use with ``--distribute-python=PYTHON_BIN`` option and additional packages to install on slaves with the ``--distributed-packages=DISTRIBUTED_PACKAGES`` option. Contribution from Adrew McFague. * GH-9: Added the ability to monitor hosts from the central host, The monitoring will also retroactively match up the concurrent users for graphing purposes. Contribution from Adrew McFague. * GH-5: ``fl-record`` now checks for ``tcpwatch`` command in addition to ``tcpwatch.py`` and ``tcpwatch-httpproxy``. * GH-6: Filter out .swfs when building funkload tests with ``fl-record``. * GH-10: Support of python2.7. * GH-15: Exposed the load_auto_links parameter to the get/put/post/method methods so that the option to not parse the payload is available when writing a test. Provided by Ali-Akber Saifee. Bug fixes ~~~~~~~~~~~ * Fix org-mode output for Org 7.5, adding missing monitoring section. * GH-14: Fix report creation when having error message with non-us characters. Patch from Krzysztof A. Adamski. * GH-11: Handling of deleted cookies * GH-7: data and demo directory missing * GH-8: funkload 1.15.0 package doesn't include ez_setup.py FunkLoad 1.15.0 ------------------ :Package: http://pypi.python.org/packages/source/f/funkload/funkload-1.15.0.tar.gz :github: https://github.com/nuxeo/FunkLoad/tree/1.15.0 :Released date: 2011-03-11 New features ~~~~~~~~~~~~~ * Now supporting emacs Org-mode_ text format as report output. This enable to edit a report as plain text and to produce professional PDF reports througth the Org-mode_ LaTeX export. Here is an example of a `PDF `_ and an `Org-mode report `_. You need to build the HTML report first:: fl-build-report --html funkload.xml Creating html report: ...done: /tmp/test_foo-20110304T160328/index.html # then create the org file fl-build-report --org funkload.xml > /tmp/test_foo-20110304T160328/index.org emacs /tmp/test_foo-20110304T160328/index.org # then export as PDF C-c C-e d # refer to the org-mode site for latex exports requirements * New `trend report `_ to display evolution of performances over time. Just use the ``--trend`` option of the ``fl-build-report`` command. * The credential server can serve a sequence. Using ``xmlrpc_get_seq`` threads can share a sequence:: from funkload.utils import xmlrpc_get_seq ... seq = xmlrpc_get_seq() * Source migrated from svn to git, hosted in gigthub https://github.com/nuxeo/FunkLoad * Bug tracker moving to github: https://github.com/nuxeo/FunkLoad/issues * New site and documentation using sphinx: http://funkload.nuxeo.org/ * CPSTestCase and ZopeTestCase have been moved to the demo folder. * Removing deprecated gdchart support, now relying only on gnuplot. Note that a mathplotlib support is on the TODO_ list. Bug fixes ~~~~~~~~~ * Fix #3: FunkLoad Failures Fail on Python 2.7 https://github.com/nuxeo/FunkLoad/issues#issue/3 * Do not block on read content when content-length is null, patch submited by Bertrand Yvain. * Fix monitoring network monitoring charts. FunkLoad 1.14.0 ------------------ :Package: http://pypi.python.org/packages/source/f/funkload/funkload-1.14.0.tar.gz :Svn: http://svn.nuxeo.org/pub/funkload/tags/1.14.0 :Released date: 2011-02-14 New features ~~~~~~~~~~~~~ * Switch to Sphinx for documentation, this work is in progress, the draft can be found in http://funkload.nuxeo.org/sphinx/. Thanks to Ali-Akber Saifee. * Support of HTTP PUT and DELETE method, provided by Ali-Akber Saifee. * Distributed mode (beta), provided by Ali-Akber Saifee, visit the new FAQ http://funkload.nuxeo.org/sphinx/faq.html#how-to-run-multiple-bencher for more information. * Support of the ``--simple-fetch`` option using the configuration file. This can be useful when using benchmaster http://pypi.python.org/pypi/benchmaster. Just add ``simple_fetch = 1`` in the bench section. * Add setUpBench and tearDownBench hooks, they are called only once per bench before and after all cycles. Note that these hooks are called only using ``fl-run-bench`` and no ``fl-run-test``. This comes in addition to the existing setUpCycle and tearDownCycle hooks that are run before and after each bench cycle. * Enable to add metadata for a bench using addMetadata(kw), metadata are stored into the bench result file and are displayed on the bench configuration section. A typical usage is to add metadata during setUpBench or tearDownBench hooks. * Handling FunkLoad todo list with an Org-mode_ file TODO_, replacing the old trac. * Mark CPSTestCase as deprecated will be removed in 1.15. * Mark GDChart support as deprecated, it will be removed in 1.15. Bug fixes ~~~~~~~~~ * Fix: ImportError are reported as IOError using ``fl-run-test`` because FunkLoad is switching in doctest mode when ImportError occurs. Fixed by adding an explicit option to run doctest: ``--doctest``. * Fix: xmlrpc_get_credential(host, port) and xmlrpc_list_credentials without a group parameter it raises a "TypeError: cannot marshal None unless allow_none is enabled". * Fix: 303 Redirect is handled as an HTTP/1.1 request by apache using a keep alive connection while FunkLoad is HTTP/1.0. This add a 15s (KeepAliveTimeout) overhead. Fixed by adding "Connection: close" header on 303. Thanks to Jan Kotuc. * Fixing invalid diff report if the report contains Apdex information. FunkLoad 1.13.0 ------------------ :Package: http://pypi.python.org/packages/source/f/funkload/funkload-1.13.0.tar.gz :Svn: http://svn.nuxeo.org/pub/funkload/tags/1.13.0 :Released date: 2010-07-27 New features ~~~~~~~~~~~~~ * Adding Apdex (Application Performance inDEX) on reporting. The Apdex is a numerical measure of user satisfaction, it is based on three zones of application responsiveness: - Satisfied: The user is fully productive. This represents the time value (T seconds) below which users are not impeded by application response time. - Tolerating: The user notices performance lagging within responses greater than T, but continues the process. - Frustrated: Performance with a response time greater than 4*T seconds is unacceptable, and users may abandon the process. By default T is set to 1.5s this means that response time between 0 and 1.5s the user is fully productive, between 1.5 and 6s the responsivness is tolerating and above 6s the user is frustrated. The T variable can be set using the ``fl-build-report`` option ``--apdex-T``. The Apdex score converts many measurements into one number on a uniform scale of 0-to-1 (0 = no users satisfied, 1 = all users satisfied). To ease interpretation the Apdex score can be converted to a rating: - Unacceptable represented in gray for a score between 0 and 0.5 - Poor represented in red for a score between 0.5 and 0.7 - Fair represented in yellow for a score between 0.7 and 0.85 - Good represented in green for a score between 0.85 and 0.94 - Excellent represented in blue for a score between 0.94 and 1 By looking at the Page stats in the report you can take the maximum throughput with a Good/Excellent rating as an overall performance score. Note that this new metric can be generated with old funkload results file. Here is a report example: http://funkload.nuxeo.org/report-example/test_seam_java6/#page-stats Visit http://www.apdex.org/ for more information on Apdex. * The selection of the best cycle to sort slowest requests in the report is found using the product of throuthput and apdex score instead of simple maximum. Bug fixes ~~~~~~~~~ * The recorder now maintain the order of input elements, patch submited by Martin Aspeli * Fix unicode decode errors when merging reports when an error occurred on a page with unicode, patch submited by Martin Aspeli. FunkLoad 1.12.0 ------------------ :Package: http://pypi.python.org/packages/source/f/funkload/funkload-1.12.0.tar.gz :Svn: http://svn.nuxeo.org/pub/funkload/tags/1.12.0 :Released date: 2010-05-26 New features ~~~~~~~~~~~~~~ * ``fl-build-report`` is now able to create a report using many result files. The goal of this feature is to create bench report for a distributed bench. To be merged the result files must have the same cycles and cycle duration. * The hostname and python version is now logged in the result file. * Try to use `psyco `_ if installed. Note that psyco works only on 32-bit system. * Miscellaneous speed improvments: removing minimal sleep time between actions, removing useless logs. * New ``--as-fast-as-possible`` or ``-f`` option to ``fl-run-bench`` to remove sleep any times between requests and test cases, this is equivalent to ``-m0 -M0 -t0``. * New ``--enable-debug-server`` option for ``fl-run-bench`` command. This option run a debug HTTP server which exposes an interface using which parameters can be modified at run-time. Currently supported parameters: - http://localhost:8000/cvu?inc= to increase the number of CVUs, - http://localhost:8000/cvu?dec= to decrease the number of CVUs, - http://localhost:8000/getcvu returns number of CVUs. You can load a server and be able to change the number of concurrent users during the run without pre-specifying it through 'cycles' argument, this is usefull during debugging or profiling. This feature has been provided by Goutham Bhat. Bug fixes ~~~~~~~~~ * Fixed a missing semicolon in WebUnit patch, caused all cookies to be combined into one ubercookie, thanks to Gareth Davidson. * Do not generate charts when there are no data available preventing: "RuntimeError: Failed to run gnuplot cmd: ... Skipping data file with no valid points" error when building a report. * Fix report charts on OS X darwin, thanks to Ethan Winn and Arshavski Alexander. FunkLoad 1.11.0 ------------------ :Package: http://pypi.python.org/packages/source/f/funkload/funkload-1.11.0.tar.gz :Svn: http://svn.nuxeo.org/pub/funkload/tags/1.11.0 :Released date: 2010-01-26 New features ~~~~~~~~~~~~~~ * Support https with ssl/tls by providing a private key and certificate (PEM formatted) to the new setKeyAndCertificateFile method. This feature has been provided by Kelvin Ward. * Better win support: - Patch recorder to work on non linux os, enable TCPWATCH env variable to set the tcpwatch binary path, thanks to David Fraser. - Patch report builder to produce gnuplot chart on win, thanks to Kelvin Ward. * Enable to view the request headers either using an api (method debugHeaders) or command line option ``-d --debug-level=3``. * Support of WebUnit 1.3.9 (in addition of 1.3.8) this new version brings cookie max-age support. Bug fixes ~~~~~~~~~ * Fixing gnuplot charts when concurrent users are not in ascending order or have duplicates. (like ``-c 1:10:50:10:1`` instead of ``-c 1:10:20:30``) * Prevent ``fl-record`` to generate invalid python test case name. * Fix regex to check valid resources urls, it was too restrictive. * Fix cookie support, there was an extra ";" if there was only one cookie, thanks to Daniel Sward. * Fixing filename in mime encoding when uploading a file using an absolute path. FunkLoad 1.10.0 ------------------ :Package: http://pypi.python.org/packages/source/f/funkload/funkload-1.10.0.tar.gz :Svn: http://svn.nuxeo.org/pub/funkload/tags/1.10.0 :Released date: 2009-03-23 New features ~~~~~~~~~~~~~~ * Easier installation on Debian Lenny/Ubuntu 8.10, See INSTALL_. * Support setuptools console_scripts entry point for funkload scripts. Thanks to Ross Patterson. * Added a two sample buildout configurations: a minimal one that just contains what is necessary to run tests and a default one that also enables recording. Thanks to Tom Lazar. * Added a ``--report-directory`` option to ``fl-build-report`` to set report directory path. * Added a ``--label`` option to ``fl-run-bench`` which the report generator then appends to the output directory of the report. This is to make it easier to keep different bench runs apart from each other. Thanks to Tom Lazar. * Added a ``--pause`` option to ``fl-run-test``, the test runner will wait for the ENTER key to be pressed between requests. * Added a ``--loop`` option to ``fl-record``, this way it is easy to type a comment then hit Ctrl-C to dump the script and continue with the next action. Bug fixes ~~~~~~~~~ * Only attempt to retrieve syntactically valid URLs links. * Fix report generation for bench with only one cycle. * Fix differential report average percent profit chart. * Fix possible error on differential report ReStructuredText. FunkLoad 1.9.0 ------------------ :Package: http://pypi.python.org/packages/source/f/funkload/funkload-1.9.0.tar.gz :Svn: http://svn.nuxeo.org/pub/funkload/tags/1.9.0 :Released date: 2008-11-07 New features ~~~~~~~~~~~~ * Switch to `gnuplot 4.2 `_ for charts generation. Gnuplot script and data are kept with the report enabling charts customization. No more gdchart charting woes. * Enhanced charts displaying more information, view a `report example `_. * New diff report option, comparing 2 reports is a long and error prone task, `fl-build-report` has been enhanced to produce a differencial report. This new feature requires gnuplot. To produce a diff report:: fl-build-report --diff path/to/report-reference path/to/report-challanger View a `diff report example `_. * Post method handles custom data and content type. For example to post a xmlrpc by hand :: from funkload.utils import Data ... data = Data('text/xml', """ getStatus """) self.post(server_url, data, description="Post user data") * The recorder translates properly ``application/xml`` or any content type using the new ``Data`` class (see above exemple). * New `test script `_ provided with ``fl-install-demo`` to bench the JBoss Seam Booking application. FunkLoad 1.8.0 -------------- :Package: http://pypi.python.org/packages/source/f/funkload/funkload-1.8.0.tar.gz :Svn: http://svn.nuxeo.org/pub/funkload/tags/1.8.0/ :Released date: 2008-07-28 New features ~~~~~~~~~~~~ * Upgrate to python-gdchart2 using libgd2 (gdchart1 is deprecated). Bug fixes ~~~~~~~~~ * Handle redirect code 303 and 307 like a 302. FunkLoad 1.7.0 -------------- :Package: http://funkload.nuxeo.org/funkload-1.7.0.tar.gz :Svn: http://svn.nuxeo.org/pub/funkload/tags/1.7.0/ :Released date: 2008-07-23 New features ~~~~~~~~~~~~ * Recorder and test take care of JSF MyFaces tokens which make FunkLoad ready to record/play Nuxeo EP or any JBoss Seam application. * Change API of listHref to be able to search on link content as well as link url. `pattern` parameter is renamed into `url_pattern` and a new `content_pattern` can be supply. Bug fixes ~~~~~~~~~ * fix: # 1838_: Upload doesn't work for CherryPy. * fix: # 1834_: multiple redirects not handled properly (jehiah patch). * fix: # 1837_: post() is sent as GET when no params defined. * Apply patch from Dan Rahmel to fix fl-record on non posix os. FunkLoad 1.6.2 -------------- :Package: http://funkload.nuxeo.org/funkload-1.6.2.tar.gz :Svn: http://svn.nuxeo.org/pub/funkload/tags/1.6.2/ :Released date: 2007-04-06 Bug fixes ~~~~~~~~~ * fix: 'Page stats' percentiles are wrong. FunkLoad 1.6.1 -------------- :Package: http://funkload.nuxeo.org/funkload-1.6.1.tar.gz :Svn: http://svn.nuxeo.org/pub/funkload/tags/1.6.1/ :Released date: 2007-03-09 Bug fixes ~~~~~~~~~ * Support of python 2.5. * Fix: 1.6.0 regression invalid encoding of parameters when posting several times the same key. FunkLoad 1.6.0 -------------- :Package: http://funkload.nuxeo.org/funkload-1.6.0.tar.gz :Svn: http://svn.nuxeo.org/pub/funkload/tags/1.6.0/ :Released date: 2007-02-27 New features ~~~~~~~~~~~~ * Do not send cookie with ``deleted`` value, this fix trouble with Zope's CookieCrumbler and enable benching of Plone_ apps, Thanks to Lin. * Works with Ruby CGI, fixing webunit mimeEncode and adding Content-type header on file upload, Thanks to Bryan Helmkamp. * # 1283_: Patching webunit to support http proxy by checking $http_proxy env. Thanks to Greg, (note that https proxy is not yet supported). * Adding Mirko Friedenhagen ``--with-percentiles`` option to ``fl-build-report`` to include percentiles in statistic tables and use 10%, 50% and 90% percentil instead of min, avg and max in charts. This is now the default option, use ``--no-percentiles`` for the old behaviour. * Upgrade to setuptools 0.6c3 * FunkLoadTestCase.conf_getList accept a separator parameter Bug fixes ~~~~~~~~~ * fix: # 1279_: Browser form submit encoding, default encoding for post is now application/x-www-form-urlencoded, multipart mime encoding is used only when uploading files. * Patching webunit mimeEncode method to send CRLF as describe in RFC 1945 3.6.2, patch provided by Ivan Kurmanov. * fix: response string representation url contains double `/` * fix: xmlrpc url contains basic auth credential in the report * fix: # 1300_: easy_install failed to install docutils from sourceforge, upgrading ez_install 0.6a10 FunkLoad 1.5.0 -------------- :Package: http://funkload.nuxeo.org/funkload-1.5.0.tar.gz :Svn: http://svn.nuxeo.org/pub/funkload/tags/1.5.0/ :Released date: 2006-01-31 New features ~~~~~~~~~~~~ * # 1284_: TestCase: support of doctest There is a new FunkLoadDocTest class that ease usage of FunkLoad from doctest:: >>> from funkload.FunkLoadDocTest import FunkLoadDocTest >>> fl = FunkLoadDocTest() >>> response = fl.get('http://localhost/') >>> 'HTML' in response.body True >>> response If you use python2.4, the test runner ``fl-run-test`` is able launch doctest from a plain text file or embedded in python docstring:: $ fl-run-test -v doctest_dummy.txt Doctest: doctest_dummy.txt Ok ----------------------------------------------------- Ran 1 test in 0.077s OK And the ``--debug`` option makes doctests verbose:: $ fl-run-test -d doctest_dummy.txt ... Trying: fl = FunkLoadDocTest() Expecting nothing ok Trying: fl.get('http://localhost/') Expecting: ok Ok ---------------------------------------------------------------------- Ran 1 test in 0.051s OK * Test runner can use a negative regex to select tests. For example if you want to launch all tests that does not ends with 'foo' :: fl-run-test myFile.py -e '!foo$' * # 1282_: TestRunner: more verbosity The new ``fl-run-test`` option ``--debug-level=2`` will produce debug output on each link (images or css) fetched. * Improve firefox view in real time by using approriate file extention for the content type. * CPSTestCase is up to date for 3.4.0, use CPS338TestCase for a CPS 3.3.8. Bug fixes ~~~~~~~~~ * fix # 1278_: BenchRunner: UserAgent from config file is not set FunkLoad 1.4.1 -------------- :Package: http://funkload.nuxeo.org/funkload-1.4.1.tar.gz :Svn: http://svn.nuxeo.org/pub/funkload/tags/1.4.1/ :Released date: 2005-12-16 Bug fixes ~~~~~~~~~ * fix # 1201_: Erroneous page stats * fix # 934_: REPORT: Charts should display origin FunkLoad 1.4.0 -------------- :Package: http://funkload.nuxeo.org/funkload-1.4.0.tar.gz :Svn: http://svn.nuxeo.org/pub/funkload/tags/1.4.0/ :Released date: 2005-12-08 New features ~~~~~~~~~~~~ * New ``--loop-on-pages`` option for ``fl-run-test``. This option enable to check response time of some specific pages inside a test without changing the script, which make easy to tune a page in a complex context. Use the ``debug`` option to find the page numbers. For example:: fl-run-test myfile.py MyTestCase.testSomething -l 3 -n 100 Run MyTestCase.testSomething, reload one hundred time the page 3 without concurrency and as fast as possible. Output response time stats. You can loop on many pages using slice -l 2:4. * New ``--accept-invalid-links`` option for ``fl-run-test`` and ``fl-run-bench`` Don't fail if css/image links are not reachable. * New ``--list`` option for ``fl-run-test`` to list the test names without running them. * # 936_: TestRunner: use regexp to load test New ``--regex`` or ``-e`` option for ``fl-run-test`` to filter test names that match a regular expression. * # 939_: Browser: Provide an option to disable image and links load New ``--simple-fetch`` option for ``fl-run-test`` and ``fl-run-bench``. * # 937_: TestRunner: Add an immediate fail option New ``--stop-on-fail`` option for ``fl-run-test`` that stops tests on first failure or error. * # 933_: Report: Add global info Adding total number of tests, pages and requests during the bench. * ``CPSTestCase.listDocumentHref`` is renamed into ``cpsListDocumentHref`` * ``FunkLoadTestCase.xmlrpc_call`` is renamed into ``xmlrpc`` (``xmlrpc_call`` is still working) * Some code cleaning, cheesecake_ index 460/560 ~82%. * New epydoc_ API_ documentation. * ``fl-run-test`` is now able to run standard unittest.TestCase. Bug fixes ~~~~~~~~~ * # 1183_: updating ez_setup to fix broken sourceforge docutils download FunkLoad 1.3.1 -------------- :Package: http://funkload.nuxeo.org/funkload-1.3.1.tar.gz :Svn: http://svn.nuxeo.org/pub/funkload/tags/1.3.1/ :Released date: 2005-11-10 Bug fixes ~~~~~~~~~ * fix # 1115_: Recorder: impossible to generate test FunkLoad 1.3.0 -------------- :Package: http://funkload.nuxeo.org/funkload-1.3.0.tar.gz :Svn: http://svn.nuxeo.org/pub/funkload/tags/1.3.0/ :Released date: 2005-11-08 New features ~~~~~~~~~~~~ * # 944_: Recorder: replace TestMaker recorder. Providing a ``fl-record`` command that drive a TCPWatch_ proxy. See INSTALL_ to setup TCPWatch_. * # 1041_: Browser: implement an addHeader method. FunkLoadTestCase provides new methods ``setUserAgent``, ``addHeader`` and ``clearHeaders``. * # 1088_: TestRunner / BenchRunner: use compatible command line option - All ``fl-*`` executables have a ``--version`` option to display the FunkLoad_ version. - All `fl-run-*` are now in color mode by default. Use ``--no-color`` options for monochrome output. You need to remove the ``-c`` option for ``fl-run-test`` and ``-C`` for ``fl-run-bench`` in your scripts. - Changing ``fl-run-bench`` short option ``-d`` into ``-D`` for duration, keeping ``-d`` for debug mode. - Removing ``fl-run-test`` short option ``-D`` to not conflict with new ``-D`` option of ``fl-run-bench``, you now have to use the long format ``--dump-directory``. Bug fixes ~~~~~~~~~ * fix # 935_: Browser: doesn't handle Referer header. FunkLoad 1.2.0 -------------- :Package: http://funkload.nuxeo.org/funkload-1.2.0.tar.gz :Svn: http://svn.nuxeo.org/pub/funkload/tags/1.2.0/ :Released date: 2005-10-18 New features ~~~~~~~~~~~~ * Credential and Monitor services have been refactored they are now true unix daemon service, controllers are now in pure python (no more bash scripts). * Switching from distutils to setuptools using EasyInstall_, installing FunkLoad is now just a question of ``sudo easy_install funkload``. * Moving demo examples into the egg, just type ``fl-install-demo`` to extract the demo folder Bug fixes ~~~~~~~~~ * fix # 1027_ Report: min and max page response time are wrong. * fix # 1017_ Report: request charts is alway the same. * fix # 1022_ Monitor: no cpu usage monitoring on linux 2.4.x * fix # 1000_ easy_install failed to install funkload. * fix # 1009_ Report: remove error scale in graph if there is no errors. * fix # 1008_ Report: missing legend. FunkLoad 1.1.0 -------------- :Package: http://funkload.nuxeo.org/funkload-1.1.0.tar.gz :Svn: http://svn.nuxeo.org/pub/funkload/tags/1.1.0/ :Released date: 2005-10-07 New features ~~~~~~~~~~~~ * FunkLoadTestCase: adding ``exists`` method. * FunkLoadTestCase: support XML RPC test/bench using ``xmlrpc_call``. * FunkLoadTestCase: adding a regex pattern to ``listHref``. * FunkLoadTestCase: new ``setUpCycle`` and ``tearDownCycle`` methods to configure bench between cycle. * FunkLoadTestCase: Patching webunit to send a User-Agent header. * # 950_ Report: display failure and error (first part). * # 948_ Report: provide the 5 slowest requests. * # 941_ Demo: provide usefull examples. * CPSTestCase: add cpsVerifyUser, cpsVerifyGroup, cpsSetLocalRole, cpsCreateSite, cpsCreateSection. * ZopeTestCase: adding zopeRestartZope, zopeFlushCache, zopePackZodb, zopeAddExternalMethod. * Lipsum: handle iso 8859-15 vocabulary. * Lipsum: adding random phone number and address generator. * credentiald: add methods listCredentials and listGroups. * Moving TODO and bugs to trac: http://svn.nuxeo.org/trac/pub/report/12 * Improve documentation. Bug fixes ~~~~~~~~~ * # 971_ Report: the network load monitor should record network speed instead of cumulative downlaod * XML result file is resetted at beginning of a test or bench. * Fix threadframe module requirement. * No more python 2.3 dependency for scripts `fl-*-ctl` FunkLoad 1.0.0 -------------- :Location: http://funkload.nuxeo.org/funkload-1.0.0.tar.gz :Released date: 2005-09-01 **First public release.** --------------------------------------------- More information on the FunkLoad_ site. .. _FunkLoad: http://funkload.nuxeo.org/ .. _EasyInstall: http://peak.telecommunity.com/DevCenter/EasyInstall .. _TCPWatch: http://hathawaymix.org/Software/TCPWatch/ .. _API: api/index.html .. _TODO: https://github.com/nuxeo/FunkLoad/blob/master/TODO.txt .. _Org-mode: http://orgmode.org/ .. _epydoc: http://epydoc.sourceforge.net/ .. _cheesecake: http://tracos.org/cheesecake/ .. _933: http://svn.nuxeo.org/trac/pub/ticket/933 .. _934: http://svn.nuxeo.org/trac/pub/ticket/934 .. _935: http://svn.nuxeo.org/trac/pub/ticket/935 .. _936: http://svn.nuxeo.org/trac/pub/ticket/936 .. _937: http://svn.nuxeo.org/trac/pub/ticket/937 .. _939: http://svn.nuxeo.org/trac/pub/ticket/939 .. _941: http://svn.nuxeo.org/trac/pub/ticket/941 .. _944: http://svn.nuxeo.org/trac/pub/ticket/944 .. _948: http://svn.nuxeo.org/trac/pub/ticket/948 .. _950: http://svn.nuxeo.org/trac/pub/ticket/950 .. _971: http://svn.nuxeo.org/trac/pub/ticket/971 .. _1000: http://svn.nuxeo.org/trac/pub/ticket/1000 .. _1008: http://svn.nuxeo.org/trac/pub/ticket/1008 .. _1009: http://svn.nuxeo.org/trac/pub/ticket/1009 .. _1017: http://svn.nuxeo.org/trac/pub/ticket/1017 .. _1022: http://svn.nuxeo.org/trac/pub/ticket/1022 .. _1027: http://svn.nuxeo.org/trac/pub/ticket/1027 .. _1041: http://svn.nuxeo.org/trac/pub/ticket/1041 .. _1088: http://svn.nuxeo.org/trac/pub/ticket/1088 .. _1115: http://svn.nuxeo.org/trac/pub/ticket/1115 .. _1183: http://svn.nuxeo.org/trac/pub/ticket/1183 .. _1201: http://svn.nuxeo.org/trac/pub/ticket/1201 .. _1278: http://svn.nuxeo.org/trac/pub/ticket/1278 .. _1279: http://svn.nuxeo.org/trac/pub/ticket/1279 .. _1282: http://svn.nuxeo.org/trac/pub/ticket/1282 .. _1283: http://svn.nuxeo.org/trac/pub/ticket/1283 .. _1284: http://svn.nuxeo.org/trac/pub/ticket/1284 .. _1300: http://svn.nuxeo.org/trac/pub/ticket/1300 .. _1834: http://svn.nuxeo.org/trac/pub/ticket/1834 .. _1837: http://svn.nuxeo.org/trac/pub/ticket/1837 .. _1838: http://svn.nuxeo.org/trac/pub/ticket/1837 .. _Plone: http://plone.org/ .. Local Variables: .. mode: rst .. End: .. vim: set filetype=rst: funkload-1.17.1/INSTALL.txt000066400000000000000000000244041302537724200152640ustar00rootroot00000000000000Installation =============== This document describes how to install the FunkLoad_ tool. Debian and Ubuntu quick installation ------------------------------------ With Debian Lenny or Ubuntu 8.10, 9.04, 10.04 ..., you can install the latest snapshot this way :: sudo aptitude install python-dev python-setuptools \ python-webunit python-docutils gnuplot sudo aptitude install tcpwatch-httpproxy --without-recommends sudo easy_install -f http://funkload.nuxeo.org/snapshots/ -U funkload To install the latest stable release replace the last line with :: sudo easy_install -U funkload If you want to use the distributed mode (since 1.14.0) you need to install paramiko and virtualenv_ :: sudo aptitude install python-paramiko python-virtualenv That's all. Note that Debian Squeeze, includes FunkLoad 0.13.0 package that can be installed this way:: sudo aptitude install python-webunit funkload CentOS quick installation ------------------------- As root :: yum install python-setuptools python-docutils gnuplot python-devel make wget http://funkload.nuxeo.org/3dparty/tcpwatch-1.3.tar.gz tar xzvf tcpwatch-1.3.tar.gz cd tcpwatch python setup.py install easy_install webunit easy_install -f http://funkload.nuxeo.org/snapshots/ -U funkload Mac OS installation -------------------- See http://pyfunc.blogspot.com/2011/12/installing-funkload-on-mac.html. Windows installation -------------------- - Install Python 2.7 - Install Gnuplot - Install distutils (easy_install) :: easy_install docutils easy_install webunit # get http://funkload.nuxeo.org/3dparty/tcpwatch-1.3.zip # unzip and run python setup.py install easy_install -f http://funkload.nuxeo.org/snapshots/ -U funkload Generic installation (all OS) ----------------------------- Some parts are OS specific: * the monitor server works only on Linux * the credential server is a unix daemon but can run on windows if launched in debug mode (using startd instead of start) Under windows there is a trick to install docutils (see below), you may also rename the scripts with a ``.py`` extension. Required packages ~~~~~~~~~~~~~~~~~~~ * libexpat1 (should not be required on recent OS) * funkload 1.14.0 requires python >= 2.5 * python 2.4 is required by the test runner only if you want to launch python doctest, other stuff work fine with a python 2.3.5. python >= 2.5 are supported with funkload > 1.6.0 * python distutils * python xml * EasyInstall_, either use a package or download ez_setup.py_, and run it:: cd /tmp wget http://peak.telecommunity.com/dist/ez_setup.py sudo python ez_setup.py This will download and install the appropriate setuptools egg for your Python version. Optional packages ~~~~~~~~~~~~~~~~~~ * To avoid soiling your system's Python modules and dependencies, it's a good practice to use a virtualenv_ to confine what's needed only for FunkLoad. For example for Debian/Ubuntu here is what to do to use virtualenv:: sudo aptitude install python-virtualenv * To produce charts with FunkLoad >= 1.9.0: gnuplot 4.2, either use a package or visit the http://www.gnuplot.info/ site. gnuplot (or wgnuplot for win) must be in the executable path. * The recorder use TCPWatch_ which is not yet available using easy_install:: cd /tmp wget http://hathawaymix.org/Software/TCPWatch/tcpwatch-1.3.tar.gz tar xzvf tcpwatch-1.3.tar.gz cd tcpwatch python setup.py build sudo python setup.py install Different installation flavors ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Easy installation __________________ * Install the latest stable package:: sudo easy_install -U funkload This will install FunkLoad_, webunit_ and docutils_ eggs. * Install the latest snapshot version:: sudo easy_install -f http://funkload.nuxeo.org/snapshots/ -U funkload Installation from source ________________________ You can install from source, typically to test/use the development version (which may be unstable). First to avoid soiling your system's Python modules and dependencies, it's a good practice to use a virtualenv_ to confine what's needed only for FunkLoad. Especially if you are experimenting and developing. This approach is also handy if you don't have `sudo` power. So let's install a virtualenv dedicated to Funkload in, let's say, `~/opt`:: mkdir --parent ~/opt virtualenv ~/opt/funkload.virtualenv . ~/opt/funkload.virtualenv/bin/activate Then you can cleanly build and install Funkload:: git clone git://github.com/nuxeo/FunkLoad.git cd FunkLoad/ python setup.py build python setup.py install Now everytime you want to use the version of FunkLoad you've built, you will have to specify which virtualenv to use:: . ~/opt/funkload.virtualenv/bin/activate fl-run-test --version Alternatively it's possible to do the following to not use a virtualenv (but you've been warned):: git clone git://github.com/nuxeo/FunkLoad.git cd FunkLoad/ python setup.py build sudo python setup.py install Installation with buildout __________________________ This approach might be handy if you don't have `sudo` power. Get the FunkLoad package from pypi: http://pypi.python.org/pypi/funkload/ or a snapshot from: http://funkload.nuxeo.org/snapshots/ Then use the buildout:: tar zxf funkload-1.14.0.tar.gz cd funkload-1.14.0 python bootstrap.py bin/buildout Then your executable are ready on the bin directory. Installation without network access ___________________________________ Transfer all the archives from http://funkload.nuxeo.org/3dparty/ in a directory:: mkdir -p /tmp/fl cd /tmp/fl wget -r -l1 -nd http://funkload.nuxeo.org/3dparty/ Transfer the latest FunkLoad package :: # get setuptools package and untar python setup.py install easy_install docutils* easy_install tcpwatch* easy_install webunit* easy_install funkload* Test it -------- Install the FunkLoad_ examples:: fl-install-demo Go to the demo/xmlrpc folder then:: cd funkload-demo/xmlrpc/ make test To test benching and report building just:: make bench See ``funkload-demo/README`` for others examples. Problems ? ----------- * got a :: error: invalid Python installation: unable to open /usr/lib/python2.4/config/Makefile (No such file or directory) Install python2.4-dev package. * easy_install complains about conflict:: ... /usr/lib/site-python/docutils Note: you can attempt this installation again with EasyInstall, and use either the --delete-conflicting (-D) option or the --ignore-conflicts-at-my-risk option, to either delete the above files and directories, or to ignore the conflicts, respectively. Note that if you ignore the conflicts, the installed package(s) may not work. ------------------------------------------------------------------------- error: Installation aborted due to conflicts If FunkLoad_, webunit_ or docutils_ were previously installed without using EasyInstall_. You need to reinstall the package that raises the conflict with the ``--delete-conflicting`` option, see easy_install_ documentation. * If you still have conflict try to remove FunkLoad_ from your system:: easy_install -m funkload rm -rf /usr/lib/python2.3/site-packages/funkload* rm -rf /usr/local/funkload/ rm /usr/local/bin/fl-* rm /usr/bin/fl-* then reinstall * easy_install ends with:: error: Unexpected HTML page found at http://prdownloads.sourceforge.net... Source Forge has changed their donwload page you need to update your setuptools:: sudo easy_install -U setuptools * Failed to install docutils 0.4 with easy_install 0.6a9 getting a:: ... Best match: docutils 0.4 Downloading http://prdownloads.sourceforge.net/docutils/docutils-0.4.tar.gz?download Requesting redirect to (randomly selected) 'mesh' mirror error: No META HTTP-EQUIV="refresh" found in Sourceforge page at http://prdownloads.sourceforge.net/docutils/docutils-0.4.tar.gz?use_mirror=mesh It looks like sourceforge change their download page again :( - download manually the docutils tar gz from http://prdownloads.sourceforge.net/docutils/docutils-0.4.tar.gz?download - then ``sudo easy_install /path/to/docutils-0.4.tar.gz`` * When testing ``make test`` return :: ### credentialctl: Stopping credential server. python: can't open file '/usr/lib/python2.4/site-packages/funkload-1.2.0-py2.4.egg/funkload/credentialctl.py': [Errno 20] Not a directory Starting with FunkLoad_ 1.2.0 scripts are installed in /usr/bin, previously they were in /usr/local/bin, you need to remove them:: sudo rm /usr/local/bin/fl-* * While runing a test you got :: Traceback (most recent call last): File "/usr/local/bin/fl-run-test", line 8, in load_entry_point('funkload==1.11.0', 'console_scripts', 'fl-run-test')() File "build/bdist.linux-i686/egg/funkload/TestRunner.py", line 478, in main File "build/bdist.linux-i686/egg/funkload/TestRunner.py", line 337, in __init__ File "build/bdist.linux-i686/egg/funkload/TestRunner.py", line 347, in loadTests File "/usr/lib/python2.6/doctest.py", line 2412, in DocFileSuite suite.addTest(DocFileTest(path, **kw)) File "/usr/lib/python2.6/doctest.py", line 2331, in DocFileTest doc, path = _load_testfile(path, package, module_relative) File "/usr/lib/python2.6/doctest.py", line 219, in _load_testfile return open(filename).read(), filename IOError: [Errno 2] No such file or directory: 'path/to/your/testcase' This means that your test file can not be loaded as a python module, (may be due to import error) FunkLoad then badly report it as an invalid doc test file. To view the original error just run the testcase using python instead of fl-run-test (``python your_test_case.py``). This is fixed since 1.14. .. _FunkLoad: http://funkload.nuxeo.org/ .. _webunit: http://mechanicalcat.net/tech/webunit/ .. _EasyInstall: http://peak.telecommunity.com/DevCenter/EasyInstall .. _easy_install: http://peak.telecommunity.com/DevCenter/EasyInstall#command-line-options .. _ez_setup.py: http://peak.telecommunity.com/dist/ez_setup.py .. _docutils: http://docutils.sourceforge.net/ .. _TCPWatch: http://hathawaymix.org/Software/TCPWatch/ .. _virtualenv: https://virtualenv.pypa.io/ .. Local Variables: .. mode: rst .. End: .. vim: set filetype=rst: funkload-1.17.1/LICENSE.txt000066400000000000000000000431101302537724200152330ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. funkload-1.17.1/Makefile000066400000000000000000000023371302537724200150560ustar00rootroot00000000000000# FunkLoad Makefile # $Id: $ # .PHONY: build pkg sdist egg install clean rpm TARGET := gateway:/opt/public-dev/funkload # use TAG=a for alpha, b for beta, rc for release candidate ifdef TAG PKGTAG := egg_info --tag-build=$(TAG) --tag-date else PKGTAG := endif build: python setup.py $(PKGTAG) build test: cd src/funkload/tests && fl-run-test -v --doctest test_Install.py pkg: sdist egg sdist: python setup.py $(PKGTAG) sdist egg: -python2.7 setup.py $(PKGTAG) bdist_egg distrib: -scp dist/funkload-*.tar.gz $(TARGET)/snapshots -scp dist/funkload-*.egg $(TARGET)/snapshots install: python setup.py $(PKGTAG) install register: -python2.7 setup.py register bdist_egg upload uninstall: -easy_install -m funkload -rm -rf /usr/lib/python2.3/site-packages/funkload* -rm -rf /usr/lib/python2.4/site-packages/funkload* -rm -rf /usr/lib/python2.5/site-packages/funkload* -rm -rf /usr/lib/python2.6/dist-packages/funkload* -rm -rf /usr/local/lib/python2.6/dist-packages/funkload* -rm -rf /usr/local/funkload/ -rm -f /usr/local/bin/fl-* -rm -f /usr/bin/fl-* clean: find . "(" -name "*~" -or -name ".#*" -or -name "#*#" -or -name "*.pyc" ")" -print0 | xargs -0 rm -f rm -rf ./build ./dist ./MANIFEST ./funkload.egg-info funkload-1.17.1/README.rst000077700000000000000000000000001302537724200165712README.txtustar00rootroot00000000000000funkload-1.17.1/README.txt000066400000000000000000000111551302537724200151120ustar00rootroot00000000000000Introduction ============== FunkLoad_ is a functional and load web tester, written in Python, whose main use cases are: * Functional testing of web projects, and thus regression testing as well. * Performance testing: by loading the web application and monitoring your servers it helps you to pinpoint bottlenecks, giving a detailed report of performance measurement. * Load testing tool to expose bugs that do not surface in cursory testing, like volume testing or longevity testing. * Stress testing tool to overwhelm the web application resources and test the application recoverability. * Writing web agents by scripting any web repetitive task. Features --------- Main FunkLoad_ features are: * Functional test are pure Python scripts using the pyUnit_ framework like normal unit test. Python enable complex scenarios to handle real world applications. * Truly emulates a web browser (single-threaded) using an enhanced Richard Jones' webunit_: - get/post/put/delete support - post any kind of content type like ``application/xml`` - DAV support - basic authentication support - file upload and multipart/form-data submission - cookies support - referrer support - accept gzip content encoding - https support - https with ssl/tls by providing a private key and certificate (PEM formatted) - http_proxy support - fetching css, javascript and images - emulating a browser cache * Advanced test runner with many command-line options: - set the target server url - display the fetched page in real time in your browser - debug mode to display http headers - check performance of a single page (or set of pages) inside a test - green/red color mode - select or exclude tests cases using a regex - support normal pyUnit_ test - support doctest_ from a plain text file or embedded in python docstring * Turn a functional test into a load test: just by invoking the bench runner you can identify scalability and performance problems. If needed the bench can distributed over a group of worker machines. * Detailed bench reports in ReST, HTML, Org-mode_, PDF (using LaTeX/PDF Org-mode export) containing: - the bench configuration - tests, pages, requests stats and charts - the requets that took the most time - monitoring one or many servers cpu usage, load average, memory/swap usage and network traffic charts - an http error summary list * Differential reports to compare 2 bench reports giving a quick overview of scalability and velocity changes. * Trend reports to view the performance evolution with multiple reports. * Easy test customization using a configuration file or command line options. * Easy test creation using embeded TCPWatch_ as proxy recorder, so you can use your web browser and produce a FunkLoad_ test automatically, including file upload or any ajax call. * Provides web assertion helpers to check expected results in responses. * Provides helpers to retrieve contents in responses page using DOM. * Easy to install (EasyInstall_). * Comes with examples look at the demo_ folder. * Successfully tested with dozen of differents web servers: PHP, python, Java... License ---------- FunkLoad_ is free software distributed under the `GNU GPL`_ license. \(C) Copyright 2005-2011 Nuxeo SAS (http://nuxeo.com). This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. .. _FunkLoad: http://funkload.nuxeo.org/ .. _Org-mode: http://orgmode.org/ .. _TCPWatch: http://hathawaymix.org/Software/TCPWatch/ .. _webunit: http://mechanicalcat.net/tech/webunit/ .. _pyUnit: http://pyunit.sourceforge.net/ .. _API: api/index.html .. _Nuxeo: http://www.nuxeo.com/ .. _`python cheese shop`: http://www.python.org/pypi/funkload/ .. _EasyInstall: http://peak.telecommunity.com/DevCenter/EasyInstall .. _`GNU GPL`: http://www.gnu.org/licenses/licenses.html#GPL .. _doctest: http://docs.python.org/lib/module-doctest.html .. _demo: https://github.com/nuxeo/FunkLoad/tree/master/src/funkload/demo/ .. Local Variables: .. mode: rst .. End: .. vim: set filetype=rst: funkload-1.17.1/THANKS000066400000000000000000000041061302537724200143250ustar00rootroot00000000000000====== THANKS ====== Mirko Friedenhagen: Providing reports percentiles features. Ivan Kurmanov: Webunit mimeEncode patch using CRLF. Greg 912: http_proxy support. Bryan Helmkamp: Webunit mimeEncode patch extra CRLF on first boundary, adding Content-Type header for file upload. Lin: Patch to remove cookie with 'deleted' value. Sylvain Hellegouarch: Python 2.5 support. M.-A. Darche: Extensive use, documentation improvements and FunkLoad name co-idea. Jehiah Czebotar: Mutiple redirects patch. Dan Rahmel: Recorder patch. Daniel Swärd: Feedback on post method and cookie patch. Ross Patterson: Support of setuptools console_scripts. Tom Lazar: Support of buildout, label option for the bench runner, many fixes. Toni Mueller: Debian package maintener. Jose Parrella: Debian man page writter. David Fraser: Patch on Recorder to support non standard tcpwatch installation. Kelvin Ward: Patch on Report builder to support win gnuplot. Goutham Bath: HTTP debug server for the bench runner. Arshavski Alexander, Ethan Winn: Bug report and patch for OS X. Gareth Davidson: Cookies patch, improve win support. Martin Aspeli: Patch on recorder/report generation. Ali-Akber Saifee: Distributed mode impl, PUT/DELETE support, Sphinx documentation layout and more. Jan Kotuc: Debugging keep alive problem on http/1.1 303. Georges Racinet: Adding metadata on report and setUpBench hook ideas. Bertrand Yvain: Patch on null content-lengh, fix on network monitoring charts. Krzysztof A. Adamski: Plugin monitors framework. Andrew McFague: Specify a custom Python binary configuration option for distributed nodes, pep8 fixes for the funkload.Distributed module. Thibault Soulcié: logo. Benito Jorge Bastida: Misc bug fixes. Joe Shaw: Allow fl-run-bench to take modules in addition to filenames. Juha Mustonen: Adding a --config option. Dylan Jay: multiple bencher and monitor per server support Seong-Kook Shin: Additional RPS chart where X axis represents the time in seconds Tarek Ziade: distributed mode improvment, greenlets rt feedback and more. Shandy (Andy) Brown: fl-run-bench discover mode and doc review. funkload-1.17.1/TODO.txt000066400000000000000000000055251302537724200147260ustar00rootroot00000000000000# -*- mode: org -*- #+TITLE: FunkLoad to do list #+AUTHOR: Benoit Delbosc If you want to report a bug or if you think that something is missing, See the [[http://funkload.nuxeo.org/reporting.html][reporting section]]. Current target release is 1.15.0 * Features ** TODO Matplotlib integration :@next:REPORT: ** TODO Add a param to join to next request action :@next:CORE: Joining distinct requests into a single page/action: self.get(url, description="foo", join_with_next_req=True) self.get(url2) url2 is reported as being on the same page as url, there is no thinktime pause between the 2 requests. ** TODO Finish funkload.metadata impl :@next: fl-buil-report should render the funkload.metadata file if present ** TODO Produce gplot script with commented extended options :@next:REPORT: - explain how to have small label on trend reports export GDFONTPATH=/usr/share/fonts/truetype/freefont set label "Some comment" at 2,51,1 rotate by 45 front font 'FreeSans,6' - ex of eps output ** TODO Add unit test on request :@next:QA: using simple file req/resp as tcpwatch + minimal fake web server. ** TODO Provide new sysstat monitoring plugin :@next:CORE: ** TODO Provide csv monitoring plugin :@next:CORE: make the same charts than in [[http://public.dev.nuxeo.com/~ben/logchart/monitor.html][logchart]] ** TODO Add common utils :@next:CORE: - extractToken(buffer, start, stop, maxlen=-1) - getRandomLineInFile() - assertAndDump() dump getBody on failure ** TODO Improve report failure section :@next:REPORT: Add an error section with: - http failure: - sort by page|req|code - display a link to the returned page if any - Assertion error: - sort by traceback ** TODO Look at what can be done with perfbase :@next:TBD: http://perfbase.tigris.org/ * Bugs ** TODO Patch cookie lib :@next:CORE: The cookie lib is too restrictive, try to merge the refactoring done by Google: http://svn.nuxeo.org/trac/pub/browser/funkload/branches/google-refactoring Added: [2010-11-18 jeu. 14:23 ** TODO Produce valid xml test result :@next:REPORT: the root element is missing * org-mode configuration - TBD :: to be defined - TODO :: doable implementation to be done... - STARTED :: task started - WAITING :: requires information to be processed - DONE :: implemented - @current :: for the current release - @next :: post pone to the next release #+TAGS: { @current(c) @next(n) CORE(o) REPORT(r) ADM(a) DOC(d) QA(q) } #+SEQ_TODO: TBD(b) TODO(t) STARTED(s) WAITING(w) | DONE(d) CANCELLED(c) DEFERRED(f) funkload-1.17.1/bootstrap.py000066400000000000000000000034171302537724200160050ustar00rootroot00000000000000############################################################################## # # Copyright (c) 2006 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. # ############################################################################## """Bootstrap a buildout-based project Simply run this script in a directory containing a buildout.cfg. The script accepts buildout command-line options, so you can use the -c option to specify an alternate configuration file. $Id$ """ import os, shutil, sys, tempfile, urllib2 tmpeggs = tempfile.mkdtemp() try: import pkg_resources except ImportError: ez = {} exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py' ).read() in ez ez['use_setuptools'](to_dir=tmpeggs, download_delay=0) import pkg_resources cmd = 'from setuptools.command.easy_install import main; main()' if sys.platform == 'win32': cmd = '"%s"' % cmd # work around spawn lamosity on windows ws = pkg_resources.working_set assert os.spawnle( os.P_WAIT, sys.executable, sys.executable, '-c', cmd, '-mqNxd', tmpeggs, 'zc.buildout', dict(os.environ, PYTHONPATH= ws.find(pkg_resources.Requirement.parse('setuptools')).location ), ) == 0 ws.add_entry(tmpeggs) ws.require('zc.buildout') import zc.buildout.buildout zc.buildout.buildout.main(sys.argv[1:] + ['bootstrap']) shutil.rmtree(tmpeggs) funkload-1.17.1/buildout.cfg000066400000000000000000000010661302537724200157240ustar00rootroot00000000000000[buildout] extends = minimal.cfg parts += tcpwatch-source tcpwatch-install [tcpwatch-source] recipe = hexagonit.recipe.download url = http://hathawaymix.org/Software/TCPWatch/tcpwatch-1.3.tar.gz [tcpwatch-install] recipe = z3c.recipe.egg:setup setup = ${tcpwatch-source:location}/tcpwatch args = install_scripts --install-dir=${tcpwatch-source:location}/bin [funkload] recipe = zc.recipe.egg:scripts eggs = docutils funkload initialization = import os os.environ['PATH'] = ( '${tcpwatch-source:location}/bin:'+os.environ['PATH']) funkload-1.17.1/contrib/000077500000000000000000000000001302537724200150515ustar00rootroot00000000000000funkload-1.17.1/contrib/FunkloadExample/000077500000000000000000000000001302537724200201305ustar00rootroot00000000000000funkload-1.17.1/contrib/FunkloadExample/FunkloadExamplePlugin/000077500000000000000000000000001302537724200243665ustar00rootroot00000000000000funkload-1.17.1/contrib/FunkloadExample/FunkloadExamplePlugin/__init__.py000066400000000000000000000000001302537724200264650ustar00rootroot00000000000000funkload-1.17.1/contrib/FunkloadExample/FunkloadExamplePlugin/example.py000066400000000000000000000022231302537724200263720ustar00rootroot00000000000000from funkload.MonitorPlugins import MonitorPlugin, Plot GNUPLOTSTYLE='lines lw 3' DATALABEL1='EXAMPLE1' DATATITLE1='example1' PLOTTITLE1='Example plot1 - single data' PLOTTITLE2='Example plot2 - multiple data' DATALABEL21='EXAMPLE21' DATATITLE21='example21' DATALABEL22='EXAMPLE22' DATATITLE22='example22' class Example(MonitorPlugin): plots=[Plot({DATALABEL1: [GNUPLOTSTYLE, DATATITLE1]}, title=PLOTTITLE1), Plot({ DATALABEL21: [GNUPLOTSTYLE, DATATITLE21], DATALABEL22: [GNUPLOTSTYLE, DATATITLE22] }, title=PLOTTITLE2)] def getStat(self): return {DATALABEL1: 70, DATALABEL21: 80, DATALABEL22: 90} def parseStats(self, stats): if not (hasattr(stats[0], DATALABEL1) and \ hasattr(stats[0], DATALABEL21) and \ hasattr(stats[0], DATALABEL22)): return None data1=[int(getattr(stats[0], DATALABEL1)) for x in stats] data21=[int(getattr(stats[0], DATALABEL21)) for x in stats] data22=[int(getattr(stats[0], DATALABEL22)) for x in stats] return {DATALABEL1: data1, DATALABEL21: data21, DATALABEL22: data22} funkload-1.17.1/contrib/FunkloadExample/setup.py000066400000000000000000000006341302537724200216450ustar00rootroot00000000000000from setuptools import setup PLUGINNAME="FunkloadExamplePlugin" PACKAGE=PLUGINNAME setup( name=PLUGINNAME, description="Funkload example monitor plugin.", author="Krzysztof A. Adamski", author_email="k@japko.eu", version="1.0", packages=[PACKAGE], entry_points= { 'funkload.plugins.monitor' : [ '%s = %s.example:Example' % (PLUGINNAME, PACKAGE) ] } ) funkload-1.17.1/contrib/FunkloadMunin/000077500000000000000000000000001302537724200176235ustar00rootroot00000000000000funkload-1.17.1/contrib/FunkloadMunin/FunkloadMunin/000077500000000000000000000000001302537724200223755ustar00rootroot00000000000000funkload-1.17.1/contrib/FunkloadMunin/FunkloadMunin/MonitorPluginMunin.py000066400000000000000000000124021302537724200265630ustar00rootroot00000000000000import shlex, re, os from subprocess import * from funkload.MonitorPlugins import MonitorPlugin, Plot RE_ENTRY=r'[a-zA-Z_][a-zA-Z0-9_]+\.[a-zA-Z0-9_]+' """ NOTES: - watch out for plugins running for a long time - no arguments can be passed to munin plugins (they should not need it anyway) - munin plugins written as shell scripts may need MUNIN_LIBDIR env defined - some munin plugins may need root priviledges in monitor.conf: [plugins.monitormunin] command1 = /usr/share/munin/plugins/vmstat;MUNIN_LIBDIR=/usr/share/munin/ command2 = /etc/munin/plugins/if_eth0 commandN = plugin_full_path;ENV_VAR=VALUE ENV_VAR2=VALUE2 """ class MonitorMunin(MonitorPlugin): def __init__(self, conf=None): super(MonitorMunin, self).__init__(conf) if conf==None or not conf.has_section('plugins.monitormunin'): return self.commands={} for opt in conf.options('plugins.monitormunin'): if re.match(r'^command\d+$', opt): config=conf.get('plugins.monitormunin', opt).split(";") if len(config)==1: config=(config[0], "") self.commands[os.path.basename(config[0])]=(config[0], config[1]) for cmd in self.commands.keys(): data=self._getConfig(cmd, self.commands[cmd][0], self.commands[cmd][1]) p={} negatives=[] counters=[] for d in data[1]: p[d[0]]=['lines lw 2', d[1]] if d[2]: negatives.append(d[2]) if d[3]: counters.append(d[0]) if len(p)==0: continue title=cmd if data[0]: title=re.sub(r'\$\{graph_period\}', 'second', data[0]) self.plots.append(Plot(p, title=title, negatives=negatives, counters=counters)) def _nameResult(self, cmd, label): return "%s_%s_%s" % (self.name, cmd, label) def _parseOutput(self, output): ret={} for line in output.split('\n'): splited=line.split(' ') if len(splited)>=2: ret[splited[0]]=" ".join(splited[1:]) return ret def _parseEnv(self, env): environment=os.environ for entry in env.split(' '): splited=entry.split('=') if len(splited)>=2: environment[splited[0]]="=".join(splited[1:]) return environment def _getConfig(self, name, cmd, env): output = Popen('%s config' % cmd, shell=True, stdout=PIPE, env=self._parseEnv(env)).communicate()[0] output_parsed=self._parseOutput(output) fields=[] for entry in output_parsed.keys(): if re.match(RE_ENTRY, entry): field=entry.split('.')[0] if field not in fields: fields.append(field) ret=[] for field in fields: label="" neg=False count=False data_name=self._nameResult(name, field) if "%s.label"%field in output_parsed: label=output_parsed["%s.label"%field] # if output_parsed.has_key("%s.info"%field): # label=output_parsed["%s.info"%field] if "%s.negative"%field in output_parsed: neg=self._nameResult(name, output_parsed["%s.negative"%field]) if "%s.type"%field in output_parsed: t=output_parsed["%s.type"%field] if t=='COUNTER' or t=='DERIVE': count=True ret.append((data_name, label, neg, count)) title=None if 'graph_vlabel' in output_parsed: title=output_parsed['graph_vlabel'] return [title, ret] def _parseStat(self, name, cmd, env): output = Popen([cmd], shell=True, stdout=PIPE, env=self._parseEnv(env)).communicate()[0] ret={} for line in output.split('\n'): splited=line.split(' ') if len(splited)==2 and re.match(RE_ENTRY, splited[0]): data_name=self._nameResult(name, splited[0].split('.')[0]) ret[data_name]=splited[1] return ret def getStat(self): ret={} for cmd in self.commands.keys(): data=self._parseStat(cmd, self.commands[cmd][0], self.commands[cmd][1]) for key in data.keys(): ret[key]=data[key] return ret def parseStats(self, stats): if len(self.plots)==0: return None for plot in self.plots: for p in plot.plots.keys(): if not (hasattr(stats[0], p)): return None ret={} for plot in self.plots: for p in plot.plots.keys(): if p in plot.counters: parsed=[] for i in range(1, len(stats)): delta=float(getattr(stats[i], p))-float(getattr(stats[i-1], p)) time=float(stats[i].time)-float(stats[i-1].time) parsed.append(delta/time) ret[p]=parsed else: ret[p]=[float(getattr(x, p)) for x in stats] if p in plot.negatives: ret[p]=[x*-1 for x in ret[p]] return ret funkload-1.17.1/contrib/FunkloadMunin/FunkloadMunin/__init__.py000066400000000000000000000000001302537724200244740ustar00rootroot00000000000000funkload-1.17.1/contrib/FunkloadMunin/setup.py000066400000000000000000000006561302537724200213440ustar00rootroot00000000000000from setuptools import setup PLUGINNAME="FunkloadMunin" PACKAGE=PLUGINNAME setup( name=PLUGINNAME, description="Funkload monitor plugin for Munin plugins.", author="Krzysztof A. Adamski", author_email="k@japko.eu", version="0.1", packages=[PACKAGE], entry_points= { 'funkload.plugins.monitor' : [ '%s = %s.MonitorPluginMunin:MonitorMunin' % (PLUGINNAME, PACKAGE) ] } ) funkload-1.17.1/contrib/FunkloadNagios/000077500000000000000000000000001302537724200177555ustar00rootroot00000000000000funkload-1.17.1/contrib/FunkloadNagios/FunkloadNagios/000077500000000000000000000000001302537724200226615ustar00rootroot00000000000000funkload-1.17.1/contrib/FunkloadNagios/FunkloadNagios/MonitorPluginNagios.py000066400000000000000000000051331302537724200272040ustar00rootroot00000000000000import shlex, re from subprocess import * from funkload.MonitorPlugins import MonitorPlugin, Plot """ NOTES: - all values are converted to float - charts unit will be set to the unit of first label returned by nagios plugin - nagios plugin name can not contain characters that are invalid for xml attribute name - nagios plugins should return data immediately otherwise you may have problems (long running plugins) - nagios plugins return codes are ignored in monitor.conf: [plugins.monitornagios] command1 = check_load;/usr/lib/nagios/plugins/check_load -w 5.0,4.0,3.0 -c 10.0,6.0,4.0 command2 = check_ping;/usr/lib/nagios/plugins/check_ping -H localhost -w 10,10% -c 10,10% -p 1 commandN = command_name;full_path [args] """ class MonitorNagios(MonitorPlugin): def __init__(self, conf=None): super(MonitorNagios, self).__init__(conf) if conf==None or not conf.has_section('plugins.monitornagios'): return self.commands={} for opt in conf.options('plugins.monitornagios'): if re.match(r'^command\d+$', opt): config=conf.get('plugins.monitornagios', opt).split(";") self.commands[config[0]]=config[1] for cmd in self.commands.keys(): data=self._parsePerf(cmd, self.commands[cmd]) p={} for d in data: p[d[1]]=['lines lw 2', d[0]] if len(p)!=0: self.plots.append(Plot(p, unit=data[0][3], title=cmd)) def _nameResult(self, cmd, label): return "%s_%s_%s" % (self.name, cmd, label) def _parsePerf(self, name, cmd): output = Popen(shlex.split(cmd), stdout=PIPE).communicate()[0] perfs=output.split('|')[-1] data=re.findall(r'([^=]+=[^;]+);\S+\s?', perfs) ret=[] i=0 for d in data: groups=re.match(r"'?([^']+)'?=([\d\.\,]+)(.+)?$", d).groups("") ret.append((groups[0], self._nameResult(name, i), groups[1], groups[2])) i+=1 return ret def getStat(self): ret={} for cmd in self.commands.keys(): data=self._parsePerf(cmd, self.commands[cmd]) for d in data: ret[d[1]]=d[2] return ret def parseStats(self, stats): if len(self.plots)==0: return None for plot in self.plots: for p in plot.plots.keys(): if not (hasattr(stats[0], p)): return None ret={} for plot in self.plots: for p in plot.plots.keys(): ret[p]=[float(getattr(x, p)) for x in stats] return ret funkload-1.17.1/contrib/FunkloadNagios/setup.py000066400000000000000000000007031302537724200214670ustar00rootroot00000000000000from setuptools import setup PLUGINNAME="FunkloadNagios" PACKAGE=PLUGINNAME setup( name=PLUGINNAME, description="Funkload monitor plugin for Nagios plugins performance data.", author="Krzysztof A. Adamski", author_email="k@japko.eu", version="0.1", packages=[PACKAGE], entry_points= { 'funkload.plugins.monitor' : [ '%s = %s.MonitorPluginNagios:MonitorNagios' % (PLUGINNAME, PACKAGE) ] } ) funkload-1.17.1/doc/000077500000000000000000000000001302537724200141565ustar00rootroot00000000000000funkload-1.17.1/doc/Makefile000066400000000000000000000062261302537724200156240ustar00rootroot00000000000000# Makefile for Sphinx documentation # TARGET := gateway:/opt/public-dev/funkload # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/funkload.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/funkload.qhc" latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." distrib: scp -r build/html/* $(TARGET)/ funkload-1.17.1/doc/source/000077500000000000000000000000001302537724200154565ustar00rootroot00000000000000funkload-1.17.1/doc/source/api/000077500000000000000000000000001302537724200162275ustar00rootroot00000000000000funkload-1.17.1/doc/source/api/core_api.rst000066400000000000000000000005321302537724200205420ustar00rootroot00000000000000Test Modules ============ :mod:`funkload.FunkLoadTestCase` -- FunkLoadTestCase ---------------------------------------------------- .. automodule:: funkload.FunkLoadTestCase :members: :mod:`funkload.FunkLoadDocTest` -- FunkLoadDocTest -------------------------------------------------- .. automodule:: funkload.FunkLoadDocTest :members: funkload-1.17.1/doc/source/api/internals.rst000066400000000000000000000006431302537724200207630ustar00rootroot00000000000000Internals ========= :mod:`funkload.TestRunner` -- TestRunner ---------------------------------------- .. automodule:: funkload.TestRunner :members: :mod:`funkload.BenchRunner` -- BenchRunner ------------------------------------------ .. automodule:: funkload.BenchRunner :members: :mod:`funkload.Distributed` -- Distributed ------------------------------------------ .. automodule:: funkload.Distributed :members: funkload-1.17.1/doc/source/api/utility.rst000066400000000000000000000001151302537724200204610ustar00rootroot00000000000000Utilities ========= utils ----- .. automodule:: funkload.utils :members: funkload-1.17.1/doc/source/benching.rst000066400000000000000000000114431302537724200177700ustar00rootroot00000000000000Benchmarks concepts ===================== The same FunkLoad test can be turned into a load test, just by invoking the bench runner ``fl-run-bench``. Page ~~~~ A page is an http get/post request with associated sub requests like redirects, images or links (css, js files). This is what users see as a single page. Note that an xmlrpc call or a put/delete is also taken in account as a page. Test ~~~~ A test is made with 3 methods: setUp/test_name/tearDown. During the test_name method each get/post request is called a page. :: [setUp][page 1] [page 2] ... [page n] [tearDown] ======================================================> time <----------------------------------> test method <--> sleeptime_min to sleeptime_max <-----> page 1 connection time Cycle ~~~~~ A cycle is a load of n concurrents test during a 'duration' period. Threads are launched every 'startupdelay' seconds, each thread executes test in a loop. Once all threads have been started we start to record stats. Only tests that end during the 'duration' period are taken into account for the test stats (in the representation below test like [---X are not take into account). Only pages and requests that finish during the 'duration' are taken into account for the request and pages statistic Before a cycle a setUpCycle method is called, after a cycle a tearDownCycle method is called, you can use these methods to test differents server configuration for each cycle. :: Threads ^ | | |n [---test--] [--------] [--|---X |... | | | |2 [------|--] [--------] [-------] | | | | |1 [------X | [--------] [-------] [--|--X | | | |[setUpCycle] | | [tearDownCycle] ===========================================================> time <------ cycle duration -------> <----- staging -----> <---- staging -----> <-> startupdelay <---> sleeptime Cycles ~~~~~~ FunkLoad_ can execute many cycles with different number of CUs (Concurrent Users), this way you can find easily the maximum number of users that your application can handle. Running n cycles with the same CUs is a good way to see how the application handles a writing test over time. Running n cycles with the same CUs with a reading test and a setUpCycle that change the application configuration will help you to find the right tuning. :: cvus = [n1, n2, ...] Threads ^ | | |n2 __________ | / \ | / \ |n1 _________ / \ | / \ / \ | / \ / \ | / \ / \ ==================================================> time <-------> duration <--------> <-----> cycle sleep time Tips ~~~~~ Here are few remarks/advices to obtain workable metrics. * Since it may use significant CPU resources, make sure that performance limits are not hit by FunkLoad before your server's limit is reached. Check this by launching a bench from another host. * Having a cycle with one user gives a usefull reference. * Run multiple cycles for a bench. * A bench is composed of a benching test (or scenario) run many times. A good benching test should not be too long so you have a higher testing rate (that is, more benching tests can come to their end). * The cycle duration for the benching test should be long enough. Around 5 times the duration of a single benching test is a value that is usually a safe bet. You can obtain this duration of a single benching test by running ``fl-run-test myfile.py MyTestCase.testSomething``. Rationale : Normally a cycle duration of a single benching test should be enough. But from the testing platform side if there are more than one concurrent user, there are many threads to start and it takes some time. And on from the tested platform side it is common that a benching test will last longer and longer as the server is used by more and more users. * You should use many cycles with the same step interval to produce readable charts (1:10:20:30:40:50:60 vs 1:10:100) * A benching test must have the same number of page and in the same order. * Use a Makefile to make reproductible bench. * There is no debug option while doing a bench (since this would be illegible with all the threads). So, if a bench fails (that is using `fl-run-bench`), use ``fl-run-test -d`` to debug. .. _FunkLoad: http://funkload.nuxeo.org/ funkload-1.17.1/doc/source/changes.rst000077700000000000000000000000001302537724200220522../../CHANGES.txtustar00rootroot00000000000000funkload-1.17.1/doc/source/conf.py000066400000000000000000000144361302537724200167650ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # funkload documentation build configuration file, created by # sphinx-quickstart on Fri Apr 16 14:25:32 2010. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.append(os.path.abspath('../../../src')) # -- General configuration ----------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.ifconfig'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8' # The master toctree document. master_doc = 'index' # General information about the project. project = u'FunkLoad' copyright = u'2015, Benoit Delbosc' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '1.17' # The full version, including alpha/beta/rc tags. release = '1.17.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. #unused_docs = [] # List of directories, relative to source directory, that shouldn't be searched # for source files. exclude_trees = [] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. html_theme = 'sphinxdoc' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. html_logo = 'funkload-logo-small.png' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_use_modindex = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'funkloaddoc' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). latex_paper_size = 'a4' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'funkload.tex', u'funkload Documentation', u'Benoit Delbosc', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_use_modindex = True funkload-1.17.1/doc/source/credential.rst000066400000000000000000000021701302537724200203220ustar00rootroot00000000000000The credential server ======================= If you are writing a bench that requires to be logged with different users FunkLoad provides an xmlrpc credential server to serve login/pwd between the different threads. It requires 2 files with the same pattern as unix /etc/passwd and /etc/group. The password file have the following format:: login1:pwd1 login2:pwd2 ... The group file format is:: group1:user1, user2 group2:user2 # you can split group declaration group1:user3 ... The credential server uses a configuration file to point to the user and group file and define the listening port:: [server] host=localhost port=22207 credentials_path=passwords.txt groups_path=groups.txt # to loop over the entire file set the value to 0 loop_on_first_credentials=0 To start the credential server:: fl-credential-ctl credential.conf start You can find more option in the usage_ page. In your test case to get a credential:: from funkload.utils import xmlrpc_get_credential ... login, pwd = xmlrpc_get_credential(host, port, "group2") .. _usage: usage-fl-credential-ctl.html funkload-1.17.1/doc/source/development.rst000066400000000000000000000006231302537724200205330ustar00rootroot00000000000000Development =========== The API can be browse `here `_ To check out the source code:: https://github.com/nuxeo/FunkLoad You can contribute using the `github fork process `_ or by sending patch or feedback to me: bdelbosc _at_ nuxeo.com. Thanks to all the contributors_ ! .. _contributors: https://github.com/nuxeo/FunkLoad/blob/master/THANKS funkload-1.17.1/doc/source/faq.rst000066400000000000000000000272731302537724200167720ustar00rootroot00000000000000FAQ ==== What do all these dots mean ? ------------------------------- During a bench cycle each "Starting threads" dot represents a running thread:: Cycle #1 with 10 virtual users ------------------------------ * setUpCycle hook: ... done. * Current time: 2011-01-26T23:23:06.234422 * Starting threads: ........ During the cycle logging each green dot represents a successful test while each red 'F' represents a test failure:: * Logging for 10s (until 2011-01-26T23:23:16.360602): ......F...... During the tear-down each dot represents a stopped thread:: * Waiting end of threads: ......... How can I accept invalid Cookies ? ---------------------------------- - ``Error : COOKIE ERROR: Cookie domain "" doesn’t start with "."`` Comment the lines in file /usr/lib/python2.6/site-packages/webunit-1.3.8-py2.6.egg/webunit/cookie.py:: #if domain[0] != '.': # raise Error, 'Cookie domain "%s" doesn\'t start with "."' % domain - ``Error : COOKIE ERROR: Cookie domain "."" doesn’t match request host ""`` Comment the lines in the file /usr/lib/python2.6/site-packages/webunit-1.3.8-py2.6.egg/webunit/cookie.py:: #if not server.endswith(domain): # raise Error, 'Cookie domain "%s" doesn\'t match ' # 'request host "%s"'%(domain, server) How can I share a counter between concurrent users ? -------------------------------------------------- The credential server can serve a sequence. Using ``xmlrpc_get_seq`` threads can share a sequence:: from funkload.utils import xmlrpc_get_seq ... seq = xmlrpc_get_seq() How can I set a timeout on request ? ----------------------------------- FunkLoad uses (a patched) webunit, which uses httplib for the actual requests. It does not explicitly set a timeout, so httplib uses the global default from socket. By default, the global default is None, meaning "wait forever". Setting it to a value will cause HTTP requests made by FunkLoad to time out if the server does not respond in time. :: import socket socket.setdefaulttimeout(SECONDS) where SECONDS is, of course, your preferred timeout in seconds. How can I submit high load ? ---------------------------- High load works fine for IO Bound tests, not on CPU bound tests. The test script must be light: - When possible don't parse HTML/xml pages, use string find methods or regular expressions - they are much much faster than any HTML parsing including getDOM and BeautifulSoup. If you start emulating a browser, you will be as slow as a browser. - Always use the ``--simple-fetch`` option to prevent parsing HTML pages and retrieving resources. - Try to generate or prepare the data before the test to minimize the processing during the test. On 32-bit Operating Systems, install psyco, it gives a 50% boost (``aptitude install python-psyco`` on Debian/Ubuntu OS). On multi-CPU servers, the Python GIL is getting infamous. To maximize FunkLoad's CPU usage, you need to set the CPU affinity. ``taskset -c 0 fl-run-bench`` is always faster than ``fl-run-bench``. Using one bench runner process per CPU is a work around to use the full server power. Use multiple machines to perform the load test, see the next section. How can I run multiple benchers ? ------------------------------- Bench result files can be merged by the ``fl-build-report`` command, but how do you run multiple benchers? There are many ways: * Use the new "distribute" mode (still in beta), it requires paramiko and virtualenv:: sudo aptitude install python-paramiko, python-virtualenv It adds 2 new command line options: - ``--distribute``: to enable distributed mode - ``--distribute-workers=uname@host,uname:pwd@host...``: user:password can be skipped if using pub-key. For instance to use 2 workers you can do something like this:: $ fl-run-bench -c 1:2:3 -D 5 -f --simple-fetch test_Simple.py Simple.test_simple --distribute --distribute-workers=node1,node2 -u http://target/ ======================================================================== Benching Simple.test_simple ======================================================================== Access 20 times the main url ------------------------------------------------------------------------ Configuration ============= * Current time: 2011-02-13T23:15:15.174148 * Configuration file: /tmp/funkload-demo/simple/Simple.conf * Distributed output: log-distributed * Server: http://node0/ * Cycles: [1, 2, 3] * Cycle duration: 5s * Sleeptime between request: from 0.0s to 0.0s * Sleeptime between test case: 0.0s * Startup delay between thread: 0.01s * Channel timeout: None * Workers :octopussy,simplet * Preparing sandboxes for 2 workers..... * Starting 2 workers.. * [node1] returned * [node2] returned * Received bench log from [node1] into log-distributed/node1-simple-bench.xml * Received bench log from [node2] into log-distributed/node2-simple-bench.xml # Now building the report $ fl-build-report --html log-distributed/node1-simple-bench.xml log-distributed/node2-simple-bench.xml Merging results files: .. nodes: node1, node2 cycles for a node: [1, 2, 3] cycles for all nodes: [2, 4, 6] Results merged in tmp file: /tmp/fl-mrg-o0MI8L.xml Creating html report: ...done: /tmp/funkload-demo/simple/test_simple-20110213T231543/index.html Note that the version of FunkLoad installed on nodes is defined in the configuration file:: [distribute] log_path = log-distributed funkload_location=http://pypi.python.org/packages/source/f/funkload/funkload-1.17.0.tar.gz You can have multiple benchers per server by defining many workers with the same hostname in your configuration file. Add a workers section to your configuration file:: [workers] hosts = host1cpu1 host1cpu2 host2cpu1 host2cpu2 And then define these workers:: [host1cpu1] host = host1 username = user password = password [host1cpu2] host = host1 username = user password = password [host2cpu1] host = host2 username = user password = password [host2cpu2] host = host2 username = user password = password When defining workers in the conf file you can alternatively specify a path to a private key file instead of writing your passwords in cleartext:: [worker1] host = worker1 username = user ssh_key = /path/to/my_key_name.private.key Then run adding just the --distribute option:: $ fl-run-bench -c 1:2:3 -D 5 -f --simple-fetch test_Simple.py Simple.test_simple --distribute -u http://target/ If your node uses a non standard ssh port (for instance, if you are using ssh tunneling) you can use:: [host1] host = host1:port By default, the timeout on the ssh channel with the workers is set to None (ie timeouts are disabled). To configure the number of seconds to wait for a pending read/write operation before raising socket.timeout you can use:: [distribute] channel_timeout = 250 * Using BenchMaster http://pypi.python.org/pypi/benchmaster * Using Fabric http://tarekziade.wordpress.com/2010/12/09/funkload-fabric-quick-and-dirty-distributed-load-system/ * Old school pssh/Makefile:: # clean all node workspaces parallel-ssh -h hosts.txt rm -rf /tmp/ftests/ # distribute tests parallel-scp -h hosts.txt -r ftests /tmp/ftests # launch a bench parallel-ssh -h hosts.txt -t -1 -o bench “(cd /tmp/ftests&& make bench URL=http://target/)†# get the results parallel-slurp -h hosts.txt -o out -L results-date -u ‘+%Y%m%d-%H%M%S’ -r /tmp/ftests/report . # build the report with fl-build-report, it supports the results merging How can I accept gzip content encoding ? --------------------------------------- You just need to add the appropriate header:: self.setHeader('Accept-encoding', 'gzip') How can I mix different scenarios in a bench ? ------------------------------------------- Simple example with percent of users:: import random ... def testMixin(self): if random.randint(1, 100) < 30: # 30% writer return self.testWriter() else: # 70% reader return self.testReader() Example with fixed number of users:: def testMixin(self): if self.thread_id < 2: # 2 importer threads return self.testImporter() elif self.thread_id < 16: # 15 back office with sleep time return self.testBackOffice() else: # front office users return self.testFrontOffice() Note that when mixing tests the detail report for each page is meaningless because you are mixing pages from multiple tests. How can I modify a report ? -------------------------- The report is in `reStructuredText `_, the ``index.rst`` can be edited by hand. The HTML version can then be rebuilt:: rst2html --stylesheet=funkload.css index.rst --traceback > index.html Charts are built with gnuplot. The gplot script files are present in the report directory. To rebuild the pages charts, for instance:: gnuplot pages.gplot Since FunkLoad 1.15 you can also use an org-mode_ output to edit or extend the report before exporting it as a PDF. How can I automate stuff ? ----------------------- Here is a sample Makefile :: CREDCTL := fl-credential-ctl credential.conf MONCTL := fl-monitor-ctl monitor.conf LOG_HOME := ./log ifdef URL FLOPS = -u $(URL) $(EXT) else FLOPS = $(EXT) endif ifdef REPORT_HOME REPORT = $(REPORT_HOME) else REPORT = report endif all: test test: start test-app stop bench: start bench-app stop start: -mkdir -p $(REPORT) $(LOG_HOME) -$(MONCTL) restart -$(CREDCTL) restart stop: -$(MONCTL) stop -$(CREDCTL) stop test-app: fl-run-test -d --debug-level=3 --simple-fetch test_app.py App.test_app $(FLOPS) bench-app: -fl-run-bench --simple-fetch test_app.py App.test_app -c 1:5:10:15:20:30:40:50 -D 45 -m 0.1 -M .5 -s 1 $(FLOPS) -fl-build-report $(LOG_HOME)/app-bench.xml --html -o $(REPORT) clean: -find . "(" -name "*~" -or -name ".#*" -or -name "*.pyc" ")" -print0 | xargs -0 rm -f It can be used like this:: make test make test URL=http://override-url/ # add extra parameters to the FunkLoad command make test EXT="-V" make bench How can I write fluent tests ? ----------------------------- You can use the `PageObject `_ and `fluent interface `_ patterns as in the `Nuxeo DM tests `_ to write test like this:: class MySuite(NuxeoTestCase): def testMyScenario(self): (LoginPage(self) .login('Administrator', 'Administrator') .getRootWorkspaces() .createWorkspace('My workspace', 'Test ws') .rights().grant('ReadWrite', 'members') .view() .createFolder('My folder', 'Test folder') .createFile('My file', 'Test file', 'foo.pdf') .getRootWorkspaces().deleteItem("My workspace") .logout()) How can I get release announcements ? --------------------------------------- Watch the github repository: https://github.com/nuxeo/FunkLoad .. _org-mode: http://orgmode.org/ funkload-1.17.1/doc/source/funkload-logo-big.png000066400000000000000000000455651302537724200215030ustar00rootroot00000000000000‰PNG  IHDR²£ùjÀUiCCPICC ProfilexÕYgTT˲î=y†œƒ„!#Ar–œ3"YÒsI’ä JA’Q@@‰¢"HD €((ˆ"’Qx=ž{ߺïý{Þ^köþ¦ªººgWuW=°,“‚‚üÔø„…˜ëjmlíˆØ× XÈ ’khº™™ø_¯­q(Ÿ‹øú_Íþg›{¨+¬vq uõ‡q3 × 0ß`ùèɰ £`LÆSØó7^;À.¿0õËÆÂ\438r)Ä ^XNŒpõ„ýPh€¡ pó€ÎÆ*®^$7XòaÿÀÜ cA—óãùo˜DrùÇ'‰äùþý[à–pÇZÞ¡A~¤¨__þ/oþ~áðûúuÑÂwò?“ƒØ0ŸE7’–!üdƒ?{A~¿bÛ@¬î–ÇaÙ p11ý«x„è˜Ãn ™…i`øAAafË㢽4M`LËsÝCµÿø¹âC28ˆ%,o 7·„1/Œ»B#ŽkÃÎ(è}´—…õß6_Ýܵþ–#Þ:ú¿m´Þaú}ÑÃ1çö 4<ÜB?àÂA|¢Àh­¿ï¢À`M¬ ¾àŒýáp›@ÿ¶Óü‰Î¯vžp»ÿî‘\aÛðúüÝîóOoàã?rÜÇî`t¡NÞIÿêóÅ¿_£¯ÿ(þãϘPü(I” J¥ŒRA)"ŠÅ DQÒ(y”:J¥ë€x{öü3ÆÿþùQŠV^°öà·»üÑ«_ÖÞÿ|ÿï‘å¶å?# Ì=žhE…x{z…Õá™ë.BÔp="B”—8Pÿ¿¹֬߃ýbþk-‚ŸüKÒÀQex-Ùü—Ì™€¦Lhüÿ%ãEÂi+@Ç#×ðˆßþP4¼RÁÊ8„ß³$J@ h` ,€-p„óÇ ÎÁpÄ‚DÒ@&¸.ƒRP®ƒ:ÐÚÀ=ð<C`Œ×`̃O` l]‚°D±@œ$ IBò ¤ Aæ-ä yBP8 †Ò lè2TUC· vè4=…&¡Yè#´}G äz;‚!†G¨# „'"HF\@ä#Ê7­ˆˆ!Äbñ ±‰H2$#’ )Š”Gj"M‘vHd2™ŠÌC–#ë‘wýÈçÈä2r…AÑ¡ˆ(Q8OõP–(WT0*•ŽºŒºŽjEõ¢ž£fQk¨=4š -ŒVDë£mОè“ètº Ý‚îC¡çÑ[ †#€‘Ãèal1>˜L:¦Ó€éÂ<ÅÌa6±X, V«Œ5Å’°aØlö¶û ;ý†#Ãqâ$q:8;\. —‡«ÁÝÇ=Ã-àvñÔx>¼"Þï†Âgà+ñwðOðóø] A€ L° ø ù„zBaŠð…ŒŒŒ›Lì™7YY>ÙM²²Y²rZr!rMr{òpò ä×È»È'É¿PPPðS¨QØQ„Q\ ¨¦è¡xCñ’Žò¥>¥eÖåCô‡”¹J=Ôxè‚MˆÍœ-†­‚m˜m“ƒ]—=ˆ½€½‡}™ƒ‘CÇ#—ã>ÇGN:NNoÎ\ÎNÎ%"QèGÌ'ö׸ظô¸Â¹Ê¸F¸v¹¸-¹“¸¸§y<ò<<¹<Ý J š V ¾¼bR …:„Þ£‡7‡Ãá‚á…ÏF¨DF|;iu²)’&2 r8J(ê\ÔB´NôÕTŒkLw,Wlbìì)õSeqPœK\wí.§|¦ù\´"¸Ò¼ª¶:¼f³6¿²¾¿‘þ…å˵¯Ò_»7Í6ßlùoín§~cùv}G~§ÿ»õ÷…Ý“?°?òþygÏpojß?ˆBúµ€wááÀÆ5˜CØÂÜaåoNñËÞb@° ŒQðnAØ0×òxha‰˜Dz¢¨t(FKŽ]ÆMᇠÝd½äý)'¨¥hJéèèÏ1ì1ű Y“ÙhØË8eˆÜ¼X¾Zã‚{Bu"ö¢KbÞâK’žR³2¶²Còr ¥JÐÑÊ7U!5Sõ\×ZÚÇtœuõô³ * [Œ§M6ÌpLjæòÇM-¼,O[•Z·ÙŒØ.ž€ìY¤œÜœcH¹.µ®Ýn“îëž/voy_? R@``LÐÙàܲÐaíá½OO¾Ž|5ý>f!öã©¥¸åøÏ +‰+I+§W’WÿZMù|féìâ¹…ÔÅ´Oé«ç·.ìg²˜²ùsdsõ/ºç¥_jÎU°_È]¤YL*9UšåVÙ`ùÛŠÍ«ø*ÖkÂ׫õk¬k]oÖEÕ'Á›{«¨±²©³yºe§æ6_»Ìµ»:÷ô:tî«u*t‰?é6ëñîMèË}XÙß0Ð2Øö¨y¨~¸òqÁHê“èQï§ÖÏ´žK¼`ÃŒ­ŒOÜ{Y>yæ•×k)î©ýé©7íoófÂfÍ߉ÏQÍ}~?<_úÁgAra{±ícÈ’àÒËO‰Ë|Ë}Ÿ?ï®”­ê¯~Y+]7Zÿ±Qÿ…ô•éëÈæ™-ù­©mÛíÑo¦ßžïø|'û>´{íGÉϺ½'ûûpü‘0‡a2À$VðÒ€ŠDb‹¢EÝAûÁ»Ÿ%l î4Þ† KFC¶N§”¦r¦N£¹Oû^’!œñ.3ņõ–Ýcx„+‡ÉÀ7* !˜zø½ðQ‘,Ñ9±#â¡-’«Òü2²‰r5ò£ _”G”ÙUxTÕDÕ%4$4ŵĴEuu¹õØô™ ¨ q†?ŒÖL¦LŸ™=:Öm~÷x‹E½eµU¥õ›"Û|»Üéö9œrŒp tö"9¹X¹»i¹+zˆ{ À¹AãƒôÙô]ð›ô è¬* Î I #…›GhždÂGmÁùñ4¶óT}\Iü…„„Ä$÷ÓvÉ–Ù¦8žq9ëqÎ'Õ/- =ð|ð…ÐŒ°Ìˆ¬¨ì¸œ¤Ü3Óó2.eçç\¼œWx©(¿¸ ¤°´øÊͲ¾ò‰ŠÅÊí*Ä5²ë´ÕÌ5lµÄˆ!…±„©ù%Ëî!n6}ö@Ž,ÎÄ~®wÜ»¼´||ü’GµëiÀù )Êw„I -¶&þRâdµT¶tŒŒ»¬‰œ¼<—Aá‹â¥£ÊE*IªjRêxõ·Íšç´´%tP:ãºÕz1ú&œë†=F¹Æ&ò¦ä¦³fmÇ.˜»W±`´X³|duÕ:ÁÆÎVÂg÷öD«ýy7G%'§EçNRžK€«Ž‡Û÷ÏP/=ov¸ž?ô-ô ò×` X ì *  1åÝ  ¯Šˆ=iÉù-j8º4&$V*võTmœo¼dü~ÂóÄú¤ÌÓQÉ^H1?cxVëÜÑTé4¡tÎóÔç^ø1”Y›u&Û)G:—ã"{ñ_¾Pøe¹BÕ"ÝbÓëR§+žežå'*Œ+5®*VÉ^“½~´Z·Æ¶6èFZ]mý㆕[”"MÍ-§[KÛîÞ~Ù¾u—ñž\‡ëýüÎÇ n©×ÞŒ¾Û§û÷Ù)9 ßa{rntë™Ûógcêã·^rOæ¾ÆLE¾á™Q~çöþʇíËT+Sëo7Y¿¥ý9ˆÿï³¥ƒš€‘ l+&C”LÀGÌ=Í(°Pˆ*€ ((òØ?õƒæ˜Ú0§Œ ô€i°1BR0 r ÐC˜ïý@0#¤æˆ@˜ÕÝ@ #–‘ä0ƒ3A!s­ÈIäOJå‹ÊBµ¡Þ ÑhQ´%:Ý€~‹¡Æ¨cÂ0×1ÓX:¬!6æV»8\î.n¯…OǸ„{dd®dɅɳÈw(\à*¥@yŠ‘ê Õµõ, Í(­>m]½:}ƒ.ã%ã[&_¦æ³,¬,u¬š¬¯…°‘³Õ±cÿÆq…Ó€s›x•ËŠÏ}Ÿ'œWŒw‰ï:¿§€ À†`çá BNÂR"‘9ÑûGŠÅâÄIº’bR,Òé™Ï²ïä^Ê(1˜?!"¤ yC™ÐMhZ‡ù½â"qæðëH¤Ò™¼‡ü€¢AEy¡.¢zPëhN´)ïfô"†c…¹€À"±jØ8l'óçó¸ <7>ÿ€@Oð&t‘±’E“Íë‘7Q)2(!ʘÓzQÍS»S/ÂŒõ+m]1½8}ƒÃc&“(Ó³ %K#«Í!p¨†ÍŠÇ~—#˜ó0çñ —37÷"ÏMÞ>=~&þ]‚…‡#…l„E8E±¢GfÄFÅ{%îH6JÕI7È4ÉÞ•ë…«×Å•£@™NEPUEÍJ=P#Qó¼V¡vÎÝ—z_àÚ%ndn|ҤĴßlÝœí¸¾E¤eÕ[f[ »œc¬Ž$§çMu×X·V÷MO9¯xïa_N¿ÿçRA—‚÷B=ÞFœŒ‹ÉŒýçÿ8Q3©3Y㯡36gSmÒW½Ðš)‘u3G&÷~žÉ¥¹‚øB®¢þ’ð+Âeo+r®jWm^¯¨1­Ý©«€«Ïfci³A+}ÛL{ýÝØƒN–®…îÖÞ䇿܃C=[Ÿ´?í|Þ76<169ýúÃôÚÛïïÐïi>Zd]¢]F|^\íYÏúb¾‰Þªý¦»ój×óÇú^䝸KƒðÒU0V!&x÷C}Ð <ãµà“œRÄb)†$Á3½¹‹’@y¢JPãh*´|ÒòƒÃb21X"Ö{G#áÚñ ø0ü|REFG– Ÿt’¤p£xGéB9OåCµAKƒ£¹D+H{ΞE_Ï`ÏHÉØÃˬÀ¼ÃÒÁš|È”m…½‡ã2g(јKžÃK<#¼M|…ü)a‚>‡]„…DœD݈E‹Ÿ“(”¼)Õ/½ ‹—;"o­¬Ø¬´¨Ì©b«š§6®Á¢é U©½¢«¬—©¿h¨mTeBfn6g~Ò‚Ó²ÃÚÔfÌÎôD‡·c’Ó4IÎ%ÃuÙÝУÁ‹É;ÅgÏ/!€"°*Ø(d/¬=BúäÕ(æèôXp*"îs‚GâìiÇä7)Îð,…+ÆË É™¢YÏr‚.âó ó ê %‹šJ$KÛÊ”Ë*-¯.\‹©¦­©½¡S÷®!îÖ¡ÆÛÍ-Sm.·—îDÜÃttJv=íŽèåî{ÑŸ>h0D>Ü>Âþ$atþ™Áó›cLãñ‹“Ç^ÝâŸÎzósÆwvrNï}Ûüö‡…o‹›W—>M.~n^¹¼»f».±Þxö¥ø«ó&Ì:ò¶ ·w¿Õì˜ïì|/ÝUÙþû“ágýžÎÞä¾ßAüC=¤$ª€È5àãÇ7ûû_øÀfð3k·|ÿg¼É„ÿéòûýÅ1>s/ê>@}‰7¸žÿ~ý¨uŸ%86« pHYs  šœ IDATxí} ÐU•î wnݪ X·¦ÔðªºU†÷Ü$Ö$ L´¼òF•‡èðy8¢B‚„€’X‚ð7· ot†@B¨;3„õ^5Éû¿ßÚkwŸþOwïîÓg>}N¾&œ³{íµ×ãÛµzw÷ù'¬]»¶Åƒ"@ˆ ­Ö6"@ˆ ŠÓŽ"@ˆ DÀ#À´€C"@ˆð0-àP D€"@<L 8ˆ D€Ó"@ˆ DÀ#À´€C"@ˆð0-àP D€"@<L 8ˆ D€Ó"@ˆ DÀ#À´€C"@ˆð0-àP D€"@<L 8ˆ D€Ó"@ˆ DÀ#À´€C"@ˆð0-àP D€"@<L 8ˆ D€Ó"@ˆ DÀ#À´€C"@ˆð0-àP D€"@<L 8ˆ D€Ó"@ˆ DÀ#À´€C"@ˆð0-àP D€"@<L 8ˆ D€À_ "@F7–½qÓM7›¸9uÚ”©S§˜ˆ¢"0Âüছ—-{ÃÄÁ«¯¹ÊDNI!L JE6"0ĬY»ö·¿ý­‰ïßçoMäPmXMºšâM„š§:"@ˆ ÍE€iAsû†–"@ˆ¨¦5NuD€"@š‹Ó‚æö -#D€"P3L jœêˆ D€4¦ÍíZFˆ D f˜Ô 8Õ"@ˆh.üÝ‚æöMÒ²{ï¹÷ÙgŸkÉ8Æäpßý¤8ªLuM™zا>u´Àƒ ÛoŸ3çö;¬¬X°ð>+Q”C†¦CÓ}[4H'˜îòÍ4OÈ¡«î»\&‘Ê ¼‚¡Œ†"@ˆ@×0-è²4HÄènB;Á„@êå¿ñ¹…œ¦)qJ‰ D >˜Ô‡u¯š‚ÑÝìæB"Á8‹6 D½¦½:ÂöD€"ÐT˜4µgRvEYAÖÅ}ârí\ø.Þ'ÈÈ$Â9AÊ$ˆ D`Ä`Z0,ª×ê•‚½^ã»Ê-„Áß8(’[¤²eÈh' D€t‹Ó‚nÆÅ}ï5~K ¯FI…ÿ¬„ Ά"@ˆ@¿`ZÐo„mäKàvÁ{Üå>dGç}Í |Ú¡Ø8D)D€"ÐD˜4±W2mò±¹Ü–$8FS@·2(Zá’ iŽJÜ6Ó6‰ D`4`Z0,ý¨¾êm‚ôÍ…œð/éD{Bó¡D™Ã°ÀE;‰ D  L ª 66r)ŸŽîÕ(ù9A{OÂñÄ›¢Ýý?ß©”"@êA€iA=8÷ª!Y#t;lG DK­^ÒKæ0þr?Aqœ.º‹9¾‹õ®qŠÒ™H+D€"0Ê0-žÞ‚¿&bw·Íá_e@PB˜Vk–ešj F´ò D€‘E€iÁÐt­†hÜÀÇüD–àU´7¤ÅÛC %D€J0-¨ÛMÐKõ8B#`‡(°0âLBYS€29ÞÎ n•Ð(ìp*‰ D .˜Ô…tÏz$<ãHû2”(!H4Õ Àmˆ´Hj$Íqæf Â̓"@F¦Ãѱ=æüã €n7 |s͆0ZIˆ U`ZPµÁ´‘+{¢<±±¯ýŽ"ž#TÏ D÷„ÁøN­D€"P L jÙB‰Fü2ÛÐæÒÍ\1‡KsMroÄYˆ…”Aˆ ÍE€iAsû¦Ã2ÙÐm|M$ÒgQ䚇0¹/ìÓ”8Ø‹ey¾U$ÂIóÂbQ¢€ D€ŒL †¦O%ŠãÅû4ÅU¥3€4%œx‰,A)C %D€J0-¨[ýï%ÈM`‘«UeÌ£H´Gk¢<å(>ç¨ßyj$D€š`ZPèùK7öq8>GÆã6eÌ?¸SöúëÝÿö=ÿc<{çY 'ˆ´ã{\°ÏÛ$xߤI.pFfú¶P…­qtÏôî÷¼»Ó,ž"@ˆÀ!À´ zg>³ò9 ¥‚Æx|&‰R%aVÂj˜^˜8 Q¶!JTf,¶˜"f¸V]ô HãAˆ D ¦€tqª \@vAZ‹N†Û‡;sOI0 Wð@õ–-[T—û”ÆRˆ"½¨IQ”_ÕÅÚ” U<¶&Ðíc¬,Fbʪ ðEc>ÀÂ*"0(ÜðlÎlé¦]€•fmGb݈VÞ˜®!9^‘Qð”8J'vÒòcŠ4”##ö 53?º«r …'æŒå²°• œÞÝÕÜ»œÞ%8Ÿ†rÙíÝwJdáÊñÉ´ úèr}îân*ÆëÒ™\¶t|% Õª¸ì¸۲ŷuÚ4ÞûHï"þ8ŠhÑ$@¾µ Ÿž§ºÏl9œHß»a``¾ ¢fÑØn†5´‚ŒC3ÎlÒÜ÷¦Õ!–Ð+‘¶Ý÷:„â°_AG³H(4B$Ž!/pB¤ì¥‰*YñE@¬EKž’âÔ¶Ò€ÇÖ„€U¿—®õàêæúõè£"Ð Xz­&]7j x™ôbraÒ ëœ@MŒ²I#üh÷ÝN Àß6&‘2(]ÍëÉíTã9·ßqçÜ»¼|MDT¯&+9””!<ô«2éJ<ÿ¼¯þæåßJV½Ÿ÷çÚkÌ/¿ü›¯œ÷ÕCùªN<þÄ“Nó¿½òí—_~ùõ×ßX¶l8—½¾líÚµ'¾wâ;àtŸ}Þ?i×IøÜn»íÂrBµV´HŽ!têÎÏøÂQGÿ}†k°¤È˜ŒV'­Y³fÙ²7^~éå5kÖêx€É:ì·Ýv[Œõ`Ò¤IÛm'§»Nš4q‡‰wKÇðË/ýfåÛoÇc¢¡ S÷Ù÷ýá<õ°ÃM\(œÚåµtÌJí‚÷Mz¦¡ ?iÒþϸ;²ÅçødZÝ›e¨[Æä@=\€–!ÐŽÙÉØœJ§0KAbxÁðƒÜCpò%ñVQì—S¥h2èD{®•Ó‚ñ!¶8µj·Ã)Ë£T³@Uk›l9ÉÓ¬²x”E·¤!<ýôÿž?o>Â@Z.–W]a‘1hí´iS?bÚ>ûì“f.¢ÔáŽÚðÀ ¡›8qâ´Ã§æyg¨(OEmtŒ¤ø|Û…ÕL½kÖ®‰C\'PBÐÝgß}¨zÊ3µxàAüËÌ×ÕH|Ο?9 Ì;ñ¤wÈJb¬ºÒDNÀ£8QCOÝvÛO€üá‡OËÌûa‰‰1EðÛ×3-èSö¢0."j´vñRÕD̨ð¼¤z·UPîM ÅH¸56;JÂ’jK³µ6oÞ¬º´Mœ $I2”=žâÔ«ñä®Ï …Èœ¶I ò–†yóæÏžuvʯ«À_» smÍÕwT5™kF÷'žxB^œs>ÙôQ÷v™µb ,|0 *[éE~ÚáÓŽ8|R„ÂV½3¼þú²«g^™Ô¦…ëØ€‘Øû9é¤;ºÕjºÉJÜÃÌlæÌkÊ÷€Ÿ=û¶… øÇ˾½k´‘ãõËZÕ“1=øÑSS¦=Á§½.ËS‰Uœ2 ’+WÄ,D©ŠFŒ+üeB4Á#‡ÚJ›K{7äôËEçñ·WÑÉ©F~bãÄ0^µW¬©Iäo/zE‹‘œ°ðÃD´àz¢C–’K.¾4¾ìè¨-<}饗Oýüi_ûút`!³2ºão®ÚywÏ·Â :äRìˆi¹ÊÜìÊ­m|Â$òBä¶–b·ÿ;žtòIûö39€ñˆˆŒÇ yêɧ/»ü“qô½ï}où`PÚËðû§ï}¿Zw`FŸvêé|í«GѾ‚» ˜ªS[Å´ z×`üù‡è(–äÓ3ræ }^c¬0¦Œ«qSÄjA¾%\ÇÌ…ªºf[4`ˆ–Hʉ)]‹M7pñ-Mîš"x΋"¦âúÜó1VŠK.º¤«M‚¸m\X»fÍ7/þæ_¿ ¹ ŵÙ#w0޲å;ê uÔxÊWù¥3‚Ì~„yZ9ïîy³nÝã0ø†«Þó¾|ÞxæYgtŒÀ@«òU3¯š‰Žòüœo¯\yî9ç^vÅeqâ²Ãĉ v°U9 Ï\yHÑfθúé§ŸÎå(QqõUWƒ+ž’xþÀp.”ÐoÆÂ´ :”nM’1¨QP JAh~xj'¾ª“ŽsÏ2EÞC@ÜuÂô¤ºX›hU¯ÍÕøV‘¢ÚB–d×é{jž·A-Œ?³ÛuIMÛeÓqì%0o{1®e•“6√3ÝÚQELªÍÌ3ÑÁGüÝ©š4ÁÌ7šÒò…òÔ“Oá²)»®{*¶ÁÃÛ!2 Öt¯±žˆ@_t Âv êžzê©_|ñKgYn”µèªÈ B—|÷úk÷Ýw_ð×8µ3¬ûò9çâ9ߌŠ.I˜’Ûm»íA9H<Îñ Ë™tÙí v -ñÂäÖ]],VF­5Ì,—ÚÁCF˜?Q¥»H#*Æhü‰–ˆy®Þ›ÓAÄ‹N›↯*—æQÉ⯱±ò§‡NùY¯:ë)±ŒÞ Ðà”ô*©Xˆ‘"14õ¥_ºj†\O¸+ŽÝv-ièN¾¦ (6¿yGÍÉ'ŸÔAIŸªK ïåõ¥¯Ÿsö¹ýÛ$HÛŒ×0H^zñå¯_øµtmÊ÷nø§… zÍ b½}ã’ëoø.0ºÒ¤7+¹êÊ™¯/5È Ô©Wμþ†tJV0&Ff€¦ÕÁ×(¨í]”å_Æv4´*ˆR• {J‚9lø]Øõ*tAžW!_RËAhªZ æ»ïž›Óæ6×$Á©"•H À¦ïe¨RoˆÓ‹²é¡ÎÓ^… 1Rä E<¸ð‹meªè3®úñ-?*ÄÃJužìàÚ´ÐŒ’ Ø[Þ÷r8`Iž1V¬Z°`á7|¯Îœ vvÁÂ…+V®¸âÊË;ô‹Jž|ò©ŸÝ}wIæ2lx·â‹.¾å–á9ƒ—^z©L“"žâ©”ð³ŸÝ p’”ËðH§äÐÏØq¦1 .üºhÇä(äêð”ÁH-ÄÃ%¦· ž8Ûüû‘0ùvbeeÔ‚ÓNWá”{Gñ< ;k“ÌÞ<•×Á,ªEŒç‘ÓôÛôO6 J9Ãüi %)bGÐ’òrœ“…"hq=ѺòÊØ5 +­V»téë·Þ2ë”ÏžhnèN^V¶àþ…VˆÁ‘/õ¥€;¾*ÃÅœ à@N0ãÊ«hÈ‹/¾töYçÜpãõ•3 à+¯˜aØËŠÆÊ+!v·Ýv3‘ܕ䲷üøÖ®š”éA’à4—\F{ï<Ûô.bk–€^ÇÒÜ¥ë•(gmº°é?!»†Q¡F0ãO%a§›òéþ¹¡Cˆ£»‚”D‰cöåQ:,è`Vóñ¦ƒÈÉRðÌmJÈB¸â<‹mÓÓ*Ÿ!5‚i™Î¶ñ Ãj\mF«vŽ—¢ï¶Û®·Þr+v Ê7é–ó§?ýiqÎaäŽëƒ ôÜ·fçñã¡-€–¡£“‚=Ox.½S¸å9BòŒ+f䪶êš"9„À•»áúñ¬k?¼ÀS)O>ñ¤‘ä.ü»òò}òS~õêÕFùÒ…c½±r· :~:ƒ9D ‹;‚Ì®2hÄ Xãˆå¸’wK+¸èîyœZÕžhâíŠ3LìïàLÑ=§£‹9‡š¡Ì9,d[dH)-ÎéNEбb®Hf(ë­zÍêµ÷ß·à˜OÿC@Œ•;™rÜ¿`Å ùm;“㳟=¥ŒX’iL™¶uòàÚñ_¿°!¦>ùÄSˆ…^ÜõŸVGfƒ ¡þá”L„—™Úª៉ÒL!HtÒ陿ˆL àT¹EÉÇWeO¤*ZPf°åÑÃÊT`[l$^¾"Té‰3O‘¯(æÇßÊ,ŸBÇìªbÛB´‰ŠÊ·7´I>‹A T˜h)b¤>_Ý ÅêzÆæ§wý,”ع“ié}÷-°òñŽùÔ;ÊO>—9¬”–ÑU»8W\våšÕ}¹Tͤûï_ð‘ô‘~¤«æ?þÑ-ÍG•7²ßáz +„›ÃØé™«ˆúGÕwS?æ‘h5>e÷M{þ·˜7£Ë\üõÜ:¥¹·mº§Èpm‘8JLÏБ á‚'Íœ(‘èeúoGð2Ux¬6Y¥u)'aJªèni„°Jµ¨L«+7N4,b¤¨…7ÄzûUÄeK—.Å Ú<Vî¤å`ù³ò¯zîóŸÍs¡ƒKÒÆtð üôºënXúúÒ›ÑaÀå—_1kö¬K§_ZV]ÜaIN‹§6”¾øªcböÁÁ¾‹©´`ͦ +ÞÙÆGfú4hb˜ÄE4>m\…ž¢ˆÂêèG!Y"¯4‘®I‘H¬a‘*aóÌQUD×Ú¼Oi/—ãQs‘âJEB¬œª¢6]K®BY=›î›$„;Šc&8Sæ<óÄóHH.E´˜(*· y,²”ñ /楖î¸A”´û®»î²Âê˜cŽé⸺F]ÒÙ®ÊO<ñäý÷ÝßU“z˜W¯^sÝu×ÏœYö9ƒ»îü©U÷ÛÁ’vÞwßý%9ûmpåTZ øúh)1SãT²  AΕڅˆY¸:ePþð'Æ–—&qÄ·Ó‚êqkW'ÝWEüaR‹²I/rÚ]I(îÐ*uŒžy\XÌÂá Ó˜3*8ÞqMä$û}Ù5¶T(1QTÆXE¶î¥½ðü Ÿþô1¹,fîtbwÿ/ÝÆ¹ŠËV !ÙŸ!¦®Q—¡º˜„×]{Íp-ÖÖ5Ç“?<òƒü@aK8òÄãO4Ö‘Nû;‡gg=ÎáQ3Óµ [A±´À¯|qìH^pè°iFÛK«!QÕqªH=Š”´Ê(­ Óƒ:±Y »ô"AõÊ·Ó«ójã«Ô&o[gUBNT¥–ˆõE y‚+Ì×°[ºÓS¦s CNG Y_sê‘äã»2Ÿ¡lmæû~yŸ#3üøôç×gט¨ÎÝa…Lo†„Z/Yòúã?qðÁ 1µDŸh,Îiˢ:\¥}ì7eäÒ·BáCG¼^áTËíBĈc昧 î¢E¢j¬DOä4I÷qW”¸ªˆßµôÌ­¢ß3B#¾‰ð©iâw&” —9…·†ÃJQ kkòÈ´¥K–|p¿fŠ´r')gÉ(´yžîó§~.ÓìÑ…ZF]Àˆœªa¹Â¾óŽ; Ó‚çŸ{!Ùé97†\bj?þØ1·‰†ŒZZŒ±NèÀˆ œj¹]ˆx„ßÕI+×AzZ¦¯$,GÜ®$:ˆJÉ£«]±R1Ý^¦S­d¡¸£D•(Ù¼y3Þ€­úVaÜV³á¨pDPThÚEçpüy¬e¬-Ó'¿ô>¸ƒ âçOoþî¶ûnÓ»ß*}pª7W|ëþà‡¶¶¢wÜ÷‰Š·ª pÅ[+vÜiǼF¢Ñè&Qž czÑ{mE<Æ& ›¸QK $ºk°u=¡½¯D¤  ÅçÑoÖIXu‚5tÇD„¢UAfoV®*©G 9¹¢](N²ÿ” ‘‚ÓM›7¡,€ûE@×Oá0>¼›ÆRSâàœ÷0UÕÁja¥(¤£u¯½–½[`êŽ÷›{ì1“î8÷üs+QÓ¨ëÖ¶%K–>U€'1/ýÖ7>ä`˜ÌìñÞüCÃwâçÎ{ÞùçåùøüóÏ›tqž|sºùr‡Î£|WúU3riAbÇkF» ñÒC©œ)!M/ ¹ Ë"š56KÙ‰ŠŸ¤{JŠ9¬Üõ½—„/-áSÿ¦M›þQVŠÖ†eÖŠŸ1‚†rS¢œž¨ Sµå Ú A~8d (¨Â¾2ÿÑ03wbTŸòÕuáîzì—sã#,¥¶Q6#]{ï=¿4<ߌr(Ú~ûí§9)Â'¦ÒjÛà±G¤H4 } ÈrN;ýÔÝ÷Øe¨Æ(2L¡ ³pj›{„œcýÌî»ïŽg{pÃeîs­ºî ä¹´ BQ– Wn$Zûj-à,æÑŠ$s$)÷Û-´" #VeÒuj §ÓÏ´¸«ÆULhM@^°qãF´ß°a2—HJnX[­BÚou_Ô‰½è*b¤¨#-ÛöÁ;æÌ-†±„§vj ®l²E÷@E´³2lú‘?Äí$ÍArpé·.ýÊù_M+—ßzkþí”sáµW‘T–ÝÙ9Áœ¹sb]ûí·ß‘GN?íÔ/ÆÑBkm=B}ëÛ—Æ~Â#ô×±Ÿ9>¦ caÔÒô Œä¸ Äètƒë±x¡ ã_>µäò>”]ÖÛH„Ü ìd$èAfh+зa㆕+Í~s>Ï£Êt r¶²R ­•°¶&J¹]–)`s ª¬tǧrHpÕÕUÃLf\ûî·ÿ~™U…DX¢ÆrÖÉà¢ì[VO;ý´LQ‡z0þš”Õóž=úرÇ}&SÑ[o™ùùß¹öš8'PuØ68ÿ+ç}ëÒogj¯B,šÚ†áißa$<:õ´Ïߌ‡K†öµ¿ ˆ!+ÿâ‚Ü•RWð Êã±0'ø‹£tÔßÒ-Ý¡…H’TÄt'[¤&‰¨u̵þ³6Èú”‡½•ªaŸY†µiVæµ%f•¬´D½“¥#¢êŠDí÷Áœÿ•soþáMø÷™cÿ׎;ì`Ü­‘íß‘ ½jt¢ï˜sG¯rœ=§¡úVbåäÏ?÷œ•Ux ³#ˆ&m”@näþóÏ>—”—Ÿîy+sÈÁÍLüÄtã‰;*Øz„Ñ‹›”’ºf»m·5„.­¢¯”ÑÚ-@¬thÉdÑ‚[7Aø Ç<1=fÖ:áQ’rä|JdŽø\IÚt•’¦oؼqÓØ¦›7­Û¼~Ó–Íïl\Ÿ£$&Ç>Å”Fj2OºÆw`Oîv¯•¢ØJlŸ^{ݵû'®Œ±Džþ…ÓNýÜixB-fë¹í™©;¢âž_ÜÓ{G`y§vêÁåšF]W>÷ìs½#£ùØ!Õ‡zÈXë[†òU¯¾öZ&³é°iMÿä‘™Z@<äcãžT^mWôì ‰0ô3úÈOd{„\Û9÷ÞûËHí}VZàÀ— ÑÑ‚”¤+‹|º’ð(ÉI |hNà?¥‘“5îH¬ß¼~ÃæM¶lX¿iC@lf•H$g2 –X›y†rÝD¤ÄÂí:0(¤«Êk¯ûN2'жX>@ÿøßMïJT€9Œ˜¹>òhïbU=ý‹§|)¬ªmÔZ’dXþÖ[VS5=`’Š0xð°Û‹[9yûê’â䩤1…et÷¡‡’dž[òsn¿#¯¶+zxjz”¹ó›ŠÚ{î¹7>®Â¨¥î'$>'³,®[’teŠö˜ûŠ<±¨3uÎH¬Jåë7oØ ÿ6¢°nSáf@‘¦ñs×Α¼Êè’}4¢Xˆ‘"µWû₩E¸\ƃø²·Ù×ÃÔGy¬À"wŽ;þ¸Þ¶ DAïf™ÙuýsÏÚt%nQgnP' Ú¿ýñ]’R¹Œx¹ÿ‡2†¨»ï±GÀ6xj¥¨PN!CÀÎdUÞŒVž=öØÃJQRi=åQK €ZÚ¥ìBˆPœq?¡à)Ú Ñ*fv-BîúKÄÈ’ë^ |gã:¤Ù„Ïò>¡Ý! bëíÄZIªÏ<+J€iøñÇ€…X” å«6ªÊË'ä<ò«Gz”†kÇ゘”1 6ôhF-]ñ`ÅʤvܱPõî{în¥nUÖo ¾%fJ¡`ï| #±RNW­Ze¥h÷Žežï{쉴@ƒLKs飖HPúB Bqø'鞺+ O\ÐÎRŽ`Ç!X¿i=6þ²qÝ;›ÖmÚ²)ÈÞseì@ÏýûwºIDAT’ú" ó ÅDQ!exJ@‰ˆ5"À(ÍFºZ¬T<ú«GzÿÙ»ãO8®ðR8äK\Wn±®…×^}Í çð˜Q[v¶9°üc;´ÃÅçžyÖÌànèµêÊ œ×¿j¥¨x¯+hIÔ:©´ Žî:Æ4²w¥Êõ€|ºRÌ W…/_ÐÓœÏeü7Û-=ŽŒÌÓnýxÖCº*Ô™šÐï:«LçdHU¡+EÐU¸¾ï´óNf›3H Ýy‘¯·O×ñŒ/ô&õ®kÔ•7Õç2Ñ ‘©¯#ÇÐíßµ}F+G`sà0ôÓ6 Ux¼Àj0¬È¼¶ïiÁþ×¹Ûl\Ý^†Û˖뾨8&E8­¼ÿõÝÿæÓ…¨´Ðö±B<%•1{E=¬«¾œÀ™™6ªT퇲n"–j™ÃäÐVts8ŒÈÁ‡BlÁïÌ[¶ãñ0Ó•ßf*Æ[^åìÐÃ:¯J«H±žÕlHµ2Ãyÿ2îôw¨“„Ò¯j5]Ÿ._¾<ÝÆjºA²ll+GÂr = z#•uê*4¦+†¾§Ö¬œ°eƒ‡HMÃÄ‘#út§„Q\utÚ¦oiß­Wäeé—“Ñ–îJøè`–ntªAׂÒOOC¬I™QyÒ5}S,ÄHQ §,B ºjP‘åY&íç?ÿÅgžar¡¸3-èñÙgð辑ôrr¬Ô½¹<ãg‹ ÜJ~áµµ•¢°C »¹N]…ÆtÅÐ÷´ÀY“\¾Ó0äݨûP Ñ<G„€¸*bÈwQ9¤A‚å˜îÕF ©Ò"ž¿l[·¡µqck›â$$ßóñ!á’¹üÖfž•¢2`–á)ƒ[9exzÔe¥¢ŒAž5«VÿdöOÎøÒA®•ð¨1Nysë7É L9uº“i@‰QÐÉ–cäQ‰-@cÂÆtšÞ”óþ§øý¾_ˆbñ8zóÆS’Yö‡JIÊð4s%ÿîù˜I~\½ÜxýIÊЕ빉à`AŒ÷9At@Ñê j*ÐfÞÒÚ´¾õÇo½ýJkÝŸ‡ß­Äàꯞ§ÂRxÅeW¤È-L?Ã;©iù¤t‹R·)S§tÛ*dž0,V·W€Lz“ yóåYs$ÛBúÔü~4Œ£˜ÚØ’ÁDV{ð£ ðÂjéÐǘÁ°w ók|^À•ï*o;żPËn¼Ô`¯O¶O•žã`ýêÖÚß·Ö…^d8‚4®6*à3ÞÏñvö¨õ~'BÄMV+ˆÊkbx‹1OÅÖ@?ê裬î¡®Œ‡IÃÛ+ða÷)âðŒœÃû±ð4Ô¨²êbG/#Žâ_ZQm”½÷ÞË=˜{FV·j!¬¨–´ÀïÀ’ÌYô¿üßþmà]ƒp÷5¨Öp³^aw®¯[‹u°A˜6Þ”©Ó¦à6¬õ&–6á G¦N›jx x?1ý“Gý÷;ﲋ<ËöàÃV»úêì”i¡-$Ðý!žêQwÇÓò#¥aè ý¿‰àw7¶ø½½q€làíiýéß™ ×à2¼¥ÚoDZ‹˜ùìB¿õޤ|dV~醕´ÊÁ.WÞC|•­Ânn(àªÔ6'€=§œrrÀ*ÛÀ€¢Úª´Õ¦kHÕw›ÀßGÈ¿q'›7)š[³ÙS¦Íd½•n€ïä`PéÖ°‘yÂÀ–na,Ïô%óÕĤÃ'H’bU†¿8¥}(ôÖ’è6€â‘yCaíü‚¡5YFê~rVMãh†?ÅÓ8ßj7·c Ù‘Ù0Àt0„¥½ZæíÑ{'|ߤh‹äZÒC’ý­CWÐÍì¬ü­Ü2`B0,C&ßÎŽuòYƒË#>X`ÛøÉC#³apÑ%ÂÒQ˜ evÎFo×ááTWZÞ$ЄàÿÆ„ ÜCCT¾IÙGÊ\5ÄÔa1Ã6lŒÒ†ù¶Câêkf–ˆ]÷»`»ÅUÃáâ©%-èÈ ðÚáï·˜ ×H)a-–“L/Kè쎥äåQwB·znô»mü™ ƒ’qw #è¬sÎBÇ•Tøaƒ’šÆÆËƒ@ô?-ð9{?Lô‡%­?,mm|'`«†L¶&ßR½¸ñûºCÚõ¶»²#³a€¸{ÑÅ6°O‘Æuq°ÉóºÂx¼ïÖVh¸54éZ /(nÙÔúÿ§µò_Zë×l °nµ>âÅ¿þÁ÷›éþÌ«¯âS}êóEvd6 ðWš¶pXa’v•FTf5i¬T™ ¡‚å£×¤ÿiž%Ä…+~#?OÄc+@¡¸iŽâ×`†â‰È¦áVÒ,²†?`¥#³a_f^s•íî–ì”L6D_üfb…ßíè÷†A¿ÓŽ4 ßÚL\¥ÿiÁ;j­ù}ôãǵùEEƒD+H£2ä3Ë=]5HÔ†\·ùWŒÌ†b0"q2Í *ï™!¿éÓ ÅMÊVõbRÿ<êŪ·íZ0piÀ hNfÀœ žþ7S”6 šô˜`ámÆ~ŠU ¯É\ŠA_=Ôx²ýx1bÚ/:0V{®y­s{u¶oˆ¨:$Ü=^ac€„8'Èó¢6ºf½ïkBÂܵdZPÛÀ£¢VÄl\¾ô>ý:ÐD¦œÂq)fxaÔ¡…§…ôc¡½ …c#;[¶¹2¤Af¿ç‚fÕž@D+ä+MÛÌú}M,#•s5<82¯6üUá<'°E—/ø·jÕª‡|ø¡‡~ø¡‡+ËÇ<Äϲâ7ù±±YY"€µ9_/}š6F7 DÓUÃN\ØÙÂ?܇ž7o>fÄòåË«9…̛ح©-ÜÂxÜDǼáúŸYôL³±Á~ö9ggÎÖE¿^TFB¿y×âÉÙY·Îž÷ü®ú¢ã§á}°3-è÷0£ül°¦`êÅ%¦…7—/Gé«‹_ÍlƒË ,(»àÏ¥ï² þØ+ÁL6‹ÀQGe›Àlàï4bÌ ÖµþiG,¿xï‹e—¿¹ü•WpŸ]fÔå…[\Ñ¢ Á'ŽÉ>`Pà`Îùðd˜ýЃ-Zô ¦pG4E¾²÷Þ{Mž<ávPFvÕq09(þÁ\· #PÈ|P—%ÚÈ:²1¬c])mó„µk×6Ê Cˆ D€ >[0(ä©—"@ˆ@ã`Zи.¡AD€"@…Ó‚A!O½D€"@‡Ó‚Æu "D€"0(˜ yê%D€"Ð8˜4®Kh D€A!À´`PÈS/ D€Æ!À´ q]Bƒˆ D€ ¦ƒBžz‰ D€4¦ëDˆ D`P0-òÔKˆ D q0-h\—Ð "@ˆ ƒB€iÁ §^"@ˆ C€iA㺄"@ˆL …<õ"@ˆhL ×%4ˆ"@ˆÀ `Z0(ä©—"@ˆ@ã`Zи.¡AD€"@…Ó‚A!O½D€"@‡Ó‚Æu "D€"0(˜ yê%D€"Ð8˜4®Kh D€A!À´`PÈS/ D€Æ!À´ q]Bƒˆ D€ ¦ƒBžz‰ D€4¦ëDˆ D`P0-òÔKˆ D q0-h\—Ð "@ˆ ƒB€iÁ §^"@ˆ C€iA㺄"@ˆL …<õ"@ˆhL ×%4ˆ"@ˆÀ `Z0(ä©—"@ˆ@ãøÿ¸©z¬Ðÿ\àIEND®B`‚funkload-1.17.1/doc/source/funkload-logo-small.png000066400000000000000000000115431302537724200220370ustar00rootroot00000000000000‰PNG  IHDRÒ2¦¿—sRGB®Îé pHYs  šœtIMEÛ)‘ÞdõIDATxÚí]y˜E–™uÒ4BvC7§(Ò¨ßpˆ0"‡¸Î7£Ž¸ãì·®‚ç¬Î纮ʂ#ºâ0Œ Š0 ­Žâ=žèނР"J£"ÊÑÝE·ôUWÆ{oÿˆª¬ªÌì¦D]óYŠ_VdFÄ‹_üÞYˆÖÖVðÅ—C+ÒW/>ì|ñaç‹/>ì|ñaç‹/>ì|ùQ‰é« ãÂ̦i†B!÷W‰D‚™jÚèÔ©“û:% !„»ƒd¤\»öÍ9ÿ=סÓD”ßo¾Ü²e‹²”[Ç ?†ˆ~¤øs\ø¡Ã.Nß³h±²313!0#SæJyyŬ›nü±3D(úóöì©w\O&“ë7¼ÇýâÐí)H)ˆ„”@$¤ $…$ÁR "–Rü?ѸiÇEDô#ÙCQ>·é²Wø§ïYú°;h‚ˆDÄDÌ@„Yœ1æ]aþqú=¾ü@a—…S–î2ôFÌD˜G€þjù°;`Ûʬ# B¤¬0³Ž"lÂË‘û°óa×AJ¸£y'er!ÄAØ —•Ú¹$"ÌšRbMlÁ`€ØÌ\#Cþjù°ëÄU|áú)Le,(Ó ®ƒ®u]ηC•—*!"ê׿ßm·ÍÍ/7Q*•bæ@ àN›‘R ¤”¦é1)˲4ņaF; „†aH)‰È+Àdw̾ÒÎóíêíÕÖ:b%¤”†a!l…uжèy!"" !˜9 º+¥ˆÈ­dÃRZ-zÀÌä5úža†ašLÄÄÀD„€†0 if‰,ËBMwĶ[—NY©TʽêÁ`°jeUccg˜aБGLœx*"ÖÔÔ<ö·ÇÝŠ ~}~QQ‘býûëßzëm2 Ø~Ñ…Á`™[ZZÞY÷î¶mÛÊz•=ú°®‡I)ó•ê*Ôl.¥Ü´éã5o¬qëaР#ÆO¯ ?ÇJw7zï}þùÕ[ªëëÊÊJûöë3pà¡PÐ0 ÛoqßÈÌ õ o¯{§®¶¶rDå°aCƒÁ ”rþ]w;t¥”š2urÿþý œˆ'Þ{oý§Ÿ|Ò«wï“N[\\ ˆ;“ˆ¿Ø22s†íˆ1·!˜Q¡…*“$!ÖèCBÃ4Ï!¢@ ðì³ÏíØ±Óöõ·§6qò”Iˆ¸'¶gùò#‘pþ½Bˆ_üâÌ¢¢"øøã|ð!‡®£ÑÈùœgYÖ¿_sí–êj› Ã8zðà»î¾3 e\^^îÖ×F¶U‰»6‡ÒÄÀÌ@ÄЯ_vTZÐĦã‰LúMÇÏvJ©Œ‘Ífëò£ b"˲0;m… ÚgMìîSˆªp…Èía•q4ˆÕí©­­Bx²Ž4Œgž~vÚ´Ó…L¬"°”rㆎ±Òè1£»wïn¯:¡³÷ìõäS ¯‘ˆæÝy{>xY/äY|áÂ{‚Á€ãfÜ1„¸úÊ«ßZ÷¦¦íž1`ž˜óŒ?ô\ìÅQJm©®nÿvöòæ÷Y>?°Cíœ2(d`Åϲ̄ŒHLÈöó`‡J)…¨RÉTk¢µ¥¥¥¹¹¹©©±¥¹%ÞÚšH$R©D*•¶· 1æ%˜)Ï“s9¾ÍØ\Îé =[´+MMPhÄ @ÈÀón¿C§]òå7ÿ 3A½{Y)åöíۿܶÍÑX)5dèÑO<¹j䨑ž£]÷ö: lذÁŽÆò%VŽ¨Ô¾k$¤•)¥\ÿþú–æfw¥Ô˜ÆèÈÚC˜öyöÉ<0Ô1 å} `‡ZËY—Nsž"Ë~D,K§Òˆ˜ñõ¦î-XÈvù;•9´3¢Ãf !Ø6²L„ä6jétúäSN6 ã͵oºÝ e)ýf@tûv´qý†Ö–V!eþþW–õë /px?„ìîÝÓÈš¦ùÀÒeºGÓÜ¿ô>)嬛f?y|(ì´¿ëÖ½sâI' !¶oÛî~²ñèc 8àúß_¿êñ'‘¬Ív†a¬]³\½'‰E÷Þ3yò¤W^^=ã²™î$å&PÚ¹[h–‡ Õ‰ ¹íl’K©TKº%®â UhO»‹d2™Ùëm÷‡˜#H$BjÇÈ"ª¶Œ,1#¡,l@D3f^zÕ5W)K{ÎyŸlÞìN‘dŸ":¬“ðÊ+«‰ òV‘nøÏëKJJ99÷ðÚŠdMÓx饗ØÑ~ÔèQ¡PȲ¬¾}û”WTÔÔìvܸqÃÆ@ @D»jv»ûêѳg¿~}âñø%ÿvÉÊ•Uaöœ©”òƒ?bWï=ztŸ<ù´ÖÖÖÑ£G¥RI÷›%É F£éë܉ß,4ØŒRQ©Èºw†£,áåáw6ïlµZ)›B W” ‰\;²¬WÙŒ™—Ùû >z°½BŽö w>…Q9(„ÈM€²` ).œ~a*™’RŽù³6ltºh sïJ˜Fàå_vtÚ½{÷³Ï9Û* º=ÇßV$›H&›ö6™¦“N*Ê+t{¥ÔÈ‘?[õø*GþõW_éе¹±ÉÑ—RjìØ‰™Ë+Ê•¥(@…©oÒù@!DÍîÝä¢ö>}*”B!D§ÎŠ‹‹ãñ„àòæí0úð>˜²VØí¨då%¹|gBSbH $ÔÆsæ a%)hû 8ꨡPºtérιçdReyþµ]Ãu–còÊSHˆîJ~™Áõ„P $¥Ô#4 ‰DÒV°]K¯ÂQ2•tX榦¦xk¼Ëa]Ü6È=_ôZª¦Æ&fB—ƒ^TTdïá¢ÎEˆN«VÛ£­©¥”ÃaPˆ½z÷²óÆnUåü+m¹'Û©S®wÓ ¶€3owà ”LKgX%2AùÁ¬Îžìl…Ýq°˜¡ç·©Ú»OÈKq¨,S' B¥œy»œîD‘,ÛFßÝ@ç{óèP…üa³»ÙN)5dèê-Õù[ššçÌž3ÁüT*åXWò.¯9% {²`<× €T2‰¨ É;“5ù>qÎuCÊUÛ”s0˜ÉêÃ1ÙÖÖVûŠRz¤‹¼J¡#Dæ<ÂCÈ…°«vÆ¡1Õv #Õ³¶"^0õ(.ímlÔ§Þëë÷°«lZ`d™ÉUI¤¬C“Éø»å×d/,&O›øÉæOœ Õª‡g^yyEEyaHرJ§¢NžµÚšš2›¦ùþûëÝóíyxÏl±.ÒÔÔì˜H}C½BJ¹é£M ìž©…CîÉÖÕÖéúo:ÞµcW$ùV…¾Ž%PX'w1ƒ6Æ 3¦¾l†Ætmûõ¶CA8·>»ä«/¿B@]]ÌýLf¶ÇÆìÙ#$ö¼Ä®y¦?FåÈG b(zàþ¥z‘ ™Óåúúú=±=ú‹íinn6LcÈÐ!îtÏ›kßÔ§l›ß}ç]÷€ ïêVRâøJJùÜ3B˜¦¹â¡úD‰#d¯Ë€ìsòå£?ª¯¯—R~°ñÃ4<çr`°c"Ò¶$´Ö@ó®ƒ[·.i§@Y^ÞÛq lÞ¼ù…ç_hjjªZQ O3MÓ4íô¸Ï!ís´ƒqä‘GjË•}ÙÒ庂ÞþÂápå°'Œ›ùŒ:á–ÿºE yáô •RŽÆ±XìæY7G£Ñ%‹–„Ãa÷`N?.NQyy¹»¯ÚÚÚoøÃü»æ?òð£¦i´5S"sÂýÉ—H4rú”i«[uíï®5M³ãk÷­ÒÅ„À ¤ 5»ß‡o¾Rð=‰eYã'Œs¤Z„Á`ðü_]0hÀ‘›6mrÏ<uúަfÝ<+?²½Ž«¯¼fŸ‹¡}µB$<ûœ³¢… t¥uÑ¢ÅÇVÞ6÷î"²Rjì‰c5Ý;f˜'÷<°ôÛçÎóèOŸvº…¦´6옠þ3PIòàbN¸ë_ß&¹eöÍÓ/š®\I)÷Ö/))YùðŠÙ—ß "ý³Ç~RÞuçüÝ»wïß"gž{º¤¤¤ýW¸±¸¸øéçž †rçÔ“Éä½÷-ö<寔ºlÆe={öl¿÷›n™Õ¯_?Ï‘wëÖ­¸¸xÿ&µ¯õßÉ‚ut G‘H¤{÷îßÖ?B$‰¹œóÔ3Oê‘ç´4·\üÛ‹_{ýÕ¾ýú²_N™Â¯t®˜é/L¯'çÊùî×m {»#ô`¿e7vŸ.Îo¬ßžò4:ûl`¿µåvô8µÙ@—ªÒV[óí ôd¥”öÛ™¬ãF{`:ÈÕˆôŒTØÆ›c%Û j3`ºptdí„ÿסørèÅÿíb_|ØùâÃÎ_|ØùâÃÎ_|ØùâÃÎ_|ØùòÓÿr Ö"¨žî†IEND®B`‚funkload-1.17.1/doc/source/glossary.rst000066400000000000000000000047361302537724200200650ustar00rootroot00000000000000Glossary ============ .. glossary:: CUs Concurrent users or number of concurrent threads executing tests. Request A single GET/POST/redirect/xmlrpc request. Page A request with redirects and resource links (image, css, js) for an html page. STPS Successful tests per second. SPPS Successful pages per second. RPS Requests per second, successful or not. maxSPPS Maximum SPPS during the cycle. maxRPS Maximum RPS during the cycle. MIN Minimum response time for a page or request. AVG Average response time for a page or request. MAX Maximmum response time for a page or request. P10 10th percentile, response time where 10 percent of pages or requests are delivered. MED Median or 50th percentile, response time where half of pages or requests are delivered. P90 90th percentile, response time where 90 percent of pages or requests are delivered. P95 95th percentile, response time where 95 percent of pages or requests are delivered. Apdex Application Performance Index, this is a numerical measure of user satisfaction, it is based on three zones of application responsiveness: * Satisfied: The user is fully productive. This represents the time value (T seconds) below which users are not impeded by application response time. * Tolerating: The user notices performance lagging within responses greater than T, but continues the process. * Frustrated: Performance with a response time greater than 4*T seconds is unacceptable, and users may abandon the process. By default T is set to 1.5s this means that response time between 0 and 1.5s the user is fully productive, between 1.5 and 6s the responsivness is tolerating and above 6s the user is frustrated. The Apdex score converts many measurements into one number on a uniform scale of 0-to-1 (0 = no users satisfied, 1 = all users satisfied). To ease interpretation the Apdex score is also represented as a rating: * U for UNACCEPTABLE represented in gray for a score between 0 and 0.5 * P for POOR represented in red for a score between 0.5 and 0.7 * F for FAIR represented in yellow for a score between 0.7 and 0.85 * G for Good represented in green for a score between 0.85 and 0.94 * E for Excellent represented in blue for a score between 0.94 and 1 Visit http://www.apdex.org/ for more information. funkload-1.17.1/doc/source/index.rst000066400000000000000000000012431302537724200173170ustar00rootroot00000000000000.. _contents: FunkLoad documentation contents ================================= This document describes the usage of the FunkLoad tool. This tool enables to do functional and load testing of web application. .. toctree:: :maxdepth: 2 intro screenshot installation tutorial writing-test benching recorder credential monitoring usage faq development reporting links Indices and tables ================== * `PDF version of the documentation <./funkload.pdf>`_ * Changes_ * Glossary_ * Links_ * :ref:`genindex` * :ref:`modindex` * :ref:`search` .. _Glossary: glossary.html .. _Changes: changes.html .. _Links: links.html funkload-1.17.1/doc/source/installation.rst000077700000000000000000000000001302537724200232012../../INSTALL.txtustar00rootroot00000000000000funkload-1.17.1/doc/source/intro.rst000077700000000000000000000000001302537724200214622../../README.txtustar00rootroot00000000000000funkload-1.17.1/doc/source/links.rst000066400000000000000000000015661302537724200173400ustar00rootroot00000000000000Links ======= * How to stress test your app using Funkload http://tarekziade.wordpress.com/2011/07/27/how-to-stress-test-your-app-using-funkload-part-1/ * How to write funkload test in few minutes http://blog.redturtle.it/redturtle-blog/how-to-write-funkload-test-in-few-minutes * Fabic + Funkload distributed load http://tarekziade.wordpress.com/2010/12/09/funkload-fabric-quick-and-dirty-distributed-load-system/ * Testing plone site with FunkLoad http://plonexp.leocorn.com/xp/plonedemo/xps9 * BenchMaster http://pypi.python.org/pypi/benchmaster * Tools for a successful Plone project http://www.martinaspeli.net/articles/tools-for-a-successful-plone-project * Using Apdex http://www.apdex.org/using.html * Apdex with FunkLoad (in french) http://www.wikigento.com/tag/funkload/ * Installing FunkLoad on Mac http://pyfunc.blogspot.com/2011/12/installing-funkload-on-mac.html funkload-1.17.1/doc/source/logo_funkload_big.png000066400000000000000000000220201302537724200216240ustar00rootroot00000000000000‰PNG  IHDR –& ìtEXtSoftwareAdobe ImageReadyqÉe<#²IDATxÚìË“Ç}Ç*çìõ)‰-[­õ$(Šzø!‚×T¥¸<äÌ݃«’Êa¹·Üv÷/àòšË.¯®J¸r%•CíÄ’(S-Q¢D=@Y'‘,P–dQäbÒ=ì3ƒyvOÏ|>*hAÌÓóë×ï;ýëî–ã8@wa@€ @€ €@€€…|ˆD¶Z-r @Ïý¬{^þéj¼dïW¿îÃòPÃöt]þYÓyÍ ¿:o…ÓÌ @€ €@€€E| @Uù‡¿ÿGGýu„3þÀ‘ïœñû‘³ÿÙø#ö> 9_ý™¼wψø|ò[Ácþßœýžw|α=÷/-r <÷³®cಿúuoë #‘BB„‹¨óý¢¢hñ‘æ @ ò*$Z|$úb–ØMr*" :6–Yñ1D*®@ªr…ø@€@môGš%v«rÅJX°F€…ˆ#ÂGr„\¥&³i‰X'@l¹b ^Ø&C´…\ÍYB®X ¶H˜«¢ÅG!WˆØ%AŒ…\ÍiG=!¨ºüH8ê&0² Õ@| @ –Ć«¤â€dˆå!WX)>²†\Íå…\ÍŠ@UåG…v5Ïr…ø@€€$ , ¹b^X'BÊÛÕÆÂû•Pq.ÂŽM}7zÔc[¾Î->"F“T@D ¿¸H >29àŽ3šš?á^wjCÂäÇÂÒ&œ°ïŠÿöï¿ìQB ém`G @€@¡"‰€ˆq#&QÇÓ2…‹ᄈ˜cÓé˜Í Å Ãì´(ªB$ ¹ sàþ›dÔ#S%t|× „aMK*<|QYAá%£"€"ˆ¦«™°§Œœ‘ˆŸ?¿#n·ó¸euÝ>P°#Ð @ Ì Q”ø˜’•-£pñ\õÊ u‰'Sé N§­ÚŒ€€våVŒyaUQÇÒ—lé  „G Ô*T …§ê㹟v—d:ÚÁ¤ì‡¡EØÊw|Þ¹¿yñ¿¶“žüãg~º$ÿ´5š$Uú¼4v埮Î|“i\ÏûÏ>óš;êÞåk8±Ã /þ÷€Vk*Ÿ× ]:u™„b‘uEÕßG -Ñêȶ¯Ì«¦Ôi“¶¯mžüÜ{_Úa˜¡Žénï iK ²á‚÷¾ïý{(mاö!@ $á&Ò®r÷ý´é [ÙÊýÌ ¤!å’»QâÃôÓ yý“2-Ý€º˜+ŽrÌ·QvbgJ¦ã¤fç>Uú¼4ªô­iκTæ³OÿDun‹¢%ŽzöœÛÉËÎqèv†Žx^þÝyá¥r+žØI[(¡¾fèò«u±£ŽõÓR˜·Ü:rÔ«'íȇ0!õ^–•‰#®^äiʉìÉú3´Ü.“6ä¸×†,Ì©3·=•퇼÷„méq÷nKsÚ°h‡çÙPx‚DÙñ¬´c?A;0 #@ ÐX—r•G|(Ü9 a :~w|Î’»¾Q“pñ‘wœ¦XFÎ(ØzÍM—ÎýFlpªªœÆgžþ±êðÖd®.º]º¤.x¤z–¿¥ÄÙÆ‹/ý¦×À<>ièÒiïMH©udÁsz]ç7åˆnáÒõ]G9jϧ™§‹ì3iC–¼®+ Ôw–äo¨û=+ï{}N¾b@€è°¡A[É`CEÇ{JÒ#@ 0G|”r•»9Ó¡TS+]ù…Éä² ‘‘%„¬~·$ïTH“ÿ³jäMÉ!WùÓgAù®T§wäYõqËqG0JI›úý®¼ÎòK/¿°S÷%mØ•?{L–!í:¯e†\åY kˆ#¡VÞ•BÄÇþèÈ~jýÍô1ƒy3U*ä*îÚ6”oÓ½(±÷ÜPÇ ­¢_ù:/ËÛi™Öƒv’éðÄGù÷¬òäüÔý:î¢V—_¯ÍØÒWnflH# ~aPVÈUîexÖÜZýÊB9ÁÜ™öôgF=ª0–gzù`“©$+¡ãpøi.±d¨7Ø’×¼|é¥^ÝòXVÑ“¢e$}u¬+†ê† ·:e0Ü* §Ô|£#‡ŸZ~ùÒž^;=åŠ §UZèfêšçäßcu(ò¬-É;ØÒ\Δ U_¦@€@”cŸ`Ôcêó”!Wy'£ï/·;gˆO8„‰½ó‰gí¹Ÿ]›úN@˜„MÎzIó7æ­6üí¨Ï*°L°5ÛO~ª a%çd:îýí¥‹Ãºä±¼ŸŽ»Ä²þäõ¤{u¬+šóO=ÞrW€³Ë4nhLÿ‰,å «­¤‰¤p ت«Ú0y¯«6—a¯½Ø2TÖ–äõ¯Hnʆ OK€ É¸#zœx)J|¸ßùVªšÂð­v5µÊUüdõ(ñ0#¢BkvÅ­yN‡“eiª);ŠÀ2ÄhÆlè ¦ñÉ'ŽHáᜪ€\gO¾NÔ&gÅÌ…[˵­+š8üÄ‘7”hìÌÛÈ‚+Bž8²üÛW^ÞÖPÖUÝm¼ßSò^½=S[ËÛ9ÃÉX“6ÜyùÒÅüK#€8á'N²„\å}z2w/ŸÎÌÎßÓ#Ó>@PÌÏù­0á¹\VÄä÷”vpBv3¯ÒS(ôGL§wèHÛ½W…E™¦î¥Ë/÷lÏãÇž\p#ûl_º|q€þÈ—wn(‘½âÃÏ–¬SBÖ©íòìudÑPYŸ¹WYŠÙéä¨}RŒ ¸‰hUBòm"„HÙ!W™C°&¿éÐØÛd_‰ì§n*î)lŸŒè #÷qæ,u~ð}Š<©Ê|¨<©~ߣ=}/ï¶DK,TÌ*œ°g{»YËÑnÛV†¹6Õ•²y¢ã Çó2ï:5º­-y_ƒWú¿í•c/·© î\†–ueN…^ªHrº2=K´H!æ…\¥™'¡Ov÷/“3ò2=P¿™¢ÿüÈór„\e ™šÜseB®B턳IÄNåEMwœjO^E„òmánŽWYTÚ–mÍãCwG{øŽš¼¿Y÷ºRr¾­;úv5×^sèàÇ._yeX½Tç)ylØ•6ìV°¬¤\þæ?·à|OâýÓ¡§þãØÞçã·ý‹'Z©À(á5ä*NÀDýN*¡¾ZÕž!öWâ¹ žrSóÚS„\¥ £MTWU{B°Â.¸T¹Ð«éô-¦ U ûS“Ïõ‡œéçt,›vÑyü‰®3~š_ëÛôBW *çk¶…;U®©Ã†s>D ä"Z|¨ÏÔzݽL$Ç®æIB®òNFw ôùÿA1öÙ”~˜s^ÜNéÁߘ¶GüõR¶ Uoámè…t_q¡â¾æÂÁÇ-^ùÝåÛòX¦{A-ÛªyëÜ£ÖÔ•òòl«ÕŒÛ?%ï÷yY·z–󺕻NEÃ`R>iF>BÇœc~ñ‘wÓn!WyC‰œQ`ÙÝ}Ï>|E«àèÄÌùó¿íC8 ÞÇ}†þ F­rTþoÇ:û9Î’­ÍIZ½òêå!å0ó¯É;gcz´#¢5»CúL•SpN‚«ˆùAºÜoò2Tõ2nC=„H'É û=úðc Ú—pm³šQSËá8\®Ñ¬d ãð+ÈYöºXÒìJ ʹŠZ!+‹);ä*8o$}£2šYÆ6(Bw*ؽÜÙ?8ûýà{áõpÔè“#âöþ(.Ü‚¬f¤ÑˆYT˜GÏ û¹KkMGﵫ¯ö(‡ãCµåý.6]àK;,½öÆ«ÛI¿ðÈC-~•»ìu „j¤‚ÄI' Ò†\…ŸW|$õóB®ò.Ãëw>O¼ƒyØîç!âCÙx4òþNFpÄE¯AV3ÒhÊA²Å~ŽöÏ[”Ã\ùµdPoI‡¿uæê¯öqQw2²©=HŽÓ“Ør]š«Üe¥w %>ò†\ù‡˜äé˹ÊÓ FÑs<üûuD}60j$e",œñÿ¼j–¡ª—qê!Øk¿‡<ªœ²¶ÆKî¼~­èÑf•CSaD-!–¯^{mÏáW"Dþé?ràÑiþóBÿ.Ô‹²ü¶_¿öÚ ¡Ýª2ú¡ÊÛ{ÙVöV ïÕì|Ž1£Äõ„\ßg¯¸Å‡\%>ü–™ÝŒq_L¨Œý¿#Ÿèp,.E¤¯þiÄ~1íÒÉ–Þ¥”V)‡ÙyèÁGL…Àl\½vu;ì€%ý‡<²,ßž3àwEâQwS¨»V_÷ÙPÚLåãiáŽ"Ù"@œNËÜÒk;ž >.y6Dˆ @4 B®üß =¿€U°ü¢AGÈU¦}@u—éÌ·í$'J'q û‘žZ—i]xðþ‡:o^£?§#î ÓëIûõØMÙx©ÚEÏiоÚH¸M1^š™Qˆž~;MÈÕ¼M “ —,¤ñr¥Â¥îŒî¸/õ^ Éí,él–ˆuHŸýiT÷ê›×¯wE^ðþÊ:WåNÉ”ý¼ï€»#´¦««¼Ùlz]Ég²<¸Ï”ùpV³QU×ëÏsž…ïùLB»]¨ºÑØ^øé˶½7Wü^c(ëÇNõmˆ©‰óV5Š91;ñûÇÄœßÎðä`æ}Ö«‰ØØuvÅíÝÛã¹ÎhF“dtM±¤Ïê4n¼õöµõ¨ƒªãzà¾ÇäÛËØoÆá9)mÙ™Oæוî±kà²Ï§L£r·4[æàüò.Ú4êP–û„vëU¿ü9ßúUÜÙeïyDŸ)1äjJ”äÙ$ >‚ýÅz(‘áŠÑXlÜqî”7OBúìJcï­wÞ\Ÿw’túüèA“c+m?yÙÅ–l JýhJû%Ûþ{t»²Ž¥rŒ•ДõMFè kW´|줰Û@Ú­êN—‰0¶^Š Ò€ÑÕoë¹Ê~"q!Wjã›Ýo\±ñÍèWxèœÞÄÕŒXËâ4¶’Oh–i|¾ªÄ„ýîÿá‹òÂmMWÞ¸þî[CêJ!7ÙÖ|—½Œy¡[€t¤ÉļWRÚ­'„‘Q®ÊÖ1ÙvôSˆå¡lÛÂÂýU ¶µÅ1‘$äjêü„!WyWÁŠ ¹R#·G·Ç#^H•Y{2„ôY“ÆþÛï\ï¥HãûM¡k%¥ÁÛï^ߦ®vÚ' gLç êÙ¸ªY¤Û±ïeH#¢C‡‹‹, ÷ ;žyþ‡'8níÞr…†åP¯Ê*º&ªXÒg[SuLÒ îÝwïýØoÿ’‹šBy6¨+…Þ£îÅndL§ªŸZ—D•õ»ûö{1% ”Øô„§Q –n…Ë_Û‚:Rm"@êå?–rU”øP|úÕ§îHGõ‘|[´’O2KÈ â÷,ê’>ƒi¼P[HWW“?Ö{gðö6u¥~ؾoÁÀ=ë’2M•_žU¦ñ&m}îvþ¦{ˆóŸÿ´¾Ðä¶ Ÿ¸í¡Ÿ‹øcþ•š'(!¶[ûσTé!û$¹J"\„ˆ$i±A|ŒïÏH§qãÝ÷Þé5ìžk“>ƒiÖÅ–5:ƒ ½C#K fË õÈÑ]‘æ”Ýák½º•aú#ˆFkÅ‹'Z|Œÿ­*kzRPÈUÜ÷Ãþ]ËîE°HŸ%i|ïFzÑZU[Ö¸Þ)‡o;6ïþÞ¼=¸÷žUª¼Ñ–Ò!@lkíÂŬ(™£ù¢$x,C3ïjž0äÊέ]!¾¼CÇ=“>+Ë D7+íüpsðþ»CìØÀû«ZziK±!Ä6ñõ~ž()KúB'.BEIáâÿþ®üûÅm!þ,EÇw¦GKjÙ¿± é«q¹!K; òÖNÉ¿ëØ±y÷WµôHO¿nyL„Ñ(Bò†\E–‘ãwÿ3è$‘<äjÞ¨‰åPbC [» +¬‚Eúê\nÒeàÖVî¹»½yãƒÁ;6ìþœÆ§çfíò˜þb^|r<–±œå ¹úÓí±àøêŽš0ÞÜÂÉF„¤¯Îå†U°öè‰q¼¼ŽUÔ5´Œ‚Ô½ý²íþª–^ÚRlˆ±åÑEªŒ¬!Wùæ‚d]bW…VMDÇ— ­¢âÒàSn !"D×Îð+?øÞ=›ïxcH9D€ú#ê‰î²£å‰ÙP*߈È<ñá8ÑâcæXö‰è“ŸRß9Ó{ŇÃ[B|ð¥oÝâã¯Æ"ñ 䂯k©ý+Naòæðýïþ SÁdõ«n7YORz ,ìÚˆ°ì«”jܨ‡ŸK‘ñ§oÆbxr`Û=óÄ©þé4”.µ\éi×[¹û»ßßüà£ß›^sÜßÀÀe»"Ã~žè¬šý†š/ùí:Øþb@x‘=ä*˱bć+:¾¿T\iD€ÌòÁÇ¿Üý×ßWm[Ó%KŸ R÷öë÷½¯òÌ–ºÖ6pÙ^ÅÊG'ƒÝ*ž¯º/y4C °X€¤‰W¹Š:æ¿f:4ùÖMOp|v‹BFŽ!×ÝñD.V¾÷Wwo~ø‡†M.‡–ÝãÁŒélc»LiìPþf\ÔJÄ™ä.{’3¡\…_ÅmT˜úØ(“ùóîxNÇëŸ ñþˆ€œ5àL0$=Í×Ë*$L<…Ì9®{H*1!ÅyÇ‚òWiæøN#°l,QrÈUÄü’„\¿I*4×Ñå„Õ*O±3]þáƒþwÿò{¡/ K±"¯¹ùÑÿ|8lj9Ìyºç1td~-¤Í/OòesÒ¤ÛcXHc»ª~*BÚ°#mØOz.s@¢±d¬´«\9ÉW¹Ú{?3£"õ§Sòùi`Ä.®¸f7­Ã(ôì1ã'‰ƒ:¬¸í˜»ß†]Ìe» Š¢B®ö~s8_ B 1¨§b)6€²Ø6pÍ•”uÒ9ÚEs<åù' ¤q`³í¼ú°hAù»`àš+/{Ö`É*X/±;/ä ñÍB=!ì%蔺˜ ÊD…­ÈrÖzGڴ펮{Jª§ƒˆ"Nô¢¼ÇÕaX&é$ޱ‰¥„¶[vâ“k”h sŸ’gi‡æ±càšÌÉNÏÀ5“†Í™p¤‡óžŽv OIÛ-Å´óç…=¡¶C×íÄ•-iGõ¥KÓP²'DæM&Ÿ7Ñç&ázªÜ{¢ä²°(lÈ 45j©ÂÙÞSv“¯¶gG¨ÜÍÂ|ì ÁÊrr|´+ņ,»_ÿQˆ]¶)‡J00ؘªÎèL ST ëš¡ŽÜ¤=À¼S«»¯Ó\ª:ÜE¢œ½-é„.‡ˆSÂ\LAvÅ`¾-z6´½ü©þªkèÚ퉮 sÅGRq{L ¯¤èøóÇï*‚<f†ÀUczºjö T4²ìªu™ ¢3¯†ÒfÛÂÌSßàƒÕ†4èªr“&„°G ÊÍA¸“•X‚•0äjžøP£úHˆOÞ”äÿPUè”°˜ Ãb.H6ž7xm%:Ôƒúb:ö~'xõ° vé' • yæ{Üùf,<þx}rPm.`ãN ˜ç¬¡ë2$%jÄJ.©8ƒ­½ìõqõGÔ®æ"f•+5â!…Çç"<À6v0vÀ±pŸ›pjÉÆFÃï¿—1d”-Ø @J! C®T¨•Ÿ"<ÀJÇk@ƒêvæJCã9c躌‚¤o·¶E³GA² 0œçü â e‹!bC®&#Ÿ¾…ðÛ9ËýcD¯n}9M±÷GP¸ £½E´8}Rˆ˜™ïá¹rîŒ7Dx@MhøÓÄwÿ@=0éœ1 ’­ÝjÚÊuÊéXÍù<ÁÏWîÒ®>$,äJ /ÿWˆO¤ðøêSrêÆrCï{•¬‡ 8gŒ‚Ðn%a#o¸¨'Üxzš3˜R‚þ ¹úú³±ðP„åt¡†xCúM{ª³ã­¨°W& :gŒ‚¤o·ÔHSB±Ô\µMèÊ”»–@€”#BÔëö—ãU­>ÿáM`Y4çÉØP4wÔ¢ “ጂd˳õ8ƒª\ž(ð÷6£ yÙÀâŇš`~󆟽+įÉ9h’óu¬!·{Œ¨!“‹0 ’ åœj,> m¯¼ßªâ(ˆ5#Ò^Ô@ª‡)w‚ù›BÜúœƒ&Š5´\÷‘åŒkèC3ê@Ï 3Ë(Hv‡ú„¨çSý²Ú«ª‚¨…+¶å µR_}BNAÓ;óí7¬Ë¬z 0ù$–Qlí–rÒÕL„,—5OÍmUiçû^(ÝQËÊÜ@T+‹Q}«LDHž(ºOH“á)Œ‚ B&aWÛ%ÛkG˜{*z~‹î2§„S¯ "N°¸ &ùŽ×™Û®Ô÷:sV¼‚¤e`¸Ü3 ’_„ØÚn ¼öJ—S»,ÌΟ9‘wiá P…‡u„ƒ!@jÙ™oZz ›^gΜH £ ö·[¶=tPé=¤³½2<f9 ´,-o“\L‰Uú8@;ó¡|­ »ž*<á±ÊjWÃ4 £ ùÛ-åX¯Šê‡d ='ò„‰öÊPèZØ|¼N D¯îüÛñís”š¨c‡®6Â:$ÌÙϪc»WcÔÔ5,B)&•sv¯|mW4‰*]÷¸É`ÕèÚÎÇóÙPWÿØ„U+ °×Èn+¿bBÄ/<¶É%(ˆ³†¯Ï(HAbR¾T{uHT',k"<–«2J«aþLíçãy6ÔQÎ&¶d„ÐX!rÌëLu7„CïºÇPRßfÃw)Ø9ô²T»ebŒ/Û:ƒŠÚè(vyÙIˆYäü)´»5¼ªœ•µA&â^h–êL¿ã5¸›¢¼‘‘':Ôþw¼ëöÈ(æ‚Ô¯ÍxóÃt´Y}Ï™?ä=(Y·aÕ'oyÙIèZVG×/¸6XÎv|ÑEõS½ñA;Á·0@ý\Ïa[õœ&5©°+_÷ÈWÛ÷šÇÐë¸UvÃktû<ñ¨Õ°– ^2 ²NV”Þfµ½6K½z¶ï$tì¾—ÛfÙþpÄJÊy^–¶Y”{íwwN»}AŒ'G³2“ØÛWkÛ+_ÊvG}å,U?8§Lu°v8-Çq°h,@€ @€ €@€ †ÿ`­ÙŸ ¿ÔIEND®B`‚funkload-1.17.1/doc/source/monitoring.rst000066400000000000000000000024631302537724200204020ustar00rootroot00000000000000The monitor server =================== If you want to monitor a linux server health during the bench, you have to run a monitor xmlrpc server on the target server, this requires to install a minimal FunkLoad package, on Debian/Ubunutu :: sudo aptitude install python-dev python-setuptools \ python-webunit python-docutils sudo easy_install -f http://funkload.nuxeo.org/snapshots/ -U funkload Then create a configuration file ``monitor.conf``:: [server] host = port = 8008 interval = .5 interface = eth0 [client] host = port = 8008 And start the monitor server:: fl-monitor-ctl monitor.conf start On the bench server add to your test configuration file the following section:: [monitor] hosts = [] description = The application server port = 8008 or if you need to have multiple ports for the same IP, for instance if you are using ssh tunneling:: [monitor] hosts = monitor1 [monitor1] host = description = The application server port = 8008 Then run the bench, the report will include server stats. Note that you can monitor multiple hosts and that the monitor is linux specific. A new contribution will be added in 1.16 to extend the monitoring, it will enable to use Nagios or Munin plugins. funkload-1.17.1/doc/source/recorder.rst000066400000000000000000000046101302537724200200160ustar00rootroot00000000000000Recording a test =============================== You can use ``fl-record`` to record your browser activity, this requires the TCPWatch_ python proxy. See installation_ for information on how to install TCPWatch_. 1. Start the recorder:: fl-record basic_navigation This will output something like this:: Hit Ctrl-C to stop recording. HTTP proxy listening on :8090 Recording to directory /tmp/tmpaYDky9_funkload. 2. Setup your browser proxy and play your scenario * set `localhost:8090` as your browser's HTTP proxy http://www.wikihow.com/Change-Proxy-Settings * Play your scenario using your browser * Hit Ctrl-C to stop recording:: ^C # Saving uploaded file: foo.png # Saving uploaded file: bar.pdf Creating script: ./test_BasicNavigation.py. Creating configuration file: ./BasicNavigation.conf. You now have a new python class in ``test_BaiscNavigation.py`` and a configuration file. Refer to the tutorial_ to learn how to turn it into a workable test. To add more requests to your test, just run ``fl-record`` again, without parameters, perform your requests with the browser, then hit Ctrl-C. ``fl-record`` will output code ready to be pasted into your test case. :: $ fl-record HTTP proxy listening on :8090 Recording to directory /tmp/tmptOl7jh_funkload. ^C TCPWatch finished. self.post(server_url + "/booking/register.seam", params=[ ['registration', 'registration'], ['registration:usernameDecorate:username', 'scott'], ['registration:nameDecorate:name', 'scott'], ['registration:passwordDecorate:password', 'tiger'], ['registration:verifyDecorate:verify', 'tiger'], ['registration:register', 'Register'], ['javax.faces.ViewState', '_id6407']], description="Post /booking/register.seam") $ Note that ``fl-record`` : * works fine with multi-part encoded form and file upload. * automaticly handles a JSF Myfaces token, which enables it to easily record and play any JBoss Seam application. * doesn't support HTTPS. The work around is to first record a scenario on HTTP, and then change the `url` back to `https` in the configuration file. .. _FunkLoad: http://funkload.nuxeo.org/ .. _TCPWatch: http://hathawaymix.org/Software/TCPWatch/ .. _tutorial: tutorial.html .. _installation: installation.html funkload-1.17.1/doc/source/reporting.rst000066400000000000000000000005631302537724200202250ustar00rootroot00000000000000Reporting bugs =============== If you want to report a bug or if you think that something is missing, either send me an email bdelbosc _at_ nuxeo.com or use the `github issue tracker `_. The list of open tasks and bugs are scheduled in the `TODO `_ file. funkload-1.17.1/doc/source/screenshot.rst000066400000000000000000000021611302537724200203650ustar00rootroot00000000000000Screenshots ============== * `HTML Benchmark report example `_. The default report. * `PDF Benchmark report `_. Generated from the emacs `org-mode output `_. * `Differential report example `_. A differential report compares 2 benchmark reports giving a quick overview of scalability and velocity changes. * `Trend report example `_. A trend report analyzes many benchmark reports to display evolution of the page statistics over time. * You can have browse some test case examples in the `demo directory `_ or check them out if you have already installed FunkLoad:: $ fl-install-demo Extract FunkLoad examples into ./funkload-demo : ... done. $ ls funkload-demo cmf README.txt seam-booking-1.1.5 simple xmlrpc zope funkload-1.17.1/doc/source/tutorial.rst000066400000000000000000000206201302537724200200530ustar00rootroot00000000000000First Steps with FunkLoad ========================== A FunkLoad test is made of a typical unittest and a configuration file. Let's look at a simple test script included in the FunkLoad examples. To get the demo examples you just need to run:: fl-install-demo # Extract FunkLoad examples into ./funkload-demo : ... done. cd funkload-demo/simple The test case ---------------------- Here is an extract of the simple demo test case ``test_Simple.py``:: import unittest from random import random from funkload.FunkLoadTestCase import FunkLoadTestCase class Simple(FunkLoadTestCase): """This test uses the configuration file Simple.conf.""" def setUp(self): """Setting up test.""" self.server_url = self.conf_get('main', 'url') def test_simple(self): # The description should be set in the configuration file server_url = self.server_url # begin test --------------------------------------------- nb_time = self.conf_getInt('test_simple', 'nb_time') for i in range(nb_time): self.get(server_url, description='Get URL') # end test ----------------------------------------------- if __name__ in ('main', '__main__'): unittest.main() The Simple test case extends ``FunkLoadTestCase`` and implements a test case named test_simple. This test case loop on a get request. The ``FunkLoadTestCase`` extends the ``unittest.TestCase``, adding methods: * to send HTTP requests (get, post, put, delete or xmlrpc) * to help build assertions with the response (getBody, getLastUrl, ...) * to customize the test by accessing a configuration file (conf_getInt) * ... The target URL and the number of requests are defined in the configuration files. By convention, the name of the configuration file is the name of the test case class and a ".conf" extension. In our case: ``Simple.conf``. The configuration file ---------------------------- It is a plain text file with sections:: # main section for the test case [main] title=Simple FunkLoad tests description=Simply testing a default static page url=http://localhost/index.html # a section for each test [test_simple] description=Access the main URL %(nb_time)s times nb_time=20 <> # a section to configure the test mode [ftest] log_to = console file log_path = simple-test.log result_path = simple-test.xml sleep_time_min = 0 sleep_time_max = 0 # a section to configure the bench mode [bench] cycles = 50:75:100:125 duration = 10 startup_delay = 0.01 sleep_time = 0.01 cycle_time = 1 log_to = log_path = simple-bench.log result_path = simple-bench.xml sleep_time_min = 0 sleep_time_max = 0.5 Runing the test ------------------ Check that the URL shown in the ``main`` section is reachable, then invoke ``fl-run-test``. It will run all the tests present in the test_Simple module:: $ fl-run-test -dv test_Simple.py test_simple (test_Simple.Simple) ... test_simple: Starting ----------------------------------- Access the main URL 20 times test_simple: GET: http://localhost/index.html Page 1: Get url ... test_simple: Done in 0.006s test_simple: Load css and images... test_simple: Done in 0.002s test_simple: GET: http://localhost/index.html Page 2: Get url ... <> Page 20: Get url ... test_simple: Done in 0.000s test_simple: Load css and images... test_simple: Done in 0.000s Ok ---------------------------------------------------------------------- Ran 1 test in 0.051s OK Runing a benchmark -------------------- To run a benchmark, you invoke ``fl-run-bench`` instead of the test runner. You also need to select which test case to run (the name of the method in ``test_Simple.py``) The result of the bench will be saved in a single XML file ``simple-bench.xml``. The name of this result file is set in the configuration file in the ``bench`` section. You can override the configuration file using command line options. Here we use ``-c`` to specify 3 cycles with 1, 10 and 20 concurrent users (CUs). :: $ fl-run-bench -c 1:10:20 test_Simple.py Simple.test_simple ======================================================================== Benching Simple.test_simple ======================================================================== Access the main URL 20 times ------------------------------------------------------------------------ Configuration ============= * Current time: 2011-01-26T23:22:51.267757 * Configuration file: /tmp/funkload-demo/simple/Simple.conf * Log xml: /tmp/funkload-demo/simple/simple-bench.xml * Server: http://localhost/index.html * Cycles: [1, 10, 20] * Cycle duration: 10s * Sleeptime between request: from 0.0s to 0.5s * Sleeptime between test case: 0.01s * Startup delay between thread: 0.01s Benching ======== * setUpBench hook: ... done. Cycle #0 with 1 virtual users ----------------------------- * setUpCycle hook: ... done. * Start monitoring localhost: ... failed, server is down. * Current time: 2011-01-26T23:22:51.279718 * Starting threads: . done. * Logging for 10s (until 2011-01-26T23:23:01.301664): .. done. * Waiting end of threads: . done. * Waiting cycle sleeptime 1s: ... done. * tearDownCycle hook: ... done. * End of cycle, 14.96s elapsed. * Cycle result: **SUCCESSFUL**, 2 success, 0 failure, 0 errors. Cycle #1 with 10 virtual users ------------------------------ * setUpCycle hook: ... done. * Current time: 2011-01-26T23:23:06.234422 * Starting threads: .......... done. * Logging for 10s (until 2011-01-26T23:23:16.360602): .............. done. * Waiting end of threads: .......... done. * Waiting cycle sleeptime 1s: ... done. * tearDownCycle hook: ... done. * End of cycle, 16.67s elapsed. * Cycle result: **SUCCESSFUL**, 14 success, 0 failure, 0 errors. Cycle #2 with 20 virtual users ------------------------------ * setUpCycle hook: ... done. * Current time: 2011-01-26T23:23:06.234422 * Starting threads: .......... done. * Logging for 10s (until 2011-01-26T23:23:16.360602): .............. done. * Waiting end of threads: .......... done. * Waiting cycle sleeptime 1s: ... done. * tearDownCycle hook: ... done. * End of cycle, 16.67s elapsed. * Cycle result: **SUCCESSFUL**, 14 success, 0 failure, 0 errors. * tearDownBench hook: ... done. Result ====== * Success: 40 * Failures: 0 * Errors: 0 Bench status: **SUCCESSFUL** Generating a report -------------------- The XML result file can be turned into an HTML report:: $ fl-build-report --html simple-bench.xml Creating html report: ...done: /tmp/funkload-demo/simple/test_simple-20110126T232251/index.html It should generate something like this: http://funkload.nuxeo.org/report-example/test_simple-20110126T232251/ Note that there was no monitoring in our simple benchmark. Write your own test ------------------- The process to write a new test is the following: * Use the recorder_ to initialize the test case and the configuration files and to grab requests. * Play the test and display each response in Firefox, this will help you to add assertions and check the response:: fl-run-test -dV test_BasicNavigation.py * Implement the dynamic parts: - For each request, add an assertion to make sure the page is the one you expect. this can be done by checking if a term is present in a response:: self.assert_('logout' in self.getBody(), "Login failure") - To Generate random input, you can use the FunkLoad.Lipsum module:: from funkload import Lipsum ... lipsum = Lipsum() # Get a random title title = lipsum.getSubject() - To Extract a token from a previous response:: from funkload.utils import extract_token ... jsf_state = extract_token(self.getBody(), ' id="javax.faces.ViewState" value="', '"') - To Use a credential_ server if you want to make a bench with different users or simply don't want to hard-code your login/password:: from funkload.utils import xmlrpc_get_credential ... # get an admin user login, pwd = xmlrpc_get_credential(host, port, "admin") * Configure the monitoring_ and automate your benchmark using a Makefile_. .. _recorder: recorder.html .. _credential: credential.html .. _monitoring: monitoring.html .. _Makefile: faq.html#how-to-automate-stuff funkload-1.17.1/doc/source/usage-fl-build-report.rst000066400000000000000000000036251302537724200223270ustar00rootroot00000000000000Report builder ``fl-build-report`` usage ========================================= fl-build-report [options] xmlfile [xmlfile...] or fl-build-report --diff REPORT_PATH1 REPORT_PATH2 fl-build-report analyze a FunkLoad bench xml result file and output a report. If there are more than one file the xml results are merged. See http://funkload.nuxeo.org/ for more information. Examples --------- fl-build-report funkload.xml ReST rendering into stdout. fl-build-report --html -o /tmp funkload.xml Build an HTML report in /tmp fl-build-report --html node1.xml node2.xml node3.xml Build an HTML report merging test result from 3 nodes. fl-build-report --diff /tmp/test_reader-20080101 /tmp/test_reader-20080102 Build a differential report to compare 2 bench reports, requires gnuplot. fl-build-report -h More options. Options --------- --version show program's version number and exit --help, -h show this help message and exit --html, -H Produce an html report. --with-percentiles, -P Include percentiles in tables, use 10%, 50% and 90% for charts, default option. --no-percentiles No percentiles in tables display min, avg and max in charts (gdchart only). --diff, -d Create differential report. --output-directory=OUTPUT_DIR, -o OUTPUT_DIR Parent directory to store reports, the directoryname of the report will be generated automatically. --report-directory=REPORT_DIR, -r REPORT_DIR Directory name to store the report. --apdex-T=APDEX_T, -T APDEX_T Apdex T constant in second, default is set to 1.5s. Visit http://www.apdex.org/ for more information. funkload-1.17.1/doc/source/usage-fl-credential-ctl.rst000066400000000000000000000005601302537724200226040ustar00rootroot00000000000000Credential server ``fl-credential-ctl`` Usage ================================================ fl-credential-ctl config_file action action can be: start|startd|stop|restart|status|test Execute action on the XML/RPC server. Options ------- --version show program's version number and exit --help, -h show this help message and exit --quiet, -q Verbose output funkload-1.17.1/doc/source/usage-fl-monitor-ctl.rst000066400000000000000000000005411302537724200221600ustar00rootroot00000000000000Monitor server ``fl-monitor-ctl`` usage ========================================= fl-monitor-ctl config_file action action can be: start|startd|stop|restart|status|test Execute action on the XML/RPC server. Options -------- --version show program's version number and exit --help, -h show this help message and exit --quiet, -q Verbose output funkload-1.17.1/doc/source/usage-fl-record.rst000066400000000000000000000023101302537724200211630ustar00rootroot00000000000000Recorder ``fl-record`` usage ============================== fl-record [options] [test_name] fl-record launch a TCPWatch proxy and record activities, then output a FunkLoad script or generates a FunkLoad unit test if test_name is specified. The default proxy port is 8090. Note that tcpwatch-httpproxy or tcpwatch.py executable must be accessible from your env. See http://funkload.nuxeo.org/ for more information. Examples ----------- fl-record foo_bar Run a proxy and create a FunkLoad test case, generates test_FooBar.py and FooBar.conf file. To test it: fl-run-test -dV test_FooBar.py fl-record -p 9090 Run a proxy on port 9090, output script to stdout. fl-record -i /tmp/tcpwatch Convert a tcpwatch capture into a script. Options --------- --version show program's version number and exit --help, -h show this help message and exit --verbose, -v Verbose output --port=PORT, -p PORT The proxy port. --tcp-watch-input=TCPWATCH_PATH, -i TCPWATCH_PATH Path to an existing tcpwatch capture. --loop=LOOP, -l LOOP Loop mode. funkload-1.17.1/doc/source/usage-fl-run-bench.rst000066400000000000000000000101231302537724200215670ustar00rootroot00000000000000Bench runner ``fl-run-bench`` usage ===================================== fl-run-bench [options] file class.method fl-run-bench launch a FunkLoad unit test as load test. A FunkLoad unittest uses a configuration file named [class].conf, this configuration is overriden by the command line options. See http://funkload.nuxeo.org/ for more information. Examples -------- fl-run-bench myFile.py MyTestCase.testSomething Bench MyTestCase.testSomething using MyTestCase.conf. fl-run-bench --config ~/test.conf myFile.py MyTestCase.testSomething Bench MyTestCase.testSomething using test.conf located in home directory. fl-run-bench -u http://localhost:8080 -c 10:20 -D 30 myFile.py \ MyTestCase.testSomething Bench MyTestCase.testSomething on localhost:8080 with 2 cycles of 10 and 20 users during 30s. fl-run-bench -h More options. Options -------- --version show program's version number and exit --help, -h show this help message and exit --config=CONFIG Path to alternative config location. Otherwise the configuration file is expected to be named after test case class, located either next to test module or path defined by environment variable ``FL_CONF_PATH`` --url=MAIN_URL, -u MAIN_URL Base URL to bench. --cycles=BENCH_CYCLES, -c BENCH_CYCLES Cycles to bench, this is a list of number of virtual concurrent users, to run a bench with 3 cycles with 5, 10 and 20 users use: -c 5:10:20 --duration=BENCH_DURATION, -D BENCH_DURATION Duration of a cycle in seconds. --sleep-time-min=BENCH_SLEEP_TIME_MIN, -m BENCH_SLEEP_TIME_MIN Minimum sleep time between requests. --sleep-time-max=BENCH_SLEEP_TIME_MAX, -M BENCH_SLEEP_TIME_MAX Maximum sleep time between requests. --test-sleep-time=BENCH_SLEEP_TIME, -t BENCH_SLEEP_TIME Sleep time between tests. --startup-delay=BENCH_STARTUP_DELAY, -s BENCH_STARTUP_DELAY Startup delay between thread. --as-fast-as-possible, -f Remove sleep times between requests and between tests, shortcut for -m0 -M0 -t0 --runner-class=BENCH_RUNNER_CLASS, -r BENCH_RUNNER_CLASS Python dotted import path to BenchRunner class to use. --no-color Monochrome output. --accept-invalid-links Do not fail if css/image links are not reachable. --simple-fetch Don't load additional links like css or images when fetching an html page. --label=LABEL, -l LABEL Add a label to this bench run for easier identification (it will be appended to the directory name for reports generated from it). --enable-debug-server Instantiates a debug HTTP server which exposes an interface using which parameters can be modified at run-time. Currently supported parameters: /cvu?inc= to increase the number of CVUs, /cvu?dec= to decrease the number of CVUs, /getcvu returns number of CVUs --debug-server-port=DEBUGPORT Port at which debug server should run during the test --distribute distributes the CVUs over a group of worker machines that are defined in the section [workers] --distribute-workers=WORKERLIST this parameter will over-ride the list of workers defined in the config file. expected notation is uname@host,uname:pwd@host or just host... --is-distributed this parameter is for internal use only. it signals to a worker node that it is in distributed mode and shouldn't perform certain actions. funkload-1.17.1/doc/source/usage-fl-run-test.rst000066400000000000000000000076511302537724200215030ustar00rootroot00000000000000Test runner ``fl-run-test`` usage ==================================== fl-run-test [options] file [class.method|class|suite] [...] fl-run-test launch a FunkLoad unit test. A FunkLoad unittest uses a configuration file named [class].conf, this configuration is overriden by the command line options. See http://funkload.nuxeo.org/ for more information. Examples ---------- fl-run-test myFile.py Run all tests. fl-run-test myFile.py test_suite Run suite named test_suite. fl-run-test myFile.py MyTestCase.testSomething Run a single test MyTestCase.testSomething. fl-run-test myFile.py MyTestCase Run all 'test*' test methods in MyTestCase. fl-run-test myFile.py MyTestCase -u http://localhost Same against localhost. fl-run-test --doctest myDocTest.txt Run doctest from plain text file (requires python2.4). fl-run-test --doctest -d myDocTest.txt Run doctest with debug output (requires python2.4). fl-run-test myfile.py -V Run default set of tests and view in real time each page fetch with firefox. fl-run-test myfile.py MyTestCase.testSomething -l 3 -n 100 Run MyTestCase.testSomething, reload one hundred time the page 3 without concurrency and as fast as possible. Output response time stats. You can loop on many pages using slice -l 2:4. fl-run-test myFile.py -e [Ss]ome Run all tests that match the regex [Ss]ome. fl-run-test myFile.py -e '!xmlrpc$' Run all tests that does not ends with xmlrpc. fl-run-test myFile.py --list List all the test names. fl-run-test -h More options. Options --------- --version show program's version number and exit --help, -h show this help message and exit --config=CONFIG Path to alternative config location. Otherwise the configuration file is expected to be named after test case class, located either next to test module or path defined by environment variable ``FL_CONF_PATH`` --quiet, -q Minimal output. --verbose, -v Verbose output. --debug, -d FunkLoad and doctest debug output. --debug-level=DEBUG_LEVEL Debug level 3 is more verbose. --url=MAIN_URL, -u MAIN_URL Base URL to bench without ending '/'. --sleep-time-min=FTEST_SLEEP_TIME_MIN, -m FTEST_SLEEP_TIME_MIN Minumum sleep time between request. --sleep-time-max=FTEST_SLEEP_TIME_MAX, -M FTEST_SLEEP_TIME_MAX Maximum sleep time between request. --dump-directory=DUMP_DIR Directory to dump html pages. --firefox-view, -V Real time view using firefox, you must have a running instance of firefox in the same host. --no-color Monochrome output. --loop-on-pages=LOOP_STEPS, -l LOOP_STEPS Loop as fast as possible without concurrency on pages, expect a page number or a slice like 3:5. Output some statistics. --loop-number=LOOP_NUMBER, -n LOOP_NUMBER Number of loop. --accept-invalid-links Do not fail if css/image links are not reachable. --simple-fetch Don't load additional links like css or images when fetching an html page. --stop-on-fail Stop tests on first failure or error. --regex=REGEX, -e REGEX The test names must match the regex. --list Just list the test names. --doctest Check for a doc test. --pause Pause between request, press ENTER to continue. funkload-1.17.1/doc/source/usage.rst000066400000000000000000000004571302537724200173220ustar00rootroot00000000000000FunkLoad command line usage ============================ All the FunkLoad command starts with ``fl-`` and have a ``--help`` options. .. toctree:: :maxdepth: 2 usage-fl-record usage-fl-run-test usage-fl-run-bench usage-fl-build-report usage-fl-credential-ctl usage-fl-monitor-ctl funkload-1.17.1/doc/source/writing-test.rst000066400000000000000000000244231302537724200206550ustar00rootroot00000000000000Writing test scripts ====================== Making HTTP requests ------------------- * HTTP GET Examples of performing HTTP GET requests:: self.get(self.server_url + "/logout", description="Logout ") self.get(self.server_url + "/search?query=foo", description="Search with params in the URL") self.get(self.server_url + "/search", params=[['query', 'foo']], description="Search using params") * HTTP POST Examples of performing HTTP POST requests:: from webunit.utility import Upload from funkload.utils import Data ... # simple post self.post(self.server_url + "/login", params=[['user_name', 'scott'], ['user_password', 'tiger']], description="Login as scott") # upload a file self.post(self.server_url + "/uploadFile", params=[['file', Upload('/tmp/foo.pdf'), ['title', 'foo file']], description="Upload a file") # post with text/xml content type self.post(self.server_url + "/xmlAPI", params=Data('text/xml', 'bar'), description="Call xml API") * HTTP PUT/DELETE:: from funkload.utils import Data ... self.put(self.server_url + '/xmlAPI", Data('text/xml', 'bar', description="Put query") self.delete(self.server_url + '/some/rest/path/object', description="Delete object') * xmlrc helper:: ret = self.xmlrpc(server_url, 'getStatus', description="Check getStatus") You should always set a description when making a request, it improves the readability of the report. If you run your test in debug mode you can see what is being sent. Debug mode is activated with the ``--debug --debug-level=3`` options. By running your test with the ``-V`` option, you will see each response in your running instance of Firefox. Adding assertions ------------------- After each request you should add an assertion to make sure you are on the expected page. You can check the response content using ``self.getBody()`` :: self.get(server_url, description="home page") self.assert_('Welcome' in self.getBody(), "Wrong home page") You can check an expected HTTP return code:: ret = self.get(...) self.assert_(ret.code in [200, 301], "expecting a 200 or 301") Note that FunkLoad tests the HTTP return code by default, assuming that any code other than 200,301,302 is an error. This can be changed using the ``ok_codes`` parameter or the config file option:: ret = self.get(self.server_url + '/404.hmtl', ok_codes=[200, 404], description="Accept 404") self.assert_(ret.code == 404) You can check the returned URL which may be different if you have been redirected:: self.post(self.server_url + "/login", params=[['user_name', 'scott'], ['user_password', 'tiger']], description="Login as scott") self.assert_('dashboard' in self.getLastUrl(), "Login failure") Basic Authentication ----------------------- :: self.setBasicAuth('scott', 'tiger') self.get(self.server_url, description="GET using basic auth") # remove basic auth self.clearBasicAuth() Extra headers --------------- :: self.setHeader('Accept-Language', 'de') # this header is set for all the next requests ... # Remove all additional headers self.clearHeaders() Extracting information ------------------------ At some point you will need to extract information from the response. When possible, search using string methods or the ``re`` (regular expression) module. Parsing XML or HTML has such a high cost that it will prevent your tests from achieving high load. FunkLoad comes with a simple ``extract_token``, using string find methods:: from funkload.utils import extract_token ... token = extract_token(self.getBody(), 'id="mytoken" value="', '"') Of course, for pure functional testing you can use FunkLoad helpers:: ret = self.get(self.server_url, description="Get some page") urls = self.listHref(url_pattern="view_document", content_pattern="View") base_url = self.getLastBaseUrl() Or the WebUnit minidom:: title = self.getDom().getByName('title')[0].getContents() Or any Python XML/HTML processing library, including Beautiful Soup. Using the configuration file --------------------------------- You can get information from the configuration file, using the appropriate ``self.conf_get*(section, key)`` methods:: # Getting value from the main section value = self.conf_get('main', 'key', 'default') count = self.conf_getInt('main', 'nb_docs', 10) percent = self.conf_getFloat('main', 'percent', 5.5) items = self.conf_getList('main', 'names') # The names in the conf file are separated with a colon # names=name1:name2:name3 Sharing credentials --------------------- If you need to share credentials among your tests you can use the FunkLoad `credential server <./credential.html>`_. Here is an example to request credentials:: from funkload.utils import xmlrpc_get_credential ... # get the credential host and port from the config file credential_host = self.conf_get('credential', 'host') credential_port = self.conf_getInt('credential', 'port') # get a login/pwd from the members group login, password = xmlrpc_get_credential(credential_host, credential_port, 'members') Since FunkLoad 1.15 the credential server can return a sequence:: from funkload.utils import xmlrpc_get_seq ... seq = xmlrpc_get_seq() The sequence starts with 0 but can be initialized in the credential server configuration file. Generating data ------------------ FunkLoad comes with a simple random text generator called Lipsum:: >>> from funkload.Lipsum import Lipsum >>> print 'Word: %s\n' % (Lipsum().getWord()) Word: albus >>> print 'UniqWord: %s\n' % (Lipsum().getUniqWord()) UniqWord: fs3ywpxg >>> print 'Subject: %s\n' % (Lipsum().getSubject()) Subject: Fulvus orientalis albus hortensis dorsum >>> print 'Subject uniq: %s\n' % (Lipsum().getSubject(uniq=True)) Subject uniq: F26v3y fuscus variegatus dolicho caulos cephalus >>> print 'Sentence: %s\n' % (Lipsum().getSentence()) Sentence: Argentatus arvensis diplo familiaris tetra trich ; vulgaris montanus folius tetra so echinus, trich pteron phyton so brachy officinalis. >>> print 'Paragraph: %s\n' % (Lipsum().getParagraph()) Paragraph: Sit pteron, tetra dermis viridis cyanos. Tetra novaehollandiae cyanos indicus major ortho archaeos montanus. Viridis cephalus, niger, it occidentalis volans delorum sativus gaster arctos phyllo dermis archaeos. Archaeos montanus erythro mauro minimus biscortborealis occidentalis morphos biscortborealis silvestris punctatus variegatus ! phyton mauro hexa. >>> print 'Message: %s\n' % (Lipsum().getMessage()) Message: Familiaris fulvus flora xanthos tomentosus lutea lineatus ?, dolicho campus maculatus ad platy gaster punctatus. So pachys rufus tris, trich montanus so variegatus cristatus orientalis diplo minimus. Petra lateralis bradus, chilensis unus officinalis striatus ad. Xanthos dolicho arvensis ennea tinctorius phyton, sit arctos mauro. Dermis zygos, ventrus oeos glycis dulcis chloreus verrucosus lineatus, pteron sinensis officinalis cyanos. Cephalus occidentalis verrucosus echinus ; lateralis protos tinctorius punctatus parvus volans. Pteron palustris gaster ad tomentosus platy arctos rhytis pedis indicus mono. Chilensis phyton, ; hortensis fuscus aquam. Variegatus deca fuscus petra rubra biscortborealis familiaris sativus leucus xanthos phyton argentatus novaehollandiae brachy. Mauro rufus saurus deca oeos thrix rostra archaeos, ortho rufus phyllo cristatus campus rostra oleum xanthos chilensis. Archaeos protos tinctorius gaster arctos niger niger variegatus thrix, mauro arctos verrucosus ennea delorum. Pedis melanus mauro occidentalis pratensis chilensis arctos gaster noveboracensis, rufus ennea minimus saurus dermis fulvus octa. >>> print 'Phone number: %s\n' % Lipsum().getPhoneNumber() Phone number: 07 20 25 56 06 >>> print 'Phone number fr short: %s\n' % Lipsum().getPhoneNumber( ... lang="fr", format="short") Phone number fr short: 0787117995 >>> print 'Phone number fr medium: %s\n' % Lipsum().getPhoneNumber( ... lang="fr", format="medium") Phone number fr medium: 07 88 31 30 06 >>> print 'Phone number fr long: %s\n' % Lipsum().getPhoneNumber( ... lang="fr", format="long") Phone number fr long: +33 (0)7 41 08 36 56 >>> print 'Phone number en_US short: %s\n' % Lipsum().getPhoneNumber( ... lang="en_US", format="short") Phone number en_US short: 863-3655 >>> print 'Phone number en_US medium: %s\n' % Lipsum().getPhoneNumber( ... lang="en_US", format="medium") Phone number en_US medium: (327) 129-2863 >>> print 'Phone number en_US long: %s\n' % Lipsum().getPhoneNumber( ... lang="en_US", format="long") Phone number en_US long: +00 1 (283) 158-7134 >>> print 'Address default: %s' % Lipsum().getAddress() Address default: 85 place Brevis 99612 Trich Adding information to the report ---------------------------------- * At runtime a bench can add metadata to the report using the setUpBench hook and the addMetadata method:: def setUpBench(self): ret = self.get(self.server_url + "/getVersion", description="Get the server version") self.addMetadata(**{'Application version': ret.getBody()}) * At runtime from the command line using the ``--label`` option of the bench runner. * After the bench using a file named ``funkload.metadata`` with a list of ``key:value``. At the moment this file is only used by the trend reports to add chart labels and bench descriptions. This file must be put on the report directory:: label: label used by trend report build: 666 builtOn: hostname Text taken as description `using ReST power `__ Can be multine text. API ----- More info on the API doc: FunkLoadTestCase_. .. _FunkLoadTestCase: http://funkload.nuxeo.com/sphinx/api/core_api.html#module-funkload.FunkLoadTestCase funkload-1.17.1/ez_setup.py000066400000000000000000000240721302537724200156260ustar00rootroot00000000000000#!python """Bootstrap setuptools installation If you want to use setuptools in your package's setup.py, just include this file in the same directory with it, and add this to the top of your setup.py:: from ez_setup import use_setuptools use_setuptools() If you want to require a specific version of setuptools, set a download mirror, or use an alternate download directory, you can do so by supplying the appropriate options to ``use_setuptools()``. This file can also be run as a script to install or upgrade setuptools. """ from __future__ import print_function import sys DEFAULT_VERSION = "0.6c11" DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3] md5_data = { 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca', 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb', 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b', 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a', 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618', 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac', 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5', 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4', 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c', 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b', 'setuptools-0.6c10-py2.3.egg': 'ce1e2ab5d3a0256456d9fc13800a7090', 'setuptools-0.6c10-py2.4.egg': '57d6d9d6e9b80772c59a53a8433a5dd4', 'setuptools-0.6c10-py2.5.egg': 'de46ac8b1c97c895572e5e8596aeb8c7', 'setuptools-0.6c10-py2.6.egg': '58ea40aef06da02ce641495523a0b7f5', 'setuptools-0.6c11-py2.3.egg': '2baeac6e13d414a9d28e7ba5b5a596de', 'setuptools-0.6c11-py2.4.egg': 'bd639f9b0eac4c42497034dec2ec0c2b', 'setuptools-0.6c11-py2.5.egg': '64c94f3bf7a72a13ec83e0b24f2749b2', 'setuptools-0.6c11-py2.6.egg': 'bfa92100bd772d5a213eedd356d64086', 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27', 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277', 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa', 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e', 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e', 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f', 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2', 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc', 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167', 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64', 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d', 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20', 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab', 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53', 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2', 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e', 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372', 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902', 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de', 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b', 'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03', 'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a', 'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6', 'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a', } import sys, os try: from hashlib import md5 except ImportError: from md5 import md5 def _validate_md5(egg_name, data): if egg_name in md5_data: digest = md5(data).hexdigest() if digest != md5_data[egg_name]: print(( "md5 validation of %s failed! (Possible download problem?)" % egg_name ), file=sys.stderr) sys.exit(2) return data def use_setuptools( version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, download_delay=15 ): """Automatically find/download setuptools and make it available on sys.path `version` should be a valid setuptools version number that is available as an egg for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where setuptools will be downloaded, if it is not already available. If `download_delay` is specified, it should be the number of seconds that will be paused before initiating a download, should one be required. If an older version of setuptools is installed, this routine will print a message to ``sys.stderr`` and raise SystemExit in an attempt to abort the calling script. """ was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules def do_download(): egg = download_setuptools(version, download_base, to_dir, download_delay) sys.path.insert(0, egg) import setuptools; setuptools.bootstrap_install_from = egg try: import pkg_resources except ImportError: return do_download() try: pkg_resources.require("setuptools>="+version); return except pkg_resources.VersionConflict as e: if was_imported: print(( "The required version of setuptools (>=%s) is not available, and\n" "can't be installed while this script is running. Please install\n" " a more recent version first, using 'easy_install -U setuptools'." "\n\n(Currently using %r)" ) % (version, e.args[0]), file=sys.stderr) sys.exit(2) except pkg_resources.DistributionNotFound: pass del pkg_resources, sys.modules['pkg_resources'] # reload ok return do_download() def download_setuptools( version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, delay = 15 ): """Download setuptools from a specified location and return its filename `version` should be a valid setuptools version number that is available as an egg for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where the egg will be downloaded. `delay` is the number of seconds to pause before an actual download attempt. """ import urllib2, shutil egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3]) url = download_base + egg_name saveto = os.path.join(to_dir, egg_name) src = dst = None if not os.path.exists(saveto): # Avoid repeated downloads try: from distutils import log if delay: log.warn(""" --------------------------------------------------------------------------- This script requires setuptools version %s to run (even to display help). I will attempt to download it for you (from %s), but you may need to enable firewall access for this script first. I will start the download in %d seconds. (Note: if this machine does not have network access, please obtain the file %s and place it in this directory before rerunning this script.) ---------------------------------------------------------------------------""", version, download_base, delay, url ); from time import sleep; sleep(delay) log.warn("Downloading %s", url) src = urllib2.urlopen(url) # Read/write all in one block, so we don't create a corrupt file # if the download is interrupted. data = _validate_md5(egg_name, src.read()) dst = open(saveto,"wb"); dst.write(data) finally: if src: src.close() if dst: dst.close() return os.path.realpath(saveto) def main(argv, version=DEFAULT_VERSION): """Install or upgrade setuptools and EasyInstall""" try: import setuptools except ImportError: egg = None try: egg = download_setuptools(version, delay=0) sys.path.insert(0,egg) from setuptools.command.easy_install import main return main(list(argv)+[egg]) # we're done here finally: if egg and os.path.exists(egg): os.unlink(egg) else: if setuptools.__version__ == '0.0.1': print(( "You have an obsolete version of setuptools installed. Please\n" "remove it from your system entirely before rerunning this script." ), file=sys.stderr) sys.exit(2) req = "setuptools>="+version import pkg_resources try: pkg_resources.require(req) except pkg_resources.VersionConflict: try: from setuptools.command.easy_install import main except ImportError: from easy_install import main main(list(argv)+[download_setuptools(delay=0)]) sys.exit(0) # try to force an exit else: if argv: from setuptools.command.easy_install import main main(argv) else: print("Setuptools version",version,"or greater has been installed.") print('(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)') def update_md5(filenames): """Update our built-in md5 registry""" import re for name in filenames: base = os.path.basename(name) f = open(name,'rb') md5_data[base] = md5(f.read()).hexdigest() f.close() data = [" %r: %r,\n" % it for it in md5_data.items()] data.sort() repl = "".join(data) import inspect srcfile = inspect.getsourcefile(sys.modules[__name__]) f = open(srcfile, 'rb'); src = f.read(); f.close() match = re.search("\nmd5_data = {\n([^}]+)}", src) if not match: print("Internal error!", file=sys.stderr) sys.exit(2) src = src[:match.start(1)] + repl + src[match.end(1):] f = open(srcfile,'w') f.write(src) f.close() if __name__=='__main__': if len(sys.argv)>2 and sys.argv[1]=='--md5update': update_md5(sys.argv[2:]) else: main(sys.argv[1:]) funkload-1.17.1/minimal.cfg000066400000000000000000000004521302537724200155210ustar00rootroot00000000000000[buildout] parts = funkload test packages eggs = funkload develop = . [funkload] recipe = zc.recipe.egg:scripts eggs = docutils funkload [test] recipe = zc.recipe.testrunner eggs = ${buildout:eggs} [packages] recipe = collective.recipe.omelette eggs = ${buildout:eggs} funkload-1.17.1/scripts/000077500000000000000000000000001302537724200151005ustar00rootroot00000000000000funkload-1.17.1/scripts/fl-build-report000077500000000000000000000016151302537724200200400ustar00rootroot00000000000000#!/bin/env python # (C) Copyright 2005-2011 Nuxeo SA # Author: bdelbosc@nuxeo.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """Convert a testmaker script into a FunkLoad test. $Id: ftest_utils.py 22915 2005-06-09 15:38:07Z bdelbosc $ """ from funkload.ReportBuilder import main main() funkload-1.17.1/scripts/fl-credential-ctl000077500000000000000000000017751302537724200203310ustar00rootroot00000000000000#!/bin/env python # (C) Copyright 2005-2011 Nuxeo SA # Author: bdelbosc@nuxeo.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """Simple client that control the credential server.""" import sys from funkload.CredentialFile import CredentialFileController def main(): """Control credentiald server.""" ctl = CredentialFileController() sys.exit(ctl()) if __name__ == '__main__': main() funkload-1.17.1/scripts/fl-install-demo000077500000000000000000000015351302537724200200210ustar00rootroot00000000000000#!/bin/env python # (C) Copyright 2005-2011 Nuxeo SA # Author: bdelbosc@nuxeo.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """Extract the demo from the funkload egg into the current path.""" from funkload.DemoInstaller import main main() funkload-1.17.1/scripts/fl-monitor-ctl000077500000000000000000000017411302537724200176770ustar00rootroot00000000000000#!/bin/env python # (C) Copyright 2005-2011 Nuxeo SA # Author: bdelbosc@nuxeo.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """Simple client that control the monitor server.""" import sys from funkload.Monitor import MonitorController def main(): """Control monitor server.""" ctl = MonitorController() sys.exit(ctl()) if __name__ == '__main__': main() funkload-1.17.1/scripts/fl-record000077500000000000000000000016161302537724200167070ustar00rootroot00000000000000#!/bin/env python # (C) Copyright 2005-2011 Nuxeo SA # Author: bdelbosc@nuxeo.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """tcpwatch Proxy recorder that output FunkLoad instruction $Id: fl-run-bench 24487 2005-08-24 16:13:39Z bdelbosc $ """ from funkload.Recorder import main main() funkload-1.17.1/scripts/fl-run-bench000077500000000000000000000015771302537724200173200ustar00rootroot00000000000000#!/bin/env python # (C) Copyright 2005-2011 Nuxeo SA # Author: bdelbosc@nuxeo.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """Run a funkload unit test in bench mode. $Id: fl-run-bench 24568 2005-08-26 15:23:49Z bdelbosc $ """ from funkload.BenchRunner import main main() funkload-1.17.1/scripts/fl-run-test000077500000000000000000000015601302537724200172100ustar00rootroot00000000000000#!/bin/env python # (C) Copyright 2005-2011 Nuxeo SA # Author: bdelbosc@nuxeo.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """Run a funkload unit test. $Id: fl-run-bench 24487 2005-08-24 16:13:39Z bdelbosc $ """ from funkload.TestRunner import main main() funkload-1.17.1/setup.py000066400000000000000000000072551302537724200151340ustar00rootroot00000000000000#! /usr/bin/env python # (C) Copyright 2005-2011 Nuxeo SAS # Author: bdelbosc@nuxeo.com # Contributors: Tom Lazar, Ross Patterson # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # # """FunkLoad package setup """ import ez_setup ez_setup.use_setuptools() from setuptools import setup, find_packages __version__ = '1.17.2' setup( name="funkload", version=__version__, description="Functional and load web tester.", long_description=''.join(open('README.txt').readlines()), author="Benoit Delbosc", author_email="bdelbosc@nuxeo.com", url="http://funkload.nuxeo.org/", download_url="http://pypi.python.org/packages/source/f/funkload/funkload-%s.tar.gz" % __version__, license='GPL', keywords='testing benching load performance functional monitoring', packages=find_packages('src'), package_dir={'': 'src'}, scripts=['scripts/fl-monitor-ctl', 'scripts/fl-credential-ctl', 'scripts/fl-run-bench', 'scripts/fl-run-test', 'scripts/fl-build-report', 'scripts/fl-install-demo', 'scripts/fl-record'], classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Console', 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', 'Natural Language :: English', 'License :: OSI Approved :: GNU General Public License (GPL)', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Topic :: Internet :: WWW/HTTP :: Site Management', 'Topic :: Software Development :: Testing', 'Topic :: Software Development :: Quality Assurance', 'Topic :: System :: Benchmark', 'Topic :: System :: Monitoring', ], # setuptools specific keywords install_requires = ['webunit >= 1.3.8', 'docutils >= 0.3.7', 'setuptools'], zip_safe=True, package_data={'funkload': ['data/*', 'demo/simple/*', 'demo/zope/*', 'demo/cmf/*', 'demo/xmlrpc/*', 'demo/cps/*', 'demo/seam-booking-1.1.5/*', 'demo/*.txt', 'tests/*', ]}, entry_points = { 'console_scripts': [ 'fl-monitor-ctl = funkload.Monitor:main', 'fl-credential-ctl = funkload.CredentialFile:main', 'fl-run-bench = funkload.BenchRunner:main', 'fl-run-test = funkload.TestRunner:main', 'fl-build-report = funkload.ReportBuilder:main', 'fl-install-demo = funkload.DemoInstaller:main', 'fl-record = funkload.Recorder:main'], 'funkload.plugins.monitor': [ 'CUs = funkload.MonitorPluginsDefault:MonitorCUs', 'MemFree = funkload.MonitorPluginsDefault:MonitorMemFree', 'CPU = funkload.MonitorPluginsDefault:MonitorCPU', 'Network = funkload.MonitorPluginsDefault:MonitorNetwork', ] }, # this test suite works only on an installed version :( # test_suite = "funkload.tests.test_Install.test_suite", ) funkload-1.17.1/src/000077500000000000000000000000001302537724200142005ustar00rootroot00000000000000funkload-1.17.1/src/funkload/000077500000000000000000000000001302537724200160035ustar00rootroot00000000000000funkload-1.17.1/src/funkload/BenchRunner.py000066400000000000000000001060231302537724200205700ustar00rootroot00000000000000#!/usr/bin/python # (C) Copyright 2005-2010 Nuxeo SAS # Author: bdelbosc@nuxeo.com # Contributors: Tom Lazar # Goutham Bhat # Andrew McFague # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """FunkLoad Bench runner. $Id: BenchRunner.py 24746 2005-08-31 09:59:27Z bdelbosc $ """ from __future__ import absolute_import import os import platform import sys import threading import time import traceback import unittest from datetime import datetime from optparse import OptionParser, TitledHelpFormatter from socket import error as SocketError from thread import error as ThreadError from xmlrpclib import ServerProxy, Fault import signal from .FunkLoadTestCase import FunkLoadTestCase from .FunkLoadHTTPServer import FunkLoadHTTPServer from .utils import mmn_encode, set_recording_flag, recording, thread_sleep, \ trace, red_str, green_str, get_version try: from funkload.rtfeedback import (FeedbackSender, DEFAULT_ENDPOINT, DEFAULT_PUBSUB) LIVE_FEEDBACK = True except ImportError: LIVE_FEEDBACK = False DEFAULT_PUBSUB = DEFAULT_ENDPOINT = None USAGE = """%prog [options] file class.method %prog launch a FunkLoad unit test as load test. A FunkLoad unittest uses a configuration file named [class].conf. This configuration may be overriden by the command line options. See http://funkload.nuxeo.org/ for more information. Examples ======== %prog myFile.py MyTestCase.testSomething %prog my_module MyTestCase.testSomething Bench MyTestCase.testSomething using MyTestCase.conf. %prog -u http://localhost:8080 -c 10:20 -D 30 myFile.py \\ MyTestCase.testSomething Bench MyTestCase.testSomething on localhost:8080 with 2 cycles of 10 and 20 users for a duration of 30s. %prog -h More options. Alternative Usage: %prog discover [options] Discover test modules in the current directory and bench all of them. """ try: import psyco psyco.full() except ImportError: pass # ------------------------------------------------------------ # utils # g_failures = 0 # result of the bench g_errors = 0 # result of the bench g_success = 0 def add_cycle_result(status): """Count number of result.""" # XXX use a thread.lock, but we don't mind if it is not accurate # as the report use the xml log global g_success, g_failures, g_errors if status == 'success': g_success += 1 elif status == 'error': g_errors += 1 else: g_failures += 1 return g_success, g_errors, g_failures def get_cycle_results(): """Return counters.""" global g_success, g_failures, g_errors return g_success, g_failures, g_errors def get_status(success, failures, errors, color=False): """Return a status and an exit code.""" if errors: status = 'ERROR' if color: status = red_str(status) code = -1 elif failures: status = 'FAILURE' if color: status = red_str(status) code = 1 else: status = 'SUCCESSFUL' if color: status = green_str(status) code = 0 return status, code def reset_cycle_results(): """Clear the previous results.""" global g_success, g_failures, g_errors g_success = g_failures = g_errors = 0 def load_module(test_module): module = __import__(test_module) parts = test_module.split('.')[1:] while parts: part = parts.pop() module = getattr(module, part) return module def load_unittest(test_module, test_class, test_name, options): """instantiate a unittest.""" module = load_module(test_module) klass = getattr(module, test_class) return klass(test_name, options) class ThreadSignaller: """ A simple class to signal whether a thread should continue running or stop. """ def __init__(self): self.keep_running = True def running(self): return self.keep_running def set_running(self, val): self.keep_running = val class ThreadData: """Container for thread related data.""" def __init__(self, thread, thread_id, thread_signaller): self.thread = thread self.thread_id = thread_id self.thread_signaller = thread_signaller # ------------------------------------------------------------ # Classes # class LoopTestRunner(threading.Thread): """Run a unit test in loop.""" def __init__(self, test_module, test_class, test_name, options, cycle, cvus, thread_id, thread_signaller, sleep_time, debug=False, feedback=None): meta_method_name = mmn_encode(test_name, cycle, cvus, thread_id) threading.Thread.__init__(self, target=self.run, name=meta_method_name, args=()) self.test = load_unittest(test_module, test_class, meta_method_name, options) if sys.platform.lower().startswith('win'): self.color = False else: self.color = not options.no_color self.sleep_time = sleep_time self.debug = debug self.thread_signaller = thread_signaller # this makes threads endings if main stop with a KeyboardInterupt self.setDaemon(1) self.feedback = feedback def run(self): """Run a test in loop.""" while (self.thread_signaller.running()): test_result = unittest.TestResult() self.test.clearContext() self.test(test_result) feedback = {} if test_result.wasSuccessful(): if recording(): feedback['count'] = add_cycle_result('success') if self.color: trace(green_str('.')) else: trace('.') feedback['result'] = 'success' else: if len(test_result.errors): if recording(): feedback['count'] = add_cycle_result('error') if self.color: trace(red_str('E')) else: trace('E') feedback['result'] = 'error' else: if recording(): feedback['count'] = add_cycle_result('failure') if self.color: trace(red_str('F')) else: trace('F') feedback['result'] = 'failure' if self.debug: feedback['errors'] = test_result.errors feedback['failures'] = test_result.failures for (test, error) in test_result.errors: trace("ERROR %s: %s" % (str(test), str(error))) for (test, error) in test_result.failures: trace("FAILURE %s: %s" % (str(test), str(error))) if self.feedback is not None: self.feedback.test_done(feedback) thread_sleep(self.sleep_time) class BenchRunner: """Run a unit test in bench mode.""" def __init__(self, module_name, class_name, method_name, options): self.module_name = module_name self.class_name = class_name self.method_name = method_name self.options = options self.color = not options.no_color # create a unittest to get the configuration file test = load_unittest(self.module_name, class_name, mmn_encode(method_name, 0, 0, 0), options) self.config_path = test._config_path self.result_path = test.result_path self.class_title = test.conf_get('main', 'title') self.class_description = test.conf_get('main', 'description') self.test_id = self.method_name self.test_description = test.conf_get(self.method_name, 'description', 'No test description') self.test_url = test.conf_get('main', 'url') self.cycles = map(int, test.conf_getList('bench', 'cycles')) self.duration = test.conf_getInt('bench', 'duration') self.startup_delay = test.conf_getFloat('bench', 'startup_delay') self.cycle_time = test.conf_getFloat('bench', 'cycle_time') self.sleep_time = test.conf_getFloat('bench', 'sleep_time') self.sleep_time_min = test.conf_getFloat('bench', 'sleep_time_min') self.sleep_time_max = test.conf_getFloat('bench', 'sleep_time_max') self.threads = [] # Contains list of ThreadData objects self.last_thread_id = -1 self.thread_creation_lock = threading.Lock() # setup monitoring monitor_hosts = [] # list of (host, port, descr) if not options.is_distributed: hosts = test.conf_get('monitor', 'hosts', '', quiet=True).split() for host in hosts: name = host host = test.conf_get(host,'host',host.strip()) monitor_hosts.append((name, host, test.conf_getInt(name, 'port'), test.conf_get(name, 'description', ''))) self.monitor_hosts = monitor_hosts # keep the test to use the result logger for monitoring # and call setUp/tearDown Cycle self.test = test # set up the feedback sender if LIVE_FEEDBACK and options.is_distributed and options.feedback: trace("* Creating Feedback sender") self.feedback = FeedbackSender(endpoint=options.feedback_endpoint or DEFAULT_ENDPOINT) else: self.feedback = None def run(self): """Run all the cycles. return 0 on success, 1 if there were some failures and -1 on errors.""" trace(str(self)) trace("Benching\n") trace("========\n\n") cycle = total_success = total_failures = total_errors = 0 self.logr_open() trace("* setUpBench hook: ...") self.test.setUpBench() trace(' done.\n') self.getMonitorsConfig() trace('\n') for cvus in self.cycles: t_start = time.time() reset_cycle_results() text = "Cycle #%i with %s virtual users\n" % (cycle, cvus) trace(text) trace('-' * (len(text) - 1) + "\n\n") monitor_key = '%s:%s:%s' % (self.method_name, cycle, cvus) trace("* setUpCycle hook: ...") self.test.setUpCycle() trace(' done.\n') self.startMonitors(monitor_key) self.startThreads(cycle, cvus) self.logging(cycle, cvus) #self.dumpThreads() self.stopThreads() self.stopMonitors(monitor_key) cycle += 1 trace("* tearDownCycle hook: ...") self.test.tearDownCycle() trace(' done.\n') t_stop = time.time() trace("* End of cycle, %.2fs elapsed.\n" % (t_stop - t_start)) success, failures, errors = get_cycle_results() status, code = get_status(success, failures, errors, self.color) trace("* Cycle result: **%s**, " "%i success, %i failure, %i errors.\n\n" % ( status, success, failures, errors)) total_success += success total_failures += failures total_errors += errors trace("* tearDownBench hook: ...") self.test.tearDownBench() trace(' done.\n\n') self.logr_close() # display bench result trace("Result\n") trace("======\n\n") trace("* Success: %s\n" % total_success) trace("* Failures: %s\n" % total_failures) trace("* Errors: %s\n\n" % total_errors) status, code = get_status(total_success, total_failures, total_errors) trace("Bench status: **%s**\n" % status) return code def createThreadId(self): self.last_thread_id += 1 return self.last_thread_id def startThreads(self, cycle, number_of_threads): """Starts threads.""" self.thread_creation_lock.acquire() try: trace("* Current time: %s\n" % datetime.now().isoformat()) trace("* Starting threads: ") set_recording_flag(False) threads = self.createThreads(cycle, number_of_threads) self.threads.extend(threads) finally: set_recording_flag(True) self.thread_creation_lock.release() def addThreads(self, number_of_threads): """Adds new threads to existing list. Used to dynamically add new threads during a debug bench run.""" self.thread_creation_lock.acquire() try: trace("Adding new threads: ") set_recording_flag(False) # In debug bench, 'cycle' value is irrelevant. threads = self.createThreads(0, number_of_threads) self.threads.extend(threads) finally: set_recording_flag(True) self.thread_creation_lock.release() def createThreads(self, cycle, number_of_threads): """Creates number_of_threads threads and returns as a list. NOTE: This method is not thread safe. Thread safety must be handled by the caller.""" threads = [] i = 0 for i in range(number_of_threads): thread_id = self.createThreadId() thread_signaller = ThreadSignaller() thread = LoopTestRunner(self.module_name, self.class_name, self.method_name, self.options, cycle, number_of_threads, thread_id, thread_signaller, self.sleep_time, feedback=self.feedback) trace(".") try: thread.start() except ThreadError: trace("\nERROR: Can not create more than %i threads, try a " "smaller stack size using: 'ulimit -s 2048' " "for example\n" % (i + 1)) raise thread_data = ThreadData(thread, thread_id, thread_signaller) threads.append(thread_data) thread_sleep(self.startup_delay) trace(' done.\n') return threads def logging(self, cycle, cvus): """Log activity during duration.""" duration = self.duration end_time = time.time() + duration mid_time = time.time() + duration / 2 trace("* Logging for %ds (until %s): " % ( duration, datetime.fromtimestamp(end_time).isoformat())) set_recording_flag(True) while time.time() < mid_time: time.sleep(1) self.test.midCycle(cycle, cvus) while time.time() < end_time: # wait time.sleep(1) set_recording_flag(False) trace(" done.\n") def stopThreads(self): """Stops all running threads.""" self.thread_creation_lock.acquire() try: trace("* Waiting end of threads: ") self.deleteThreads(len(self.threads)) self.threads = [] trace(" done.\n") trace("* Waiting cycle sleeptime %ds: ..." % self.cycle_time) time.sleep(self.cycle_time) trace(" done.\n") self.last_thread_id = -1 finally: self.thread_creation_lock.release() def removeThreads(self, number_of_threads): """Removes threads. Used to dynamically remove threads during a debug bench run.""" self.thread_creation_lock.acquire() try: trace('* Removing threads: ') self.deleteThreads(number_of_threads) trace(' done.\n') finally: self.thread_creation_lock.release() def deleteThreads(self, number_of_threads): """Stops given number of threads and deletes from thread list. NOTE: This method is not thread safe. Thread safety must be handled by the caller.""" removed_threads = [] if number_of_threads > len(self.threads): number_of_threads = len(self.threads) for i in range(number_of_threads): thread_data = self.threads.pop() thread_data.thread_signaller.set_running(False) removed_threads.append(thread_data) for thread_data in removed_threads: thread_data.thread.join() del thread_data trace('.') def getNumberOfThreads(self): return len(self.threads) def dumpThreads(self): """Display all different traceback of Threads for debugging. Require threadframe module.""" import threadframe stacks = {} frames = threadframe.dict() for thread_id, frame in frames.iteritems(): stack = ''.join(traceback.format_stack(frame)) stacks[stack] = stacks.setdefault(stack, []) + [thread_id] def sort_stack(x, y): """sort stack by number of thread.""" return cmp(len(x[1]), len(y[1])) stacks = stacks.items() stacks.sort(sort_stack) for stack, thread_ids in stacks: trace('=' * 72 + '\n') trace('%i threads : %s\n' % (len(thread_ids), str(thread_ids))) trace('-' * 72 + '\n') trace(stack + '\n') def getMonitorsConfig(self): """ Get monitors configuration from hosts """ if not self.monitor_hosts: return monitor_hosts = [] for (name, host, port, desc) in self.monitor_hosts: trace("* Getting monitoring config from %s: ..." % name) server = ServerProxy("http://%s:%s" % (host, port)) try: config = server.getMonitorsConfig() data = [] for key in config.keys(): xml = '' % ( name, key, config[key]) data.append(xml) self.logr("\n".join(data)) except Fault: trace(' not supported.\n') monitor_hosts.append((name, host, port, desc)) except SocketError: trace(' failed, server is down.\n') else: trace(' done.\n') monitor_hosts.append((name, host, port, desc)) self.monitor_hosts = monitor_hosts def startMonitors(self, monitor_key): """Start monitoring on hosts list.""" if not self.monitor_hosts: return monitor_hosts = [] for (name, host, port, desc) in self.monitor_hosts: trace("* Start monitoring %s: ..." % name) server = ServerProxy("http://%s:%s" % (host, port)) try: server.startRecord(monitor_key) except SocketError: trace(' failed, server is down.\n') else: trace(' done.\n') monitor_hosts.append((name, host, port, desc)) self.monitor_hosts = monitor_hosts def stopMonitors(self, monitor_key): """Stop monitoring and save xml result.""" if not self.monitor_hosts: return for (name, host, port, desc) in self.monitor_hosts: trace('* Stop monitoring %s: ' % name) server = ServerProxy("http://%s:%s" % (host, port)) try: server.stopRecord(monitor_key) xml = server.getXmlResult(monitor_key) except SocketError: trace(' failed, server is down.\n') else: trace(' done.\n') self.logr(xml) def logr(self, message): """Log to the test result file.""" self.test._logr(message, force=True) def logr_open(self): """Start logging tag.""" config = {'id': self.test_id, 'description': self.test_description, 'class_title': self.class_title, 'class_description': self.class_description, 'module': self.module_name, 'class': self.class_name, 'method': self.method_name, 'cycles': self.cycles, 'duration': self.duration, 'sleep_time': self.sleep_time, 'startup_delay': self.startup_delay, 'sleep_time_min': self.sleep_time_min, 'sleep_time_max': self.sleep_time_max, 'cycle_time': self.cycle_time, 'configuration_file': self.config_path, 'server_url': self.test_url, 'log_xml': self.result_path, 'node': platform.node(), 'python_version': platform.python_version()} if self.options.label: config['label'] = self.options.label for (name, host, port, desc) in self.monitor_hosts: config[name] = desc self.test._open_result_log(**config) def logr_close(self): """Stop logging tag.""" self.test._close_result_log() self.test.logger_result.handlers = [] def __repr__(self): """Display bench information.""" text = [] text.append('=' * 72) text.append('Benching %s.%s' % (self.class_name, self.method_name)) text.append('=' * 72) text.append(self.test_description) text.append('-' * 72 + '\n') text.append("Configuration") text.append("=============\n") text.append("* Current time: %s" % datetime.now().isoformat()) text.append("* Configuration file: %s" % self.config_path) text.append("* Log xml: %s" % self.result_path) text.append("* Server: %s" % self.test_url) text.append("* Cycles: %s" % self.cycles) text.append("* Cycle duration: %ss" % self.duration) text.append("* Sleeptime between request: from %ss to %ss" % ( self.sleep_time_min, self.sleep_time_max)) text.append("* Sleeptime between test case: %ss" % self.sleep_time) text.append("* Startup delay between thread: %ss\n\n" % self.startup_delay) return '\n'.join(text) class BenchLoader(unittest.TestLoader): suiteClass = list def loadTestsFromTestCase(self, testCaseClass): if not issubclass(testCaseClass, FunkLoadTestCase): trace(red_str("Skipping "+ testCaseClass)) return [] testCaseNames = self.getTestCaseNames(testCaseClass) if not testCaseNames and hasattr(testCaseClass, 'runTest'): testCaseNames = ['runTest'] return [dict(module_name = testCaseClass.__module__, class_name = testCaseClass.__name__, method_name = x) for x in testCaseNames] def discover(sys_args): parser = get_shared_OptionParser() options, args = parser.parse_args(sys_args) options.label = None loader = BenchLoader() suite = loader.discover('.') def flatten_test_suite(suite): if type(suite) != BenchLoader.suiteClass: # Wasn't a TestSuite - must have been a Test return [suite] flat = [] for x in suite: flat += flatten_test_suite(x) return flat flattened = flatten_test_suite(suite) retval = 0 for test in flattened: module_name = test['module_name'] class_name = test['class_name'] method_name = test['method_name'] if options.distribute: dist_args = sys_args[:] dist_args.append(module_name) dist_args.append('%s.%s' % (class_name, method_name)) ret = run_distributed(options, module_name, class_name, method_name, dist_args) else: ret = run_local(options, module_name, class_name, method_name) # Handle failures if ret != 0: retval = ret if options.failfast: break return retval _manager = None def shutdown(*args): trace('Aborting run...') if _manager is not None: _manager.abort() trace('Aborted') sys.exit(0) def get_runner_class(class_path): try: module_path, class_name = class_path.rsplit('.', 1) except ValueError: raise Exception('Invalid class path {0}'.format(class_path)) _module = __import__(module_path, globals(), locals(), class_name, -1) return getattr(_module, class_name) def parse_sys_args(sys_args): parser = get_shared_OptionParser() parser.add_option("", "--config", type="string", dest="config", metavar='CONFIG', help="Path to alternative config file") parser.add_option("-l", "--label", type="string", help="Add a label to this bench run for easier " "identification (it will be appended to the " "directory name for reports generated from it).") options, args = parser.parse_args(sys_args) if len(args) != 2: parser.error("incorrect number of arguments") if not args[1].count('.'): parser.error("invalid argument; should be [class].[method]") if options.as_fast_as_possible: options.bench_sleep_time_min = '0' options.bench_sleep_time_max = '0' options.bench_sleep_time = '0' if os.path.exists(args[0]): # We were passed a file for the first argument module_name = os.path.basename(os.path.splitext(args[0])[0]) else: # We were passed a module name module_name = args[0] return options, args, module_name def get_shared_OptionParser(): '''Make an OptionParser that can be used in both normal mode and in discover mode. ''' parser = OptionParser(USAGE, formatter=TitledHelpFormatter(), version="FunkLoad %s" % get_version()) parser.add_option("-r", "--runner-class", type="string", dest="bench_runner_class", default="funkload.BenchRunner.BenchRunner", help="Python dotted import path to BenchRunner class to use.") parser.add_option("", "--no-color", action="store_true", help="Monochrome output.") parser.add_option("", "--accept-invalid-links", action="store_true", help="Do not fail if css/image links are not reachable.") parser.add_option("", "--simple-fetch", action="store_true", dest="bench_simple_fetch", help="Don't load additional links like css or images " "when fetching an html page.") parser.add_option("--enable-debug-server", action="store_true", dest="debugserver", help="Instantiates a debug HTTP server which exposes an " "interface using which parameters can be modified " "at run-time. Currently supported parameters: " "/cvu?inc= to increase the number of " "CVUs, /cvu?dec= to decrease the number " "of CVUs, /getcvu returns number of CVUs ") parser.add_option("--debug-server-port", type="string", dest="debugport", help="Port at which debug server should run during the " "test") parser.add_option("--distribute", action="store_true", dest="distribute", help="Distributes the CVUs over a group of worker " "machines that are defined in the workers section") parser.add_option("--distribute-workers", type="string", dest="workerlist", help="This parameter will override the list of " "workers defined in the config file. expected " "notation is uname@host,uname:pwd@host or just " "host...") parser.add_option("--distribute-python", type="string", dest="python_bin", help="When running in distributed mode, this Python " "binary will be used across all hosts.") parser.add_option("--is-distributed", action="store_true", dest="is_distributed", help="This parameter is for internal use only. It " "signals to a worker node that it is in " "distributed mode and shouldn't perform certain " "actions.") parser.add_option("--distributed-packages", type="string", dest="distributed_packages", help="Additional packages to be passed to easy_install " "on remote machines when being run in distributed " "mode.") parser.add_option("--distributed-log-path", type="string", dest="distributed_log_path", help="Path where all the logs will be stored when " "running a distributed test") parser.add_option("--distributed-key-filename", type="string", dest="distributed_key_filename", help=("Path of the SSH key to use when running a " "distributed test")) parser.add_option("--feedback-endpoint", type="string", dest="feedback_endpoint", help=("ZMQ push/pull socket used between the master and " "the node to send feedback.")) parser.add_option("--feedback-pubsub-endpoint", type="string", dest="feedback_pubsub_endpoint", help="ZMQ pub/sub socket use to publish feedback.") parser.add_option("--feedback", action="store_true", dest="feedback", help="Activates the realtime feedback") parser.add_option("--failfast", action="store_true", dest="failfast", help="Stop on first fail or error. (For discover mode)") parser.add_option("-u", "--url", type="string", dest="main_url", help="Base URL to bench.") parser.add_option("-c", "--cycles", type="string", dest="bench_cycles", help="Cycles to bench, colon-separated list of " "virtual concurrent users. To run a bench with 3 " "cycles of 5, 10 and 20 users, use: -c 5:10:20") parser.add_option("-D", "--duration", type="string", dest="bench_duration", help="Duration of a cycle in seconds.") parser.add_option("-m", "--sleep-time-min", type="string", dest="bench_sleep_time_min", help="Minimum sleep time between requests.") parser.add_option("-M", "--sleep-time-max", type="string", dest="bench_sleep_time_max", help="Maximum sleep time between requests.") parser.add_option("-t", "--test-sleep-time", type="string", dest="bench_sleep_time", help="Sleep time between tests.") parser.add_option("-s", "--startup-delay", type="string", dest="bench_startup_delay", help="Startup delay between thread.") parser.add_option("-f", "--as-fast-as-possible", action="store_true", help="Remove sleep times between requests and between " "tests, shortcut for -m0 -M0 -t0") return parser def run_distributed(options, module_name, class_name, method_name, sys_args): ret = None from funkload.Distributed import DistributionMgr global _manager try: distmgr = DistributionMgr( module_name, class_name, method_name, options, sys_args) _manager = distmgr except UserWarning as error: trace(red_str("Distribution failed with:%s \n" % (error))) return 1 try: try: distmgr.prepare_workers(allow_errors=True) ret = distmgr.run() distmgr.final_collect() except KeyboardInterrupt: trace("* ^C received *") finally: # in any case we want to stop the workers at the end distmgr.abort() _manager = None return ret def run_local(options, module_name, class_name, method_name): ret = None RunnerClass = get_runner_class(options.bench_runner_class) bench = RunnerClass(module_name, class_name, method_name, options) # Start a HTTP server optionally if options.debugserver: http_server_thread = FunkLoadHTTPServer(bench, options.debugport) http_server_thread.start() try: ret = bench.run() except KeyboardInterrupt: trace("* ^C received *") return ret def main(sys_args=sys.argv[1:]): """Default main.""" # enable loading of modules in the current path cur_path = os.path.abspath(os.path.curdir) sys.path.insert(0, cur_path) # registering signals if not sys.platform.lower().startswith('win'): signal.signal(signal.SIGTERM, shutdown) signal.signal(signal.SIGINT, shutdown) signal.signal(signal.SIGQUIT, shutdown) # special case: 'discover' argument if sys_args and sys_args[0].lower() == 'discover': return discover(sys_args) options, args, module_name = parse_sys_args(sys_args) klass, method = args[1].split('.') if options.distribute: return run_distributed(options, module_name, klass, method, sys_args) else: return run_local(options, module_name, klass, method) if __name__ == '__main__': ret = main() sys.exit(ret) funkload-1.17.1/src/funkload/CredentialBase.py000066400000000000000000000024061302537724200212240ustar00rootroot00000000000000# (C) Copyright 2005 Nuxeo SAS # Author: bdelbosc@nuxeo.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """Interface of a Credential Server. $Id$ """ class CredentialBaseServer: """Interface of a Credential server.""" def getCredential(self, group=None): """Return a credential (login, password). If group is not None return a credential that belong to the group. """ def listCredentials(self, group=None): """Return a list of all credentials. If group is not None return a list of credentials that belong to the group. """ def listGroups(self): """Return a list of all groups.""" funkload-1.17.1/src/funkload/CredentialFile.py000066400000000000000000000153301302537724200212310ustar00rootroot00000000000000# (C) Copyright 2005 Nuxeo SAS # Author: bdelbosc@nuxeo.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """A file credential server/controller. $Id$ """ from __future__ import absolute_import import sys from ConfigParser import NoOptionError from .XmlRpcBase import XmlRpcBaseServer, XmlRpcBaseController from .CredentialBase import CredentialBaseServer # ------------------------------------------------------------ # classes # class Group: """A class to handle groups.""" def __init__(self, name): self.name = name self.index = 0 self.count = 0 self.users = [] def add(self, user): """Add a user to the group.""" if not self.users.count(user): self.users.append(user) def __len__(self): """Return the lenght of group.""" return len(self.users) def next(self): """Return the next user or the group. loop from begining.""" nb_users = len(self.users) if nb_users == 0: raise ValueError('No users for group %s' % self.name) self.index = self.count % nb_users user = self.users[self.index] self.count += 1 return user def __repr__(self): """Representation.""" return '' % ( self.name, self.count, self.index, len(self)) # ------------------------------------------------------------ # Server # class CredentialFileServer(XmlRpcBaseServer, CredentialBaseServer): """A file credential server.""" server_name = "file_credential" method_names = XmlRpcBaseServer.method_names + [ 'getCredential', 'listCredentials', 'listGroups', 'getSeq'] credential_sep = ':' # login:password users_sep = ',' # group_name:user1, user2 def __init__(self, argv=None): self.lofc = 0 self._groups = {} self._passwords = {} self.seq = 0 XmlRpcBaseServer.__init__(self, argv) def _init_cb(self, conf, options): """init procedure to override in sub classes.""" credentials_path = conf.get('server', 'credentials_path') self.lofc = conf.getint('server', 'loop_on_first_credentials') try: self.seq = conf.getint('server', 'seq') except NoOptionError: self.seq = 0 self._loadPasswords(credentials_path) try: groups_path = conf.get('server', 'groups_path') self._loadGroups(groups_path) except NoOptionError: pass def _loadPasswords(self, file_path): """Load a password file.""" self.logd("CredentialFile use credential file %s." % file_path) lines = open(file_path).readlines() self._groups = {} group = Group('default') self._groups[None] = group for line in lines: line = line.strip() if not line or line.startswith('#'): continue user, password = [x.strip() for x in line.split( self.credential_sep, 1)] self._passwords[user] = password if not self.lofc or len(group) < self.lofc: group.add(user) def _loadGroups(self, file_path): """Load a group file.""" self.logd("CredentialFile use group file %s." % file_path) lines = open(file_path).readlines() for line in lines: line = line.strip() if not line or line.startswith('#'): continue name, users = [x.strip() for x in line.split( self.credential_sep, 1)] users = filter( None, [user.strip() for user in users.split(self.users_sep)]) group = self._groups.setdefault(name, Group(name)) for user in users: if self.lofc and len(group) >= self.lofc: break if user in self._passwords: group.add(user) else: self.logd('Missing password for %s in group %s' % (user, name)) # # RPC def getCredential(self, group=None): """Return a credential from group if specified. Credential are taken incrementally in a loop. """ user = next(self._groups[group]) password = self._passwords[user] self.logd("getCredential(%s) return (%s, %s)" % ( group, user, password)) return (user, password) def listCredentials(self, group=None): """Return a list of credentials.""" if group is None: ret = list(self._passwords) else: users = self._groups[group].users ret = [(user, self._passwords[user]) for user in users] self.logd("listUsers(%s) return (%s)" % (group, ret)) return ret def listGroups(self): """Return a list of groups.""" ret = filter(None, self._groups.keys()) self.logd("listGroup() return (%s)" % str(ret)) return ret def getSeq(self): """Return a sequence.""" self.seq += 1 return self.seq # ------------------------------------------------------------ # Controller # class CredentialFileController(XmlRpcBaseController): """A file credential controller.""" server_class = CredentialFileServer def test(self): """Testing credential server.""" server = self.server self.log(server.listGroups()) for i in range(10): self.log("%s getCredential() ... " % i) user, password = server.getCredential() self.log(" return (%s, %s)\n" % (user, password)) for group in server.listGroups(): self.log("group %s\n" % group) self.log(" content: %s\n" % server.listCredentials(group)) for i in range(5): self.log("seq : %d" % server.getSeq()) return 0 # ------------------------------------------------------------ # main # def main(): """Control credentiald server.""" ctl = CredentialFileController() sys.exit(ctl()) if __name__ == '__main__': main() funkload-1.17.1/src/funkload/CredentialRandom.py000066400000000000000000000070771302537724200216030ustar00rootroot00000000000000# (C) Copyright 2005 Nuxeo SAS # Author: bdelbosc@nuxeo.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """A random credential server/controller. $Id$ """ from __future__ import absolute_import import sys from .Lipsum import Lipsum from .XmlRpcBase import XmlRpcBaseServer, XmlRpcBaseController from .CredentialBase import CredentialBaseServer # ------------------------------------------------------------ # Server # class CredentialRandomServer(XmlRpcBaseServer, CredentialBaseServer): """A random credential server.""" server_name = "random_credential" method_names = XmlRpcBaseServer.method_names + [ 'getCredential', 'listCredentials', 'listGroups'] def __init__(self, argv=None): XmlRpcBaseServer.__init__(self, argv) self.lipsum = Lipsum() def getCredential(self, group=None): """Return a random (login, password). return a random user login, the login is taken from the lipsum vocabulary so the number of login is limited to the length of the vocabulary. The group asked will prefix the login name. The password is just the reverse of the login, this give a coherent behaviour if it return twice the same credential. """ self.logd('getCredential(%s) request.' % group) # use group as login prefix user = (group or 'user') + '_' + self.lipsum.getWord() # pwd is the reverse of the login tmp = list(user) tmp.reverse() password = ''.join(tmp) self.logd(" return (%s, %s)" % (user, password)) return (user, password) def listCredentials(self, group=None): """Return a list of 10 random credentials.""" self.logd('listCredentials request.') return [self.getCredential(group) for x in range(10)] def listGroups(self): """Retrun a list of 10 random group name.""" self.logd('listGroups request.') lipsum = self.lipsum return ['grp' + lipsum.getUniqWord(length_min=2, length_max=3) for x in range(10)] # ------------------------------------------------------------ # Controller # class CredentialRandomController(XmlRpcBaseController): """A random credential controller.""" server_class = CredentialRandomServer def test(self): """Testing credential server.""" server = self.server self.log(server.listGroups()) for i in range(10): self.log("%s getCredential() ... " % i) user, password = server.getCredential() self.log(" return (%s, %s)\n" % (user, password)) for group in server.listGroups(): self.log("group %s\n" % group) self.log(" content: %s\n" % server.listCredentials(group)) return 0 # ------------------------------------------------------------ # main # def main(): """Control credentiald server.""" ctl = CredentialRandomController() sys.exit(ctl()) if __name__ == '__main__': main() funkload-1.17.1/src/funkload/DemoInstaller.py000066400000000000000000000011211302537724200211120ustar00rootroot00000000000000"""Extract the demo from the funkload egg into the current path.""" from __future__ import print_function import os from shutil import copytree from pkg_resources import resource_filename, cleanup_resources def main(): """main.""" demo_path = 'funkload-demo' print("Extract FunkLoad examples into ./%s : ... " % demo_path, end=' ') cache_path = resource_filename('funkload', 'demo') demo_path = os.path.join(os.path.abspath(os.path.curdir), demo_path) copytree(cache_path, demo_path) cleanup_resources() print("done.") if __name__ == '__main__': main() funkload-1.17.1/src/funkload/Distributed.py000066400000000000000000000716631302537724200206540ustar00rootroot00000000000000#!/usr/bin/python # Author: Ali-Akber Saifee # Contributors: Andrew McFague # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # from __future__ import print_function from __future__ import absolute_import import os import platform import re import socket import threading import time from datetime import datetime from socket import error as SocketError from stat import S_ISREG, S_ISDIR from glob import glob from xml.etree.ElementTree import ElementTree from xmlrpclib import ServerProxy import json import sys import paramiko from .utils import mmn_encode, trace, package_tests, get_virtualenv_script, \ get_version try: from funkload.rtfeedback import (FeedbackPublisher, DEFAULT_ENDPOINT, DEFAULT_PUBSUB) LIVE_FEEDBACK = True except ImportError: LIVE_FEEDBACK = False DEFAULT_PUBSUB = DEFAULT_ENDPOINT = None def load_module(test_module): module = __import__(test_module) parts = test_module.split('.')[1:] while parts: part = parts.pop() module = getattr(module, part) return module def load_unittest(test_module, test_class, test_name, options): """instantiate a unittest.""" module = load_module(test_module) klass = getattr(module, test_class) return klass(test_name, options) def _print_rt(msg): msg = json.loads(msg[0]) if msg['result'] == 'failure': sys.stdout.write('F') else: sys.stdout.write('.') sys.stdout.flush() class DistributorBase(object): """ base class for any XXXDistributor objects that can be used to distribute benches accross multiple machines. """ def __init__(self, host, username, password): self.host = host self.username = username self.password = password self.connected = False def requiresconnection(fn): """ decorator for :class:`~SSHDistributor` object that raises a runtime exception upon calling methods if the object hasn't been connected properly. """ def _requiresconnect(self, *args, **kwargs): if not self.connected: raise RuntimeError( "%s requires an ssh connection to be created" % fn.__name__) return fn(self, *args, **kwargs) _requiresconnect.__name__ = fn.__name__ _requiresconnect.__doc__ = fn.__doc__ return _requiresconnect class SSHDistributor(DistributorBase): """ Provides commands to perform distirbuted actions using an ssh connection (depends on paramiko). Essentially used by :class:`~DistributionMgr`. """ def __init__(self, name, host, username=None, password=None, key_filename=None, channel_timeout=None): """ performs authentication and tries to connect to the `host`. """ DistributorBase.__init__(self, host, username, password) self.connection = paramiko.client.SSHClient() self.connection.load_system_host_keys() self.connection.set_missing_host_key_policy(paramiko.WarningPolicy()) self.error = "" self.name = name # So we can have multiples tests per host self.channel_timeout = channel_timeout credentials = {} if username and password: credentials = {"username": username, "password": password} elif username and key_filename: credentials = {"username": username, "key_filename": key_filename} elif username: credentials = {"username": username} host_port = host.split(':') if len(host_port) > 1: host = host_port[0] port = int(host_port[1]) else: port = 22 try: # print "connect to " + host + " port " + str(port) + " " + str(credentials) self.connection.connect(host, timeout=5, port=port, **credentials) self.connected = True except socket.gaierror as error: self.error = error except socket.timeout as error: self.error = error self.killed = False @requiresconnection def get(self, remote_path, local_path): """ performs a copy from ``remote_path`` to ``local_path``. For performing the inverse operation, use the :meth:`put` """ try: sftp = self.connection.open_sftp() sftp.get(remote_path, local_path) except Exception as error: trace("failed to get %s->%s with error %s\n" % (local_path, remote_path, error)) @requiresconnection def put(self, local_path, remote_path): """ performs a copy from `local_path` to `remote_path` For performing the inverse operation, use the :meth:`get` """ try: sftp = self.connection.open_sftp() sftp.put(local_path, remote_path) except Exception as error: trace("failed to put %s->%s with error %s\n" % (local_path, remote_path, error)) @requiresconnection def execute(self, cmd_string, shell_interpreter="bash -c", cwdir=None): """ evaluated the command specified by ``cmd_string`` in the context of ``cwdir`` if it is specified. The optional ``shell_interpreter`` parameter allows overloading the default bash. """ obj = self.threaded_execute(cmd_string, shell_interpreter, cwdir) obj.join() out = "" err = "" while True: if self.killed: break e = obj.err.read(1) err += e #trace(e) o = obj.output.read(1) out += o #trace(o) if not o and not e: break return out, err @requiresconnection def threaded_execute(self, cmd_string, shell_interpreter="bash -c", cwdir=None): """ basically the same as :meth:`execute` execept that it returns a started :mod:`threading.Thread` object instead of the output. """ class ThreadedExec(threading.Thread): "simple Thread wrapper on :meth:`execute`" # FIXME Remove the dependency on self.connection def __init__(self_, cmd_string, shell_interpreter, cwdir): threading.Thread.__init__(self_) self_.cmd_string = cmd_string self_.shell_interpreter = shell_interpreter self_.cwdir = cwdir def run(self_): exec_str = "" if self_.cwdir: exec_str += "pushd .; cd %s;" % cwdir exec_str += "%s \"%s\"" % ( self_.shell_interpreter, self_.cmd_string) if self_.cwdir: exec_str += "; popd;" #trace("DEBUG: %s\n" %exec_str) try: self_.input, self_.output, self_.err = \ self_.exec_command(self.connection, exec_str, bufsize=1, timeout=self.channel_timeout) except Exception as e: if not self.killed: raise def exec_command(self, connection, command, bufsize=-1, timeout=None): # Override to set timeout properly see # http://mohangk.org/blog/2011/07/paramiko-sshclient-exec_command-timeout-workaround/ chan = connection._transport.open_session() chan.settimeout(timeout) print(command) chan.exec_command(command) stdin = chan.makefile('wb', bufsize) stdout = chan.makefile('rb', bufsize) stderr = chan.makefile_stderr('rb', bufsize) return stdin, stdout, stderr th_obj = ThreadedExec(cmd_string, shell_interpreter, cwdir) th_obj.start() return th_obj @requiresconnection def isdir(self, remote_path): """ test to see if the path pointing to ``remote_dir`` exists as a directory. """ try: sftp = self.connection.open_sftp() st = sftp.stat(remote_path) return S_ISDIR(st.st_mode) except Exception: return False @requiresconnection def isfile(self, remote_path): """ test to see if the path pointing to ``remote_path`` exists as a file. """ try: sftp = self.connection.open_sftp() st = sftp.stat(remote_path) return S_ISREG(st.st_mode) except Exception: return False def die(self): """ kills the ssh connection """ self.connection.close() self.killed = True class DistributionMgr(threading.Thread): """ Interface for use by :mod:`funkload.TestRunner` to distribute the bench over multiple machines. """ def __init__(self, module_name, class_name, method_name, options, cmd_args): """ mirrors the initialization of :class:`funkload.BenchRunner.BenchRunner` """ # store the args. these can be passed to BenchRunner later. self.module_name = module_name self.class_name = class_name self.method_name = method_name self.options = options self.cmd_args = cmd_args wanted = lambda x: ('--distribute' not in x) and ('discover' != x) self.cmd_args = filter(wanted, self.cmd_args) self.cmd_args.append("--is-distributed") # ? Won't this double the --feedback option? if options.feedback: self.cmd_args.append("--feedback") module = load_module(module_name) module_file = module.__file__ self.tarred_tests, self.tarred_testsdir = package_tests(module_file) self.remote_res_dir = "/tmp/funkload-bench-sandbox/" test = load_unittest(self.module_name, class_name, mmn_encode(method_name, 0, 0, 0), options) self.config_path = test._config_path self.result_path = test.result_path self.class_title = test.conf_get('main', 'title') self.class_description = test.conf_get('main', 'description') self.test_id = self.method_name self.test_url = test.conf_get('main', 'url') self.cycles = map(int, test.conf_getList('bench', 'cycles')) self.duration = test.conf_getInt('bench', 'duration') self.startup_delay = test.conf_getFloat('bench', 'startup_delay') self.cycle_time = test.conf_getFloat('bench', 'cycle_time') self.sleep_time = test.conf_getFloat('bench', 'sleep_time') self.sleep_time_min = test.conf_getFloat('bench', 'sleep_time_min') self.sleep_time_max = test.conf_getFloat('bench', 'sleep_time_max') if test.conf_get('distribute', 'channel_timeout', '', quiet=True): self.channel_timeout = test.conf_getFloat( 'distribute', 'channel_timeout') else: self.channel_timeout = None self.threads = [] # Contains list of ThreadData objects self.last_thread_id = -1 self.thread_creation_lock = threading.Lock() if options.python_bin: self.python_bin = options.python_bin else: self.python_bin = test.conf_get( 'distribute', 'python_bin', 'python') if options.distributed_packages: self.distributed_packages = options.distributed_packages else: self.distributed_packages = test.conf_get( 'distribute', 'packages', '') try: desc = getattr(test, self.method_name).__doc__.strip() except: desc = "" self.test_description = test.conf_get(self.method_name, 'description', desc) # make a collection output location if options.distributed_log_path: self.distribution_output = options.distributed_log_path elif test.conf_get('distribute', 'log_path', '', quiet=True): self.distribution_output = test.conf_get('distribute', 'log_path') else: raise UserWarning("log_path isn't defined in section [distribute]") # check if user has overridden the default funkload distro download # location this will be used to download funkload on the worker nodes. self.funkload_location = test.conf_get( 'distribute', 'funkload_location', 'funkload') if not os.path.isdir(self.distribution_output): os.makedirs(self.distribution_output) # check if hosts are in options workers = [] # list of (host, port, descr) if options.workerlist: for h in options.workerlist.split(","): cred_host = h.split("@") if len(cred_host) == 1: uname, pwd, host = None, None, cred_host[0] else: cred = cred_host[0] host = cred_host[1] uname_pwd = cred.split(":") if len(uname_pwd) == 1: uname, pwd = uname_pwd[0], None else: uname, pwd = uname_pwd worker = {"name": host.replace(":", "_"), "host": host, "password": pwd, "username": uname, "channel_timeout": self.channel_timeout} if options.distributed_key_filename: worker['key_filename'] = options.distributed_key_filename workers.append(worker) else: hosts = test.conf_get('workers', 'hosts', '', quiet=True).split() for host in hosts: host = host.strip() if options.distributed_key_filename: key_filename = options.distributed_key_filename else: key_filename = test.conf_get(host, 'ssh_key', '') workers.append({ "name": host.replace(":", "_"), "host": test.conf_get(host, "host", host), "password": test.conf_get(host, 'password', ''), "username": test.conf_get(host, 'username', ''), "key_filename": key_filename, "channel_timeout": self.channel_timeout}) self._workers = [] [self._workers.append(SSHDistributor(**w)) for w in workers] self._worker_results = {} trace(str(self)) # setup monitoring monitor_hosts = [] # list of (host, port, descr) if not options.is_distributed: hosts = test.conf_get('monitor', 'hosts', '', quiet=True).split() for host in sorted(hosts): name = host host = test.conf_get(host, 'host', host.strip()) monitor_hosts.append((name, host, test.conf_getInt(name, 'port'), test.conf_get(name, 'description', ''))) self.monitor_hosts = monitor_hosts # keep the test to use the result logger for monitoring # and call setUp/tearDown Cycle self.test = test # start the feedback receiver if LIVE_FEEDBACK and options.feedback: trace("* Starting the Feedback Publisher\n") self.feedback = FeedbackPublisher( endpoint=options.feedback_endpoint or DEFAULT_ENDPOINT, pubsub_endpoint=options.feedback_pubsub_endpoint or DEFAULT_PUBSUB, handler=_print_rt) self.feedback.start() else: self.feedback = None def __repr__(self): """Display distributed bench information.""" text = [] text.append('=' * 72) text.append('Benching %s.%s' % (self.class_name, self.method_name)) text.append('=' * 72) text.append(self.test_description) text.append('-' * 72 + '\n') text.append("Configuration") text.append("=============\n") text.append("* Current time: %s" % datetime.now().isoformat()) text.append("* Configuration file: %s" % self.config_path) text.append("* Distributed output: %s" % self.distribution_output) size = os.path.getsize(self.tarred_tests) text.append("* Tarred tests: %0.2fMB" % (float(size) / 10.0 ** 6)) text.append("* Server: %s" % self.test_url) text.append("* Cycles: %s" % self.cycles) text.append("* Cycle duration: %ss" % self.duration) text.append("* Sleeptime between request: from %ss to %ss" % ( self.sleep_time_min, self.sleep_time_max)) text.append("* Sleeptime between test case: %ss" % self.sleep_time) text.append("* Startup delay between thread: %ss" % self.startup_delay) text.append("* Channel timeout: %s%s" % ( self.channel_timeout, "s" if self.channel_timeout else "")) text.append("* Workers :%s\n\n" % ",".join( w.name for w in self._workers)) return '\n'.join(text) def prepare_workers(self, allow_errors=False): """ Initialize the sandboxes in each worker node to prepare for a bench run. The additional parameter `allow_errors` will essentially make the distinction between ignoring unresponsive/inappropriate nodes - or raising an error and failing the entire bench. """ # right, lets figure out if funkload can be setup on each host def local_prep_worker(worker): remote_res_dir = os.path.join(self.remote_res_dir, worker.name) virtual_env = os.path.join( remote_res_dir, self.tarred_testsdir) if worker.isdir(virtual_env): worker.execute("rm -rf %s" % virtual_env) worker.execute("mkdir -p %s" % virtual_env) worker.put( get_virtualenv_script(), ## os.path.join(remote_res_dir, "virtualenv.py")) os.path.join(remote_res_dir, "tmpvenv.py")) trace(".") worker.execute( # "%s virtualenv.py %s" % ( "%s tmpvenv.py %s" % ( self.python_bin, os.path.join(remote_res_dir, self.tarred_testsdir)), cwdir=remote_res_dir) tarball = os.path.split(self.tarred_tests)[1] remote_tarball = os.path.join(remote_res_dir, tarball) # setup funkload cmd = "./bin/easy_install setuptools ez_setup {funkload}".format( funkload=self.funkload_location) if self.distributed_packages: cmd += " %s" % self.distributed_packages worker.execute(cmd, cwdir=virtual_env) # unpackage tests. worker.put( self.tarred_tests, os.path.join(remote_res_dir, tarball)) worker.execute( "tar -xvf %s" % tarball, cwdir=remote_res_dir) worker.execute("rm %s" % remote_tarball) # workaround for https://github.com/pypa/virtualenv/issues/330 worker.execute("rm lib64", cwdir=virtual_env) worker.execute("ln -s lib lib64", cwdir=virtual_env) threads = [] trace("* Preparing sandboxes for %d workers." % len(self._workers)) for worker in list(self._workers): if not worker.connected: if allow_errors: trace("%s is not connected, removing from pool.\n" % worker.name) self._workers.remove(worker) continue else: raise RuntimeError( "%s is not contactable with error %s" % ( worker.name, worker.error)) # Verify that the Python binary is available which_python = "test -x `which %s 2>&1 > /dev/null` && echo true" \ % (self.python_bin) out, err = worker.execute(which_python) if out.strip() == "true": threads.append(threading.Thread( target=local_prep_worker, args=(worker,))) elif allow_errors: trace("Cannot find Python binary at path `%s` on %s, " + "removing from pool" % (self.python_bin, worker.name)) self._workers.remove(worker) else: raise RuntimeError("%s is not contactable with error %s" % ( worker.name, worker.error)) [k.start() for k in threads] [k.join() for k in threads] trace("\n") if not self._workers: raise RuntimeError("no workers available for distribution") def abort(self): for worker in self._workers: worker.die() def run(self): """ """ threads = [] trace("* Starting %d workers" % len(self._workers)) self.startMonitors() for worker in self._workers: remote_res_dir = os.path.join(self.remote_res_dir, worker.name) venv = os.path.join(remote_res_dir, self.tarred_testsdir) obj = worker.threaded_execute( 'bin/fl-run-bench ' + ' '.join(self.cmd_args), cwdir=venv) trace(".") threads.append(obj) trace("\n") while True: if all([not thread.is_alive() for thread in threads]): # we're done break time.sleep(5.) trace("\n") for thread, worker in zip(threads, self._workers): self._worker_results[worker] = thread.output.read() trace("* [%s] returned\n" % worker.name) err_string = thread.err.read() if err_string: trace("\n".join(" [%s]: %s" % (worker.name, k) for k in err_string.split("\n") if k.strip())) trace("\n") self.stopMonitors() self.correlate_statistics() def final_collect(self): expr = re.compile("Log\s+xml:\s+(.*?)\n") for worker, results in self._worker_results.items(): res = expr.findall(results) if res: remote_file = res[0] filename = os.path.split(remote_file)[1] local_file = os.path.join( self.distribution_output, "%s-%s" % ( worker.name, filename)) if os.access(local_file, os.F_OK): os.rename(local_file, local_file + '.bak-' + str(int(time.time()))) worker.get(remote_file, local_file) trace("* Received bench log from [%s] into %s\n" % ( worker.name, local_file)) def startMonitors(self): """Start monitoring on hosts list.""" if not self.monitor_hosts: return monitor_hosts = [] monitor_key = "%s:0:0" % self.method_name for (name, host, port, desc) in self.monitor_hosts: trace("* Start monitoring %s: ..." % name) server = ServerProxy("http://%s:%s" % (host, port)) try: server.startRecord(monitor_key) except SocketError: trace(' failed, server is down.\n') else: trace(' done.\n') monitor_hosts.append((name, host, port, desc)) self.monitor_hosts = monitor_hosts def stopMonitors(self): """Stop monitoring and save xml result.""" if not self.monitor_hosts: return monitor_key = "%s:0:0" % self.method_name successful_results = [] for (name, host, port, desc) in self.monitor_hosts: trace('* Stop monitoring %s: ' % host) server = ServerProxy("http://%s:%s" % (host, port)) try: server.stopRecord(monitor_key) successful_results.append(server.getXmlResult(monitor_key)) except SocketError: trace(' failed, server is down.\n') else: trace(' done.\n') self.write_statistics(successful_results) if self.feedback is not None: self.feedback.close() def write_statistics(self, successful_results): """ Write the distributed stats to a file in the output dir """ path = os.path.join(self.distribution_output, "stats.xml") if os.access(path, os.F_OK): os.rename(path, path + '.bak-' + str(int(time.time()))) config = {'id': self.test_id, 'description': self.test_description, 'class_title': self.class_title, 'class_description': self.class_description, 'module': self.module_name, 'class': self.class_name, 'method': self.method_name, 'cycles': self.cycles, 'duration': self.duration, 'sleep_time': self.sleep_time, 'startup_delay': self.startup_delay, 'sleep_time_min': self.sleep_time_min, 'sleep_time_max': self.sleep_time_max, 'cycle_time': self.cycle_time, 'configuration_file': self.config_path, 'server_url': self.test_url, 'log_xml': self.result_path, 'python_version': platform.python_version()} for (name, host, port, desc) in self.monitor_hosts: config[name] = desc with open(path, "w+") as fd: fd.write('\n'.format( version=get_version(), time=time.time())) for key, value in config.items(): # Write out the config values fd.write('\n'.format( key=key, value=value)) for xml in successful_results: fd.write(xml) fd.write("\n") fd.write("\n") def _calculate_time_skew(self, results, stats): if not results or not stats: return 1 def min_time(vals): keyfunc = lambda elem: float(elem.attrib['time']) return keyfunc(min(vals, key=keyfunc)) results_min = min_time(results) monitor_min = min_time(stats) return results_min / monitor_min def _calculate_results_ranges(self, results): seen = [] times = {} for element in results: cycle = int(element.attrib['cycle']) if cycle not in seen: seen.append(cycle) cvus = int(element.attrib['cvus']) start_time = float(element.attrib['time']) times[start_time] = (cycle, cvus) return times def correlate_statistics(self): result_path = None if not self.monitor_hosts: return for worker, results in self._worker_results.items(): files = glob("%s/%s-*.xml" % (self.distribution_output, worker.name)) if files: result_path = files[0] break if not result_path: trace("* No output files found; unable to correlate stats.\n") return # Calculate the ratio between results and monitoring results_tree = ElementTree(file=result_path) stats_path = os.path.join(self.distribution_output, "stats.xml") stats_tree = ElementTree(file=stats_path) results = results_tree.findall("testResult") stats = stats_tree.findall("monitor") ratio = self._calculate_time_skew(results, stats) # Now that we have the ratio, we can calculate the sessions! times = self._calculate_results_ranges(results) times_desc = sorted(times.keys(), reverse=True) # Now, parse the stats tree and update values def find_range(start_time): for time_ in times_desc: if start_time > time_: return times[time_] else: return times[time_] for stat in stats: adj_time = float(stat.attrib['time']) * ratio cycle, cvus = find_range(adj_time) key, cycle_, cvus_ = stat.attrib['key'].partition(':') stat.attrib['key'] = "%s:%d:%d" % (key, cycle, cvus) stats_tree.write(stats_path) funkload-1.17.1/src/funkload/FunkLoadDocTest.py000066400000000000000000000040131302537724200213440ustar00rootroot00000000000000# (C) Copyright 2006 Nuxeo SAS # Author: bdelbosc@nuxeo.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """FunkLoad doc test $Id$ """ from __future__ import absolute_import import os from tempfile import gettempdir from .FunkLoadTestCase import FunkLoadTestCase from . import PatchWebunit class FunkLoadDocTest(FunkLoadTestCase): """Class to use in doctest. >>> from FunkLoadDocTest import FunkLoadDocTest >>> fl = FunkLoadDocTest() >>> ret = fl.get('http://localhost') >>> ret.code 200 >>> 'HTML' in ret.body True """ def __init__(self, debug=False, debug_level=1): """Initialise the test case.""" class Dummy: pass option = Dummy() option.ftest_sleep_time_max = .001 option.ftest_sleep_time_min = .001 if debug: option.ftest_log_to = 'console file' if debug_level: option.debug_level = debug_level else: option.ftest_log_to = 'file' tmp_path = gettempdir() option.ftest_log_path = os.path.join(tmp_path, 'fl-doc-test.log') option.ftest_result_path = os.path.join(tmp_path, 'fl-doc-test.xml') FunkLoadTestCase.__init__(self, 'runTest', option) def runTest(self): """FL doctest""" return def _test(): import doctest, FunkLoadDocTest return doctest.testmod(FunkLoadDocTest) if __name__ == "__main__": _test() funkload-1.17.1/src/funkload/FunkLoadHTTPServer.py000066400000000000000000000060411302537724200217500ustar00rootroot00000000000000#!/usr/bin/python # (C) Copyright 2010 Nuxeo SAS # Author: Goutham Bhat # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. """Debug HTTPServer module for Funkload.""" from __future__ import absolute_import import BaseHTTPServer import threading import urlparse from .utils import trace class FunkLoadHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): """Handles HTTP requests from client in debug bench mode. These are the requests currently supported: /cvu?inc= :: Increments number of CVU by given value. /cvu?dec= :: Decrements number of CVU by given value. """ benchrunner = None def do_GET(self): benchrunner = FunkLoadHTTPRequestHandler.benchrunner parsed_url = urlparse.urlparse(self.path) if parsed_url.path == '/cvu': query_args = parsed_url.query.split('&') if len(query_args) > 0: query_parts = query_args[0].split('=') if len(query_parts) == 2: message = 'Number of threads changed from %d to %d.' old_num_threads = benchrunner.getNumberOfThreads() if query_parts[0] == 'inc': benchrunner.addThreads(int(query_parts[1])) elif query_parts[0] == 'dec': benchrunner.removeThreads(int(query_parts[1])) new_num_threads = benchrunner.getNumberOfThreads() self.respond('CVU changed from %d to %d.' % (old_num_threads, new_num_threads)) elif parsed_url.path == '/getcvu': self.respond('CVU = %d' % benchrunner.getNumberOfThreads()) def respond(self, message): self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(message) class FunkLoadHTTPServer(threading.Thread): """Starts a HTTP server in a separate thread.""" def __init__(self, benchrunner, port): threading.Thread.__init__(self) self.benchrunner = benchrunner self.port = port FunkLoadHTTPRequestHandler.benchrunner = benchrunner def run(self): port = 8000 if self.port: port = int(self.port) server_address = ('', port) trace("Starting debug HTTP server at port %d\n" % port) httpd = BaseHTTPServer.HTTPServer(server_address, FunkLoadHTTPRequestHandler) httpd.serve_forever() funkload-1.17.1/src/funkload/FunkLoadTestCase.py000066400000000000000000001247031302537724200215230ustar00rootroot00000000000000# (C) Copyright 2005-2011 Nuxeo SAS # Author: bdelbosc@nuxeo.com # Contributors: Tom Lazar # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """FunkLoad test case using Richard Jones' webunit. $Id: FunkLoadTestCase.py 24757 2005-08-31 12:22:19Z bdelbosc $ """ from __future__ import print_function from __future__ import absolute_import import os import sys import time import string import re import logging import gzip import threading from StringIO import StringIO from warnings import warn from socket import error as SocketError from types import DictType, ListType, TupleType from datetime import datetime import unittest import traceback from random import random from urllib import urlencode from tempfile import mkdtemp from xml.sax.saxutils import quoteattr from urlparse import urljoin from ConfigParser import ConfigParser, NoSectionError, NoOptionError from webunit.webunittest import WebTestCase, HTTPError from . import PatchWebunit from .utils import get_default_logger, mmn_is_bench, mmn_decode, Data from .utils import recording, thread_sleep, is_html, get_version, trace from xmlrpclib import ServerProxy _marker = [] # ------------------------------------------------------------ # Classes # class ConfSectionFinder(object): '''Convenience class. Lets us access conf sections and attrs by doing MyTestCase().conf.sectionName.attrName ''' allowedChars = string.ascii_letters + string.digits + '_' def __init__(self, testcase): self.testcase = testcase self.quiet = False def __getattr__(self, section): class ConfKeyFinder(object): def __getattr__(sself, attr): assert sself.validInput(section, attr), \ 'To use the convenient .section.attr access to your'\ ' config variables, they can not contain any special'\ ' characters (alphanumeric and _ only)' return self.testcase.conf_get(section, attr, quiet=self.quiet) def validInput(sself, section, attr): return set(section+attr).issubset(self.allowedChars) return ConfKeyFinder() class FunkLoadTestCase(unittest.TestCase): """Unit test with browser and configuration capabilties.""" # ------------------------------------------------------------ # Initialisation # def __init__(self, methodName='runTest', options=None): """Initialise the test case. Note that methodName is encoded in bench mode to provide additional information like thread_id, concurrent virtual users...""" if mmn_is_bench(methodName): self.in_bench_mode = True else: self.in_bench_mode = False self.test_name, self.cycle, self.cvus, self.thread_id = mmn_decode( methodName) self.meta_method_name = methodName self.suite_name = self.__class__.__name__ unittest.TestCase.__init__(self, methodName=self.test_name) self._response = None self._options = options self.debug_level = getattr(options, 'debug_level', 0) self._funkload_init() self._dump_dir = getattr(options, 'dump_dir', None) self._dumping = self._dump_dir and True or False self._viewing = getattr(options, 'firefox_view', False) self._accept_invalid_links = getattr(options, 'accept_invalid_links', False) self._bench_label = getattr(options, 'label', None) self._stop_on_fail = getattr(options, 'stop_on_fail', False) self._pause = getattr(options, 'pause', False) self._keyfile_path = None self._certfile_path = None self._accept_gzip = False if self._viewing and not self._dumping: # viewing requires dumping contents self._dumping = True self._dump_dir = mkdtemp('_funkload') self._loop_mode = getattr(options, 'loop_steps', False) if self._loop_mode: if ':' in options.loop_steps: steps = options.loop_steps.split(':') self._loop_steps = range(int(steps[0]), int(steps[1])) else: self._loop_steps = [int(options.loop_steps)] self._loop_number = options.loop_number self._loop_recording = False self._loop_records = [] if sys.version_info >= (2, 5): self.__exc_info = sys.exc_info def _funkload_init(self): """Initialize a funkload test case using a configuration file.""" # look into configuration file config_path = getattr(self._options, 'config', None) if not config_path: config_directory = os.getenv('FL_CONF_PATH', '.') config_path = os.path.join(config_directory, self.__class__.__name__ + '.conf') config_path = os.path.abspath(os.path.expanduser(config_path)) if not os.path.exists(config_path): config_path = "Missing: "+ config_path config = ConfigParser() config.read(config_path) self._config = config self._config_path = config_path self.conf = ConfSectionFinder(self) self.default_user_agent = self.conf_get('main', 'user_agent', 'FunkLoad/%s' % get_version(), quiet=True) if self.in_bench_mode: section = 'bench' else: section = 'ftest' self.setOkCodes( self.conf_getList(section, 'ok_codes', [200, 301, 302, 303, 307], quiet=True) ) self.sleep_time_min = self.conf_getFloat(section, 'sleep_time_min', 0) self.sleep_time_max = self.conf_getFloat(section, 'sleep_time_max', 0) self._simple_fetch = self.conf_getInt(section, 'simple_fetch', 0, quiet=True) self.log_to = self.conf_get(section, 'log_to', 'console file') self.log_path = self.conf_get(section, 'log_path', 'funkload.log') self.result_path = os.path.abspath( self.conf_get(section, 'result_path', 'funkload.xml')) # init loggers if self.in_bench_mode: level = logging.INFO else: level = logging.DEBUG self.logger = get_default_logger(self.log_to, self.log_path, level=level) self.logger_result = get_default_logger(log_to="xml", log_path=self.result_path, name="FunkLoadResult") #self.logd('_funkload_init config [%s], log_to [%s],' # ' log_path [%s], result [%s].' % ( # self._config_path, self.log_to, self.log_path, self.result_path)) # init webunit browser (passing a fake methodName) self._browser = WebTestCase(methodName='log') self.clearContext() #self.logd('# FunkLoadTestCase._funkload_init done') def setOkCodes(self, ok_codes): """Set ok codes.""" self.ok_codes = map(int, ok_codes) def clearContext(self): """Reset the testcase.""" self._browser.clearContext() self._browser.css = {} self._browser.history = [] self._browser.extra_headers = [] if self.debug_level >= 3: self._browser.debug_headers = True else: self._browser.debug_headers = False self.step_success = True self.test_status = 'Successful' self.steps = 0 self.page_responses = 0 self.total_responses = 0 self.total_time = 0.0 self.total_pages = self.total_images = 0 self.total_links = self.total_redirects = 0 self.total_xmlrpc = 0 self.clearBasicAuth() self.clearHeaders() self.clearKeyAndCertificateFile() self.setUserAgent(self.default_user_agent) self.logdd('FunkLoadTestCase.clearContext done') #------------------------------------------------------------ # browser simulation # def _connect(self, url, params, ok_codes, rtype, description, redirect=False, consumer=None): """Handle fetching, logging, errors and history.""" if params is None and rtype in ('post','put'): # enable empty put/post params = [] t_start = time.time() try: response = self._browser.fetch(url, params, ok_codes=ok_codes, key_file=self._keyfile_path, cert_file=self._certfile_path, method=rtype, consumer=consumer) except: etype, value, tback = sys.exc_info() t_stop = time.time() t_delta = t_stop - t_start self.total_time += t_delta self.step_success = False self.test_status = 'Failure' self.logd(' Failed in %.3fs' % t_delta) if etype is HTTPError: self._log_response(value.response, rtype, description, t_start, t_stop, log_body=True) if self._dumping: self._dump_content(value.response, description) raise self.failureException(str(value.response)) else: self._log_response_error(url, rtype, description, t_start, t_stop) if etype is SocketError: raise SocketError("Can't load %s." % url) raise t_stop = time.time() # Log response t_delta = t_stop - t_start self.total_time += t_delta if redirect: self.total_redirects += 1 elif rtype != 'link': self.total_pages += 1 else: self.total_links += 1 if rtype in ('put', 'post', 'get', 'delete'): # this is a valid referer for the next request self.setHeader('Referer', url) self._browser.history.append((rtype, url)) self.logd(' Done in %.3fs' % t_delta) if self._accept_gzip: if response.headers is not None and response.headers.get('Content-Encoding') == 'gzip': buf = StringIO(response.body) response.body = gzip.GzipFile(fileobj=buf).read() self._log_response(response, rtype, description, t_start, t_stop) if self._dumping: self._dump_content(response, description) return response def _browse(self, url_in, params_in=None, description=None, ok_codes=None, method='post', follow_redirect=True, load_auto_links=True, sleep=True): """Simulate a browser handle redirects, load/cache css and images.""" self._response = None # Loop mode if self._loop_mode: if self.steps == self._loop_steps[0]: self._loop_recording = True self.logi('Loop mode start recording') if self._loop_recording: self._loop_records.append((url_in, params_in, description, ok_codes, method, follow_redirect, load_auto_links, False)) # ok codes if ok_codes is None: ok_codes = self.ok_codes if type(params_in) is DictType: params_in = params_in.items() params = [] if params_in: if isinstance(params_in, Data): params = params_in else: for key, value in params_in: if type(value) is DictType: for val, selected in value.items(): if selected: params.append((key, val)) elif type(value) in (ListType, TupleType): for val in value: params.append((key, val)) else: params.append((key, value)) if method == 'get' and params: url = url_in + '?' + urlencode(params) else: url = url_in if method == 'get': params = None if method == 'get': if not self.in_bench_mode: self.logd('GET: %s\n\tPage %i: %s ...' % (url, self.steps, description or '')) else: url = url_in if not self.in_bench_mode: self.logd('%s: %s %s\n\tPage %i: %s ...' % ( method.upper(), url, str(params), self.steps, description or '')) # Fetching response = self._connect(url, params, ok_codes, method, description) # Check redirection if follow_redirect and response.code in (301, 302, 303, 307): max_redirect_count = 10 thread_sleep() # give a chance to other threads while response.code in (301, 302, 303, 307) and max_redirect_count: # Figure the location - which may be relative newurl = response.headers['Location'] url = urljoin(url_in, newurl) # Save the current url as the base for future redirects url_in = url self.logd(' Load redirect link: %s' % url) # Use the appropriate method for redirection if response.code in (302, 303): method = 'get' if response.code == 303: # 303 is HTTP/1.1, make sure the connection # is not in keep alive mode self.setHeader('Connection', 'close') response = self._connect(url, None, ok_codes, rtype=method, description=None, redirect=True) max_redirect_count -= 1 if not max_redirect_count: self.logd(' WARNING Too many redirects give up.') # Load auto links (css and images) response.is_html = is_html(response.body) if load_auto_links and response.is_html and not self._simple_fetch: self.logd(' Load css and images...') page = response.body t_start = time.time() c_start = self.total_time try: # pageImages is patched to call _log_response on all links self._browser.pageImages(url, page, self) except HTTPError as error: if self._accept_invalid_links: if not self.in_bench_mode: self.logd(' ' + str(error)) else: t_stop = time.time() t_delta = t_stop - t_start self.step_success = False self.test_status = 'Failure' self.logd(' Failed in ~ %.2fs' % t_delta) # XXX The duration logged for this response is wrong self._log_response(error.response, 'link', None, t_start, t_stop, log_body=True) raise self.failureException(str(error)) c_stop = self.total_time self.logd(' Done in %.3fs' % (c_stop - c_start)) if sleep: self.sleep() self._response = response # Loop mode if self._loop_mode and self.steps == self._loop_steps[-1]: self._loop_recording = False self.logi('Loop mode end recording.') t_start = self.total_time count = 0 for i in range(self._loop_number): self.logi('Loop mode replay %i' % i) for record in self._loop_records: count += 1 self.steps += 1 self._browse(*record) t_delta = self.total_time - t_start text = ('End of loop: %d pages rendered in %.3fs, ' 'avg of %.3fs per page, ' '%.3f SPPS without concurrency.' % (count, t_delta, t_delta / count, count/t_delta)) self.logi(text) trace(text + '\n') return response def post(self, url, params=None, description=None, ok_codes=None, load_auto_links=True, follow_redirect=True): """Make an HTTP POST request to the specified url with params. Returns a webunit.webunittest.HTTPResponse object. """ self.steps += 1 self.page_responses = 0 response = self._browse(url, params, description, ok_codes, method="post", load_auto_links=load_auto_links, follow_redirect=follow_redirect) return response def get(self, url, params=None, description=None, ok_codes=None, load_auto_links=True, follow_redirect=True): """Make an HTTP GET request to the specified url with params. Returns a webunit.webunittest.HTTPResponse object. """ self.steps += 1 self.page_responses = 0 response = self._browse(url, params, description, ok_codes, method="get", load_auto_links=load_auto_links, follow_redirect=follow_redirect) return response def method(self, method, url, params=None, description=None, ok_codes=None, load_auto_links=True): """Generic HTTP request method. Can be used to make MOVE, MKCOL, etc method name HTTP requests. Returns a webunit.webunittest.HTTPResponse object. """ self.steps += 1 self.page_responses = 0 response = self._browse(url, params, description, ok_codes, method=method, load_auto_links=load_auto_links) return response def put(self, url, params=None, description=None, ok_codes=None, load_auto_links=True): """Make an HTTP PUT request to the specified url with params.""" return self.method('put', url, params, description, ok_codes, load_auto_links=load_auto_links) def delete(self, url, description=None, ok_codes=None): """Make an HTTP DELETE request to the specified url.""" return self.method('delete', url, None, description, ok_codes) def head(self, url, description=None, ok_codes=None): """Make an HTTP HEAD request to the specified url with params.""" return self.method('head', url, None, description, ok_codes) def options(self, url, description=None, ok_codes=None): """Make an HTTP OPTIONS request to the specified url.""" return self.method('options', url, None, description, ok_codes) def propfind(self, url, params=None, depth=None, description=None, ok_codes=None): """Make a DAV PROPFIND request to the specified url with params.""" if ok_codes is None: codes = [207, ] else: codes = ok_codes if depth is not None: self.setHeader('depth', str(depth)) ret = self.method('PROPFIND', url, params=params, description=description, ok_codes=codes) if depth is not None: self.delHeader('depth') return ret def exists(self, url, params=None, description="Checking existence"): """Try a GET on URL return True if the page exists or False.""" resp = self.get(url, params, description=description, ok_codes=[200, 301, 302, 303, 307, 404, 503], load_auto_links=False) if resp.code not in [200, 301, 302, 303, 307]: self.logd('Page %s not found.' % url) return False return True def xmlrpc(self, url_in, method_name, params=None, description=None): """Call an xml rpc method_name on url with params.""" self.steps += 1 self.page_responses = 0 self.logd('XMLRPC: %s::%s\n\tCall %i: %s ...' % (url_in, method_name, self.steps, description or '')) response = None t_start = time.time() if self._authinfo is not None: url = url_in.replace('//', '//'+self._authinfo) else: url = url_in try: server = ServerProxy(url) method = getattr(server, method_name) if params is not None: response = method(*params) else: response = method() except: etype, value, tback = sys.exc_info() t_stop = time.time() t_delta = t_stop - t_start self.total_time += t_delta self.step_success = False self.test_status = 'Error' self.logd(' Failed in %.3fs' % t_delta) self._log_xmlrpc_response(url_in, method_name, description, response, t_start, t_stop, -1) if etype is SocketError: raise SocketError("Can't access %s." % url) raise t_stop = time.time() t_delta = t_stop - t_start self.total_time += t_delta self.total_xmlrpc += 1 self.logd(' Done in %.3fs' % t_delta) self._log_xmlrpc_response(url_in, method_name, description, response, t_start, t_stop, 200) self.sleep() return response def xmlrpc_call(self, url_in, method_name, params=None, description=None): """BBB of xmlrpc, this method will be removed for 1.6.0.""" warn('Since 1.4.0 the method "xmlrpc_call" is renamed into "xmlrpc".', DeprecationWarning, stacklevel=2) return self.xmlrpc(url_in, method_name, params, description) def waitUntilAvailable(self, url, time_out=20, sleep_time=2): """Wait until url is available. Try a get on url every sleep_time until server is reached or time is out.""" time_start = time.time() while(True): try: self._browser.fetch(url, None, ok_codes=[200, 301, 302, 303, 307], key_file=self._keyfile_path, cert_file=self._certfile_path, method="get") except SocketError: if time.time() - time_start > time_out: self.fail('Time out service %s not available after %ss' % (url, time_out)) else: return time.sleep(sleep_time) def comet(self, url, consumer, description=None): """Initiate a comet request and process the input in a separate thread. This call is async and return a thread object. The consumer method takes as parameter an input string, it can close the comet connection by returning 0.""" self.steps += 1 self.page_responses = 0 thread = threading.Thread(target=self._cometFetcher, args=(url, consumer, description)) thread.start() return thread def _cometFetcher(self, url, consumer, description): self._connect(url, None, self.ok_codes, 'GET', description, consumer=consumer) def setBasicAuth(self, login, password): """Set HTTP basic authentication for the following requests.""" self._browser.setBasicAuth(login, password) self._authinfo = '%s:%s@' % (login, password) def clearBasicAuth(self): """Remove basic authentication.""" self._browser.clearBasicAuth() self._authinfo = None def addHeader(self, key, value): """Add an http header.""" self._browser.extra_headers.append((key, value)) def setHeader(self, key, value): """Add or override an http header. If value is None, the key is removed.""" headers = self._browser.extra_headers for i, (k, v) in enumerate(headers): if k == key: if value is not None: headers[i] = (key, value) else: del headers[i] break else: if value is not None: headers.append((key, value)) if key.lower() == 'accept-encoding': if value and value.lower() == 'gzip': self._accept_gzip = True else: self._accept_gzip = False def delHeader(self, key): """Remove an http header key.""" self.setHeader(key, None) def clearHeaders(self): """Remove all http headers set by addHeader or setUserAgent. Note that the Referer is also removed.""" self._browser.extra_headers = [] def debugHeaders(self, debug_headers=True): """Print request headers.""" self._browser.debug_headers = debug_headers def setUserAgent(self, agent): """Set User-Agent http header for the next requests. If agent is None, the user agent header is removed.""" self.setHeader('User-Agent', agent) def sleep(self): """Sleeps a random amount of time. Between the predefined sleep_time_min and sleep_time_max values. """ if self._pause: raw_input("Press ENTER to continue ") return s_min = self.sleep_time_min s_max = self.sleep_time_max if s_max != s_min: s_val = s_min + abs(s_max - s_min) * random() else: s_val = s_min # we should always sleep something thread_sleep(s_val) def setKeyAndCertificateFile(self, keyfile_path, certfile_path): """Set the paths to a key file and a certificate file that will be used by a https (ssl/tls) connection when calling the post or get methods. keyfile_path : path to a PEM formatted file that contains your private key. certfile_path : path to a PEM formatted certificate chain file. """ self._keyfile_path = keyfile_path self._certfile_path = certfile_path def clearKeyAndCertificateFile(self): """Clear any key file or certificate file paths set by calls to setKeyAndCertificateFile. """ self._keyfile_path = None self._certfile_path = None #------------------------------------------------------------ # Assertion helpers # def getLastUrl(self): """Return the last accessed url taking into account redirection.""" response = self._response if response is not None: return response.url return '' def getBody(self): """Return the last response content.""" response = self._response if response is not None: return response.body return '' def listHref(self, url_pattern=None, content_pattern=None): """Return a list of href anchor url present in the last html response. Filtering href with url pattern or link text pattern.""" response = self._response ret = [] if response is not None: a_links = response.getDOM().getByName('a') if a_links: for link in a_links: try: ret.append((link.getContentString(), link.href)) except AttributeError: pass if url_pattern is not None: pat = re.compile(url_pattern) ret = [link for link in ret if pat.search(link[1]) is not None] if content_pattern is not None: pat = re.compile(content_pattern) ret = [link for link in ret if link[0] and (pat.search(link[0]) is not None)] return [link[1] for link in ret] def getLastBaseUrl(self): """Return the base href url.""" response = self._response if response is not None: base = response.getDOM().getByName('base') if base: return base[0].href return '' #------------------------------------------------------------ # configuration file utils # def conf_get(self, section, key, default=_marker, quiet=False): """Return an entry from the options or configuration file.""" # check for a command line options opt_key = '%s_%s' % (section, key) opt_val = getattr(self._options, opt_key, None) if opt_val: #print('[%s] %s = %s from options.' % (section, key, opt_val)) return opt_val # check for the configuration file if opt val is None # or nul try: val = self._config.get(section, key) except (NoSectionError, NoOptionError): if not quiet: self.logi('[%s] %s not found' % (section, key)) if default is _marker: raise val = default #print('[%s] %s = %s from config.' % (section, key, val)) return val def conf_getInt(self, section, key, default=_marker, quiet=False): """Return an integer from the configuration file.""" return int(self.conf_get(section, key, default, quiet)) def conf_getFloat(self, section, key, default=_marker, quiet=False): """Return a float from the configuration file.""" return float(self.conf_get(section, key, default, quiet)) def conf_getList(self, section, key, default=_marker, quiet=False, separator=None): """Return a list from the configuration file.""" value = self.conf_get(section, key, default, quiet) if value is default: return value if separator is None: separator = ':' if separator in value: return value.split(separator) return [value] #------------------------------------------------------------ # Extend unittest.TestCase to provide bench cycle hook # def setUpCycle(self): """Called on bench mode before a cycle start. Note that you can not initialize your testcase instance with this method, you need to use the setUp method instead. """ pass def midCycle(self, cycle, cvus): """Called in the middle of a bench cycle.""" pass def tearDownCycle(self): """Called after a cycle in bench mode.""" pass #------------------------------------------------------------ # Extend unittest.TestCase to provide bench setup/teardown hook # def setUpBench(self): """Called before the start of the bench. Note that you can not initialize your testcase instance with this method, you need to use the setUp method instead. """ pass def tearDownBench(self): """Called after a the bench.""" pass #------------------------------------------------------------ # logging # def logd(self, message): """Debug log.""" self.logger.debug(self.meta_method_name +': ' +message) def logdd(self, message): """Verbose Debug log.""" if self.debug_level >= 2: self.logger.debug(self.meta_method_name +': ' +message) def logi(self, message): """Info log.""" if hasattr(self, 'logger'): self.logger.info(self.meta_method_name+': '+message) else: print(self.meta_method_name+': '+message) def _logr(self, message, force=False): """Log a result.""" if force or not self.in_bench_mode or recording(): self.logger_result.info(message) def _open_result_log(self, **kw): """Open the result log.""" self._logr('' % ( get_version(), datetime.now().isoformat()), force=True) self.addMetadata(ns=None, **kw) def addMetadata(self, ns="meta", **kw): """Add metadata info.""" xml = [] for key, value in kw.items(): if ns is not None: xml.append('' % ( ns, key, quoteattr(str(value)))) else: xml.append('' % ( key, quoteattr(str(value)))) self._logr('\n'.join(xml), force=True) def _close_result_log(self): """Close the result log.""" self._logr('', force=True) def _log_response_error(self, url, rtype, description, time_start, time_stop): """Log a response that raise an unexpected exception.""" self.total_responses += 1 self.page_responses += 1 info = {} info['cycle'] = self.cycle info['cvus'] = self.cvus info['thread_id'] = self.thread_id info['suite_name'] = self.suite_name info['test_name'] = self.test_name info['step'] = self.steps info['number'] = self.page_responses info['type'] = rtype info['url'] = quoteattr(url) info['code'] = -1 info['description'] = description and quoteattr(description) or '""' info['time_start'] = time_start info['duration'] = time_stop - time_start info['result'] = 'Error' info['traceback'] = quoteattr(' '.join( traceback.format_exception(*sys.exc_info()))) message = '''''' % info self._logr(message) def _log_response(self, response, rtype, description, time_start, time_stop, log_body=False): """Log a response.""" self.total_responses += 1 self.page_responses += 1 info = {} info['cycle'] = self.cycle info['cvus'] = self.cvus info['thread_id'] = self.thread_id info['suite_name'] = self.suite_name info['test_name'] = self.test_name info['step'] = self.steps info['number'] = self.page_responses info['type'] = rtype info['url'] = quoteattr(response.url) info['code'] = response.code info['description'] = description and quoteattr(description) or '""' info['time_start'] = time_start info['duration'] = time_stop - time_start info['result'] = self.step_success and 'Successful' or 'Failure' response_start = '''' else: response_start = response_start + '>\n ' header_xml = [] if response.headers is not None: for key, value in response.headers.items(): header_xml.append('
' % ( key, quoteattr(value))) headers = '\n'.join(header_xml) + '\n ' message = '\n'.join([ response_start, headers, ' \n ' % response.body, '']) self._logr(message) def _log_xmlrpc_response(self, url, method, description, response, time_start, time_stop, code): """Log a response.""" self.total_responses += 1 self.page_responses += 1 info = {} info['cycle'] = self.cycle info['cvus'] = self.cvus info['thread_id'] = self.thread_id info['suite_name'] = self.suite_name info['test_name'] = self.test_name info['step'] = self.steps info['number'] = self.page_responses info['type'] = 'xmlrpc' info['url'] = quoteattr(url + '#' + method) info['code'] = code info['description'] = description and quoteattr(description) or '""' info['time_start'] = time_start info['duration'] = time_stop - time_start info['result'] = self.step_success and 'Successful' or 'Failure' message = '''"''' % info self._logr(message) def _log_result(self, time_start, time_stop): """Log the test result.""" info = {} info['cycle'] = self.cycle info['cvus'] = self.cvus info['thread_id'] = self.thread_id info['suite_name'] = self.suite_name info['test_name'] = self.test_name info['steps'] = self.steps info['time_start'] = time_start info['duration'] = time_stop - time_start info['connection_duration'] = self.total_time info['requests'] = self.total_responses info['pages'] = self.total_pages info['xmlrpc'] = self.total_xmlrpc info['redirects'] = self.total_redirects info['images'] = self.total_images info['links'] = self.total_links info['result'] = self.test_status if self.test_status != 'Successful': info['traceback'] = 'traceback=' + quoteattr(' '.join( traceback.format_exception(*sys.exc_info()))) + ' ' else: info['traceback'] = '' text = '''''' % info self._logr(text) def _dump_content(self, response, description): """Dump the html content in a file. Use firefox to render the content if we are in rt viewing mode.""" dump_dir = self._dump_dir if dump_dir is None: return if getattr(response, 'code', 301) in [301, 302, 303, 307]: return if not response.body: return if not os.access(dump_dir, os.W_OK): os.mkdir(dump_dir, 0o775) content_type = response.headers.get('content-type') if content_type == 'text/xml': ext = '.xml' else: ext = os.path.splitext(response.url)[1] if not ext.startswith('.') or len(ext) > 4: ext = '.html' file_path = os.path.abspath( os.path.join(dump_dir, '%3.3i%s' % (self.steps, ext))) f = open(file_path, 'w') f.write(response.body) f.close() if self._viewing: cmd = 'firefox -remote "openfile(file://%s#%s,new-tab)"' % ( file_path, description) ret = os.system(cmd) if ret != 0 and not sys.platform.lower().startswith('win'): self.logi('Failed to remote control firefox: %s' % cmd) self._viewing = False #------------------------------------------------------------ # Overriding unittest.TestCase # def __call__(self, result=None): """Run the test method. Override to log test result.""" t_start = time.time() if result is None: result = self.defaultTestResult() result.startTest(self) if sys.version_info >= (2, 5): testMethod = getattr(self, self._testMethodName) else: testMethod = getattr(self, self._TestCase__testMethodName) try: ok = False try: if not self.in_bench_mode: self.logd('Starting -----------------------------------\n\t%s' % self.conf_get(self.meta_method_name, 'description', '')) self.setUp() except KeyboardInterrupt: raise except: result.addError(self, self.__exc_info()) self.test_status = 'Error' self._log_result(t_start, time.time()) return try: testMethod() ok = True except self.failureException: result.addFailure(self, self.__exc_info()) self.test_status = 'Failure' except KeyboardInterrupt: raise except: result.addFailure(self, self.__exc_info()) self.test_status = 'Error' try: self.tearDown() except KeyboardInterrupt: raise except: result.addFailure(self, self.__exc_info()) self.test_status = 'Error' ok = False if ok: result.addSuccess(self) finally: self._log_result(t_start, time.time()) if not ok and self._stop_on_fail: result.stop() result.stopTest(self) # ------------------------------------------------------------ # testing # class DummyTestCase(FunkLoadTestCase): """Testing Funkload TestCase.""" def test_apache(self): """Simple apache test.""" self.logd('start apache test') for i in range(2): self.get('http://localhost/') self.logd('base_url: ' + self.getLastBaseUrl()) self.logd('url: ' + self.getLastUrl()) self.logd('hrefs: ' + str(self.listHref())) self.logd("Total connection time = %s" % self.total_time) if __name__ == '__main__': unittest.main() funkload-1.17.1/src/funkload/Lipsum.py000066400000000000000000000177041302537724200176370ustar00rootroot00000000000000# -*- coding: ISO-8859-15 -*- # (C) Copyright 2005 Nuxeo SAS # Author: bdelbosc@nuxeo.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """A simple Lorem ipsum generator. $Id: Lipsum.py 24649 2005-08-29 14:20:19Z bdelbosc $ """ from __future__ import print_function import random # vacabulary simple ascii V_ASCII = ('ad', 'aquam', 'albus', 'archaeos', 'arctos', 'argentatus', 'arvensis', 'australis', 'biscort' 'borealis', 'brachy', 'bradus', 'brevis', 'campus', 'cauda', 'caulos', 'cephalus', 'chilensis', 'chloreus', 'cola', 'cristatus', 'cyanos', 'dactylus', 'deca', 'dermis', 'delorum', 'di', 'diplo', 'dodeca', 'dolicho', 'domesticus', 'dorsum', 'dulcis', 'echinus', 'ennea', 'erythro', 'familiaris', 'flora', 'folius', 'fuscus', 'fulvus', 'gaster', 'glycis', 'hexa', 'hortensis', 'it', 'indicus', 'lateralis', 'leucus', 'lineatus', 'lipsem', 'lutea', 'maculatus', 'major', 'maximus', 'melanus', 'minimus', 'minor', 'mono', 'montanus', 'morphos', 'mauro', 'niger', 'nona', 'nothos', 'notos', 'novaehollandiae', 'novaeseelandiae', 'noveboracensis', 'obscurus', 'occidentalis', 'octa', 'oeos', 'officinalis', 'oleum', 'orientalis', 'ortho', 'pachys', 'palustris', 'parvus', 'pedis', 'pelagius', 'penta', 'petra', 'phyllo', 'phyton', 'platy', 'pratensis', 'protos', 'pteron', 'punctatus', 'rhiza', 'rhytis', 'rubra', 'rostra', 'rufus', 'sativus', 'saurus', 'sinensis', 'stoma', 'striatus', 'silvestris', 'sit', 'so', 'tetra', 'tinctorius', 'tomentosus', 'tres', 'tris', 'trich', 'thrix', 'unus', 'variabilis', 'variegatus', 'ventrus', 'verrucosus', 'via', 'viridis', 'vitis', 'volans', 'vulgaris', 'xanthos', 'zygos', ) # vocabulary with some diacritics V_DIAC = ('acanth', 'acro', 'actino', 'adelphe', 'adéno', 'aéro', 'agogue', 'agro', 'algie', 'allo', 'amphi', 'andro', 'anti', 'anthropo', 'aqui', 'archéo', 'archie', 'auto', 'bio', 'calli', 'cephal', 'chiro', 'chromo', 'chrono', 'dactyle', 'démo', 'eco', 'eudaimonia', 'êthos', 'géo', 'glyphe', 'gone', 'gramme', 'graphe', 'hiéro', 'homo', 'iatrie', 'lipi', 'lipo', 'logie', 'lyco', 'lyse', 'machie', 'mélan', 'méta', 'naute', 'nèse', 'pedo', 'phil', 'phobie', 'podo', 'polis', 'poly', 'rhino', 'xeno', 'zoo', ) # latin 9 vocabulary V_8859_15 = ('jàcánth', 'zâcrö', 'bãctinõ', 'zädelphe', 'kådénô', 'zæró', 'agòguê', 'algië', 'allð', 'amphi', 'añdro', 'añti', 'aqúi', 'aùtø', 'biø', 'caßi', 'çephal', 'lýco', 'rÿtøñ', 'oþiß', 'es', 'du', 'de', 'le', 'as', 'us', 'i', 'ave', 'ov ¼', 'zur ½', 'ab ¾', ) # common char to build identifier CHARS = "abcdefghjkmnopqrstuvwxyz123456789" # separator SEP = ',' * 10 + ';?!' class Lipsum: """Kind of Lorem ipsum generator.""" def __init__(self, vocab=V_ASCII, chars=CHARS, sep=SEP): self.vocab = vocab self.chars = chars self.sep = sep def getWord(self): """Return a random word.""" return random.choice(self.vocab) def getUniqWord(self, length_min=None, length_max=None): """Generate a kind of uniq identifier.""" length_min = length_min or 5 length_max = length_max or 9 length = random.randrange(length_min, length_max) chars = self.chars return ''.join([random.choice(chars) for i in range(length)]) def getSubject(self, length=5, prefix=None, uniq=False, length_min=None, length_max=None): """Return a subject of length words.""" subject = [] if prefix: subject.append(prefix) if uniq: subject.append(self.getUniqWord()) if length_min and length_max: length = random.randrange(length_min, length_max+1) for i in range(length): subject.append(self.getWord()) return ' '.join(subject).capitalize() def getSentence(self): """Return a random sentence.""" sep = self.sep length = random.randrange(5, 20) sentence = [self.getWord() for i in range(length)] for i in range(random.randrange(0, 3)): sentence.insert(random.randrange(length-4)+2, random.choice(sep)) sentence = ' '.join(sentence).capitalize() + '.' sentence = sentence.replace(' ,', ',') sentence = sentence.replace(',,', ',') return sentence def getParagraph(self, length=4): """Return a paragraph.""" return ' '.join([self.getSentence() for i in range(length)]) def getMessage(self, length=7): """Return a message paragraph length.""" return '\n\n'.join([self.getParagraph() for i in range( random.randrange(3,length))]) def getPhoneNumber(self, lang="fr", format="medium"): """Return a random Phone number.""" if lang == "en_US": num = [] num.append("%3.3i" % random.randrange(0, 999)) num.append("%4.4i" % random.randrange(0, 9999)) if format == "short": return "-".join(num) num.insert(0, "%3.3i" % random.randrange(0, 999)) if format == "medium": return "(%s) %s-%s" % tuple(num) # default long return "+00 1 (%s) %s-%s" % tuple(num) # default lang == 'fr': num = ['07'] for i in range(4): num.append('%2.2i' % random.randrange(0, 99)) if format == "medium": return " ".join(num) elif format == "long": num[0] = '(0)7' return "+33 "+ " ".join(num) # default format == 'short': return "".join(num) def getAddress(self, lang="fr"): """Return a random address.""" # default lang == fr return "%i %s %s\n%5.5i %s" % ( random.randrange(1, 100), random.choice(['rue', 'avenue', 'place', 'boulevard']), self.getSubject(length_min=1, length_max=3), random.randrange(99000, 99999), self.getSubject(length_min=1, length_max=2)) def main(): """Testing.""" print('Word: %s\n' % (Lipsum().getWord())) print('UniqWord: %s\n' % (Lipsum().getUniqWord())) print('Subject: %s\n' % (Lipsum().getSubject())) print('Subject uniq: %s\n' % (Lipsum().getSubject(uniq=True))) print('Sentence: %s\n' % (Lipsum().getSentence())) print('Paragraph: %s\n' % (Lipsum().getParagraph())) print('Message: %s\n' % (Lipsum().getMessage())) print('Phone number: %s\n' % Lipsum().getPhoneNumber()) print('Phone number fr short: %s\n' % Lipsum().getPhoneNumber( lang="fr", format="short")) print('Phone number fr medium: %s\n' % Lipsum().getPhoneNumber( lang="fr", format="medium")) print('Phone number fr long: %s\n' % Lipsum().getPhoneNumber( lang="fr", format="long")) print('Phone number en_US short: %s\n' % Lipsum().getPhoneNumber( lang="en_US", format="short")) print('Phone number en_US medium: %s\n' % Lipsum().getPhoneNumber( lang="en_US", format="medium")) print('Phone number en_US long: %s\n' % Lipsum().getPhoneNumber( lang="en_US", format="long")) print('Address default: %s' % Lipsum().getAddress()) if __name__ == '__main__': main() funkload-1.17.1/src/funkload/Makefile000066400000000000000000000001601302537724200174400ustar00rootroot00000000000000clean: find . "(" -name "*~" -or -name ".#*" -or -name "*py.class" ")" -print0 | xargs -0 rm -f rm -f *.html funkload-1.17.1/src/funkload/MergeResultFiles.py000066400000000000000000000133731302537724200216050ustar00rootroot00000000000000# (C) Copyright 2010 Nuxeo SAS # Author: bdelbosc@nuxeo.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """Merge FunkLoad result files to produce a report for distributed bench reports.""" from __future__ import print_function from __future__ import absolute_import import xml.parsers.expat from .utils import trace class EndOfConfig(Exception): pass class FunkLoadConfigXmlParser: """Parse the config part of a funkload xml results file.""" def __init__(self): """Init setup expat handlers.""" self.current_element = [{'name': 'root'}] self.cycles = None self.cycle_duration = 0 self.nodes = {} self.config = {} self.files = [] self.current_file = None def parse(self, xml_file): """Do the parsing.""" self.current_file = xml_file parser = xml.parsers.expat.ParserCreate() parser.StartElementHandler = self.handleStartElement try: parser.ParseFile(file(xml_file)) except xml.parsers.expat.ExpatError as msg: if (self.current_element[-1]['name'] == 'funkload' and str(msg).startswith('no element found')): print("Missing tag.") else: print('Error: invalid xml bench result file') if len(self.current_element) <= 1 or ( self.current_element[1]['name'] != 'funkload'): print("""Note that you can generate a report only for a bench result done with fl-run-bench (and not on a test result done with fl-run-test).""") else: print("""You may need to remove non ascii char that comes from error pages catched during the bench. iconv or recode may help you.""") print('Xml parser element stack: %s' % [ x['name'] for x in self.current_element]) raise except EndOfConfig: return def handleStartElement(self, name, attrs): """Called by expat parser on start element.""" if name == 'funkload': self.config['version'] = attrs['version'] self.config['time'] = attrs['time'] elif name == 'config': self.config[attrs['key']] = attrs['value'] if attrs['key'] == 'duration': if self.cycle_duration and attrs['value'] != self.cycle_duration: trace('Skipping file %s with different cycle duration %s' % (self.current_file, attrs['value'])) raise EndOfConfig self.cycle_duration = attrs['value'] elif attrs['key'] == 'cycles': if self.cycles and attrs['value'] != self.cycles: trace('Skipping file %s with different cycles %s != %s' % (self.current_file, attrs['value'], self.cycles)) raise EndOfConfig self.cycles = attrs['value'] elif attrs['key'] == 'node': self.nodes[self.current_file] = attrs['value'] else: self.files.append(self.current_file) raise EndOfConfig def replace_all(text, dic): for i, j in dic.iteritems(): if isinstance(text, str): text = text.decode('utf-8', 'ignore') text = text.replace(i, j) return text.encode('utf-8') class MergeResultFiles: def __init__(self, input_files, output_file): xml_parser = FunkLoadConfigXmlParser() for input_file in input_files: trace (".") xml_parser.parse(input_file) node_count = len(xml_parser.files) # compute cumulated cycles node_cycles = [int(item) for item in xml_parser.cycles[1:-1].split(',')] cycles = map(lambda x: x * node_count, node_cycles) # node names node_names = [] i = 0 for input_file in xml_parser.files: node_names.append(xml_parser.nodes.get(input_file, 'node-' + str(i))) i += 1 trace("\nnodes: %s\n" % ', '.join(node_names)) trace("cycles for a node: %s\n" % node_cycles) trace("cycles for all nodes: %s\n" % cycles) output = open(output_file, 'w+') i = 0 for input_file in xml_parser.files: dic = {xml_parser.cycles: str(cycles), 'host="localhost"': 'host="%s"' % node_names[i], 'thread="': 'thread="' + str(i)} if i == 0: dic['key="node" value="%s"' % node_names[0]] = 'key="node" value="%s"' % ( ', '.join(node_names)) c = 0 for cycle in node_cycles: dic['cycle="%3.3i" cvus="%3.3i"' % (c, node_cycles[c])] = 'cycle="%3.3i" cvus="%3.3i"' % (c, cycles[c]) c += 1 f = open(input_file) for line in f: if "" in line: continue elif i > 0 and ('\n") output.close() funkload-1.17.1/src/funkload/Monitor.py000077500000000000000000000151221302537724200200100ustar00rootroot00000000000000# (C) Copyright 2005-2011 Nuxeo SAS # Author: bdelbosc@nuxeo.com # Contributors: # Krzysztof A. Adamski # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """A Linux monitor server/controller. """ from __future__ import absolute_import import sys from time import time, sleep from threading import Thread from .XmlRpcBase import XmlRpcBaseServer, XmlRpcBaseController from .MonitorPlugins import MonitorPlugins # ------------------------------------------------------------ # classes # class MonitorInfo: """A simple class to collect info.""" def __init__(self, host, plugins): self.time = time() self.host = host for plugin in (plugins.MONITORS.values()): for key, value in plugin.getStat().items(): setattr(self, key, value) def __repr__(self, extra_key=None): text = " 0: self.monitor() t2=time() to_sleep=self._interval-(t2-t1) if to_sleep>0: sleep(to_sleep) def stop(self): """Stop the thread.""" self._running = False def monitor(self): """The monitor task.""" self.records.append(MonitorInfo(self._host, self._plugins)) def startRecord(self): """Enable recording.""" self._recorder_count += 1 def stopRecord(self): """Stop recording.""" self._recorder_count -= 1 def countRecorders(self): """Return the number of recorder.""" return self._recorder_count # ------------------------------------------------------------ # Server # class MonitorServer(XmlRpcBaseServer): """The XML RPC monitor server.""" server_name = "monitor" method_names = XmlRpcBaseServer.method_names + [ 'startRecord', 'stopRecord', 'getResult', 'getXmlResult', 'getMonitorsConfig'] def __init__(self, argv=None): self.interval = None self.records = [] self._keys = {} XmlRpcBaseServer.__init__(self, argv) self.plugins=MonitorPlugins(self._conf) self.plugins.registerPlugins() self._monitor = MonitorThread(self.records, self.plugins, self.host, self.interval) self._monitor.start() def _init_cb(self, conf, options): """init callback.""" self.interval = conf.getfloat('server', 'interval') self._conf=conf def startRecord(self, key): """Start to monitor if it is the first key.""" self.logd('startRecord %s' % key) if key not in self._keys or self._keys[key][1] is not None: self._monitor.startRecord() self._keys[key] = [len(self.records), None] return 1 def stopRecord(self, key): """Stop to monitor if it is the last key.""" self.logd('stopRecord %s' % key) if key not in self._keys or self._keys[key][1] is not None: return 0 self._keys[key] = [self._keys[key][0], len(self.records)] self._monitor.stopRecord() return 1 def getResult(self, key): """Return stats for key.""" self.logd('getResult %s' % key) if key not in self._keys.keys(): return [] ret = self.records[self._keys[key][0]:self._keys[key][1]] return ret def getMonitorsConfig(self): ret = {} for plugin in (self.plugins.MONITORS.values()): conf = plugin.getConfig() if conf: ret[plugin.name] = conf return ret def getXmlResult(self, key): """Return result as xml.""" self.logd('getXmlResult %s' % key) ret = self.getResult(key) ret = [stat.__repr__(key) for stat in ret] return '\n'.join(ret) def test(self): """auto test.""" key = 'internal_test_monitor' self.startRecord(key) sleep(3) self.stopRecord(key) self.log(self.records) self.log(self.getXmlResult(key)) return 1 # ------------------------------------------------------------ # Controller # class MonitorController(XmlRpcBaseController): """Monitor controller.""" server_class = MonitorServer def test(self): """Testing monitor server.""" server = self.server key = 'internal_test_monitor' server.startRecord(key) sleep(2) server.stopRecord(key) self.log(server.getXmlResult(key)) return 0 # ------------------------------------------------------------ # main # def main(): """Control monitord server.""" ctl = MonitorController() sys.exit(ctl()) def test(): """Test wihtout rpc server.""" mon = MonitorServer() mon.test() if __name__ == '__main__': main() funkload-1.17.1/src/funkload/MonitorPlugins.py000066400000000000000000000147301302537724200213530ustar00rootroot00000000000000# (C) 2011-2012 Nuxeo SAS # Author: Krzysztof A. Adamski # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # import sys import re import pickle import pkg_resources ENTRYPOINT = 'funkload.plugins.monitor' gd_colors = [['red', 0xff0000], ['green', 0x00ff00], ['blue', 0x0000ff], ['yellow', 0xffff00], ['purple', 0x7f007f], ] class MonitorPlugins(): MONITORS = {} def __init__(self, conf=None): self.conf = conf self.enabled = None self.disabled = None if conf == None or not conf.has_section('plugins'): return if conf.has_option('plugins', 'monitors_enabled'): self.enabled = re.split(r'\s+', conf.get('plugins', 'monitors_enabled')) if conf.has_option('plugins', 'monitors_disabled'): self.disabled = re.split(r'\s+', conf.get('plugins', 'monitors_disabled')) def registerPlugins(self): for entrypoint in pkg_resources.iter_entry_points(ENTRYPOINT): p = entrypoint.load()(self.conf) if self.enabled != None: if p.name in self.enabled: self.MONITORS[p.name] = p elif self.disabled != None: if p.name not in self.disabled: self.MONITORS[p.name] = p else: self.MONITORS[p.name] = p def configure(self, config): for plugin in self.MONITORS.values(): if plugin.name in config: plugin.setConfig(config[plugin.name]) class Plot: def __init__(self, plots, title="", ylabel="", unit="", **kwargs): self.plots = plots self.title = title self.ylabel = ylabel self.unit = unit for key in kwargs: setattr(self, key, kwargs[key]) class MonitorPlugin(object): def __init__(self, conf=None): if not hasattr(self, 'name') or self.name == None: self.name = self.__class__.__name__ if not hasattr(self, 'plots'): self.plots = [] self._conf = conf def _getKernelRev(self): """Get the kernel version.""" version = open("/proc/version").readline() kernel_rev = float(re.search(r'version (\d+\.\d+)\.\d+', version).group(1)) return kernel_rev def _checkKernelRev(self): """Check the linux kernel revision.""" kernel_rev = self._getKernelRev() if (kernel_rev > 2.6) or (kernel_rev < 2.4): sys.stderr.write( "Sorry, kernel v%0.1f is not supported\n" % kernel_rev) sys.exit(-1) return kernel_rev def gnuplot(self, times, host, image_prefix, data_prefix, gplot_path, chart_size, stats): parsed = self.parseStats(stats) if parsed == None: return None image_path = "%s.png" % image_prefix data_path = "%s.data" % data_prefix data = [times] labels = ["TIME"] plotlines = [] plotsno = 0 for plot in self.plots: if len(plot.plots) == 0: continue ylabel = plot.ylabel if plot.unit != "": ylabel += '[%s]' % plot.unit plotlines.append('set title "%s"' % plot.title) plotlines.append('set ylabel "%s"' % ylabel) plot_line = 'plot "%s"' % data_path li = [] for p in plot.plots.keys(): data.append(parsed[p]) labels.append(p) li.append(' u 1:%d title "%s" with %s' % (len(data), plot.plots[p][1], plot.plots[p][0])) plotlines.append(plot_line + ', ""'.join(li)) plotsno += 1 lines = [] lines.append('set output "%s"' % image_path) lines.append('set terminal png size %d,%d' % (chart_size[0], chart_size[1] * plotsno)) lines.append('set grid back') lines.append('set xdata time') lines.append('set timefmt "%H:%M:%S"') lines.append('set format x "%H:%M"') lines.append('set multiplot layout %d, 1' % plotsno) lines.extend(plotlines) data = zip(*data) f = open(data_path, 'w') f.write("%s\n" % " ".join(labels)) for line in data: f.write(' '.join([str(item) for item in line]) + '\n') f.close() f = open(gplot_path, 'w') f.write('\n'.join(lines) + '\n') f.close() return [(self.name, image_path)] def gdchart(self, x, times, host, image_prefix, stats): parsed = self.parseStats(stats) if parsed == None: return None ret = [] i = 0 for plot in self.plots: image_path = "%s_%d.png" % (image_prefix, i) i += 1 title = "%s:" % host data = [] title_parts = [] j = 0 for p in plot.plots.keys(): data.append(parsed[p]) title_parts.append(" %s (%s)" % (plot.plots[p][1], gd_colors[j][0])) j += 1 title += ", ".join(title_parts) colors = [] for c in gd_colors: colors.append(c[1]) x.title = title x.ytitle = plot.ylabel x.ylabel_fmt = '%%.2f %s' % plot.unit x.set_color = tuple(colors) x.title = title x.xtitle = 'time and CUs' x.setLabels(times) x.setData(*data) x.draw(image_path) ret.append((plot.title, image_path)) return ret def getConfig(self): return pickle.dumps(self.plots).replace("\n", "\\n") def setConfig(self, config): config = str(config.replace("\\n", "\n")) self.plots = pickle.loads(config) def getStat(self): """ Read stats from system """ pass def parseStats(self, stats): """ Parse MonitorInfo object list """ pass funkload-1.17.1/src/funkload/MonitorPluginsDefault.py000066400000000000000000000206121302537724200226540ustar00rootroot00000000000000from __future__ import absolute_import # (C) 2012 Nuxeo SAS # Authors: Krzysztof A. Adamski # bdelbosc@nuxeo.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # from .MonitorPlugins import MonitorPlugin, Plot class MonitorCUs(MonitorPlugin): plot1 = {'CUs': ['impulse', 'CUs']} plots = [Plot(plot1, title="Concurent users", ylabel="CUs")] def getStat(self): return {} def parseStats(self, stats): if not (hasattr(stats[0], 'cvus')): return None cus = [int(x.cvus) for x in stats] return {'CUs': cus} class MonitorMemFree(MonitorPlugin): plot1 = {'MEM': ['lines lw 2', 'Memory'], 'SWAP': ['lines lw 2', 'Swap']} plots = [Plot(plot1, title="Memory usage", unit="kB")] def getStat(self): meminfo_fields = ["MemTotal", "MemFree", "SwapTotal", "SwapFree", "Buffers", "Cached"] meminfo = open("/proc/meminfo") kernel_rev = self._getKernelRev() if kernel_rev <= 2.4: # Kernel 2.4 has extra lines of info, duplicate of later info meminfo.readline() meminfo.readline() meminfo.readline() lines = meminfo.readlines() meminfo.close() meminfo_stats = {} for line in lines: line = line[:-1] stats = line.split() field = stats[0][:-1] if field in meminfo_fields: meminfo_stats[field[0].lower() + field[1:]] = stats[1] return meminfo_stats def parseStats(self, stats): if not (hasattr(stats[0], 'memTotal') and hasattr(stats[0], 'memFree') and hasattr(stats[0], 'swapTotal') and hasattr(stats[0], 'swapFree')): return None mem_total = int(stats[0].memTotal) if hasattr(stats[0], 'buffers'): mem_used = [mem_total - int(x.memFree) - int(x.buffers) - int(x.cached) for x in stats] else: # old monitoring does not have cached or buffers info mem_used = [mem_total - int(x.memFree) for x in stats] mem_used_start = mem_used[0] mem_used = [x - mem_used_start for x in mem_used] swap_total = int(stats[0].swapTotal) swap_used = [swap_total - int(x.swapFree) for x in stats] swap_used_start = swap_used[0] swap_used = [x - swap_used_start for x in swap_used] return {'MEM': mem_used, 'SWAP': swap_used} class MonitorCPU(MonitorPlugin): plot1 = {'CPU': ['impulse lw 2', 'CPU 1=100%%'], 'LOAD1': ['lines lw 2', 'Load 1min'], 'LOAD5': ['lines lw 2', 'Load 5min'], 'LOAD15': ['lines lw 2', 'Load 15min']} plots = [Plot(plot1, title="Load average", ylabel="loadavg")] def getStat(self): return dict(self._getCPU().items() + self._getLoad().items()) def _getCPU(self): """Read the current system cpu usage from /proc/stat.""" lines = open("/proc/stat").readlines() for line in lines: #print "l = %s" % line l = line.split() if len(l) < 5: continue if l[0].startswith('cpu'): # cpu = sum of usr, nice, sys cpu = long(l[1]) + long(l[2]) + long(l[3]) idl = long(l[4]) return {'CPUTotalJiffies': cpu, 'IDLTotalJiffies': idl, } return {} def _getLoad(self): """Read the current system load from /proc/loadavg.""" loadavg = open("/proc/loadavg").readline() loadavg = loadavg[:-1] # Contents are space separated: # 5, 10, 15 min avg. load, running proc/total threads, last pid stats = loadavg.split() running = stats[3].split("/") load_stats = {} load_stats['loadAvg1min'] = stats[0] load_stats['loadAvg5min'] = stats[1] load_stats['loadAvg15min'] = stats[2] load_stats['running'] = running[0] load_stats['tasks'] = running[1] return load_stats def parseStats(self, stats): if not (hasattr(stats[0], 'loadAvg1min') and hasattr(stats[0], 'loadAvg5min') and hasattr(stats[0], 'loadAvg15min')): return None cpu_usage = [0] for i in range(1, len(stats)): if not (hasattr(stats[i], 'CPUTotalJiffies') and hasattr(stats[i - 1], 'CPUTotalJiffies')): cpu_usage.append(None) else: dt = ((long(stats[i].IDLTotalJiffies) + long(stats[i].CPUTotalJiffies)) - (long(stats[i - 1].IDLTotalJiffies) + long(stats[i - 1].CPUTotalJiffies))) if dt: ttl = (float(long(stats[i].CPUTotalJiffies) - long(stats[i - 1].CPUTotalJiffies)) / dt) else: ttl = None cpu_usage.append(ttl) load_avg_1 = [float(x.loadAvg1min) for x in stats] load_avg_5 = [float(x.loadAvg5min) for x in stats] load_avg_15 = [float(x.loadAvg15min) for x in stats] return {'LOAD1': load_avg_1, 'LOAD5': load_avg_5, 'LOAD15': load_avg_15, 'CPU': cpu_usage} class MonitorNetwork(MonitorPlugin): interface = 'eth0' plot1 = {'NETIN': ['lines lw 2', 'In'], 'NETOUT': ['lines lw 2', 'Out']} plots = [Plot(plot1, title="Network traffic", ylabel="", unit="kB")] def __init__(self, conf): super(MonitorNetwork, self).__init__(conf) if conf != None: self.interface = conf.get('server', 'interface') def getStat(self): """Read the stats from an interface.""" ifaces = open("/proc/net/dev") # Skip the information banner ifaces.readline() ifaces.readline() # Read the rest of the lines lines = ifaces.readlines() ifaces.close() # Process the interface lines net_stats = {} for line in lines: # Parse the interface line # Interface is followed by a ':' and then bytes, possibly with # no spaces between : and bytes line = line[:-1] (device, data) = line.split(':') # Get rid of leading spaces device = device.lstrip() # get the stats stats = data.split() if device == self.interface: net_stats['receiveBytes'] = stats[0] net_stats['receivePackets'] = stats[1] net_stats['transmitBytes'] = stats[8] net_stats['transmitPackets'] = stats[9] return net_stats def parseStats(self, stats): if not (hasattr(stats[0], 'transmitBytes') or hasattr(stats[0], 'receiveBytes')): return None net_in = [None] net_out = [None] for i in range(1, len(stats)): if not (hasattr(stats[i], 'receiveBytes') and hasattr(stats[i - 1], 'receiveBytes')): net_in.append(None) else: net_in.append((int(stats[i].receiveBytes) - int(stats[i - 1].receiveBytes)) / (1024 * (float(stats[i].time) - float(stats[i - 1].time)))) if not (hasattr(stats[i], 'transmitBytes') and hasattr(stats[i - 1], 'transmitBytes')): net_out.append(None) else: net_out.append((int(stats[i].transmitBytes) - int(stats[i - 1].transmitBytes)) / (1024 * (float(stats[i].time) - float(stats[i - 1].time)))) return {'NETIN': net_in, 'NETOUT': net_out} funkload-1.17.1/src/funkload/PatchWebunit.py000066400000000000000000000473571302537724200207720ustar00rootroot00000000000000# (C) Copyright 2005 Nuxeo SAS # Author: bdelbosc@nuxeo.com # Contributors: Tom Lazar # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """Patching Richard Jones' webunit for FunkLoad. * Add cache for links (css, js) * store a browser history * add headers * log response * remove webunit log * fix HTTPResponse __repr__ * patching webunit mimeEncode to be rfc 1945 3.6.2 compliant using CRLF * patching to remove cookie with a 'deleted' value * patching to have application/x-www-form-urlencoded by default and only multipart when a file is posted * patch fetch postdata must be [(key, value) ...] no more dict or list value $Id: PatchWebunit.py 24649 2005-08-29 14:20:19Z bdelbosc $ """ from __future__ import print_function from __future__ import absolute_import import os import sys import time import urlparse from urllib import urlencode import httplib import cStringIO from mimetypes import guess_type import datetime import Cookie from webunit import cookie from webunit.IMGSucker import IMGSucker from webunit.webunittest import WebTestCase, WebFetcher from webunit.webunittest import HTTPResponse, HTTPError, VERBOSE from webunit.utility import Upload from .utils import thread_sleep, Data import re valid_url = re.compile(r'^(http|https)://[a-z0-9\.\-\:]+(\/[^\ \t\<\>]*)?$', re.I) BOUNDARY = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254' SEP_BOUNDARY = '--' + BOUNDARY END_BOUNDARY = SEP_BOUNDARY + '--' def mimeEncode(data, sep_boundary=SEP_BOUNDARY, end_boundary=END_BOUNDARY): '''Take the mapping of data and construct the body of a multipart/form-data message with it using the indicated boundaries. ''' ret = cStringIO.StringIO() first_part = True for key, value in data: if not key: continue # Don't add newline before first part if first_part: first_part = False else: ret.write('\r\n') ret.write(sep_boundary) if isinstance(value, Upload): ret.write('\r\nContent-Disposition: form-data; name="%s"'%key) ret.write('; filename="%s"\r\n' % os.path.basename(value.filename)) if value.filename: mimetype = guess_type(value.filename)[0] if mimetype is not None: ret.write('Content-Type: %s\r\n' % mimetype) value = open(os.path.join(value.filename), "rb").read() else: value = '' ret.write('\r\n') else: ret.write('\r\nContent-Disposition: form-data; name="%s"'%key) ret.write("\r\n\r\n") ret.write(str(value)) if value and value[-1] == '\r': ret.write('\r\n') # write an extra newline ret.write('\r\n') ret.write(end_boundary) ret.write('\r\n') return ret.getvalue() class FKLIMGSucker(IMGSucker): """Image and links loader, patched to log response stats.""" def __init__(self, url, session, ftestcase=None): IMGSucker.__init__(self, url, session) self.ftestcase = ftestcase def do_img(self, attributes): """Process img tag.""" newattributes = [] for name, value in attributes: if name == 'src': # construct full url url = urlparse.urljoin(self.base, value) # make sure it's syntactically valid if not valid_url.match(url): continue # TODO: figure the re-write path # newattributes.append((name, path)) if url not in self.session.images: self.ftestcase.logdd(' img: %s ...' % url) t_start = time.time() self.session.images[url] = self.session.fetch(url) t_stop = time.time() self.ftestcase.logdd(' Done in %.3fs' % (t_stop - t_start)) self.session.history.append(('image', url)) self.ftestcase.total_time += (t_stop - t_start) self.ftestcase.total_images += 1 self.ftestcase._log_response(self.session.images[url], 'image', None, t_start, t_stop) thread_sleep() # give a chance to other threads else: newattributes.append((name, value)) # Write the img tag to file (with revised paths) self.unknown_starttag('img', newattributes) def do_link(self, attributes): """Process link tag.""" newattributes = [('rel', 'stylesheet'), ('type', 'text/css')] for name, value in attributes: if name == 'href': # construct full url url = urlparse.urljoin(self.base, value) # make sure it's syntactically valid if not valid_url.match(url): continue # TODO: figure the re-write path # newattributes.append((name, path)) if url not in self.session.css: self.ftestcase.logdd(' link: %s ...' % url) t_start = time.time() self.session.css[url] = self.session.fetch(url) t_stop = time.time() self.ftestcase.logdd(' Done in %.3fs' % (t_stop - t_start)) self.session.history.append(('link', url)) self.ftestcase.total_time += (t_stop - t_start) self.ftestcase.total_links += 1 self.ftestcase._log_response(self.session.css[url], 'link', None, t_start, t_stop) thread_sleep() # give a chance to other threads else: newattributes.append((name, value)) # Write the link tag to file (with revised paths) self.unknown_starttag('link', newattributes) # remove webunit logging def WTC_log(self, message, content): """Remove webunit logging.""" pass WebTestCase.log = WTC_log def decodeCookies(url, server, headers, cookies): """Decode cookies into the supplied cookies dictionary, according to RFC 6265. Relevant specs: http://www.ietf.org/rfc/rfc2109.txt (obsolete) http://www.ietf.org/rfc/rfc2965.txt (obsolete) http://www.ietf.org/rfc/rfc6265.txt (proposed standard) """ # see rfc 6265, section 5.1.4 # empty path => '/' # path must begin with '/', so we only weed out the rightmost '/' request_path = urlparse.urlparse(url)[2] if len(request_path) > 2 and request_path[-1] == '/': request_path = request_path[:-1] else: request_path = '/' # XXX - tried slurping all the set-cookie and joining them on # '\n', some cookies were not parsed. This below worked flawlessly. for ch in headers.getallmatchingheaders('set-cookie'): cookie = Cookie.SimpleCookie(ch.strip()).values()[0] # see rfc 6265, section 5.3, step 7 path = cookie['path'] or request_path # see rfc 6265, section 5.3, step 4 to 6 # XXX - we don't bother with cookie persistence # XXX - we don't check for public suffixes if cookie['domain']: domain = cookie['domain'] # see rfc6265, section 5.2.3 if domain[0] == '.': domain = domain[1:] if not server.endswith(domain): continue else: domain = server # all date handling is done is UTC # XXX - need reviewing by someone familiar with python datetime objects now = datetime.datetime.utcnow() expire = datetime.datetime.min maxage = cookie['max-age'] # see rfc 6265, section 5.3, step 3 if maxage != '': timedelta = int(maxage) if timedelta > 0: expire = now + datetime.timedelta(seconds=timedelta) else: if cookie['expires'] == '': expire = datetime.datetime.max else: expire = datetime.datetime.strptime(cookie['expires'],"%a, %d-%b-%Y %H:%M:%S %Z") cookie['expires'] = expire bydom = cookies.setdefault(domain, {}) bypath = bydom.setdefault(path, {}) if expire > now: bypath[cookie.key] = cookie elif cookie.key in bypath: del bypath[cookie.key] # use fl img sucker def WTC_pageImages(self, url, page, testcase=None): '''Given the HTML page that was loaded from url, grab all the images. ''' sucker = FKLIMGSucker(url, self, testcase) sucker.feed(page) sucker.close() WebTestCase.pageImages = WTC_pageImages # WebFetcher fetch def WF_fetch(self, url, postdata=None, server=None, port=None, protocol=None, ok_codes=None, key_file=None, cert_file=None, method="GET", consumer=None): '''Run a single test request to the indicated url. Use the POST data if supplied. Accepts key and certificate file paths for https (ssl/tls) connections. Raises failureException if the returned data contains any of the strings indicated to be Error Content. Returns a HTTPReponse object wrapping the response from the server. ''' # see if the url is fully-qualified (not just a path) t_protocol, t_server, t_url, x, t_args, x = urlparse.urlparse(url) if t_server: protocol = t_protocol if ':' in t_server: server, port = t_server.split(':') else: server = t_server if protocol == 'http': port = '80' else: port = '443' url = t_url if t_args: url = url + '?' + t_args # ignore the machine name if the URL is for localhost if t_server == 'localhost': server = None elif not server: # no server was specified with this fetch, or in the URL, so # see if there's a base URL to use. base = self.get_base_url() if base: t_protocol, t_server, t_url, x, x, x = urlparse.urlparse(base) if t_protocol: protocol = t_protocol if t_server: server = t_server if t_url: url = urlparse.urljoin(t_url, url) # TODO: allow override of the server and port from the URL! if server is None: server = self.server if port is None: port = self.port if protocol is None: protocol = self.protocol if ok_codes is None: ok_codes = self.expect_codes webproxy = {} if protocol == 'http': try: proxystring = os.environ["http_proxy"].replace("http://", "") webproxy['host'] = proxystring.split(":")[0] webproxy['port'] = int(proxystring.split(":")[1]) except (KeyError, IndexError, ValueError): webproxy = False if webproxy: h = httplib.HTTPConnection(webproxy['host'], webproxy['port']) else: h = httplib.HTTP(server, int(port)) if int(port) == 80: host_header = server else: host_header = '%s:%s' % (server, port) elif protocol == 'https': #if httpslib is None: #raise ValueError, "Can't fetch HTTPS: M2Crypto not installed" # FL Patch ------------------------- try: proxystring = os.environ["https_proxy"].replace("http://", "").replace("https://", "") webproxy['host'] = proxystring.split(":")[0] webproxy['port'] = int(proxystring.split(":")[1]) except (KeyError, IndexError, ValueError): webproxy = False # patched to use the given key and cert file if webproxy: h = httplib.HTTPSConnection(webproxy['host'], webproxy['port'], key_file, cert_file) else: h = httplib.HTTPS(server, int(port), key_file, cert_file) # FL Patch end ------------------------- if int(port) == 443: host_header = server else: host_header = '%s:%s' % (server, port) else: raise ValueError(protocol) headers = [] params = None if postdata is not None: if webproxy: h.putrequest(method.upper(), "%s://%s%s" % (protocol, host_header, url)) else: # Normal post h.putrequest(method.upper(), url) if postdata: if isinstance(postdata, Data): # User data and content_type params = postdata.data if postdata.content_type: headers.append(('Content-type', postdata.content_type)) else: # Check for File upload is_multipart = False for field, value in postdata: if isinstance(value, Upload): # Post with a data file requires multipart mimeencode is_multipart = True break if is_multipart: params = mimeEncode(postdata) headers.append(('Content-type', 'multipart/form-data; boundary=%s'% BOUNDARY)) else: params = urlencode(postdata) headers.append(('Content-type', 'application/x-www-form-urlencoded')) headers.append(('Content-length', str(len(params)))) else: if webproxy: h.putrequest(method.upper(), "%s://%s%s" % (protocol, host_header, url)) else: h.putrequest(method.upper(), url) # Other Full Request headers if self.authinfo: headers.append(('Authorization', "Basic %s"%self.authinfo)) #If a value is specified for 'Host' then another value should not be appended if not webproxy and not 'Host' in [k for k, v in self.extra_headers]: # HTTPConnection seems to add a host header itself. # So we only need to do this if we are not using a proxy. headers.append(('Host', host_header)) # FL Patch ------------------------- for key, value in self.extra_headers: headers.append((key, value)) # FL Patch end --------------------- # Send cookies # - check the domain, max-age (seconds), path and secure # (http://www.ietf.org/rfc/rfc2109.txt) cookies_used = [] cookie_list = [] for domain, cookies in self.cookies.items(): # check cookie domain if not server.endswith(domain) and domain[1:] != server: continue for path, cookies in cookies.items(): # check that the path matches urlpath = urlparse.urlparse(url)[2] if not urlpath.startswith(path) and not (path == '/' and urlpath == ''): continue for sendcookie in cookies.values(): # and that the cookie is or isn't secure if sendcookie['secure'] and protocol != 'https': continue # TODO: check for expires (max-age is working) # hard coded value that application can use to work # around expires if sendcookie.coded_value in ('"deleted"', "null", "deleted"): continue cookie_list.append("%s=%s;"%(sendcookie.key, sendcookie.coded_value)) cookies_used.append(sendcookie.key) if cookie_list: headers.append(('Cookie', ' '.join(cookie_list))) # check that we sent the cookies we expected to if self.expect_cookies is not None: assert cookies_used == self.expect_cookies, \ "Didn't use all cookies (%s expected, %s used)"%( self.expect_cookies, cookies_used) # write and finish the headers for header in headers: h.putheader(*header) h.endheaders() if self.debug_headers: for header in headers: print("Putting header -- %s: %s" % header) if params is not None: h.send(params) # handle the reply if webproxy: r = h.getresponse() errcode = r.status errmsg = r.reason headers = r.msg if headers is None or 'content-length' in headers and headers['content-length'] == "0": data = None else: data = r.read() response = HTTPResponse(self.cookies, protocol, server, port, url, errcode, errmsg, headers, data, self.error_content) else: # get the body and save it errcode, errmsg, headers = h.getreply() if headers is None or 'content-length' in headers and headers['content-length'] == "0": response = HTTPResponse(self.cookies, protocol, server, port, url, errcode, errmsg, headers, None, self.error_content) else: f = h.getfile() g = cStringIO.StringIO() if consumer is None: d = f.read() else: d = f.readline(1) while d: g.write(d) if consumer is None: d = f.read() else: ret = consumer(d) if ret == 0: # consumer close connection d = None else: d = f.readline(1) response = HTTPResponse(self.cookies, protocol, server, port, url, errcode, errmsg, headers, g.getvalue(), self.error_content) f.close() if errcode not in ok_codes: if VERBOSE: sys.stdout.write('e') sys.stdout.flush() raise HTTPError(response) # decode the cookies if self.accept_cookies: try: # decode the cookies and update the cookies store decodeCookies(url, server, headers, self.cookies) except: if VERBOSE: sys.stdout.write('c') sys.stdout.flush() raise # Check errors if self.error_content: data = response.body for content in self.error_content: if data.find(content) != -1: msg = "Matched error: %s" % content if hasattr(self, 'results') and self.results: self.writeError(url, msg) self.log('Matched error'+repr((url, content)), data) if VERBOSE: sys.stdout.write('c') sys.stdout.flush() raise self.failureException(msg) if VERBOSE: sys.stdout.write('_') sys.stdout.flush() return response WebFetcher.fetch = WF_fetch def HR___repr__(self): """fix HTTPResponse rendering.""" return """""" % ( self.protocol, self.server, self.port, self.url, self.code, self.message) HTTPResponse.__repr__ = HR___repr__ funkload-1.17.1/src/funkload/Recorder.py000066400000000000000000000377601302537724200201370ustar00rootroot00000000000000# (C) Copyright 2005 Nuxeo SAS # Author: bdelbosc@nuxeo.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """TCPWatch FunkLoad Test Recorder. Requires tcpwatch-httpproxy or tcpwatch.py available at: * http://hathawaymix.org/Software/TCPWatch/tcpwatch-1.3.tar.gz Credits goes to Ian Bicking for parsing tcpwatch files. $Id$ """ from __future__ import print_function from __future__ import absolute_import import os import sys import re from cStringIO import StringIO from optparse import OptionParser, TitledHelpFormatter from tempfile import mkdtemp import rfc822 from cgi import FieldStorage from urlparse import urlsplit from .utils import truncate, trace, get_version, Data def get_null_file(): if sys.platform.lower().startswith('win'): return "NUL" else: return "/dev/null" class Request: """Store a tcpwatch request.""" def __init__(self, file_path): """Load a tcpwatch request file.""" self.file_path = file_path f = open(file_path, 'rb') line = f.readline().replace('\r\r', '\r').split(None, 2) if not line: trace('# Warning: empty first line on %s\n' % self.file_path) line = f.readline().replace('\r\r', '\r').split(None, 2) self.method = line[0] url = line[1] scheme, host, path, query, fragment = urlsplit(url) self.host = scheme + '://' + host self.rurl = url[len(self.host):] self.url = url self.path = path self.version = line[2].strip() self.headers = dict(rfc822.Message(f).items()) self.body = f.read().replace('\r\r\n','', 1).replace('\r\r', '\r').replace('\r\n', '\n') f.close() def extractParam(self): """Turn muti part encoded form into params.""" params = [] try: environ = { 'CONTENT_TYPE': self.headers['content-type'], 'CONTENT_LENGTH': self.headers['content-length'], 'REQUEST_METHOD': 'POST', } except KeyError: trace('# Warning: missing header content-type or content-length' ' in file: %s not an http request ?\n' % self.file_path) return params form = FieldStorage(fp=StringIO(self.body), environ=environ, keep_blank_values=True) try: keys = form.keys() except TypeError: trace('# Using custom data for request: %s ' % self.file_path) params = Data(self.headers['content-type'], self.body) return params for item in form.list: key = item.name value = item.value filename = item.filename if filename is None: params.append([key, value]) else: # got a file upload filename = filename or '' params.append([key, 'Upload("%s")' % filename]) if filename: if os.path.exists(filename): trace('# Warning: uploaded file: %s already' ' exists, keep it.\n' % filename) else: trace('# Saving uploaded file: %s\n' % filename) f = open(filename, 'w') f.write(str(value)) f.close() return params def __repr__(self): params = '' if self.body: params = self.extractParam() return '' % ( self.method, self.url, str(params)) class Response: """Store a tcpwatch response.""" def __init__(self, file_path): """Load a tcpwatch response file.""" self.file_path = file_path f = open(file_path, 'rb') line = f.readline().split(None, 2) self.version = line[0] self.status_code = line[1].strip() if len(line) > 2: self.status_message = line[2].strip() else: self.status_message = '' self.headers = dict(rfc822.Message(f).items()) self.body = f.read() f.close() def __repr__(self): return '' % ( self.status_code, self.headers.get('content-type'), self.status_message) class RecorderProgram: """A tcpwatch to funkload recorder.""" tcpwatch_cmd = ['tcpwatch-httpproxy', 'tcpwatch.py', 'tcpwatch'] MYFACES_STATE = 'org.apache.myfaces.trinidad.faces.STATE' MYFACES_FORM = 'org.apache.myfaces.trinidad.faces.FORM' USAGE = """%prog [options] [test_name] %prog launch a TCPWatch proxy and record activities, then output a FunkLoad script or generates a FunkLoad unit test if test_name is specified. The default proxy port is 8090. Note that tcpwatch-httpproxy or tcpwatch.py executable must be accessible from your env. See http://funkload.nuxeo.org/ for more information. Examples ======== %prog foo_bar Run a proxy and create a FunkLoad test case, generates test_FooBar.py and FooBar.conf file. To test it: fl-run-test -dV test_FooBar.py %prog -p 9090 Run a proxy on port 9090, output script to stdout. %prog -i /tmp/tcpwatch Convert a tcpwatch capture into a script. """ def __init__(self, argv=None): if argv is None: argv = sys.argv[1:] self.verbose = False self.tcpwatch_path = None self.prefix = 'watch' self.port = "8090" self.server_url = None self.class_name = None self.test_name = None self.loop = 1 self.script_path = None self.configuration_path = None self.use_myfaces = False self.parseArgs(argv) def getTcpWatchCmd(self): """Return the tcpwatch cmd to use.""" tcpwatch_cmd = self.tcpwatch_cmd[:] if os.getenv("TCPWATCH"): tcpwatch_cmd.insert(0, os.getenv("TCPWATCH")) for cmd in tcpwatch_cmd: ret = os.system(cmd + ' -h 2> %s' % get_null_file()) if ret == 0: return cmd raise RuntimeError('Tcpwatch is not installed no %s found. ' 'Visit http://funkload.nuxeo.org/INSTALL.html' % str(self.tcpwatch_cmd)) def parseArgs(self, argv): """Parse programs args.""" parser = OptionParser(self.USAGE, formatter=TitledHelpFormatter(), version="FunkLoad %s" % get_version()) parser.add_option("-v", "--verbose", action="store_true", help="Verbose output") parser.add_option("-p", "--port", type="string", dest="port", default=self.port, help="The proxy port.") parser.add_option("-L", type="string", dest="forward", default=None, help="Forwarded connection ::.") parser.add_option("-i", "--tcp-watch-input", type="string", dest="tcpwatch_path", default=None, help="Path to an existing tcpwatch capture.") parser.add_option("-l", "--loop", type="int", dest="loop", default=1, help="Loop mode.") options, args = parser.parse_args(argv) if len(args) == 1: test_name = args[0] else: test_name = None self.verbose = options.verbose self.tcpwatch_path = options.tcpwatch_path self.port = options.port self.forward = options.forward if not test_name and not self.tcpwatch_path: self.loop = options.loop if test_name: test_name = test_name.replace('-', '_') class_name = ''.join([x.capitalize() for x in re.split('_|-', test_name)]) self.test_name = test_name self.class_name = class_name self.script_path = './test_%s.py' % class_name self.configuration_path = './%s.conf' % class_name def startProxy(self): """Start a tcpwatch session.""" self.tcpwatch_path = mkdtemp('_funkload') if self.forward is not None: cmd = self.getTcpWatchCmd() + ' -L %s -s -r %s' % (self.forward, self.tcpwatch_path) else: cmd = self.getTcpWatchCmd() + ' -p %s -s -r %s' % (self.port, self.tcpwatch_path) if os.name == 'posix': if self.verbose: cmd += ' | grep "T http"' else: cmd += ' > %s' % get_null_file() trace("Hit Ctrl-C to stop recording.\n") try: os.system(cmd) except KeyboardInterrupt: pass def searchFiles(self): """Search tcpwatch file.""" items = {} prefix = self.prefix for filename in os.listdir(self.tcpwatch_path): if not filename.startswith(prefix): continue name, ext = os.path.splitext(filename) name = name[len(self.prefix):] ext = ext[1:] if ext == 'errors': trace("Error in response %s\n" % name) continue assert ext in ('request', 'response'), "Bad extension: %r" % ext items.setdefault(name, {})[ext] = os.path.join( self.tcpwatch_path, filename) items = items.items() items.sort() return [(v['request'], v['response']) for name, v in items if 'response' in v] def extractRequests(self, files): """Filter and extract request from tcpwatch files.""" last_code = None filter_ctypes = ('image', 'css', 'javascript', 'x-shockwave-flash') filter_url = ('.jpg', '.png', '.gif', '.css', '.js', '.swf') requests = [] for request_path, response_path in files: response = Response(response_path) request = Request(request_path) if self.server_url is None: self.server_url = request.host ctype = response.headers.get('content-type', '') url = request.url if request.method != "POST" and ( last_code in ('301', '302') or [x for x in filter_ctypes if x in ctype] or [x for x in filter_url if url.endswith(x)]): last_code = response.status_code continue last_code = response.status_code requests.append(request) return requests def reindent(self, code, indent=8): """Improve indentation.""" spaces = ' ' * indent code = code.replace('], [', '],\n%s [' % spaces) code = code.replace('[[', '[\n%s [' % spaces) code = code.replace(', description=', ',\n%s description=' % spaces) code = code.replace('self.', '\n%sself.' % spaces) return code def convertToFunkLoad(self, request): """return a funkload python instruction.""" text = [] text.append(' # ' + request.file_path) if request.host != self.server_url: text.append('self.%s("%s"' % (request.method.lower(), request.url)) else: text.append('self.%s(server_url + "%s"' % ( request.method.lower(), request.rurl.strip())) description = "%s %s" % (request.method.capitalize(), request.path | truncate(42)) if request.body: params = request.extractParam() if isinstance(params, Data): params = "Data('%s', '''%s''')" % (params.content_type, params.data) else: myfaces_form = None if self.MYFACES_STATE not in [key for key, value in params]: params = 'params=%s' % params else: # apache myfaces state add a wrapper self.use_myfaces = True new_params = [] for key, value in params: if key == self.MYFACES_STATE: continue if key == self.MYFACES_FORM: myfaces_form = value continue new_params.append([key, value]) params = " self.myfacesParams(%s, form='%s')" % ( new_params, myfaces_form) params = re.sub("'Upload\(([^\)]*)\)'", "Upload(\\1)", params) text.append(', ' + params) text.append(', description="%s")' % description) return ''.join(text) def extractScript(self): """Convert a tcpwatch capture into a FunkLoad script.""" files = self.searchFiles() requests = self.extractRequests(files) code = [self.convertToFunkLoad(request) for request in requests] if not code: trace("Sorry no action recorded.\n") return '' code.insert(0, '') return self.reindent('\n'.join(code)) def writeScript(self, script): """Write the FunkLoad test script.""" trace('Creating script: %s.\n' % self.script_path) from pkg_resources import resource_string if self.use_myfaces: tpl_name = 'data/MyFacesScriptTestCase.tpl' else: tpl_name = 'data/ScriptTestCase.tpl' tpl = resource_string('funkload', tpl_name) content = tpl % {'script': script, 'test_name': self.test_name, 'class_name': self.class_name} if os.path.exists(self.script_path): trace("Error file %s already exists.\n" % self.script_path) return f = open(self.script_path, 'w') f.write(content) f.close() def writeConfiguration(self): """Write the FunkLoad configuration test script.""" trace('Creating configuration file: %s.\n' % self.configuration_path) from pkg_resources import resource_string tpl = resource_string('funkload', 'data/ConfigurationTestCase.tpl') content = tpl % {'server_url': self.server_url, 'test_name': self.test_name, 'class_name': self.class_name} if os.path.exists(self.configuration_path): trace("Error file %s already exists.\n" % self.configuration_path) return f = open(self.configuration_path, 'w') f.write(content) f.close() def run(self): """run it.""" count = self.loop while count: count -= 1 if count: print("Remaining loop: %i" % count) if self.tcpwatch_path is None: self.startProxy() script = self.extractScript() if not script: self.tcpwatch_path = None continue if self.test_name is not None: self.writeScript(script) self.writeConfiguration() else: print(script) print() self.tcpwatch_path = None def main(): RecorderProgram().run() if __name__ == '__main__': main() funkload-1.17.1/src/funkload/ReportBuilder.py000066400000000000000000000331441302537724200211440ustar00rootroot00000000000000# (C) Copyright 2005-2011 Nuxeo SAS # Author: bdelbosc@nuxeo.com # Contributors: # Krzysztof A. Adamski # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """Create an ReST or HTML report with charts from a FunkLoad bench xml result. Producing html and png chart require python-docutils and gnuplot $Id: ReportBuilder.py 24737 2005-08-31 09:00:16Z bdelbosc $ """ from __future__ import print_function from __future__ import absolute_import USAGE = """%prog [options] xmlfile [xmlfile...] or %prog --diff REPORT_PATH1 REPORT_PATH2 %prog analyze a FunkLoad bench xml result file and output a report. If there are more than one file the xml results are merged. See http://funkload.nuxeo.org/ for more information. Examples ======== %prog funkload.xml ReST rendering into stdout. %prog --html -o /tmp funkload.xml Build an HTML report in /tmp %prog --html node1.xml node2.xml node3.xml Build an HTML report merging test results from 3 nodes. %prog --diff /path/to/report-reference /path/to/report-challenger Build a differential report to compare 2 bench reports, requires gnuplot. %prog --trend /path/to/report-dir1 /path/to/report-1 ... /path/to/report-n Build a trend report using multiple reports. %prog -h More options. """ try: import psyco psyco.full() except ImportError: pass import os import xml.parsers.expat from optparse import OptionParser, TitledHelpFormatter from tempfile import NamedTemporaryFile from .ReportStats import AllResponseStat, PageStat, ResponseStat, TestStat from .ReportStats import MonitorStat, ErrorStat from .ReportRenderRst import RenderRst from .ReportRenderHtml import RenderHtml from .ReportRenderDiff import RenderDiff from .ReportRenderTrend import RenderTrend from .MergeResultFiles import MergeResultFiles from .utils import trace, get_version from .apdex import Apdex # ------------------------------------------------------------ # Xml parser # class FunkLoadXmlParser: """Parse a funkload xml results.""" def __init__(self): """Init setup expat handlers.""" parser = xml.parsers.expat.ParserCreate() parser.CharacterDataHandler = self.handleCharacterData parser.StartElementHandler = self.handleStartElement parser.EndElementHandler = self.handleEndElement parser.StartCdataSectionHandler = self.handleStartCdataSection parser.EndCdataSectionHandler = self.handleEndCdataSection self.parser = parser self.current_element = [{'name': 'root'}] self.is_recording_cdata = False self.current_cdata = '' self.cycles = None self.cycle_duration = 0 self.stats = {} # cycle stats self.monitor = {} # monitoring stats self.monitorconfig = {} # monitoring config self.config = {} self.error = {} def parse(self, xml_file): """Do the parsing.""" try: self.parser.ParseFile(file(xml_file)) except xml.parsers.expat.ExpatError as msg: if (self.current_element[-1]['name'] == 'funkload' and str(msg).startswith('no element found')): print("Missing tag.") else: print('Error: invalid xml bench result file') if len(self.current_element) <= 1 or ( self.current_element[1]['name'] != 'funkload'): print("""Note that you can generate a report only for a bench result done with fl-run-bench (and not on a test resu1lt done with fl-run-test).""") else: print("""You may need to remove non ascii characters which come from error pages caught during the bench test. iconv or recode may help you.""") print('Xml parser element stack: %s' % [ x['name'] for x in self.current_element]) raise def handleStartElement(self, name, attrs): """Called by expat parser on start element.""" if name == 'funkload': self.config['version'] = attrs['version'] self.config['time'] = attrs['time'] elif name == 'config': self.config[attrs['key']] = attrs['value'] if attrs['key'] == 'duration': self.cycle_duration = attrs['value'] elif name == 'header': # save header as extra response attribute headers = self.current_element[-2]['attrs'].setdefault( 'headers', {}) headers[str(attrs['name'])] = str(attrs['value']) self.current_element.append({'name': name, 'attrs': attrs}) def handleEndElement(self, name): """Processing element.""" element = self.current_element.pop() attrs = element['attrs'] if name == 'testResult': cycle = attrs['cycle'] stats = self.stats.setdefault(cycle, {'response_step': {}}) stat = stats.setdefault( 'test', TestStat(cycle, self.cycle_duration, attrs['cvus'])) stat.add(attrs['result'], attrs['pages'], attrs.get('xmlrpc', 0), attrs['redirects'], attrs['images'], attrs['links'], attrs['connection_duration'], attrs.get('traceback')) stats['test'] = stat elif name == 'response': cycle = attrs['cycle'] stats = self.stats.setdefault(cycle, {'response_step':{}}) stat = stats.setdefault( 'response', AllResponseStat(cycle, self.cycle_duration, attrs['cvus'])) stat.add(attrs['time'], attrs['result'], attrs['duration']) stats['response'] = stat stat = stats.setdefault( 'page', PageStat(cycle, self.cycle_duration, attrs['cvus'])) stat.add(attrs['thread'], attrs['step'], attrs['time'], attrs['result'], attrs['duration'], attrs['type']) stats['page'] = stat step = '%s.%s' % (attrs['step'], attrs['number']) stat = stats['response_step'].setdefault( step, ResponseStat(attrs['step'], attrs['number'], attrs['cvus'])) stat.add(attrs['type'], attrs['result'], attrs['url'], attrs['duration'], attrs.get('description')) stats['response_step'][step] = stat if attrs['result'] != 'Successful': result = str(attrs['result']) stats = self.error.setdefault(result, []) stats.append(ErrorStat( attrs['cycle'], attrs['step'], attrs['number'], attrs.get('code'), attrs.get('headers'), attrs.get('body'), attrs.get('traceback'))) elif name == 'monitor': host = attrs.get('host') stats = self.monitor.setdefault(host, []) stats.append(MonitorStat(attrs)) elif name =='monitorconfig': host = attrs.get('host') config = self.monitorconfig.setdefault(host, {}) config[attrs.get('key')]=attrs.get('value') def handleStartCdataSection(self): """Start recording cdata.""" self.is_recording_cdata = True self.current_cdata = '' def handleEndCdataSection(self): """Save CDATA content into the parent element.""" self.is_recording_cdata = False # assume CDATA is encapsulate in a container element name = self.current_element[-1]['name'] self.current_element[-2]['attrs'][name] = self.current_cdata self.current_cdata = '' def handleCharacterData(self, data): """Extract cdata.""" if self.is_recording_cdata: self.current_cdata += data # ------------------------------------------------------------ # main # def main(): """ReportBuilder main.""" parser = OptionParser(USAGE, formatter=TitledHelpFormatter(), version="FunkLoad %s" % get_version()) parser.add_option("-H", "--html", action="store_true", default=False, dest="html", help="Produce an html report.") parser.add_option("--org", action="store_true", default=False, dest="org", help="Org-mode report.") parser.add_option("-P", "--with-percentiles", action="store_true", default=True, dest="with_percentiles", help=("Include percentiles in tables, use 10%, 50% and" " 90% for charts, default option.")) parser.add_option("--no-percentiles", action="store_false", dest="with_percentiles", help=("No percentiles in tables display min, " "avg and max in charts.")) cur_path = os.path.abspath(os.path.curdir) parser.add_option("-d", "--diff", action="store_true", default=False, dest="diffreport", help=("Create differential report.")) parser.add_option("-t", "--trend", action="store_true", default=False, dest="trendreport", help=("Build a trend reprot.")) parser.add_option("-o", "--output-directory", type="string", dest="output_dir", help="Parent directory to store reports, the directory" "name of the report will be generated automatically.", default=cur_path) parser.add_option("-r", "--report-directory", type="string", dest="report_dir", help="Directory name to store the report.", default=None) parser.add_option("-T", "--apdex-T", type="float", dest="apdex_t", help="Apdex T constant in second, default is set to 1.5s. " "Visit http://www.apdex.org/ for more information.", default=Apdex.T) parser.add_option("-x", "--css", type="string", dest="css_file", help="Custom CSS file to use for the HTML reports", default=None) parser.add_option("", "--skip-definitions", action="store_true", default=False, dest="skip_definitions", help="If True, will skip the definitions") parser.add_option("-q", "--quiet", action="store_true", default=False, dest="quiet", help=("Report no system messages when generating" " html from rst.")) options, args = parser.parse_args() if options.diffreport: if len(args) != 2: parser.error("incorrect number of arguments") trace("Creating diff report ... ") output_dir = options.output_dir html_path = RenderDiff(args[0], args[1], options, css_file=options.css_file) trace("done: \n") trace("%s\n" % html_path) elif options.trendreport: if len(args) < 2: parser.error("incorrect number of arguments") trace("Creating trend report ... ") output_dir = options.output_dir html_path = RenderTrend(args, options, css_file=options.css_file) trace("done: \n") trace("%s\n" % html_path) else: if len(args) < 1: parser.error("incorrect number of arguments") if len(args) > 1: trace("Merging results files: ") f = NamedTemporaryFile(prefix='fl-mrg-', suffix='.xml') tmp_file = f.name f.close() MergeResultFiles(args, tmp_file) trace("Results merged in tmp file: %s\n" % os.path.abspath(tmp_file)) args = [tmp_file] options.xml_file = args[0] Apdex.T = options.apdex_t xml_parser = FunkLoadXmlParser() xml_parser.parse(options.xml_file) if options.html: trace("Creating html report: ...") html_path = RenderHtml(xml_parser.config, xml_parser.stats, xml_parser.error, xml_parser.monitor, xml_parser.monitorconfig, options, css_file=options.css_file)() trace("done: \n") trace(html_path + "\n") elif options.org: from .ReportRenderOrg import RenderOrg print(unicode(RenderOrg(xml_parser.config, xml_parser.stats, xml_parser.error, xml_parser.monitor, xml_parser.monitorconfig, options)).encode("utf-8")) else: print(unicode(RenderRst(xml_parser.config, xml_parser.stats, xml_parser.error, xml_parser.monitor, xml_parser.monitorconfig, options)).encode("utf-8")) if __name__ == '__main__': main() funkload-1.17.1/src/funkload/ReportRenderDiff.py000066400000000000000000000250231302537724200215630ustar00rootroot00000000000000# (C) Copyright 2008 Nuxeo SAS # Author: bdelbosc@nuxeo.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """Classes that render a differential report $Id$ """ from __future__ import print_function from __future__ import absolute_import import os from .ReportRenderRst import rst_title from .ReportRenderHtmlBase import RenderHtmlBase from .ReportRenderHtmlGnuPlot import gnuplot def getReadableDiffReportName(a, b): """Return a readeable diff report name using 2 reports""" a = os.path.basename(a) b = os.path.basename(b) if a == b: return "diff_" + a + "_vs_idem" for i in range(min(len(a), len(b))): if a[i] != b[i]: break for i in range(i, 0, -1): # try to keep numbers if a[i] not in "_-0123456789": i += 1 break r = b[:i] + "_" + b[i:] + "_vs_" + a[i:] if r.startswith('test_'): r = r[5:] r = r.replace('-_', '_') r = r.replace('_-', '_') r = r.replace('__', '_') return "diff_" + r def getRPath(a, b): """Return a relative path of b from a.""" a_path = a.split('/') b_path = b.split('/') for i in range(min(len(a_path), len(b_path))): if a_path[i] != b_path[i]: break return '../' * len(a_path[i:]) + '/'.join(b_path[i:]) class RenderDiff(RenderHtmlBase): """Differential report.""" report_dir1 = None report_dir2 = None header = None sep = ', ' data_file = None output_dir = None script_file = None def __init__(self, report_dir1, report_dir2, options, css_file=None): # Swap windows path separator backslashes for forward slashes # Windows accepts '/' but some file formats like rest treat the # backslash specially. self.report_dir1 = os.path.abspath(report_dir1).replace('\\', '/') self.report_dir2 = os.path.abspath(report_dir2).replace('\\', '/') self.options = options self.css_file = css_file self.quiet = options.quiet def generateReportDirectory(self, output_dir): """Generate a directory name for a report.""" output_dir = os.path.abspath(output_dir) report_dir = os.path.join(output_dir, getReadableDiffReportName( self.report_dir1, self.report_dir2)) if not os.access(report_dir, os.W_OK): os.mkdir(report_dir, 0o775) return report_dir def createCharts(self): """Render stats.""" self.createGnuplotData() self.createGnuplotScript() gnuplot(self.script_file) def createRstFile(self): """Create the ReST file.""" rst_path = os.path.join(self.report_dir, 'index.rst') lines = [] b1 = os.path.basename(self.report_dir1) b2 = os.path.basename(self.report_dir2) # Swap windows path separator backslashes for forward slashes b1_rpath = getRPath(self.report_dir.replace('\\', '/'), os.path.join(self.report_dir1, 'index.html').replace('\\', '/')) b2_rpath = getRPath(self.report_dir.replace('\\', '/'), os.path.join(self.report_dir2, 'index.html').replace('\\', '/')) if b1 == b2: b2 = b2 +"(2)" lines.append(rst_title("FunkLoad_ differential report", level=0)) lines.append("") lines.append(".. sectnum:: :depth: 2") lines.append("") lines.append(rst_title("%s vs %s" % (b2, b1), level=1)) lines.append(" * Reference bench report **B1**: `" + b1 + " <" + b1_rpath + ">`_ [#]_") lines.append(" * Challenger bench report **B2**: `" + b2 + " <" + b2_rpath + ">`_ [#]_") lines.append("") lines.append(rst_title("Requests", level=2)) lines.append(" .. image:: rps_diff.png") lines.append(" .. image:: request.png") lines.append(rst_title("Pages", level=2)) lines.append(" .. image:: spps_diff.png") escapeReportDir = lambda rd: rd.replace('\\', '/').replace('_', '\\_') lines.append(" .. [#] B1 path: " + escapeReportDir(self.report_dir1)) lines.append(" .. [#] B2 path: " + escapeReportDir(self.report_dir2)) lines.append(" .. _FunkLoad: http://funkload.nuxeo.org/") lines.append("") f = open(rst_path, 'w') f.write('\n'.join(lines)) f.close() self.rst_path = rst_path def copyXmlResult(self): pass def __repr__(self): return self.render() def extract_stat(self, tag, report_dir): """Extract stat from the ReST index file.""" lines = open(os.path.join(report_dir, "index.rst")).readlines() try: idx = lines.index("%s stats\n" % tag) except ValueError: print("ERROR tag %s not found in rst report %s" % (tag, report_dir)) return [] delim = 0 ret = [] for line in lines[idx:]: if line.startswith(" ====="): delim += 1 continue if delim == 1: self.header = line.strip().split() if delim < 2: continue if delim == 3: break ret.append([x.replace("%","") for x in line.strip().split()]) return ret def createGnuplotData(self): """Render rst stat.""" def output_stat(tag, rep): stat = self.extract_stat(tag, rep) text = [] text.append('# ' + tag + " stat for: " + rep) text.append('# ' + ' '.join(self.header)) for line in stat: text.append(' '.join(line)) return '\n'.join(text) def output_stat_diff(tag, rep1, rep2): stat1 = self.extract_stat(tag, rep1) stat2 = self.extract_stat(tag, rep2) text = [] text.append('# ' + tag + " stat for: " + rep1 + " and " + rep2) text.append('# ' + ' '.join(self.header) + ' ' + ' '.join([x+ "-2" for x in self.header])) for s1 in stat1: for s2 in stat2: if s1[0] == s2[0]: text.append(' '.join(s1) + ' ' + ' '.join(s2)) break if s1[0] != s2[0]: text.append(' '.join(s1)) return '\n'.join(text) rep1 = self.report_dir1 rep2 = self.report_dir2 data_file = os.path.join(self.report_dir, 'diffbench.dat') self.data_file = data_file f = open(data_file, 'w') f.write('# ' + rep1 + ' vs ' + rep2 + '\n') for tag, rep in (('Page', rep1), ('Page', rep2), ('Request', rep1), ('Request', rep2)): f.write(output_stat(tag, rep) + '\n\n\n') f.write(output_stat_diff('Page', rep1, rep2) + '\n\n\n') f.write(output_stat_diff('Request', rep1, rep2)) f.close() def createGnuplotScript(self): """Build gnuplot script""" script_file = os.path.join(self.report_dir, 'script.gplot') self.script_file = script_file f = open(script_file, 'w') rep1 = self.report_dir1 rep2 = self.report_dir2 f.write('# ' + rep1 + ' vs ' + rep2 + '\n') f.write('''# COMMON SETTINGS set grid back set xlabel "Concurrent Users" set boxwidth 0.9 relative set style fill solid 1 # SPPS set output "spps_diff.png" set terminal png size 640,380 set title "Successful Pages Per Second" set ylabel "SPPS" plot "diffbench.dat" i 4 u 1:4:19 w filledcurves above t "B2B1", "" i 4 u 1:4 w lines lw 2 t "B1", "" i 4 u 1:19 w lines lw 2 t "B2" # RPS set output "rps_diff.png" set terminal png size 640,380 set multiplot title "Requests Per Second (Scalability)" set title "Requests Per Second" offset 0, -2 set size 1, 0.67 set origin 0, 0.3 set ylabel "" set format x "" set xlabel "" plot "diffbench.dat" i 5 u 1:4:19 w filledcurves above t "B2B1", "" i 5 u 1:4 w lines lw 2 t "B1", "" i 5 u 1:19 w lines lw 2 t "B2" # % RPS set title "RPS B2/B1 %" offset 0, -2 set size 1, 0.33 set origin 0, 0 set format y "% g%%" set format x "% g" set xlabel "Concurrent Users" plot "diffbench.dat" i 5 u 1:($19<$4?((($19*100)/$4) - 100): 0) w boxes notitle, "" i 5 u 1:($19>=$4?((($19*100)/$4)-100): 0) w boxes notitle unset multiplot # RESPONSE TIMES set output "request.png" set terminal png size 640,640 set multiplot title "Request Response time (Velocity)" # AVG set title "Average" offset 0, -2 set size 0.5, 0.67 set origin 0, 0.30 set ylabel "" set format y "% gs" set xlabel "" set format x "" plot "diffbench.dat" i 5 u 1:25:10 w filledcurves above t "B2B1", "" i 5 u 1:10 w lines lw 2 t "B1", "" i 5 u 1:25 w lines lw 2 t "B2 # % AVG set title "Average B1/B2 %" offset 0, -2 set size 0.5, 0.31 set origin 0, 0 set format y "% g%%" set format x "% g" set xlabel "Concurrent Users" plot "diffbench.dat" i 5 u 1:($25>$10?((($10*100)/$25) - 100): 0) w boxes notitle, "" i 5 u 1:($25<=$10?((($10*100)/$25) - 100): 0) w boxes notitle # MEDIAN set size 0.5, 0.31 set format y "% gs" set xlabel "" set format x "" set title "Median" set origin 0.5, 0.66 plot "diffbench.dat" i 5 u 1:28:13 w filledcurves above notitle, "" i 5 u 1:28:13 w filledcurves below notitle, "" i 5 u 1:13 w lines lw 2 notitle, "" i 5 u 1:28 w lines lw 2 notitle # P90 set title "p90" set origin 0.5, 0.33 plot "diffbench.dat" i 5 u 1:29:14 w filledcurves above notitle, "" i 5 u 1:29:14 w filledcurves below notitle, "" i 5 u 1:14 w lines lw 2 notitle, "" i 5 u 1:29 w lines lw 2 notitle # MAX set title "Max" set origin 0.5, 0 set format x "% g" set xlabel "Concurrent Users" plot "diffbench.dat" i 5 u 1:26:11 w filledcurves above notitle, "" i 5 u 1:26:11 w filledcurves below notitle, "" i 5 u 1:11 w lines lw 2 notitle, "" i 5 u 1:26 w lines lw 2 notitle unset multiplot ''') f.close() funkload-1.17.1/src/funkload/ReportRenderHtml.py000066400000000000000000000021551302537724200216200ustar00rootroot00000000000000# (C) Copyright 2008 Nuxeo SAS # Author: bdelbosc@nuxeo.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """Choose the best html rendering $Id: ReportRenderHtml.py 53544 2009-03-09 16:28:58Z tlazar $ """ from __future__ import absolute_import try: # 1/ gnuplot from .ReportRenderHtmlGnuPlot import RenderHtmlGnuPlot as RenderHtml except ImportError: # 2/ no charts from .ReportRenderHtmlBase import RenderHtmlBase as RenderHtml from .ReportRenderHtmlGnuPlot import RenderHtmlGnuPlot as RenderHtml funkload-1.17.1/src/funkload/ReportRenderHtmlBase.py000066400000000000000000000142211302537724200224100ustar00rootroot00000000000000# (C) Copyright 2005-2011 Nuxeo SAS # Author: bdelbosc@nuxeo.com # Contributors: # Tom Lazar # Krzysztof A. Adamski # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """Html rendering $Id$ """ from __future__ import print_function from __future__ import absolute_import import os from shutil import copyfile from .ReportRenderRst import RenderRst, rst_title class RenderHtmlBase(RenderRst): """Render stats in html. Simply render stuff in ReST then ask docutils to build an html doc. """ chart_size = (350, 250) big_chart_size = (640, 480) def __init__(self, config, stats, error, monitor, monitorconfig, options, css_file=None): RenderRst.__init__(self, config, stats, error, monitor, monitorconfig, options) self.css_file = css_file self.quiet = options.quiet self.report_dir = self.css_path = self.rst_path = self.html_path = None def getChartSize(self, cvus): """Compute the right size lenght depending on the number of cvus.""" size = list(self.chart_size) len_cvus = len(cvus) chart_size = self.chart_size big_chart_size = self.big_chart_size if ((len_cvus * 50) > chart_size[0]): if (len_cvus * 50 < big_chart_size): return ((len_cvus * 50), big_chart_size[1]) return big_chart_size return chart_size def generateReportDirectory(self, output_dir): """Generate a directory name for a report.""" config = self.config stamp = config['time'][:19].replace(':', '') stamp = stamp.replace('-', '') if config.get('label', None) is None: report_dir = os.path.join(output_dir, '%s-%s' % ( config['id'], stamp)) else: report_dir = os.path.join(output_dir, '%s-%s-%s' % ( config['id'], stamp, config.get('label'))) return report_dir def prepareReportDirectory(self): """Create a report directory.""" if self.options.report_dir: report_dir = os.path.abspath(self.options.report_dir) else: # init output dir output_dir = os.path.abspath(self.options.output_dir) if not os.access(output_dir, os.W_OK): os.mkdir(output_dir, 0o775) # init report dir report_dir = self.generateReportDirectory(output_dir) if not os.access(report_dir, os.W_OK): os.mkdir(report_dir, 0o775) self.report_dir = report_dir def createRstFile(self): """Create the ReST file.""" rst_path = os.path.join(self.report_dir, 'index.rst') f = open(rst_path, 'w') f.write(unicode(self).encode("utf-8")) f.close() self.rst_path = rst_path def copyCss(self): """Copy the css to the report dir.""" css_file = self.css_file if css_file is not None: css_filename = os.path.split(css_file)[-1] css_dest_path = os.path.join(self.report_dir, css_filename) copyfile(css_file, css_dest_path) else: # use the one in our package_data from pkg_resources import resource_string css_content = resource_string('funkload', 'data/funkload.css') css_dest_path = os.path.join(self.report_dir, 'funkload.css') f = open(css_dest_path, 'w') f.write(css_content) f.close() self.css_path = css_dest_path def copyXmlResult(self): """Make a copy of the xml result.""" xml_src_path = self.options.xml_file xml_dest_path = os.path.join(self.report_dir, 'funkload.xml') copyfile(xml_src_path, xml_dest_path) def generateHtml(self): """Ask docutils to convert our rst file into html.""" from docutils.core import publish_cmdline html_path = os.path.join(self.report_dir, 'index.html') cmdline = [] if self.quiet: cmdline.append('-q') cmdline.extend(['-t', '--stylesheet-path', self.css_path, self.rst_path, html_path]) publish_cmdline(writer_name='html', argv=cmdline) self.html_path = html_path def render(self): """Create the html report.""" self.prepareReportDirectory() self.createRstFile() self.copyCss() try: self.generateHtml() pass except ImportError: print("WARNING docutils not found, no html output.") return '' self.createCharts() self.copyXmlResult() return os.path.abspath(self.html_path) __call__ = render def createCharts(self): """Create all charts.""" self.createTestChart() self.createPageChart() self.createAllResponseChart() for step_name in self.steps: self.createResponseChart(step_name) # monitoring charts def createMonitorCharts(self): """Create all montirored server charts.""" if not self.monitor or not self.with_chart: return self.append(rst_title("Monitored hosts", 2)) charts={} for host in self.monitor.keys(): charts[host]=self.createMonitorChart(host) return charts def createTestChart(self): """Create the test chart.""" def createPageChart(self): """Create the page chart.""" def createAllResponseChart(self): """Create global responses chart.""" def createResponseChart(self, step): """Create responses chart.""" def createMonitorChart(self, host): """Create monitrored server charts.""" funkload-1.17.1/src/funkload/ReportRenderHtmlGnuPlot.py000066400000000000000000000615771302537724200231460ustar00rootroot00000000000000# (C) Copyright 2009 Nuxeo SAS # Author: bdelbosc@nuxeo.com # Contributors: Kelvin Ward # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """Render chart using gnuplot >= 4.2 $Id$ """ from __future__ import print_function from __future__ import absolute_import import os import sys import re from commands import getstatusoutput from .apdex import Apdex from .ReportRenderRst import rst_title from .ReportRenderHtmlBase import RenderHtmlBase from datetime import datetime from .MonitorPlugins import MonitorPlugins from .MonitorPluginsDefault import MonitorCPU, MonitorMemFree, MonitorNetwork, MonitorCUs def gnuplot(script_path): """Execute a gnuplot script.""" path = os.path.dirname(os.path.abspath(script_path)) if sys.platform.lower().startswith('win'): # commands module doesn't work on win and gnuplot is named # wgnuplot ret = os.system('cd "' + path + '" && wgnuplot "' + os.path.abspath(script_path) + '"') if ret != 0: raise RuntimeError("Failed to run wgnuplot cmd on " + os.path.abspath(script_path)) else: cmd = 'cd "' + path + '"; gnuplot "' + os.path.abspath(script_path) + '"' ret, output = getstatusoutput(cmd) if ret != 0: raise RuntimeError("Failed to run gnuplot cmd: " + cmd + "\n" + str(output)) def gnuplot_scriptpath(base, filename): """Return a file path string from the join of base and file name for use inside a gnuplot script. Backslashes (the win os separator) are replaced with forward slashes. This is done because gnuplot scripts interpret backslashes specially even in path elements. """ return os.path.join(base, filename).replace("\\", "/") class FakeMonitorConfig: def __init__(self, name): self.name = name class RenderHtmlGnuPlot(RenderHtmlBase): """Render stats in html using gnuplot Simply render stuff in ReST then ask docutils to build an html doc. """ chart_size = (640, 540) #big_chart_size = (640, 480) ticpattern = re.compile('(\:\d+)\ ') def getChartSizeTmp(self, cvus): """Override for gnuplot format""" return str(self.chart_size[0]) + ',' + str(self.chart_size[1]) def getXRange(self): """Return the max CVUs range.""" maxCycle = self.config['cycles'].split(',')[-1] maxCycle = str(maxCycle[:-1].strip()) if maxCycle.startswith("["): maxCycle = maxCycle[1:] return "[0:" + str(int(maxCycle) + 1) + "]" def useXTicLabels(self): """Guess if we need to use labels for x axis or number.""" cycles = self.config['cycles'][1:-1].split(',') if len(cycles) <= 1: # single cycle return True if len(cycles) != len(set(cycles)): # duplicates cycles return True cycles = [int(i) for i in cycles] for i, v in enumerate(cycles[1:]): # unordered cycles if cycles[i] > v: return True return False def fixXLabels(self, lines): """Fix gnuplot script if CUs are not ordered.""" if not self.useXTicLabels(): return lines # remove xrange line out = lines.replace('set xrange', '#set xrange') # rewrite plot using xticlabels out = out.replace(' 1:', ' :') out = self.ticpattern.sub(r'\1:xticlabels(1) ', out) return out def createTestChart(self): """Create the test chart.""" image_path = gnuplot_scriptpath(self.report_dir, 'tests.png') gplot_path = str(os.path.join(self.report_dir, 'tests.gplot')) data_path = gnuplot_scriptpath(self.report_dir, 'tests.data') stats = self.stats # data lines = ["CUs STPS ERROR"] cvus = [] has_error = False for cycle in self.cycles: if 'test' not in stats[cycle]: continue values = [] test = stats[cycle]['test'] values.append(str(test.cvus)) cvus.append(str(test.cvus)) values.append(str(test.tps)) error = test.error_percent if error: has_error = True values.append(str(error)) lines.append(' '.join(values)) if len(lines) == 1: # No tests finished during the cycle return f = open(data_path, 'w') f.write('\n'.join(lines) + '\n') f.close() # script lines = ['set output "' + image_path +'"'] lines.append('set title "Successful Tests Per Second"') lines.append('set terminal png size ' + self.getChartSizeTmp(cvus)) lines.append('set xlabel "Concurrent Users"') lines.append('set ylabel "Test/s"') lines.append('set grid back') lines.append('set xrange ' + self.getXRange()) if not has_error: lines.append('plot "%s" u 1:2 w linespoints lw 2 lt 2 t "STPS"' % data_path) else: lines.append('set format x ""') lines.append('set multiplot') lines.append('unset title') lines.append('unset xlabel') lines.append('set size 1, 0.7') lines.append('set origin 0, 0.3') lines.append('set lmargin 5') lines.append('set bmargin 0') lines.append('plot "%s" u 1:2 w linespoints lw 2 lt 2 t "STPS"' % data_path) lines.append('set format x "% g"') lines.append('set bmargin 3') lines.append('set autoscale y') lines.append('set style fill solid .25') lines.append('set size 1.0, 0.3') lines.append('set ytics 20') lines.append('set xlabel "Concurrent Users"') lines.append('set ylabel "% errors"') lines.append('set origin 0.0, 0.0') lines.append('set yrange [0:100]') lines.append('plot "%s" u 1:3 w linespoints lt 1 lw 2 t "%% Errors"' % data_path) lines.append('unset multiplot') f = open(gplot_path, 'w') lines = self.fixXLabels('\n'.join(lines) + '\n') f.write(lines) f.close() gnuplot(gplot_path) return def appendDelays(self, delay, delay_low, delay_high, stats): """ Show percentiles or min, avg and max in chart. """ if self.options.with_percentiles: delay.append(stats.percentiles.perc50) delay_low.append(stats.percentiles.perc10) delay_high.append(stats.percentiles.perc90) else: delay.append(stats.avg) delay_low.append(stats.min) delay_high.append(stats.max) def createPageChart(self): """Create the page chart.""" image_path = gnuplot_scriptpath(self.report_dir, 'pages_spps.png') image2_path = gnuplot_scriptpath(self.report_dir, 'pages.png') gplot_path = str(os.path.join(self.report_dir, 'pages.gplot')) data_path = gnuplot_scriptpath(self.report_dir, 'pages.data') stats = self.stats # data lines = ["CUs SPPS ERROR MIN AVG MAX P10 P50 P90 P95 APDEX E G F P U"] cvus = [] has_error = False for cycle in self.cycles: if 'page' not in stats[cycle]: continue values = [] page = stats[cycle]['page'] values.append(str(page.cvus)) cvus.append(str(page.cvus)) values.append(str(page.rps)) error = page.error_percent if error: has_error = True values.append(str(error)) values.append(str(page.min)) values.append(str(page.avg)) values.append(str(page.max)) values.append(str(page.percentiles.perc10)) values.append(str(page.percentiles.perc50)) values.append(str(page.percentiles.perc90)) values.append(str(page.percentiles.perc95)) score = page.apdex_score values.append(str(score)) apdex = ['0', '0', '0', '0', '0'] score_cls = Apdex.get_score_class(score) score_classes = Apdex.score_classes[:] #copy #flip from worst-to-best to best-to-worst score_classes.reverse() index = score_classes.index(score_cls) apdex[index] = str(score) values = values + apdex lines.append(' '.join(values)) if len(lines) == 1: # No pages finished during a cycle return f = open(data_path, 'w') f.write('\n'.join(lines) + '\n') f.close() # script lines = ['set output "' + image_path +'"'] lines.append('set title "Successful Pages Per Second"') lines.append('set ylabel "Pages Per Second"') lines.append('set grid back') lines.append('set xrange ' + self.getXRange()) lines.append('set terminal png size ' + self.getChartSizeTmp(cvus)) lines.append('set format x ""') lines.append('set multiplot') lines.append('unset title') lines.append('unset xlabel') lines.append('set bmargin 0') lines.append('set lmargin 8') lines.append('set rmargin 9.5') lines.append('set key inside top') if has_error: lines.append('set size 1, 0.4') lines.append('set origin 0, 0.6') else: lines.append('set size 1, 0.6') lines.append('set origin 0, 0.4') lines.append('plot "%s" u 1:2 w linespoints lw 2 lt 2 t "SPPS"' % data_path) # apdex lines.append('set boxwidth 0.8') lines.append('set style fill solid .7') lines.append('set ylabel "Apdex %.1f" ' % Apdex.T) lines.append('set yrange [0:1]') lines.append('set key outside top') if has_error: lines.append('set origin 0.0, 0.3') lines.append('set size 1.0, 0.3') else: lines.append('set size 1.0, 0.4') lines.append('set bmargin 3') lines.append('set format x "% g"') lines.append('set xlabel "Concurrent Users"') lines.append('set origin 0.0, 0.0') lines.append('plot "%s" u 1:12 w boxes lw 2 lt rgb "#99CDFF" t "E", "" u 1:13 w boxes lw 2 lt rgb "#00FF01" t "G", "" u 1:14 w boxes lw 2 lt rgb "#FFFF00" t "F", "" u 1:15 w boxes lw 2 lt rgb "#FF7C81" t "P", "" u 1:16 w boxes lw 2 lt rgb "#C0C0C0" t "U"' % data_path) lines.append('unset boxwidth') lines.append('set key inside top') if has_error: lines.append('set bmargin 3') lines.append('set format x "% g"') lines.append('set xlabel "Concurrent Users"') lines.append('set origin 0.0, 0.0') lines.append('set size 1.0, 0.3') lines.append('set ylabel "% errors"') lines.append('set yrange [0:100]') lines.append('plot "%s" u 1:3 w boxes lt 1 lw 2 t "%% Errors"' % data_path) lines.append('unset yrange') lines.append('set autoscale y') lines.append('unset multiplot') lines.append('set size 1.0, 1.0') lines.append('unset rmargin') lines.append('set output "%s"' % image2_path) lines.append('set title "Pages Response time"') lines.append('set ylabel "Duration (s)"') lines.append('set bars 5.0') lines.append('set style fill solid .25') lines.append('plot "%s" u 1:8:8:10:9 t "med/p90/p95" w candlesticks lt 1 lw 1 whiskerbars 0.5, "" u 1:7:4:8:8 w candlesticks lt 2 lw 1 t "min/p10/med" whiskerbars 0.5, "" u 1:5 t "avg" w lines lt 3 lw 2' % data_path) f = open(gplot_path, 'w') lines = self.fixXLabels('\n'.join(lines) + '\n') f.write(lines) f.close() gnuplot(gplot_path) def createRPSTimeChart(self): """Create a RPS chart where X-axis represent the time in seconds.""" img_path = gnuplot_scriptpath(self.report_dir, 'time_rps.png') plot_path = gnuplot_scriptpath(self.report_dir, 'time_rps.gplot') stats = self.stats start_timeline = sys.maxsize end_timeline = -1 max_rps = 0 min_rps = 0 for cycle in self.cycles: dpath = gnuplot_scriptpath(self.report_dir, 'time_rps-{0}.data'.format(cycle)) f = open(dpath, 'w') f.write('Timeline RPS\n') try: st = stats[cycle]['response'] for k in sorted(st.per_second.iterkeys()): if k < start_timeline: start_timeline = k if k > end_timeline: end_timeline = k if st.per_second[k] > max_rps: max_rps = st.per_second[k] f.write('{0} {1}\n'.format(k, st.per_second[k])) except Exception as e: print("Exception: {0}".format(e)) finally: f.close #print "max rps: {0}".format(max_rps) #print "time range: {0}-{1}".format(start_timeline, end_timeline) max_rps = int(max_rps * 1.25) f = open(plot_path, "w") lines = [] lines.append('set output "{0}"'.format(img_path)) lines.append('set title "Request Per Second over time"') lines.append('set xlabel "Time line"') lines.append('set xdata time') lines.append('set timefmt "%s"') lines.append('set format x "%H:%M"') lines.append('set ylabel "RPS"') lines.append('set grid') #lines.append('set xrange [{0}:{1}]'.format(0, end_timeline - start_timeline)) lines.append('set yrange [{0}:{1}]'.format(min_rps, max_rps)) # I don't know why self.getChartSizeTmp() accept cvus which is not used currently. cvus = [] lines.append('set terminal png size ' + self.getChartSizeTmp(cvus)) plot_line = 'plot ' colors = [ # This RGB value used for the line color for each cycle. # TODO: use more pretty color? "000000", "0000FF", "00FA9A", "191970", "8B008B", "FF00FF", "FFD700", "0000CD", "00BFFF", "00FF00", "7FFF00", "FF0000", "FF8C00", ]; for i, cycle in enumerate(self.cycles): if i != 0: plot_line += ', \\\n' dpath = gnuplot_scriptpath(self.report_dir, 'time_rps-{0}.data'.format(cycle)) #lines.append('set size 1,1\n') #lines.append('set origin 0,0\n') #plot_line += '"' + dpath + '" u ($1 - {0}):($2)'.format(start_timeline) plot_line += '"' + dpath + '" u ($1):($2)' plot_line += ' w linespoints smooth sbezier lw 1 lt 2 lc ' + \ 'rgbcolor "#696969" notitle' plot_line += ', \\\n' #plot_line += '"' + dpath + '" u ($1 - {0}):($2)'.format(start_timeline) plot_line += '"' + dpath + '" u ($1):($2)' plot_line += ' w linespoints lw 1 lt 2 lc ' + \ 'rgbcolor "#{0}" t "{1} CUs"'.format(colors[i % len(colors)], stats[cycle]['response'].cvus) lines.append(plot_line) #lines.append('unset multiplot\n') lines = self.fixXLabels('\n'.join(lines) + '\n') f.write(lines) f.close() gnuplot(plot_path) return def createAllResponseChart(self): """Create global responses chart.""" self.createRPSTimeChart() image_path = gnuplot_scriptpath(self.report_dir, 'requests_rps.png') image2_path = gnuplot_scriptpath(self.report_dir, 'requests.png') gplot_path = str(os.path.join(self.report_dir, 'requests.gplot')) data_path = gnuplot_scriptpath(self.report_dir, 'requests.data') stats = self.stats # data lines = ["CUs RPS ERROR MIN AVG MAX P10 P50 P90 P95 APDEX"] cvus = [] has_error = False for cycle in self.cycles: if 'response' not in stats[cycle]: continue values = [] resp = stats[cycle]['response'] values.append(str(resp.cvus)) cvus.append(str(resp.cvus)) values.append(str(resp.rps)) error = resp.error_percent if error: has_error = True values.append(str(error)) values.append(str(resp.min)) values.append(str(resp.avg)) values.append(str(resp.max)) values.append(str(resp.percentiles.perc10)) values.append(str(resp.percentiles.perc50)) values.append(str(resp.percentiles.perc90)) values.append(str(resp.percentiles.perc95)) values.append(str(resp.apdex_score)) lines.append(' '.join(values)) if len(lines) == 1: # No result during a cycle return f = open(data_path, 'w') f.write('\n'.join(lines) + '\n') f.close() # script lines = ['set output "' + image_path +'"'] lines.append('set title "Requests Per Second"') lines.append('set xlabel "Concurrent Users"') lines.append('set ylabel "Requests Per Second"') lines.append('set grid') lines.append('set xrange ' + self.getXRange()) lines.append('set terminal png size ' + self.getChartSizeTmp(cvus)) if not has_error: lines.append('plot "%s" u 1:2 w linespoints lw 2 lt 2 t "RPS"' % data_path) else: lines.append('set format x ""') lines.append('set multiplot') lines.append('unset title') lines.append('unset xlabel') lines.append('set size 1, 0.7') lines.append('set origin 0, 0.3') lines.append('set lmargin 5') lines.append('set bmargin 0') lines.append('plot "%s" u 1:2 w linespoints lw 2 lt 2 t "RPS"' % data_path) lines.append('set format x "% g"') lines.append('set bmargin 3') lines.append('set autoscale y') lines.append('set style fill solid .25') lines.append('set size 1.0, 0.3') lines.append('set xlabel "Concurrent Users"') lines.append('set ylabel "% errors"') lines.append('set origin 0.0, 0.0') #lines.append('set yrange [0:100]') #lines.append('set ytics 20') lines.append('plot "%s" u 1:3 w linespoints lt 1 lw 2 t "%% Errors"' % data_path) lines.append('unset multiplot') lines.append('set size 1.0, 1.0') lines.append('set output "%s"' % image2_path) lines.append('set title "Requests Response time"') lines.append('set ylabel "Duration (s)"') lines.append('set bars 5.0') lines.append('set grid back') lines.append('set style fill solid .25') lines.append('plot "%s" u 1:8:8:10:9 t "med/p90/p95" w candlesticks lt 1 lw 1 whiskerbars 0.5, "" u 1:7:4:8:8 w candlesticks lt 2 lw 1 t "min/p10/med" whiskerbars 0.5, "" u 1:5 t "avg" w lines lt 3 lw 2' % data_path) f = open(gplot_path, 'w') lines = self.fixXLabels('\n'.join(lines) + '\n') f.write(lines) f.close() gnuplot(gplot_path) return def createResponseChart(self, step): """Create responses chart.""" image_path = gnuplot_scriptpath(self.report_dir, 'request_%s.png' % step) gplot_path = str(os.path.join(self.report_dir, 'request_%s.gplot' % step)) data_path = gnuplot_scriptpath(self.report_dir, 'request_%s.data' % step) stats = self.stats # data lines = ["CUs STEP ERROR MIN AVG MAX P10 P50 P90 P95 APDEX"] cvus = [] has_error = False for cycle in self.cycles: if step not in stats[cycle]['response_step']: continue values = [] resp = stats[cycle]['response_step'].get(step) values.append(str(resp.cvus)) cvus.append(str(resp.cvus)) values.append(str(step)) error = resp.error_percent if error: has_error = True values.append(str(error)) values.append(str(resp.min)) values.append(str(resp.avg)) values.append(str(resp.max)) values.append(str(resp.percentiles.perc10)) values.append(str(resp.percentiles.perc50)) values.append(str(resp.percentiles.perc90)) values.append(str(resp.percentiles.perc95)) values.append(str(resp.apdex_score)) lines.append(' '.join(values)) if len(lines) == 1: # No result during a cycle return f = open(data_path, 'w') f.write('\n'.join(lines) + '\n') f.close() # script lines = [] lines.append('set output "%s"' % image_path) lines.append('set terminal png size ' + self.getChartSizeTmp(cvus)) lines.append('set grid') lines.append('set bars 5.0') lines.append('set title "Request %s Response time"' % step) lines.append('set xlabel "Concurrent Users"') lines.append('set ylabel "Duration (s)"') lines.append('set grid back') lines.append('set style fill solid .25') lines.append('set xrange ' + self.getXRange()) if not has_error: lines.append('plot "%s" u 1:8:8:10:9 t "med/p90/p95" w candlesticks lt 1 lw 1 whiskerbars 0.5, "" u 1:7:4:8:8 w candlesticks lt 2 lw 1 t "min/p10/med" whiskerbars 0.5, "" u 1:5 t "avg" w lines lt 3 lw 2' % data_path) else: lines.append('set format x ""') lines.append('set multiplot') lines.append('unset title') lines.append('unset xlabel') lines.append('set size 1, 0.7') lines.append('set origin 0, 0.3') lines.append('set lmargin 5') lines.append('set bmargin 0') lines.append('plot "%s" u 1:8:8:10:9 t "med/p90/p95" w candlesticks lt 1 lw 1 whiskerbars 0.5, "" u 1:7:4:8:8 w candlesticks lt 2 lw 1 t "min/p10/med" whiskerbars 0.5, "" u 1:5 t "avg" w lines lt 3 lw 2' % data_path) lines.append('set format x "% g"') lines.append('set bmargin 3') lines.append('set autoscale y') lines.append('set style fill solid .25') lines.append('set size 1.0, 0.3') lines.append('set xlabel "Concurrent Users"') lines.append('set ylabel "% errors"') lines.append('set origin 0.0, 0.0') #lines.append('set yrange [0:100]') #lines.append('set ytics 20') lines.append('plot "%s" u 1:3 w linespoints lt 1 lw 2 t "%% Errors"' % data_path) lines.append('unset multiplot') lines.append('set size 1.0, 1.0') f = open(gplot_path, 'w') lines = self.fixXLabels('\n'.join(lines) + '\n') f.write(lines) f.close() gnuplot(gplot_path) return def createMonitorChart(self, host): """Create monitrored server charts.""" stats = self.monitor[host] times = [] cvus_list = [] for stat in stats: test, cycle, cvus = stat.key.split(':') stat.cvus=cvus date = datetime.fromtimestamp(float(stat.time)) times.append(date.strftime("%H:%M:%S")) #times.append(int(float(stat.time))) # - time_start)) cvus_list.append(cvus) Plugins = MonitorPlugins() Plugins.registerPlugins() Plugins.configure(self.getMonitorConfig(host)) charts=[] for plugin in Plugins.MONITORS.values(): image_prefix = gnuplot_scriptpath(self.report_dir, '%s_%s' % (host, plugin.name)) data_prefix = gnuplot_scriptpath(self.report_dir, '%s_%s' % (host, plugin.name)) gplot_path = str(os.path.join(self.report_dir, '%s_%s.gplot' % (host, plugin.name))) r=plugin.gnuplot(times, host, image_prefix, data_prefix, gplot_path, self.chart_size, stats) if r!=None: gnuplot(gplot_path) charts.extend(r) return charts funkload-1.17.1/src/funkload/ReportRenderOrg.py000066400000000000000000000140521302537724200214420ustar00rootroot00000000000000# (C) Copyright 2011 Nuxeo SAS # Author: bdelbosc@nuxeo.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """Classes that render statistics in emacs org-mode format. """ from __future__ import absolute_import import re from .ReportRenderRst import RenderRst from .ReportRenderRst import BaseRst from . import ReportRenderRst from .MonitorPlugins import MonitorPlugins FL_SITE = "http://funkload.nuxeo.org" def org_title(title, level=1, newpage=True): """Return an org section.""" org = [] if newpage: org.append("") org.append("") org.append("#+BEGIN_LaTeX") org.append("\\newpage") org.append('#+END_LaTeX') org.append('*' * (level - 1) + ' ' + title + '\n') return '\n'.join(org) def org_image(self): org = ["#+BEGIN_LaTeX"] org.append('\\begin{center}') for image_name in self.image_names: org.append("\includegraphics[scale=0.5]{{./%s}.png}" % image_name) org.append('\\end{center}') org.append('#+END_LaTeX') return '\n'.join(org) + '\n' def org_header(self, with_chart=False): headers = self.headers[:] if self.with_percentiles: self._attach_percentiles_header(headers) org = [self.render_image()] org.append("#+BEGIN_LaTeX") org.append("\\tiny") org.append('#+END_LaTeX') org.append(' |' + '|'.join(headers) + '|\n |-') return '\n'.join(org) def org_footer(self): org = [' |-'] org.append("#+BEGIN_LaTeX") org.append("\\normalsize") org.append('#+END_LaTeX') return '\n'.join(org) ReportRenderRst.rst_title = org_title ReportRenderRst.LI = '-' BaseRst.render_header = org_header BaseRst.render_footer = org_footer BaseRst.render_image = org_image BaseRst.sep = '|' class RenderOrg(RenderRst): """Render stats in emacs org-mode format.""" # number of slowest requests to display slowest_items = 5 with_chart = True def __init__(self, config, stats, error, monitor, monitorconfig, options): options.html = True RenderRst.__init__(self, config, stats, error, monitor, monitorconfig, options) def renderHeader(self): config = self.config self.append('# -*- mode: org -*-') self.append('#+TITLE: FunkLoad bench report') self.append('#+DATE: ' + self.date) self.append('''#+STYLE: #+LaTeX_CLASS: koma-article #+LaTeX_CLASS_OPTIONS: [a4paper,landscape] #+LATEX_HEADER: \usepackage[utf8]{inputenc} #+LATEX_HEADER: \usepackage[en]{babel} #+LATEX_HEADER: \usepackage{fullpage} #+LATEX_HEADER: \usepackage[hyperref,x11names]{xcolor} #+LATEX_HEADER: \usepackage[colorlinks=true,urlcolor=SteelBlue4,linkcolor=Firebrick4]{hyperref} #+LATEX_HEADER: \usepackage{graphicx} #+LATEX_HEADER: \usepackage[T1]{fontenc}''') description = [config['class_description']] description += ["Bench result of ``%s.%s``: " % (config['class'], config['method'])] description += [config['description']] self.append('#+TEXT: Bench result of =%s.%s=: %s' % ( config['class'], config['method'], ' '.join(description))) self.append('#+OPTIONS: toc:1') self.append('') def renderMonitor(self, host, charts): """Render a monitored host.""" description = self.config.get(host, '') self.append(org_title("%s: %s" % (host, description), 3)) for chart in charts: self.append('#+BEGIN_LaTeX') self.append('\\begin{center}') self.append("\includegraphics[scale=0.5]{{./%s}.png}" % chart[1]) self.append('\\end{center}') self.append('#+END_LaTeX') def renderHook(self): self.rst = [line.replace('``', '=') for line in self.rst] lapdex = "Apdex_{%s}" % str(self.options.apdex_t) kv = re.compile("^(\ *\- [^\:]*)\:(.*)") bold = re.compile("\*\*([^\*]+)\*\*") link = re.compile("\`([^\<]+)\<([^\>]+)\>\`\_") ret = [] for line in self.rst: line = re.sub(kv, lambda m: "%s :: %s\n\n" % ( m.group(1), m.group(2)), line) line = re.sub(bold, lambda m: "*%s*" % (m.group(1)), line) line = re.sub(link, lambda m: "[[%s][%s]]" % (m.group(2), m.group(1).strip()), line) line = line.replace('|APDEXT|', lapdex) line = line.replace('Apdex*', lapdex) line = line.replace('Apdex T', 'Apdex_{T}') line = line.replace('FunkLoad_', '[[%s][FunkLoad]]' % FL_SITE) ret.append(line) self.rst = ret def createMonitorCharts(self): """Create all montirored server charts.""" if not self.monitor or not self.with_chart: return self.append(org_title("Monitored hosts", 2)) charts = {} for host in self.monitor.keys(): charts[host] = self.createMonitorChart(host) return charts def createMonitorChart(self, host): """Create monitrored server charts.""" charts = [] Plugins = MonitorPlugins() Plugins.registerPlugins() Plugins.configure(self.getMonitorConfig(host)) for plugin in Plugins.MONITORS.values(): image_path = ('%s_%s' % (host, plugin.name)).replace("\\", "/") charts.append((plugin.name, image_path)) return charts funkload-1.17.1/src/funkload/ReportRenderRst.py000066400000000000000000000577341302537724200215010ustar00rootroot00000000000000# (C) Copyright 2005 Nuxeo SAS # Author: bdelbosc@nuxeo.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """Classes that render statistics. $Id$ """ from __future__ import absolute_import import os from .utils import get_version from .apdex import Apdex from .MonitorPluginsDefault import MonitorCPU, MonitorMemFree, MonitorNetwork, MonitorCUs LI = '*' # ------------------------------------------------------------ # ReST rendering # def rst_title(title, level=1, newpage=True): """Return a rst title.""" rst_level = ['=', '=', '-', '~'] if level == 0: rst = [rst_level[level] * len(title)] else: rst = [''] rst.append(title) rst.append(rst_level[level] * len(title)) rst.append('') return '\n'.join(rst) def dumb_pluralize(num, word): #Doesn't follow all English rules, but sufficent for our purpose return ' %s %s' % (num, word + ['s',''][num==1]) class BaseRst: """Base class for ReST renderer.""" fmt_int = "%18d" fmt_float = "%18.3f" fmt_str = "%18s" fmt_percent = "%16.2f%%" fmt_deco = "==================" sep = " " headers = [] indent = 0 image_names = [] with_percentiles = False with_apdex = False def __init__(self, stats): self.stats = stats def __repr__(self): """Render stats.""" ret = [''] ret.append(self.render_header()) ret.append(self.render_stat()) ret.append(self.render_footer()) return '\n'.join(ret) def render_images(self): """Render images link.""" indent = ' ' * self.indent rst = [] for image_name in self.image_names: rst.append(indent + " .. image:: %s.png" % image_name) rst.append('') return '\n'.join(rst) def render_header(self, with_chart=False): """Render rst header.""" headers = self.headers[:] if self.with_percentiles: self._attach_percentiles_header(headers) deco = ' ' + " ".join([self.fmt_deco] * len(headers)) header = " " + " ".join([ "%18s" % h for h in headers ]) indent = ' ' * self.indent ret = [] if with_chart: ret.append(self.render_images()) ret.append(indent + deco) ret.append(indent + header) ret.append(indent + deco) return '\n'.join(ret) def _attach_percentiles_header(self, headers): """ Attach percentile headers. """ headers.extend( ["P10", "MED", "P90", "P95"]) def _attach_percentiles(self, ret): """ Attach percentiles, if this is wanted. """ percentiles = self.stats.percentiles fmt = self.fmt_float ret.extend([ fmt % percentiles.perc10, fmt % percentiles.perc50, fmt % percentiles.perc90, fmt % percentiles.perc95 ]) def render_footer(self): """Render rst footer.""" headers = self.headers[:] if self.with_percentiles: self._attach_percentiles_header(headers) deco = " ".join([self.fmt_deco] * len(headers)) footer = ' ' * (self.indent + 1) + deco footer += '\n\n' if self.with_apdex: footer += ' ' * (self.indent + 1) + "\* Apdex |APDEXT|" return footer def render_stat(self): """Render rst stat.""" raise NotImplemented class AllResponseRst(BaseRst): """AllResponseStat rendering.""" headers = [ "CUs", "Apdex*", "Rating*", "RPS", "maxRPS", "TOTAL", "SUCCESS","ERROR", "MIN", "AVG", "MAX"] image_names = ['requests_rps', 'requests', 'time_rps'] with_apdex = True def render_stat(self): """Render rst stat.""" ret = [' ' * self.indent] stats = self.stats stats.finalize() ret.append(self.fmt_int % stats.cvus) if self.with_apdex: ret.append(self.fmt_float % stats.apdex_score) ret.append(self.fmt_str % Apdex.get_label(stats.apdex_score)) ret.append(self.fmt_float % stats.rps) ret.append(self.fmt_float % stats.rps_max) ret.append(self.fmt_int % stats.count) ret.append(self.fmt_int % stats.success) ret.append(self.fmt_percent % stats.error_percent) ret.append(self.fmt_float % stats.min) ret.append(self.fmt_float % stats.avg) ret.append(self.fmt_float % stats.max) if self.with_percentiles: self._attach_percentiles(ret) ret = self.sep.join(ret) return ret class PageRst(AllResponseRst): """Page rendering.""" headers = ["CUs", "Apdex*", "Rating", "SPPS", "maxSPPS", "TOTAL", "SUCCESS", "ERROR", "MIN", "AVG", "MAX"] image_names = ['pages_spps', 'pages'] with_apdex = True class ResponseRst(BaseRst): """Response rendering.""" headers = ["CUs", "Apdex*", "Rating", "TOTAL", "SUCCESS", "ERROR", "MIN", "AVG", "MAX"] indent = 4 image_names = ['request_'] with_apdex = True def __init__(self, stats): BaseRst.__init__(self, stats) # XXX quick fix for #1017 self.image_names = [name + str(stats.step) + '.' + str(stats.number) for name in self.image_names] def render_stat(self): """Render rst stat.""" stats = self.stats stats.finalize() ret = [' ' * self.indent] ret.append(self.fmt_int % stats.cvus) ret.append(self.fmt_float % stats.apdex_score) ret.append(self.fmt_str % Apdex.get_label(stats.apdex_score)) ret.append(self.fmt_int % stats.count) ret.append(self.fmt_int % stats.success) ret.append(self.fmt_percent % stats.error_percent) ret.append(self.fmt_float % stats.min) ret.append(self.fmt_float % stats.avg) ret.append(self.fmt_float % stats.max) if self.with_percentiles: self._attach_percentiles(ret) ret = self.sep.join(ret) return ret class TestRst(BaseRst): """Test Rendering.""" headers = ["CUs", "STPS", "TOTAL", "SUCCESS", "ERROR"] image_names = ['tests'] with_percentiles = False def render_stat(self): """Render rst stat.""" stats = self.stats stats.finalize() ret = [' ' * self.indent] ret.append(self.fmt_int % stats.cvus) ret.append(self.fmt_float % stats.tps) ret.append(self.fmt_int % stats.count) ret.append(self.fmt_int % stats.success) ret.append(self.fmt_percent % stats.error_percent) ret = self.sep.join(ret) return ret class RenderRst: """Render stats in ReST format.""" # number of slowest requests to display slowest_items = 5 def __init__(self, config, stats, error, monitor, monitorconfig, options): self.config = config self.stats = stats self.error = error self.monitor = monitor self.monitorconfig = monitorconfig self.options = options self.rst = [] cycles = stats.keys() cycles.sort() self.cycles = cycles if options.with_percentiles: BaseRst.with_percentiles = True if options.html: self.with_chart = True else: self.with_chart = False self.date = config['time'][:19].replace('T', ' ') def getRepresentativeCycleStat(self): """Return the cycle stat with the maximum number of steps.""" stats = self.stats max_steps = 0 cycle_r = None for cycle in self.cycles: steps = stats[cycle]['response_step'].keys() if cycle_r is None: cycle_r = stats[cycle] if len(steps) > max_steps: max_steps = steps cycle_r = stats[cycle] return cycle_r def getBestStpsCycle(self): """Return the cycle with the maximum STPS.""" stats = self.stats max_stps = -1 cycle_r = None for cycle in self.cycles: if 'test' not in stats[cycle]: continue stps = stats[cycle]['test'].tps if stps > max_stps: max_stps = stps cycle_r = cycle if cycle_r is None and len(self.cycles): # no test ends during a cycle return the first one cycle_r = self.cycles[0] return cycle_r def getBestCycle(self): """Return the cycle with the maximum Apdex and SPPS.""" stats = self.stats max_spps = -1 cycle_r = None for cycle in self.cycles: if 'page' not in stats[cycle]: continue if stats[cycle]['page'].apdex_score < 0.85: continue spps = stats[cycle]['page'].rps * stats[cycle]['page'].apdex_score if spps > max_spps: max_spps = spps cycle_r = cycle if cycle_r is None and len(self.cycles): # no test ends during a cycle return the first one cycle_r = self.cycles[0] return cycle_r def append(self, text): """Append text to rst output.""" self.rst.append(text) def renderHeader(self): config = self.config self.append(rst_title("FunkLoad_ bench report", 0)) self.append('') self.append(':date: ' + self.date) description = [config['class_description']] description += ["Bench result of ``%s.%s``: " % (config['class'], config['method'])] description += [config['description']] indent = "\n " self.append(':abstract: ' + indent.join(description)) self.append('') self.append(".. _FunkLoad: http://funkload.nuxeo.org/") self.append(".. sectnum:: :depth: 2") self.append(".. contents:: Table of contents") self.append(".. |APDEXT| replace:: \ :sub:`%.1f`" % self.options.apdex_t) def renderConfig(self): """Render bench configuration and metadata.""" self.renderHeader() config = self.config self.append(rst_title("Bench configuration", 2)) self.append(LI + " Launched: %s" % self.date) if config.get('node'): self.append(LI + " From: %s" % config['node']) self.append(LI + " Test: ``%s.py %s.%s``" % (config['module'], config['class'], config['method'])) if config.get('label'): self.append(LI + " Label: %s" % config['label']) self.append(LI + " Target server: %s" % config['server_url']) self.append(LI + " Cycles of concurrent users: %s" % config['cycles']) self.append(LI + " Cycle duration: %ss" % config['duration']) self.append(LI + " Sleeptime between requests: from %ss to %ss" % ( config['sleep_time_min'], config['sleep_time_max'])) self.append(LI + " Sleeptime between test cases: %ss" % config['sleep_time']) self.append(LI + " Startup delay between threads: %ss" % config['startup_delay']) self.append(LI + " Apdex: |APDEXT|") self.append(LI + " FunkLoad_ version: %s" % config['version']) self.append("") # check for metadata has_meta = False for key in config.keys(): if key.startswith("meta:"): if not has_meta: self.append("Bench metadata:") self.append('') has_meta = True self.append(LI + " %s: %s" % (key[5:], config[key])) if has_meta: self.append("") def renderTestContent(self, test): """Render global information about test content.""" self.append(rst_title("Bench content", 2)) config = self.config self.append('The test ``%s.%s`` contains: ' % (config['class'], config['method'])) self.append('') self.append(LI + dumb_pluralize(test.pages, 'page')) self.append(LI + dumb_pluralize(test.redirects, 'redirect')) self.append(LI + dumb_pluralize(test.links, 'link')) self.append(LI + dumb_pluralize(test.images, 'image')) self.append(LI + dumb_pluralize(test.xmlrpc, 'XML-RPC call')) self.append('') self.append('The bench contains:') total_tests = 0 total_tests_error = 0 total_pages = 0 total_pages_error = 0 total_responses = 0 total_responses_error = 0 stats = self.stats for cycle in self.cycles: if 'test' in stats[cycle]: total_tests += stats[cycle]['test'].count total_tests_error += stats[cycle]['test'].error if 'page' in stats[cycle]: stat = stats[cycle]['page'] stat.finalize() total_pages += stat.count total_pages_error += stat.error if 'response' in stats[cycle]: total_responses += stats[cycle]['response'].count total_responses_error += stats[cycle]['response'].error self.append('') pluralized_t_errs = dumb_pluralize(total_tests_error, 'error') pluralized_p_errs = dumb_pluralize(total_pages_error, 'error') pluralized_r_errs = dumb_pluralize(total_responses_error, 'error') self.append(LI + " %s tests" % total_tests + ( total_tests_error and "," + pluralized_t_errs or '')) self.append(LI + " %s pages" % total_pages + ( total_pages_error and "," + pluralized_p_errs or '')) self.append(LI + " %s requests" % total_responses + ( total_responses_error and "," + pluralized_r_errs or '')) self.append('') def renderCyclesStat(self, key, title, description=''): """Render a type of stats for all cycles.""" stats = self.stats first = True if key == 'test': klass = TestRst elif key == 'page': klass = PageRst elif key == 'response': klass = AllResponseRst self.append(rst_title(title, 2)) if description: self.append(description) self.append('') renderer = None for cycle in self.cycles: if key not in stats[cycle]: continue renderer = klass(stats[cycle][key]) if first: self.append(renderer.render_header(self.with_chart)) first = False self.append(renderer.render_stat()) if renderer is not None: self.append(renderer.render_footer()) else: self.append('Sorry no %s have finished during a cycle, ' 'the cycle duration is too short.\n' % key) def renderCyclesStepStat(self, step): """Render a step stats for all cycle.""" stats = self.stats first = True renderer = None for cycle in self.cycles: stat = stats[cycle]['response_step'].get(step) if stat is None: continue renderer = ResponseRst(stat) if first: self.append(renderer.render_header(self.with_chart)) first = False self.append(renderer.render_stat()) if renderer is not None: self.append(renderer.render_footer()) def renderPageDetail(self, cycle_r): """Render a page detail.""" self.append(rst_title("Page detail stats", 2)) cycle_r_steps = cycle_r['response_step'] steps = cycle_r['response_step'].keys() steps.sort() self.steps = steps current_step = -1 newpage = False for step_name in steps: a_step = cycle_r_steps[step_name] if a_step.step != current_step: current_step = a_step.step self.append(rst_title("PAGE %s: %s" % ( a_step.step, a_step.description or a_step.url), 3, newpage)) newpage = True self.append(LI + ' Req: %s, %s, url ``%s``' % (a_step.number, a_step.type, a_step.url)) self.append('') self.renderCyclesStepStat(step_name) def createMonitorCharts(self): pass def renderMonitors(self): """Render all monitored hosts.""" if not self.monitor or not self.with_chart: return charts = self.createMonitorCharts() if charts == None: return for host in charts.keys(): self.renderMonitor(host, charts[host]) def renderMonitor(self, host, charts): """Render a monitored host.""" description = self.config.get(host, '') if len(charts)>0: self.append(rst_title("%s: %s" % (host, description), 3)) for chart in charts: self.append("**%s**\n\n.. image:: %s\n" % ( chart[0], os.path.basename(chart[1]))) def renderSlowestRequests(self, number): """Render the n slowest requests of the best cycle.""" stats = self.stats self.append(rst_title("Slowest requests", 2)) cycle = self.getBestCycle() cycle_name = None if not (cycle and 'response_step' in stats[cycle]): return steps = stats[cycle]['response_step'].keys() items = [] for step_name in steps: stat = stats[cycle]['response_step'][step_name] stat.finalize() items.append((stat.avg, stat.step, stat.type, stat.url, stat.description, stat.apdex_score)) if not cycle_name: cycle_name = stat.cvus items.sort() items.reverse() self.append('The %d slowest average response time during the ' 'best cycle with **%s** CUs:\n' % (number, cycle_name)) for item in items[:number]: self.append(LI + ' In page %s, Apdex rating: %s, avg response time: %3.2fs, %s: ``%s``\n' ' `%s`' % ( item[1], Apdex.get_label(item[5]), item[0], item[2], item[3], item[4])) def renderErrors(self): """Render error list.""" if not len(self.error): return self.append(rst_title("Failures and Errors", 2)) for status in ('Failure', 'Error'): if status not in self.error: continue stats = self.error[status] errors = {} for stat in stats: header = stat.header key = (stat.code, header.get('bobo-exception-file'), header.get('bobo-exception-line'), ) err_list = errors.setdefault(key, []) err_list.append(stat) err_types = errors.keys() err_types.sort() self.append(rst_title(status + 's', 3)) for err_type in err_types: stat = errors[err_type][0] pluralized_times = dumb_pluralize(len(errors[err_type]), 'time') if err_type[1]: self.append(LI + '%s, code: %s, %s\n' ' in %s, line %s: %s' %( pluralized_times, err_type[0], header.get('bobo-exception-type'), err_type[1], err_type[2], header.get('bobo-exception-value'))) else: traceback = stat.traceback and stat.traceback.replace( 'File ', '\n File ') or 'No traceback.' self.append(LI + '%s, code: %s::\n\n' ' %s\n' %( pluralized_times, err_type[0], traceback)) def renderDefinitions(self): """Render field definition.""" self.append(rst_title("Definitions", 2)) self.append(LI + ' CUs: Concurrent users or number of concurrent threads' ' executing tests.') self.append(LI + ' Request: a single GET/POST/redirect/XML-RPC request.') self.append(LI + ' Page: a request with redirects and resource' ' links (image, css, js) for an HTML page.') self.append(LI + ' STPS: Successful tests per second.') self.append(LI + ' SPPS: Successful pages per second.') self.append(LI + ' RPS: Requests per second, successful or not.') self.append(LI + ' maxSPPS: Maximum SPPS during the cycle.') self.append(LI + ' maxRPS: Maximum RPS during the cycle.') self.append(LI + ' MIN: Minimum response time for a page or request.') self.append(LI + ' AVG: Average response time for a page or request.') self.append(LI + ' MAX: Maximmum response time for a page or request.') self.append(LI + ' P10: 10th percentile, response time where 10 percent' ' of pages or requests are delivered.') self.append(LI + ' MED: Median or 50th percentile, response time where half' ' of pages or requests are delivered.') self.append(LI + ' P90: 90th percentile, response time where 90 percent' ' of pages or requests are delivered.') self.append(LI + ' P95: 95th percentile, response time where 95 percent' ' of pages or requests are delivered.') self.append(LI + Apdex.description_para) self.append(LI + Apdex.rating_para) self.append('') self.append('Report generated with FunkLoad_ ' + get_version() + ', more information available on the ' '`FunkLoad site `_.') def renderHook(self): """Hook for post processing""" pass def __repr__(self): self.renderConfig() if not self.cycles: self.append('No cycle found') return '\n'.join(self.rst) cycle_r = self.getRepresentativeCycleStat() if 'test' in cycle_r: self.renderTestContent(cycle_r['test']) self.renderCyclesStat('test', 'Test stats', 'The number of Successful **Tests** Per Second ' '(STPS) over Concurrent Users (CUs).') self.renderCyclesStat('page', 'Page stats', 'The number of Successful **Pages** Per Second ' '(SPPS) over Concurrent Users (CUs).\n' 'Note: an XML-RPC call counts as a page.') self.renderCyclesStat('response', 'Request stats', 'The number of **Requests** Per Second (RPS) ' '(successful or not) over Concurrent Users (CUs).') self.renderSlowestRequests(self.slowest_items) self.renderMonitors() self.renderPageDetail(cycle_r) self.renderErrors() if not self.options.skip_definitions: self.renderDefinitions() self.renderHook() return '\n'.join(self.rst) def getMonitorConfig(self, host): """Return the host config or a default for backward compat""" if host in self.monitorconfig: return self.monitorconfig[host] return {'MonitorCPU': MonitorCPU().getConfig(), 'MonitorMemFree': MonitorMemFree().getConfig(), 'MonitorNetwork': MonitorNetwork(None).getConfig(), 'MonitorCUs': MonitorCUs().getConfig()} funkload-1.17.1/src/funkload/ReportRenderTrend.py000066400000000000000000000225361302537724200217750ustar00rootroot00000000000000# (C) Copyright 2011 Nuxeo SAS # Author: bdelbosc@nuxeo.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """Trend report rendering The trend report uses metadata from a funkload.metadata file if present. The format of the metadata file is the following: label:short label to be displayed in the graph anykey:anyvalue a multi line description in ReST will be displayed in the listing parts """ from __future__ import print_function from __future__ import absolute_import import os from .ReportRenderRst import rst_title from .ReportRenderHtmlBase import RenderHtmlBase from .ReportRenderHtmlGnuPlot import gnuplot from .ReportRenderDiff import getRPath def extract(report_dir, startswith): """Extract line form the ReST index file.""" f = open(os.path.join(report_dir, "index.rst")) line = f.readline() while line: if line.startswith(startswith): f.close() return line[len(startswith):].strip() line = f.readline() f.close() return None def extract_date(report_dir): """Extract the bench date form the ReST index file.""" tag = "* Launched: " value = extract(report_dir, tag) if value is None: print("ERROR no date found in rst report %s" % report_dir) return "NA" return value def extract_max_cus(report_dir): """Extract the maximum concurrent users form the ReST index file.""" tag = "* Cycles of concurrent users: " value = extract(report_dir, tag) if value is None: print("ERROR no max CUs found in rst report %s" % report_dir) return "NA" return value.split(', ')[-1][:-1] def extract_metadata(report_dir): """Extract the metadata from a funkload.metadata file.""" ret = {} try: f = open(os.path.join(report_dir, "funkload.metadata")) except IOError: return ret lines = f.readlines() f.close() for line in lines: sep = None if line.count(':'): sep = ':' elif line.count('='): sep = '=' else: key = 'misc' value = line.strip() if sep is not None: key, value = line.split(sep, 1) ret[key.strip()] = value.strip() elif value: v = ret.setdefault('misc', '') ret['misc'] = v + ' ' + value return ret def extract_stat(tag, report_dir): """Extract stat from the ReST index file.""" lines = open(os.path.join(report_dir, "index.rst")).readlines() try: idx = lines.index("%s stats\n" % tag) except ValueError: print("ERROR tag %s not found in rst report %s" % (tag, report_dir)) return [] delim = 0 ret = [] header = "" for line in lines[idx:]: if line.startswith(" ====="): delim += 1 continue if delim == 1: header = line.strip().split() if delim < 2: continue if delim == 3: break ret.append([x.replace("%","") for x in line.strip().split()]) return header, ret def get_metadata(metadata): """Format metadata.""" ret = [] keys = metadata.keys() keys.sort() for key in keys: if key not in ('label', 'misc'): ret.append('%s: %s' % (key, metadata[key])) if metadata.get('misc'): ret.append(metadata['misc']) return ', '.join(ret) class RenderTrend(RenderHtmlBase): """Trend report.""" report_dir1 = None report_dir2 = None header = None sep = ', ' data_file = None output_dir = None script_file = None def __init__(self, args, options, css_file=None): # Swap windows path separator backslashes for forward slashes # Windows accepts '/' but some file formats like rest treat the # backslash specially. self.args = [os.path.abspath(arg).replace('\\', '/') for arg in args] self.options = options self.css_file = css_file self.quiet = options.quiet def generateReportDirectory(self, output_dir): """Generate a directory name for a report.""" output_dir = os.path.abspath(output_dir) report_dir = os.path.join(output_dir, 'trend-report') if not os.access(report_dir, os.W_OK): os.mkdir(report_dir, 0o775) report_dir.replace('\\', '/') return report_dir def createCharts(self): """Render stats.""" self.createGnuplotData() self.createGnuplotScript() gnuplot(self.script_file) def createRstFile(self): """Create the ReST file.""" rst_path = os.path.join(self.report_dir, 'index.rst') lines = [] reports = self.args reports_name = [os.path.basename(report) for report in reports] reports_date = [extract_date(report) for report in reports] self.reports_name = reports_name reports_metadata = [extract_metadata(report) for report in reports] self.reports_metadata = reports_metadata reports_rpath = [getRPath(self.report_dir, os.path.join(report, 'index.html').replace( '\\', '/')) for report in reports] self.max_cus = extract_max_cus(reports[0]) # TODO: handles case where reports_name are the same lines.append(rst_title("FunkLoad_ trend report", level=0)) lines.append("") lines.append(".. sectnum:: :depth: 2") lines.append("") lines.append(rst_title("Trends", level=2)) lines.append(" .. image:: trend_apdex.png") lines.append(" .. image:: trend_spps.png") lines.append(" .. image:: trend_avg.png") lines.append("") lines.append(rst_title("List of reports", level=1)) count = 0 for report in reports_name: count += 1 lines.append(" * Bench **%d** %s: `%s <%s>`_ %s" % ( count, reports_date[count - 1], report, reports_rpath[count - 1], get_metadata(reports_metadata[count - 1]))) lines.append("") lines.append(" .. _FunkLoad: http://funkload.nuxeo.org/") lines.append("") f = open(rst_path, 'w') f.write('\n'.join(lines)) f.close() self.rst_path = rst_path def copyXmlResult(self): pass def __repr__(self): return self.render() def createGnuplotData(self): """Render rst stat.""" def output_stat(tag, count): header, stat = extract_stat(tag, rep) text = [] for line in stat: line.insert(0, str(count)) line.append(extract_date(rep)) text.append(' '.join(line)) return '\n'.join(text) data_file = os.path.join(self.report_dir, 'trend.dat') self.data_file = data_file f = open(data_file, 'w') count = 0 for rep in self.args: count += 1 f.write(output_stat('Page', count) + '\n\n') f.close() def createGnuplotScript(self): """Build gnuplot script""" labels = [] count = 0 for metadata in self.reports_metadata: count += 1 if metadata.get('label'): labels.append('set label "%s" at %d,%d,1 rotate by 45 front' % ( metadata.get('label'), count, int(self.max_cus) + 2)) labels = '\n'.join(labels) script_file = os.path.join(self.report_dir, 'script.gplot') self.script_file = script_file f = open(script_file, 'w') f.write('# ' + ' '.join(self.reports_name)) f.write('''# COMMON SETTINGS set grid back set boxwidth 0.9 relative # Apdex set output "trend_apdex.png" set terminal png size 640,380 set border 895 front linetype -1 linewidth 1.000 set grid nopolar set grid xtics nomxtics ytics nomytics noztics nomztics \ nox2tics nomx2tics noy2tics nomy2tics nocbtics nomcbtics set grid layerdefault linetype 0 linewidth 1.000, linetype 0 linewidth 1.000 set style line 100 linetype 5 linewidth 0.10 pointtype 100 pointsize default #set view map unset surface set style data pm3d set style function pm3d set ticslevel 0 set nomcbtics set xrange [ * : * ] noreverse nowriteback set yrange [ * : * ] noreverse nowriteback set zrange [ * : * ] noreverse nowriteback set cbrange [ * : * ] noreverse nowriteback set lmargin 0 set pm3d at s scansforward # set pm3d scansforward interpolate 0,1 set view map set title "Apdex Trend" set xlabel "Bench" set ylabel "CUs" %s splot "trend.dat" using 1:2:3 with linespoints unset label set view set output "trend_spps.png" set title "Pages per second Trend" splot "trend.dat" using 1:2:5 with linespoints set output "trend_avg.png" set palette negative set title "Average response time (s)" splot "trend.dat" using 1:2:11 with linespoints ''' % labels) f.close() funkload-1.17.1/src/funkload/ReportStats.py000066400000000000000000000277401302537724200206610ustar00rootroot00000000000000# (C) Copyright 2005 Nuxeo SAS # Author: bdelbosc@nuxeo.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """Classes that collect statistics submitted by the result parser. $Id: ReportStats.py 24737 2005-08-31 09:00:16Z bdelbosc $ """ from __future__ import absolute_import from .apdex import Apdex class MonitorStat: """Collect system monitor info.""" def __init__(self, attrs): for key, value in attrs.items(): setattr(self, key, value) class ErrorStat: """Collect Error or Failure stats.""" def __init__(self, cycle, step, number, code, header, body, traceback): self.cycle = cycle self.step = step self.number = number self.code = code self.header = header and header.copy() or {} self.body = body or None self.traceback = traceback class Percentiles: """ Calculate Percentiles with the given stepsize. """ def __init__(self, stepsize=10, name="UNKNOWN", results=None): self.stepsize = stepsize self.name = name if results is None: self.results = [] else: self.results = results def addResult(self, newresult): """Add a new result.""" self.results.append(newresult) def calcPercentiles(self): """Compute percentiles.""" results = self.results results.sort() len_results = len(results) old_value = -1 for perc in range(0, 100, self.stepsize): index = int(perc / 100.0 * len_results) try: value = results[index] except IndexError: value = -1.0 setattr(self, "perc%02d" % perc, float(value)) old_value = value def __str__(self): self.calcPercentiles() fmt_string = ["Percentiles: %s" % self.name] for perc in range(0, 100, self.stepsize): name = "perc%02d" % perc fmt_string.append("%s=%s" % (name, getattr(self, name))) return ", ".join(fmt_string) def __repr__(self): return "Percentiles(stepsize=%r, name=%r, results=%r)" % ( self.stepsize, self.name, self.results) class ApdexStat: def __init__(self): self.apdex_satisfied = 0 self.apdex_tolerating = 0 self.apdex_frustrated = 0 self.count = 0 def add(self, duration): if Apdex.satisfying(duration): self.apdex_satisfied += 1 elif Apdex.tolerable(duration): self.apdex_tolerating += 1 else: self.apdex_frustrated += 1 self.count += 1 def getScore(self): return Apdex.score(self.apdex_satisfied, self.apdex_tolerating, self.apdex_frustrated) class AllResponseStat: """Collect stat for all response in a cycle.""" def __init__(self, cycle, cycle_duration, cvus): self.cycle = cycle self.cycle_duration = cycle_duration self.cvus = int(cvus) self.per_second = {} self.max = 0 self.min = 999999999 self.avg = 0 self.total = 0 self.count = 0 self.success = 0 self.error = 0 self.error_percent = 0 self.rps = 0 self.rps_min = 0 self.rps_max = 0 self.finalized = False self.percentiles = Percentiles(stepsize=5, name=cycle) self.apdex = ApdexStat() self.apdex_score = None def add(self, date, result, duration): """Add a new response to stat.""" date_s = int(float(date)) self.per_second[date_s] = self.per_second.setdefault( int(date_s), 0) + 1 self.count += 1 if result == 'Successful': self.success += 1 else: self.error += 1 duration_f = float(duration) self.max = max(self.max, duration_f) self.min = min(self.min, duration_f) self.total += duration_f self.finalized = False self.percentiles.addResult(duration_f) self.apdex.add(duration_f) def finalize(self): """Compute avg times.""" if self.finalized: return if self.count: self.avg = self.total / float(self.count) self.min = min(self.max, self.min) if self.error: self.error_percent = 100.0 * self.error / float(self.count) rps_min = rps_max = 0 for date in self.per_second.keys(): rps_max = max(rps_max, self.per_second[date]) rps_min = min(rps_min, self.per_second[date]) if self.cycle_duration: rps = self.count / float(self.cycle_duration) if rps < 1: # average is lower than 1 this means that sometime there was # no request during one second rps_min = 0 self.rps = rps self.rps_max = rps_max self.rps_min = rps_min self.percentiles.calcPercentiles() self.apdex_score = self.apdex.getScore() self.finalized = True class SinglePageStat: """Collect stat for a single page.""" def __init__(self, step): self.step = step self.count = 0 self.date_s = None self.duration = 0.0 self.result = 'Successful' def addResponse(self, date, result, duration): """Add a response to a page.""" self.count += 1 if self.date_s is None: self.date_s = int(float(date)) self.duration += float(duration) if result != 'Successful': self.result = result def __repr__(self): """Representation.""" return 'page %s %s %ss' % (self.step, self.result, self.duration) class PageStat(AllResponseStat): """Collect stat for asked pages in a cycle.""" def __init__(self, cycle, cycle_duration, cvus): AllResponseStat.__init__(self, cycle, cycle_duration, cvus) self.threads = {} def add(self, thread, step, date, result, duration, rtype): """Add a new response to stat.""" thread = self.threads.setdefault(thread, {'count': 0, 'pages': {}}) if str(rtype) in ('post', 'get', 'xmlrpc', 'put', 'delete', 'head'): new_page = True else: new_page = False if new_page: thread['count'] += 1 self.count += 1 if not thread['count']: # don't take into account request that belongs to a staging up page return stat = thread['pages'].setdefault(thread['count'], SinglePageStat(step)) stat.addResponse(date, result, duration) self.apdex.add(float(duration)) self.finalized = False def finalize(self): """Compute avg times.""" if self.finalized: return for thread in self.threads.keys(): for page in self.threads[thread]['pages'].values(): if str(page.result) == 'Successful': if page.date_s: count = self.per_second.setdefault(page.date_s, 0) + 1 self.per_second[page.date_s] = count self.success += 1 self.total += page.duration self.percentiles.addResult(page.duration) else: self.error += 1 continue duration = page.duration self.max = max(self.max, duration) self.min = min(self.min, duration) AllResponseStat.finalize(self) if self.cycle_duration: # override rps to srps self.rps = self.success / float(self.cycle_duration) self.percentiles.calcPercentiles() self.finalized = True class ResponseStat: """Collect stat a specific response in a cycle.""" def __init__(self, step, number, cvus): self.step = step self.number = number self.cvus = int(cvus) self.max = 0 self.min = 999999999 self.avg = 0 self.total = 0 self.count = 0 self.success = 0 self.error = 0 self.error_percent = 0 self.url = '?' self.description = '' self.type = '?' self.finalized = False self.percentiles = Percentiles(stepsize=5, name=step) self.apdex = ApdexStat() self.apdex_score = None def add(self, rtype, result, url, duration, description=None): """Add a new response to stat.""" self.count += 1 if result == 'Successful': self.success += 1 else: self.error += 1 self.max = max(self.max, float(duration)) self.min = min(self.min, float(duration)) self.total += float(duration) self.percentiles.addResult(float(duration)) self.url = url self.type = rtype if description is not None: self.description = description self.finalized = False self.apdex.add(float(duration)) def finalize(self): """Compute avg times.""" if self.finalized: return if self.total: self.avg = self.total / float(self.count) self.min = min(self.max, self.min) if self.error: self.error_percent = 100.0 * self.error / float(self.count) self.percentiles.calcPercentiles() self.apdex_score = self.apdex.getScore() self.finalized = True class TestStat: """Collect test stat for a cycle. Stat on successful test case. """ def __init__(self, cycle, cycle_duration, cvus): self.cycle = cycle self.cycle_duration = float(cycle_duration) self.cvus = int(cvus) self.max = 0 self.min = 999999999 self.avg = 0 self.total = 0 self.count = 0 self.success = 0 self.error = 0 self.error_percent = 0 self.traceback = [] self.pages = self.images = self.redirects = self.links = 0 self.xmlrpc = 0 self.tps = 0 self.finalized = False self.percentiles = Percentiles(stepsize=5, name=cycle) def add(self, result, pages, xmlrpc, redirects, images, links, duration, traceback=None): """Add a new response to stat.""" self.finalized = False self.count += 1 if traceback is not None: self.traceback.append(traceback) if result == 'Successful': self.success += 1 else: self.error += 1 return self.max = max(self.max, float(duration)) self.min = min(self.min, float(duration)) self.total += float(duration) self.pages = max(self.pages, int(pages)) self.xmlrpc = max(self.xmlrpc, int(xmlrpc)) self.redirects = max(self.redirects, int(redirects)) self.images = max(self.images, int(images)) self.links = max(self.links, int(links)) self.percentiles.addResult(float(duration)) def finalize(self): """Compute avg times.""" if self.finalized: return if self.success: self.avg = self.total / float(self.success) self.min = min(self.max, self.min) if self.error: self.error_percent = 100.0 * self.error / float(self.count) if self.cycle_duration: self.tps = self.success / float(self.cycle_duration) self.percentiles.calcPercentiles() self.finalized = True funkload-1.17.1/src/funkload/TestRunner.py000066400000000000000000000442421302537724200204740ustar00rootroot00000000000000#!/usr/bin/python # (C) Copyright 2005 Nuxeo SAS # Author: bdelbosc@nuxeo.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """FunkLoad Test runner. Similar to unittest.TestProgram but: * you can pass the python module to load * able to override funkload configuration file using command line options * cool color output * support doctest with python2.4 $Id: TestRunner.py 24758 2005-08-31 12:33:00Z bdelbosc $ """ from __future__ import print_function from __future__ import absolute_import try: import psyco psyco.full() except ImportError: pass import os import sys import types import time import unittest import re from StringIO import StringIO from optparse import OptionParser, TitledHelpFormatter from .utils import red_str, green_str, get_version from funkload.FunkLoadTestCase import FunkLoadTestCase # ------------------------------------------------------------ # doctest patch to command verbose mode only available with python2.4 # g_doctest_verbose = False try: from doctest import DocTestSuite, DocFileSuite, DocTestCase, DocTestRunner from doctest import REPORTING_FLAGS, _unittest_reportflags g_has_doctest = True except ImportError: g_has_doctest = False else: def DTC_runTest(self): test = self._dt_test old = sys.stdout new = StringIO() optionflags = self._dt_optionflags if not (optionflags & REPORTING_FLAGS): # The option flags don't include any reporting flags, # so add the default reporting flags optionflags |= _unittest_reportflags # Patching doctestcase to enable verbose mode global g_doctest_verbose runner = DocTestRunner(optionflags=optionflags, checker=self._dt_checker, verbose=g_doctest_verbose) # End of patch try: runner.DIVIDER = "-"*70 failures, tries = runner.run( test, out=new.write, clear_globs=False) finally: sys.stdout = old if failures: raise self.failureException(self.format_failure(new.getvalue())) elif g_doctest_verbose: print(new.getvalue()) DocTestCase.runTest = DTC_runTest # ------------------------------------------------------------ # # class TestLoader(unittest.TestLoader): """Override to add options when instanciating test case.""" def loadTestsFromTestCase(self, testCaseClass): """Return a suite of all tests cases contained in testCaseClass""" if not issubclass(testCaseClass, FunkLoadTestCase): return unittest.TestLoader.loadTestsFromTestCase(self, testCaseClass) options = getattr(self, 'options', None) return self.suiteClass([testCaseClass(name, options) for name in self.getTestCaseNames(testCaseClass)]) def loadTestsFromModule(self, module): """Return a suite of all tests cases contained in the given module""" global g_has_doctest tests = [] doctests = None if g_has_doctest: try: doctests = DocTestSuite(module) except ValueError: pass for name in dir(module): obj = getattr(module, name) if (isinstance(obj, type) and issubclass(obj, unittest.TestCase)): tests.append(self.loadTestsFromTestCase(obj)) suite = self.suiteClass(tests) if doctests is not None: suite.addTest(doctests) return suite def loadTestsFromName(self, name, module=None): """Return a suite of all tests cases given a string specifier. The name may resolve either to a module, a test case class, a test method within a test case class, or a callable object which returns a TestCase or TestSuite instance. The method optionally resolves the names relative to a given module. """ parts = name.split('.') if module is None: if not parts: raise ValueError("incomplete test name: %s" % name) else: parts_copy = parts[:] while parts_copy: try: module = __import__('.'.join(parts_copy)) break except ImportError: del parts_copy[-1] if not parts_copy: raise parts = parts[1:] obj = module for part in parts: obj = getattr(obj, part) import unittest if type(obj) == types.ModuleType: return self.loadTestsFromModule(obj) elif (isinstance(obj, type) and issubclass(obj, unittest.TestCase)): return self.loadTestsFromTestCase(obj) elif type(obj) == types.UnboundMethodType: # pass funkload options if issubclass(obj.__self__.__class__, FunkLoadTestCase): return obj.__self__.__class__(obj.__name__, self.options) else: return obj.__self__.__class__(obj.__name__) elif callable(obj): test = obj() if not isinstance(test, unittest.TestCase) and \ not isinstance(test, unittest.TestSuite): raise ValueError("calling %s returned %s, not a test" % (obj,test)) return test else: raise ValueError("don't know how to make test from: %s" % obj) class ColoredStream(object): def __init__(self, stream): self.stream = stream def write(self, arg): if arg in ['OK', 'Ok', 'ok', '.']: arg = green_str(arg) elif arg in ['ERROR', 'E', 'FAILED', 'FAIL', 'F']: arg = red_str(arg) sys.stderr.write(arg) def __getattr__(self, attr): return getattr(self.stream, attr) class _ColoredTextTestResult(unittest._TextTestResult): """Colored version.""" def printErrorList(self, flavour, errors): flavour = red_str(flavour) super(_ColoredTextTestResult, self).printErrorList(flavour, errors) def filter_testcases(suite, cpattern, negative_pattern=False): """Filter a suite with test names that match the compiled regex pattern.""" new = unittest.TestSuite() for test in suite._tests: if isinstance(test, unittest.TestCase): name = test.id() # Full test name: package.module.class.method name = name[1 + name.rfind('.'):] # extract method name if cpattern.search(name): if not negative_pattern: new.addTest(test) elif negative_pattern: new.addTest(test) else: filtered = filter_testcases(test, cpattern, negative_pattern) if filtered: new.addTest(filtered) return new def display_testcases(suite): """Display test cases of the suite.""" for test in suite._tests: if isinstance(test, unittest.TestCase): name = test.id() name = name[1 + name.find('.'):] print(name) else: display_testcases(test) class TestProgram(unittest.TestProgram): """Override to add a python module and more options.""" USAGE = """%prog [options] file [class.method|class|suite] [...] %prog launch a FunkLoad unit test. A FunkLoad unittest uses a configuration file named [class].conf, this configuration is overriden by the command line options. See http://funkload.nuxeo.org/ for more information. Examples ======== %prog myFile.py Run all tests. %prog myFile.py test_suite Run suite named test_suite. %prog myFile.py MyTestCase.testSomething Run a single test MyTestCase.testSomething. %prog myFile.py MyTestCase Run all 'test*' test methods in MyTestCase. %prog myFile.py MyTestCase -u http://localhost Same against localhost. %prog --doctest myDocTest.txt Run doctest from plain text file (requires python2.4). %prog --doctest -d myDocTest.txt Run doctest with debug output (requires python2.4). %prog myfile.py -V Run default set of tests and view in real time each page fetch with firefox. %prog myfile.py MyTestCase.testSomething -l 3 -n 100 Run MyTestCase.testSomething, reload one hundred time the page 3 without concurrency and as fast as possible. Output response time stats. You can loop on many pages using slice -l 2:4. %prog myFile.py -e [Ss]ome Run all tests that match the regex [Ss]ome. %prog myFile.py -e '!xmlrpc$' Run all tests that does not ends with xmlrpc. %prog myFile.py --list List all the test names. %prog -h More options. """ def __init__(self, module=None, defaultTest=None, argv=None, testRunner=None, testLoader=unittest.defaultTestLoader): if argv is None: argv = sys.argv self.module = module self.testNames = None self.verbosity = 1 self.color = True self.profile = False self.defaultTest = defaultTest self.testLoader = testLoader self.progName = os.path.basename(argv[0]) self.parseArgs(argv) self.testRunner = testRunner self.checkAsDocFile = False module = self.module if type(module) == type(''): try: self.module = __import__(module) except ImportError: global g_has_doctest if g_has_doctest: # may be a doc file case self.checkAsDocFile = True else: raise else: for part in module.split('.')[1:]: self.module = getattr(self.module, part) else: self.module = module self.loadTests() if self.list_tests: display_testcases(self.test) else: self.runTests() def loadTests(self): """Load unit and doc tests from modules or names.""" if self.checkAsDocFile: self.test = DocFileSuite(os.path.abspath(self.module), module_relative=False) else: if self.testNames is None: self.test = self.testLoader.loadTestsFromModule(self.module) else: self.test = self.testLoader.loadTestsFromNames(self.testNames, self.module) if self.test_name_pattern is not None: test_name_pattern = self.test_name_pattern negative_pattern = False if test_name_pattern.startswith('!'): test_name_pattern = test_name_pattern[1:] negative_pattern = True cpattern = re.compile(test_name_pattern) self.test = filter_testcases(self.test, cpattern, negative_pattern) def parseArgs(self, argv): """Parse programs args.""" global g_doctest_verbose parser = OptionParser(self.USAGE, formatter=TitledHelpFormatter(), version="FunkLoad %s" % get_version()) parser.add_option("", "--config", type="string", dest="config", metavar='CONFIG', help="Path to alternative config file.") parser.add_option("-q", "--quiet", action="store_true", help="Minimal output.") parser.add_option("-v", "--verbose", action="store_true", help="Verbose output.") parser.add_option("-d", "--debug", action="store_true", help="FunkLoad and doctest debug output.") parser.add_option("--debug-level", type="int", help="Debug level 3 is more verbose.") parser.add_option("-u", "--url", type="string", dest="main_url", help="Base URL to bench without ending '/'.") parser.add_option("-m", "--sleep-time-min", type="string", dest="ftest_sleep_time_min", help="Minumum sleep time between request.") parser.add_option("-M", "--sleep-time-max", type="string", dest="ftest_sleep_time_max", help="Maximum sleep time between request.") parser.add_option("--dump-directory", type="string", dest="dump_dir", help="Directory to dump html pages.") parser.add_option("-V", "--firefox-view", action="store_true", help="Real time view using firefox, " "you must have a running instance of firefox " "in the same host.") parser.add_option("--no-color", action="store_true", help="Monochrome output.") parser.add_option("-l", "--loop-on-pages", type="string", dest="loop_steps", help="Loop as fast as possible without concurrency " "on pages, expect a page number or a slice like 3:5." " Output some statistics.") parser.add_option("-n", "--loop-number", type="int", dest="loop_number", default=10, help="Number of loop.") parser.add_option("--accept-invalid-links", action="store_true", help="Do not fail if css/image links are " "not reachable.") parser.add_option("--simple-fetch", action="store_true", dest="ftest_simple_fetch", help="Don't load additional links like css " "or images when fetching an html page.") parser.add_option("--stop-on-fail", action="store_true", help="Stop tests on first failure or error.") parser.add_option("-e", "--regex", type="string", default=None, help="The test names must match the regex.") parser.add_option("--list", action="store_true", help="Just list the test names.") parser.add_option("--doctest", action="store_true", default=False, help="Check for a doc test.") parser.add_option("--pause", action="store_true", help="Pause between request, " "press ENTER to continue.") parser.add_option("--profile", action="store_true", help="Run test under the Python profiler.") options, args = parser.parse_args() if self.module is None: if len(args) == 0: parser.error("incorrect number of arguments") # remove the .py module = args[0] if module.endswith('.py'): module = os.path.basename(os.path.splitext(args[0])[0]) self.module = module else: args.insert(0, self.module) if not options.doctest: global g_has_doctest g_has_doctest = False if options.verbose: self.verbosity = 2 if options.quiet: self.verbosity = 0 g_doctest_verbose = False if options.debug or options.debug_level: options.ftest_debug_level = 1 options.ftest_log_to = 'console file' g_doctest_verbose = True if options.debug_level: options.ftest_debug_level = int(options.debug_level) if sys.platform.lower().startswith('win'): self.color = False else: self.color = not options.no_color self.test_name_pattern = options.regex self.list_tests = options.list self.profile = options.profile # set testloader options self.testLoader.options = options if self.defaultTest is not None: self.testNames = [self.defaultTest] elif len(args) > 1: self.testNames = args[1:] # else we have to load all module test def runTests(self): """Launch the tests.""" if self.testRunner is None: if self.color: self.testRunner = unittest.TextTestRunner( stream =ColoredStream(sys.stderr), resultclass = _ColoredTextTestResult, verbosity = self.verbosity) else: self.testRunner = unittest.TextTestRunner( verbosity=self.verbosity) if self.profile: import profile, pstats pr = profile.Profile() d = {'self': self} pr.runctx('result = self.testRunner.run(self.test)', {}, d) result = d['result'] pr.dump_stats('profiledata') ps = pstats.Stats('profiledata') ps.strip_dirs() ps.sort_stats('cumulative') ps.print_stats() else: result = self.testRunner.run(self.test) sys.exit(not result.wasSuccessful()) # ------------------------------------------------------------ # main # def main(): """Default main.""" # enable to load module in the current path cur_path = os.path.abspath(os.path.curdir) sys.path.insert(0, cur_path) # use our testLoader test_loader = TestLoader() TestProgram(testLoader=test_loader) if __name__ == '__main__': main() funkload-1.17.1/src/funkload/XmlRpcBase.py000066400000000000000000000265271302537724200203710ustar00rootroot00000000000000# (C) Copyright 2005 Nuxeo SAS # Author: bdelbosc@nuxeo.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """Base class to build XML RPC daemon server. $Id$ """ from __future__ import absolute_import import sys, os from socket import error as SocketError from time import sleep from ConfigParser import ConfigParser, NoOptionError from SimpleXMLRPCServer import SimpleXMLRPCServer from xmlrpclib import ServerProxy import logging from optparse import OptionParser, TitledHelpFormatter from .utils import get_default_logger, close_logger from .utils import trace, get_version def is_server_running(host, port): """Check if the XML/RPC server is running checking getStatus RPC.""" server = ServerProxy("http://%s:%s" % (host, port)) try: server.getStatus() except SocketError: return False return True # ------------------------------------------------------------ # rpc to manage the server # class MySimpleXMLRPCServer(SimpleXMLRPCServer): """SimpleXMLRPCServer with allow_reuse_address.""" # this property set SO_REUSEADDR which tells the operating system to allow # code to connect to a socket even if it's waiting for other potential # packets allow_reuse_address = True # ------------------------------------------------------------ # Server # class XmlRpcBaseServer: """The base class for xml rpc server.""" usage = """%prog [options] config_file Start %prog XML/RPC daemon. """ server_name = None # list RPC Methods method_names = ['stopServer', 'getStatus'] def __init__(self, argv=None): if self.server_name is None: self.server_name = self.__class__.__name__ if argv is None: argv = sys.argv conf_path, options = self.parseArgs(argv) self.default_log_path = self.server_name + '.log' self.default_pid_path = self.server_name + '.pid' self.server = None self.quit = False # read conf conf = ConfigParser() conf.read(conf_path) self.conf_path = conf_path self.host = conf.get('server', 'host') self.port = conf.getint('server', 'port') try: self.pid_path = conf.get('server', 'pid_path') except NoOptionError: self.pid_path = self.default_pid_path try: log_path = conf.get('server', 'log_path') except NoOptionError: log_path = self.default_log_path if is_server_running(self.host, self.port): trace("Server already running on %s:%s." % (self.host, self.port)) sys.exit(0) trace('Starting %s server at http://%s:%s/' % (self.server_name, self.host, self.port)) # init logger if options.verbose: level = logging.DEBUG else: level = logging.INFO if options.debug: log_to = 'file console' else: log_to = 'file' self.logger = get_default_logger(log_to, log_path, level=level, name=self.server_name) # subclass init self._init_cb(conf, options) # daemon mode if not options.debug: trace(' as daemon.\n') close_logger(self.server_name) self.create_daemon() # re init the logger self.logger = get_default_logger(log_to, log_path, level=level, name=self.server_name) else: trace(' in debug mode.\n') # init rpc self.initServer() def _init_cb(self, conf, options): """init procedure intend to be implemented by subclasses. This method is called before to switch in daemon mode. conf is a ConfigParser object.""" pass def logd(self, message): """Debug log.""" self.logger.debug(message) def log(self, message): """Log information.""" self.logger.info(message) def parseArgs(self, argv): """Parse programs args.""" parser = OptionParser(self.usage, formatter=TitledHelpFormatter(), version="FunkLoad %s" % get_version()) parser.add_option("-v", "--verbose", action="store_true", help="Verbose output") parser.add_option("-d", "--debug", action="store_true", help="debug mode, server is run in forground") options, args = parser.parse_args(argv) if len(args) != 2: parser.error("Missing configuration file argument") return args[1], options def initServer(self): """init the XMLR/PC Server.""" self.log("Init XML/RPC server %s:%s." % (self.host, self.port)) server = MySimpleXMLRPCServer((self.host, self.port)) for method_name in self.method_names: self.logd('register %s' % method_name) server.register_function(getattr(self, method_name)) self.server = server def run(self): """main server loop.""" server = self.server pid = os.getpid() open(self.pid_path, "w").write(str(pid)) self.log("XML/RPC server pid=%i running." % pid) while not self.quit: server.handle_request() sleep(.5) server.server_close() self.log("XML/RPC server pid=%i stopped." % pid) os.remove(self.pid_path) __call__ = run # RPC # def stopServer(self): """Stop the server.""" self.log("stopServer request.") self.quit = True return 1 def getStatus(self): """Return a status.""" self.logd("getStatus request.") return "%s running pid = %s" % (self.server_name, os.getpid()) # ------------------------------------------------------------ # daemon # # See the Chad J. Schroeder example for a full explanation # this version does not chdir to '/' to keep relative path def create_daemon(self): """Detach a process from the controlling terminal and run it in the background as a daemon. """ try: pid = os.fork() except OSError as msg: raise Exception("%s [%d]" % (msg.strerror, msg.errno)) if (pid == 0): # child os.setsid() try: pid = os.fork() except OSError as msg: raise Exception("%s [%d]" % (msg.strerror, msg.errno)) if (pid == 0): # child os.umask(0) else: os._exit(0) else: while (not is_server_running(self.host, self.port)): sleep(1) os._exit(0) import resource maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1] if (maxfd == resource.RLIM_INFINITY): maxfd = 1024 for fd in range(0, maxfd): try: os.close(fd) except OSError: pass os.open('/dev/null', os.O_RDWR) os.dup2(0, 1) os.dup2(0, 2) return(0) # ------------------------------------------------------------ # Controller # class XmlRpcBaseController: """An XML/RPC controller.""" usage = """%prog config_file action action can be: start|startd|stop|restart|status|test Execute action on the XML/RPC server. """ # the server class server_class = XmlRpcBaseServer def __init__(self, argv=None): if argv is None: argv = sys.argv conf_path, self.action, options = self.parseArgs(argv) # read conf conf = ConfigParser() conf.read(conf_path) self.host = conf.get('server', 'host') self.conf_path = conf_path self.port = conf.getint('server', 'port') self.url = 'http://%s:%s/' % (self.host, self.port) self.quiet = options.quiet self.verbose = options.verbose self.server = ServerProxy(self.url) def parseArgs(self, argv): """Parse programs args.""" parser = OptionParser(self.usage, formatter=TitledHelpFormatter(), version="FunkLoad %s" % get_version()) parser.add_option("-q", "--quiet", action="store_true", help="Suppress console output") parser.add_option("-v", "--verbose", action="store_true", help="Verbose mode (log-level debug)") options, args = parser.parse_args(argv) if len(args) != 3: parser.error("Missing argument") return args[1], args[2], options def log(self, message, force=False): """Log a message.""" if force or not self.quiet: trace(str(message)) def startServer(self, debug=False): """Start an XML/RPC server.""" argv = ['cmd', self.conf_path] if debug: argv.append('-dv') elif self.verbose: argv.append('-v') daemon = self.server_class(argv) daemon.run() def __call__(self, action=None): """Call the xml rpc action""" server = self.server if action is None: action = self.action is_running = is_server_running(self.host, self.port) if action == 'status': if is_running: ret = server.getStatus() self.log('%s %s.\n' % (self.url, ret)) else: self.log('No server reachable at %s.\n' % self.url) return 0 elif action in ('stop', 'restart'): if is_running: ret = server.stopServer() self.log('Server %s is stopped.\n' % self.url) is_running = False elif action == 'stop': self.log('No server reachable at %s.\n' % self.url) if action == 'restart': self('start') elif 'start' in action: if is_running: self.log('Server %s is already running.\n' % self.url) else: return self.startServer(action=='startd') elif not is_running: self.log('No server reachable at %s.\n' % self.url) return -1 elif action == 'reload': ret = server.reloadConf() self.log('done\n') elif action == 'test': return self.test() else: raise NotImplementedError('Unknow action %s' % action) return 0 # this method is done to be overriden in sub classes def test(self): """Testing the XML/RPC. Must return an exit code, 0 for success. """ ret = self.server.getStatus() self.log('Testing getStatus: %s\n' % ret) return 0 def main(): """Main""" ctl = XmlRpcBaseController() ret = ctl() sys.exit(ret) if __name__ == '__main__': main() funkload-1.17.1/src/funkload/__init__.py000066400000000000000000000017041302537724200201160ustar00rootroot00000000000000# (C) Copyright 2005 Nuxeo SAS # Author: bdelbosc@nuxeo.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # # """Funkload package init. $Id: __init__.py 24649 2005-08-29 14:20:19Z bdelbosc $ """ # if gevent is present, patch the stdlib so we speed things up try: from gevent.monkey import patch_all patch_all() except ImportError: pass funkload-1.17.1/src/funkload/apdex.py000066400000000000000000000064361302537724200174670ustar00rootroot00000000000000#! /usr/bin/env python class Apdex(object): """Application Performance Index The Apdex score converts many measurements into one number on a uniform scale of 0-to-1 (0 = no users satisfied, 1 = all users satisfied). Visit http://www.apdex.org/ for more information. """ # T "constant" (can be changed by clients) T = 1.5 # in seconds @classmethod def satisfying(cls, duration): return duration < cls.T @classmethod def tolerable(cls, duration): return duration < cls.T*4 @classmethod def frustrating(cls, duration): return duration >= cls.T*4 @classmethod def score(cls, satisfied_users, tolerating_users, frustrated_users): count = sum([satisfied_users, tolerating_users, frustrated_users]) if count == 0: return 0 numeric_score = (satisfied_users + (tolerating_users/2.0))/count klass = cls.get_score_class(numeric_score) return klass(numeric_score) class Unacceptable(float): label = 'UNACCEPTABLE' threshold = 0.5 class Poor(float): label = 'POOR' threshold = 0.7 class Fair(float): label = 'FAIR' threshold = 0.85 class Good(float): label = 'Good' threshold = 0.94 class Excellent(float): label = 'Excellent' threshold = None # anythin above 0.94 is excellent # An ordered list of score classes, worst-to-best score_classes = [Unacceptable, Poor, Fair, Good, Excellent] @classmethod def get_score_class(cls, score): '''Given numeric score, return a score class''' for klass in cls.score_classes: if klass == cls.Excellent or score < klass.threshold: return klass @classmethod def get_label(cls, score): return cls.get_score_class(score).label description_para = '''\ Apdex T: Application Performance Index, this is a numerical measure of user satisfaction, it is based on three zones of application responsiveness: - Satisfied: The user is fully productive. This represents the time value (T seconds) below which users are not impeded by application response time. - Tolerating: The user notices performance lagging within responses greater than T, but continues the process. - Frustrated: Performance with a response time greater than 4*T seconds is unacceptable, and users may abandon the process. By default T is set to 1.5s. This means that response time between 0 and 1.5s the user is fully productive, between 1.5 and 6s the responsivness is tolerable and above 6s the user is frustrated. The Apdex score converts many measurements into one number on a uniform scale of 0-to-1 (0 = no users satisfied, 1 = all users satisfied). Visit http://www.apdex.org/ for more information.''' rating_para = '''\ Rating: To ease interpretation, the Apdex score is also represented as a rating: - U for UNACCEPTABLE represented in gray for a score between 0 and 0.5 - P for POOR represented in red for a score between 0.5 and 0.7 - F for FAIR represented in yellow for a score between 0.7 and 0.85 - G for Good represented in green for a score between 0.85 and 0.94 - E for Excellent represented in blue for a score between 0.94 and 1. ''' funkload-1.17.1/src/funkload/data/000077500000000000000000000000001302537724200167145ustar00rootroot00000000000000funkload-1.17.1/src/funkload/data/ConfigurationTestCase.tpl000066400000000000000000000052121302537724200237000ustar00rootroot00000000000000# FunkLoad test configuration file # $Id: $ # ------------------------------------------------------------ # Main section # [main] title=%(class_name)s description=XXX the TestCase class description # the server url to test url=%(server_url)s # the User-Agent header to send default is 'FunkLoad/1.xx' examples: #user_agent = Opera/8.0 (Windows NT 5.1; U; en) #user_agent = Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1) #user_agent = Mozilla/5.0 (X11; U; Linux i686; en; rv:1.7.10) Gecko/20050912 Firefox/1.0.6 # ------------------------------------------------------------ # Test description and configuration # [test_%(test_name)s] description=XXX the test case description # ------------------------------------------------------------ # Credential access # [credential] host=localhost port=8007 # ------------------------------------------------------------ # Monitoring configuration # [monitor] #hosts=localhost # Each host in [monitor] should have a section # with 'port' and 'description' keys [localhost] port=8008 description=The benching machine # ------------------------------------------------------------ # Configuration for unit test mode fl-run-test # [ftest] # log_to destination = # console - to the screen # file - to a file log_to = console file # log_path = full path to the log file (assumes ./ if no leading /) log_path = %(test_name)s-test.log # result_path = full path to the xml result file result_path = %(test_name)s-test.xml # ok_codes = list of HTTP response codes to consider "successful" # ok_codes = 200:301:302 # sleeptime_min / sleeptime_max = # minimum / maximum amount of time in seconds to sleep between # requests to the host sleep_time_min = 0 sleep_time_max = 0 # ------------------------------------------------------------ # Configuration for bench mode fl-run-bench # [bench] # cycles = list of cycles with their number of concurrent users cycles = 1:2:3 # duration = duration of a cycle in seconds duration = 30 # startup_delay = time to wait between starting-up threads in seconds startup_delay = 0.2 # sleep_time = time to wait between test in seconds sleep_time = 1 # cycle_time = time to wait between cycle in seconds cycle_time = 1 # same keys as in [ftest] section - see descriptions above log_to = file log_path = %(test_name)s-bench.log result_path = %(test_name)s-bench.xml #ok_codes = 200:301:302 sleep_time_min = 0 sleep_time_max = 2 # ------------------------------------------------------------ # Configuration for using the --distribute flag in fl-run-bench # [distribute] log_path = log-distributed funkload_location=http://pypi.python.org/packages/source/f/funkload/funkload-1.17.0.tar.gz funkload-1.17.1/src/funkload/data/MyFacesScriptTestCase.tpl000066400000000000000000000042331302537724200236070ustar00rootroot00000000000000# -*- coding: iso-8859-15 -*- """%(test_name)s FunkLoad test $Id: $ """ import unittest from funkload.FunkLoadTestCase import FunkLoadTestCase from webunit.utility import Upload from funkload.utils import Data #from funkload.utils import xmlrpc_get_credential class %(class_name)s(FunkLoadTestCase): """XXX This test uses the configuration file %(class_name)s.conf. """ MYFACES_STATE = 'org.apache.myfaces.trinidad.faces.STATE' MYFACES_FORM = 'org.apache.myfaces.trinidad.faces.FORM' MYFACES_TAG = ' # Author: bdelbosc@nuxeo.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """cmf FunkLoad test $Id$ """ import unittest from random import random from funkload.FunkLoadTestCase import FunkLoadTestCase from funkload.utils import xmlrpc_get_credential, xmlrpc_list_credentials from funkload.Lipsum import Lipsum class CmfTestCase(FunkLoadTestCase): """FL TestCase with common cmf tasks. self.server_url must be set. """ def cmfLogin(self, login, pwd): params = [["__ac_name", login], ["__ac_password", pwd], ["__ac_persistent", "1"], ["submit", " Login "]] self.post('%s/logged_in' % self.server_url, params, description="Log xin user %s" % login) self.assert_('Login success' in self.getBody(), "Invalid credential %s:%s" % (login, pwd)) self._cmf_login = login def cmfLogout(self): self.get('%s/logout' % self.server_url, description="logout %s" % self._cmf_login) def cmfCreateNews(self, parent_url): # return the doc_url lipsum = Lipsum() doc_id = lipsum.getUniqWord().lower() params = [["type_name", "News Item"], ["id", doc_id], ["add", "Add"]] self.post("%s/folder_factories" % parent_url, params, description="Create an empty news") params = [["allow_discussion", "default"], ["title", lipsum.getSubject()], ["description:text", lipsum.getParagraph()], ["subject:lines", lipsum.getWord()], ["format", "text/plain"], ["change_and_view", "Change and View"]] doc_url = "%s/%s" % (parent_url, doc_id) self.post("%s/metadata_edit_form" % doc_url, params, description="Set metadata") self.assert_('Metadata changed.' in self.getBody()) params = [["text_format", "plain"], ["description:text", lipsum.getParagraph()], ["text:text", lipsum.getMessage()], ["change_and_view", "Change and View"]] self.post("%s/newsitem_edit_form" % doc_url, params, description="Set news content") self.assert_('News Item changed.' in self.getBody()) return doc_url class Cmf(CmfTestCase): """Simple test of default CMF Site This test uses the configuration file Cmf.conf. """ def setUp(self): """Setting up test.""" self.logd("setUp") self.server_url = self.conf_get('main', 'url') credential_host = self.conf_get('credential', 'host') credential_port = self.conf_getInt('credential', 'port') self.credential_host = credential_host self.credential_port = credential_port self.cred_admin = xmlrpc_get_credential(credential_host, credential_port, 'AdminZope') self.cred_member = xmlrpc_get_credential(credential_host, credential_port, 'FL_Member') def test_00_verifyCmfSite(self): server_url = self.server_url if self.exists(server_url): self.logd('CMF Site already exists') return site_id = server_url.split('/')[-1] zope_url = server_url[:-(len(site_id)+1)] self.setBasicAuth(*self.cred_admin) self.get("%s/manage_addProduct/CMFDefault/addPortal" % zope_url) params = [["id", site_id], ["title", "FunkLoad CMF Site"], ["create_userfolder", "1"], ["description", "See http://svn.nuxeo.org/pub/funkload/trunk/README.txt " "for more info about FunkLoad"], ["submit", " Add "]] self.post("%s/manage_addProduct/CMFDefault/manage_addCMFSite" % zope_url, params, description="Create a CMF Site") self.get(server_url, description="View home page") self.clearBasicAuth() def test_05_verifyUsers(self): server_url = self.server_url user_mail = self.conf_get('test_05_verifyUsers', 'mail') lipsum = Lipsum() self.setBasicAuth(*self.cred_admin) for user_id, user_pwd in xmlrpc_list_credentials( self.credential_host, self.credential_port, 'FL_Member'): params = [["member_id", user_id], ["member_email", user_mail], ["password", user_pwd], ["confirm", user_pwd], ["add", "Register"]] self.post("%s/join_form" % server_url, params) html = self.getBody() self.assert_( 'Member registered' in html or 'The login name you selected is already in use' in html, "Member %s not created" % user_id) self.clearBasicAuth() def test_anonymous_reader(self): server_url = self.server_url self.get("%s/Members" % server_url, description="Try to see Members area") self.get("%s/recent_news" % server_url, description="Recent news") self.get("%s/search_form" % server_url, description="View search form") self.get("%s/login_form" % server_url, description="View login form") self.get("%s/join_form" % server_url, description="View join form") def test_member_reader(self): server_url = self.server_url self.cmfLogin(*self.cred_member) url = '%s/Members/%s/folder_contents' % (server_url, self.cred_member[0]) self.get(url, description="Personal workspace") self.get('%s/personalize_form' % server_url, description="Preference page") self.cmfLogout() def test_10_create_doc(self): nb_docs = self.conf_getInt('test_10_create_doc', 'nb_docs') server_url = self.server_url login = self.cred_member[0] self.cmfLogin(*self.cred_member) for i in range(nb_docs): self.cmfCreateNews("%s/Members/%s" % (server_url, login)) self.cmfLogout() # end of test ----------------------------------------------- def tearDown(self): """Setting up test.""" self.logd("tearDown.\n") if __name__ in ('main', '__main__'): unittest.main() funkload-1.17.1/src/funkload/demo/cps/000077500000000000000000000000001302537724200175145ustar00rootroot00000000000000funkload-1.17.1/src/funkload/demo/cps/CPS338TestCase.py000066400000000000000000000332221302537724200224070ustar00rootroot00000000000000# (C) Copyright 2005 Nuxeo SAS # Author: bdelbosc@nuxeo.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """FunkLoad test case for Nuxeo CPS. $Id: CPSTestCase.py 24728 2005-08-31 08:13:54Z bdelbosc $ """ import time import random from Lipsum import Lipsum from ZopeTestCase import ZopeTestCase class CPSTestCase(ZopeTestCase): """Common CPS tasks. setUp must set a server_url attribute.""" cps_test_case_version = (3, 3, 8) server_url = None _lipsum = Lipsum() _all_langs = ['en', 'fr', 'de', 'it', 'es', 'pt_BR', 'nl', 'mg', 'ro', 'eu'] _default_langs = _all_langs[:4] _cps_login = None # ------------------------------------------------------------ # cps actions # def cpsLogin(self, login, password, comment=None): """Log in a user. Raise on invalid credential.""" self._cps_login = None params = [['__ac_name', login], ['__ac_password', password], ['__ac_persistent', 'on'], ['submit', 'Login'], ] self.post("%s/logged_in" % self.server_url, params, description="Log in user [%s] %s" % (login, comment or '')) # assume we are logged in if we have a logout link... self.assert_('%s/logout' % self.server_url in self.listHref(), 'invalid credential: [%s:%s].' % (login, password)) self._cps_login = login def cpsLogout(self): """Log out the current user.""" if self._cps_login is not None: self.get('%s/logout' % self.server_url, description="Log out [%s]" % self._cps_login) def cpsCreateSite(self, admin_id, admin_pwd, manager_id, manager_password, manager_mail, langs=None, title=None, description=None, interface="portlets", zope_url=None, site_id=None): """Create a CPS Site. if zope_url or site_id is not provided guess them from the server_url. """ if zope_url is None or site_id is None: zope_url, site_id = self.cpsGuessZopeUrl() self.setBasicAuth(admin_id, admin_pwd) params = {"id": site_id, "title": title or "CPS Portal", "description": description or "A funkload cps test site", "manager_id": manager_id, "manager_password": manager_password, "manager_password_confirmation": manager_password, "manager_email": manager_mail, "manager_sn": "CPS", "manager_givenName": "Manager", "langs_list:list": langs or self._default_langs, "interface": interface, "submit": "Create"} self.post("%s/manage_addProduct/CPSDefault/manage_addCPSDefaultSite" % zope_url, params, description="Create a CPS Site") self.clearBasicAuth() def cpsCreateGroup(self, group_name): """Create a cps group.""" server_url = self.server_url params = [["dirname", "groups"], ["id", ""], ["widget__group", group_name], ["widget__members:tokens:default", ""], ["cpsdirectory_entry_create_form:method", "Create"]] self.post("%s/" % server_url, params) self.assert_(self.getLastUrl().find('psm_entry_created')!=-1, 'Failed to create group %s' % group_name) def cpsVerifyGroup(self, group_name): """Check existance or create a cps group.""" server_url = self.server_url params = [["dirname", "groups"], ["id", group_name],] if self.exists("%s/cpsdirectory_entry_view" % server_url, params, description="Check that group [%s] exists." % group_name): self.logd('Group %s exists.') else: self.cpsCreateGroup(group_name) def cpsCreateUser(self, user_id=None, user_pwd=None, user_givenName=None, user_sn=None, user_email=None, groups=None): """Create a cps user with the Member role. return login, pwd""" lipsum = self._lipsum sign = lipsum.getUniqWord() user_id = user_id or 'fl_' + sign.lower() user_givenName = user_givenName or lipsum.getWord().capitalize() user_sn = user_sn or user_id.upper() user_email = user_email or "root@127.0.0.01" user_pwd = user_pwd or lipsum.getUniqWord(length_min=6) params = [["dirname", "members"], ["id", ""], ["widget__id", user_id], ["widget__password", user_pwd], ["widget__confirm", user_pwd], ["widget__givenName", user_givenName], ["widget__sn", user_sn], ["widget__email", user_email], ["widget__roles:tokens:default", ""], ["widget__roles:list", "Member"], ["widget__groups:tokens:default", ""], ["widget__homeless", "0"], ["cpsdirectory_entry_create_form:method", "Create"]] for group in groups: params.append(["widget__groups:list", group]) self.post("%s/" % self.server_url, params, description="Create user [%s]" % user_id) self.assert_(self.getLastUrl().find('psm_entry_created')!=-1, 'Failed to create user %s' % user_id) return user_id, user_pwd def cpsVerifyUser(self, user_id=None, user_pwd=None, user_givenName=None, user_sn=None, user_email=None, groups=None): """Verify if user exists or create him. return login, pwd if user exists pwd is None. """ if user_id: params = [["dirname", "members"], ["id", user_id],] if self.exists( "%s/cpsdirectory_entry_view" % self.server_url, params): self.logd('User %s exists.') return user_id, None return self.cpsCreateUser(user_id, user_pwd, user_givenName, user_sn, user_email, groups) def cpsSetLocalRole(self, url, name, role): """Setup local role role to url.""" params = [["member_ids:list", name], ["member_role", role]] self.post("%s/folder_localrole_add" % url, params, description="Grant local role %s to %s" % (role, name)) def cpsCreateSection(self, parent_url, title, description="ftest section for funkload testing.", lang=None): """Create a section.""" return self.cpsCreateFolder('Section', parent_url, title, description, lang or self.cpsGetRandomLanguage()) def cpsCreateWorkspace(self, parent_url, title, description="ftest workspace for funkload testing.", lang=None): """Create a workspace.""" return self.cpsCreateFolder('Workspace', parent_url, title, description, lang or self.cpsGetRandomLanguage()) def cpsCreateFolder(self, type, parent_url, title, description, lang): """Create a section or a workspace. Return the section full url.""" params = [["type_name", type], ["widget__Title", title], ["widget__Description", description], ["widget__LanguageSelectorCreation", lang], ["widget__hidden_folder", "0"], ["cpsdocument_create_button", "Create"]] self.post("%s/cpsdocument_create_form" % parent_url, params, "Create a %s" % type) return self.cpsCleanUrl(self.getLastBaseUrl()) def cpsCreateDocument(self, parent_url): """Create a simple random document. return a tuple: (doc_url, doc_id) """ language = self.cpsGetRandomLanguage() title = self._lipsum.getSubject(uniq=True, prefix='test %s' % language) params = [["type_name", "Document"], ["widget__Title", title], ["widget__Description", self._lipsum.getSubject(10)], ["widget__LanguageSelectorCreation", language], ["widget__content", self._lipsum.getMessage()], ["widget__content_rformat", "text"], ["cpsdocument_create_button", "Create"]] self.post("%s/cpsdocument_create_form" % parent_url, params, description="Creating a document") self.assert_(self.getLastUrl().find('psm_content_created')!=-1, 'Failed to create [%s] in %s/.' % (title, parent_url)) doc_url = self.cpsCleanUrl(self.getLastBaseUrl()) doc_id = doc_url.split('/')[-1] return doc_url, doc_id def cpsCreateNewsItem(self, parent_url): """Create a random news. return a tuple: (doc_url, doc_id).""" language = self.cpsGetRandomLanguage() title = self._lipsum.getSubject(uniq=True, prefix='test %s' % language) params = [["type_name", "News Item"], ["widget__Title", title], ["widget__Description", self._lipsum.getSubject(10)], ["widget__LanguageSelectorCreation", language], ["widget__photo_title", "none"], ["widget__photo_filename", ""], ["widget__photo_choice", "keep"], ["widget__photo", ""], ["widget__photo_resize", "img_auto_size"], ["widget__photo_rposition", "left"], ["widget__photo_subtitle", ""], ["widget__content", self._lipsum.getMessage()], ["widget__content_rformat", "text"], ["widget__Subject:tokens:default", ""], ["widget__Subject:list", "Business"], # prevent invalid date depending on ui locale ["widget__publication_date_date", time.strftime('01/01/%Y')], ["widget__publication_date_hour", time.strftime('%H')], ["widget__publication_date_minute", time.strftime('%M')], ["cpsdocument_create_button", "Create"]] self.post("%s/cpsdocument_create_form" % parent_url, params, description="Creating a news item") last_url = self.getLastUrl() self.assert_('psm_content_created' in last_url, 'Failed to create [%s] in %s/.' % (title, parent_url)) doc_url = self.cpsCleanUrl(self.getLastBaseUrl()) doc_id = doc_url.split('/')[-1] return doc_url, doc_id def cpsChangeUiLanguage(self, lang): """Change the ui language and return the referer page.""" self.get("%s/cpsportlet_change_language" % self.server_url, params=[['lang', lang]], description="Change UI language to %s" % lang) # ------------------------------------------------------------ # helpers # def cpsGetRandomLanguage(self): """Return a random language.""" return random.choice(self._all_langs) def cpsGuessZopeUrl(self, cps_url=None): """Guess a zope url and site_id from a CPS Site url. return a tuple (zope_url, site_id) """ if cps_url is None: cps_url = self.server_url site_id = cps_url.split('/')[-1] zope_url = cps_url[:-(len(site_id)+1)] return zope_url, site_id def cpsSearchDocId(self, doc_id): """Return the list of url that ends with doc_id. Using catalog search.""" params = [["SearchableText", doc_id]] self.post("%s/search_form" % self.server_url, params, description="Searching doc_id %s" % doc_id) ret = self.cpsListDocumentHref(pattern='%s$' % doc_id) self.logd('found %i link ends with %s' % (len(ret), doc_id)) return ret def cpsCleanUrl(self, url_in): """Try to remove server_url and clean ending.""" url = url_in server_url = self.server_url for ending in ('/', '/view', '/folder_contents', '/folder_view', '/cpsdocument_metadata', '/cpsdocument_edit_form'): if url.endswith(ending): url = url[:-len(ending)] if url.startswith(server_url): url = url[len(server_url):] return url def cpsListDocumentHref(self, pattern=None): """Return a clean list of document href that matches pattern. Try to remove server_url and other cps trailings, return a list of uniq url.""" ret = [] for href in [self.cpsCleanUrl(x) for x in self.listHref(pattern)]: if href not in ret: ret.append(href) return ret funkload-1.17.1/src/funkload/demo/cps/CPS340DocTest.py000066400000000000000000000033301302537724200222270ustar00rootroot00000000000000# (C) Copyright 2006 Nuxeo SAS # Author: Olivier Grisel ogrisel@nuxeo.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """Doctest support for CPS340TestCase $Id$ """ from CPS340TestCase import CPSTestCase from FunkLoadDocTest import FunkLoadDocTest class CPSDocTest(FunkLoadDocTest, CPSTestCase): """Class to use to doctest a CPS portal >>> from CPS340DocTest import CPSDocTest >>> cps_url = 'http://localhost:8080/cps' >>> fl = CPSDocTest(cps_url) >>> fl.cps_test_case_version (3, 4, 0) >>> fl.server_url == cps_url True Then you can use the CPS340TestCase API like fl.cpsLogin('manager', 'pwd'). """ def __init__(self, server_url, debug=False, debug_level=1): """init CPSDocTest server_url is the CPS server url.""" FunkLoadDocTest.__init__(self, debug=debug, debug_level=debug_level) # FunkLoadDocTest handles the init of FunkLoadTestCase which is the # same as CPSTestCase self.server_url = server_url def _test(): import doctest, CPS340DocTest return doctest.testmod(CPS340DocTest) if __name__ == "__main__": _test() funkload-1.17.1/src/funkload/demo/cps/CPS340TestCase.py000066400000000000000000000340011302537724200223740ustar00rootroot00000000000000# (C) Copyright 2005 Nuxeo SAS # Author: bdelbosc@nuxeo.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """FunkLoad test case for Nuxeo CPS. $Id: CPSTestCase.py 24728 2005-08-31 08:13:54Z bdelbosc $ """ import time import random from Lipsum import Lipsum from ZopeTestCase import ZopeTestCase from webunit.utility import Upload class CPSTestCase(ZopeTestCase): """Common CPS tasks. setUp must set a server_url attribute.""" cps_test_case_version = (3, 4, 0) server_url = None _lipsum = Lipsum() _all_langs = ['en', 'fr', 'de', 'it', 'es', 'pt_BR', 'nl', 'mg', 'ro', 'eu'] _default_langs = _all_langs[:4] _default_extensions = ['CPSForum:default', 'CPSSkins:cps3', 'CPSSubscriptions:default'] _cps_login = None # ------------------------------------------------------------ # cps actions # def cpsLogin(self, login, password, comment=None): """Log in a user. Raise on invalid credential.""" self._cps_login = None params = [['__ac_name', login], ['__ac_password', password], ['__ac_persistent', 'on'], ['submit', 'Login'], ] self.post("%s/logged_in" % self.server_url, params, description="Log in user [%s] %s" % (login, comment or '')) # assume we are logged in if we have a logout link... self.assert_([link for link in self.listHref() if link.endswith('logout')], 'invalid credential: [%s:%s].' % (login, password)) self._cps_login = login def cpsLogout(self): """Log out the current user.""" if self._cps_login is not None: self.get('%s/logout' % self.server_url, description="Log out [%s]" % self._cps_login) def cpsCreateSite(self, admin_id, admin_pwd, manager_id, manager_password, manager_mail, langs=None, title=None, description=None, interface="portlets", zope_url=None, site_id=None, extensions=None): """Create a CPS Site. if zope_url or site_id is not provided guess them from the server_url. """ if zope_url is None or site_id is None: zope_url, site_id = self.cpsGuessZopeUrl() self.setBasicAuth(admin_id, admin_pwd) params = { 'site_id': site_id, 'title': title or "FunkLoad CPS Portal", 'manager_id': manager_id, 'password': manager_password, 'password_confirm': manager_password, 'manager_email': manager_mail, 'manager_firstname': 'Manager', 'manager_lastname': 'CPS Manager', 'extension_ids:list': extensions or self._default_extensions, 'description': description or "A funkload cps test site", 'languages:list': langs or self._default_langs, 'submit': 'Add', 'profile_id': 'CPSDefault:default'} self.post("%s/manage_addProduct/CPSDefault/addConfiguredCPSSite" % zope_url, params, description="Create a CPS Site") self.clearBasicAuth() def cpsCreateGroup(self, group_name): """Create a cps group.""" server_url = self.server_url params = [["dirname", "groups"], ["id", ""], ["widget__group", group_name], ["widget__members:tokens:default", ""], ["cpsdirectory_entry_create_form:method", "Create"]] self.post("%s/" % server_url, params) self.assert_(self.getLastUrl().find('psm_entry_created')!=-1, 'Failed to create group %s' % group_name) def cpsVerifyGroup(self, group_name): """Check existance or create a cps group.""" server_url = self.server_url params = [["dirname", "groups"], ["id", group_name],] if self.exists("%s/cpsdirectory_entry_view" % server_url, params, description="Check that group [%s] exists." % group_name): self.logd('Group %s exists.') else: self.cpsCreateGroup(group_name) def cpsCreateUser(self, user_id=None, user_pwd=None, user_givenName=None, user_sn=None, user_email=None, groups=None): """Create a cps user with the Member role. return login, pwd""" lipsum = self._lipsum sign = lipsum.getUniqWord() user_id = user_id or 'fl_' + sign.lower() user_givenName = user_givenName or lipsum.getWord().capitalize() user_sn = user_sn or user_id.upper() user_email = user_email or "root@127.0.0.01" user_pwd = user_pwd or lipsum.getUniqWord(length_min=6) params = [["dirname", "members"], ["id", ""], ["widget__id", user_id], ["widget__password", user_pwd], ["widget__confirm", user_pwd], ["widget__givenName", user_givenName], ["widget__sn", user_sn], ["widget__email", user_email], ["widget__roles:tokens:default", ""], ["widget__roles:list", "Member"], ["widget__groups:tokens:default", ""], ["widget__homeless:boolean", "False"], ["cpsdirectory_entry_create_form:method", "Create"]] for group in groups: params.append(["widget__groups:list", group]) self.post("%s/" % self.server_url, params, description="Create user [%s]" % user_id) self.assert_(self.getLastUrl().find('psm_entry_created')!=-1, 'Failed to create user %s' % user_id) return user_id, user_pwd def cpsVerifyUser(self, user_id=None, user_pwd=None, user_givenName=None, user_sn=None, user_email=None, groups=None): """Verify if user exists or create him. return login, pwd if user exists pwd is None. """ if user_id: params = [["dirname", "members"], ["id", user_id],] if self.exists( "%s/cpsdirectory_entry_view" % self.server_url, params): self.logd('User %s exists.') return user_id, None return self.cpsCreateUser(user_id, user_pwd, user_givenName, user_sn, user_email, groups) def cpsSetLocalRole(self, url, name, role): """Setup local role role to url.""" params = [["member_ids:list", name], ["member_role", role]] self.post("%s/folder_localrole_add" % url, params, description="Grant local role %s to %s" % (role, name)) def cpsCreateSection(self, parent_url, title, description="ftest section for funkload testing.", lang=None): """Create a section.""" return self.cpsCreateFolder('Section', parent_url, title, description, lang or self.cpsGetRandomLanguage()) def cpsCreateWorkspace(self, parent_url, title, description="ftest workspace for funkload testing.", lang=None): """Create a workspace.""" return self.cpsCreateFolder('Workspace', parent_url, title, description, lang or self.cpsGetRandomLanguage()) def cpsCreateFolder(self, type, parent_url, title, description, lang): """Create a section or a workspace. Return the section full url.""" params = [["type_name", type], ["widget__Title", title], ["widget__Description", description], ["widget__LanguageSelectorCreation", lang], ["widget__hidden_folder:boolean", False], ["cpsdocument_create_button", "Create"]] self.post("%s/cpsdocument_create" % parent_url, params, "Create a %s" % type) return self.cpsCleanUrl(self.getLastBaseUrl()) def cpsCreateDocument(self, parent_url): """Create a simple random document. return a tuple: (doc_url, doc_id) """ language = self.cpsGetRandomLanguage() title = self._lipsum.getSubject(uniq=True, prefix='test %s' % language) params = [["type_name", "Document"], ["widget__Title", title], ["widget__Description", self._lipsum.getSubject(10)], ["widget__LanguageSelectorCreation", language], ["widget__content", self._lipsum.getMessage()], ["widget__content_rformat", "text"], ["cpsdocument_create_button", "Create"]] self.post("%s/cpsdocument_create" % parent_url, params, description="Creating a document") self.assert_(self.getLastUrl().find('psm_content_created')!=-1, 'Failed to create [%s] in %s/.' % (title, parent_url)) doc_url = self.cpsCleanUrl(self.getLastBaseUrl()) doc_id = doc_url.split('/')[-1] return doc_url, doc_id def cpsCreateNewsItem(self, parent_url, photo_path=None): """Create a random news. return a tuple: (doc_url, doc_id).""" language = self.cpsGetRandomLanguage() title = self._lipsum.getSubject(uniq=True, prefix='test %s' % language) params = [["cpsformuid", self._lipsum.getUniqWord()], ["type_name", "News Item"], ["widget__Title", title], ["widget__Description", self._lipsum.getSubject(10)], ['widget__photo_filename', ''], ['widget__photo_choice', photo_path and 'change' or 'keep'], ['widget__photo', Upload(photo_path or '')], ['widget__photo_resize', 'img_auto_size'], ['widget__photo_rposition', 'left'], ['widget__photo_subtitle', ''], ["widget__content", self._lipsum.getMessage()], ["widget__content_rformat", "text"], ['widget__content_fileupload', Upload('')], ["widget__Subject:tokens:default", ""], ["widget__Subject:list", "Business"], # prevent invalid date depending on ui locale ["widget__publication_date_date", time.strftime('01/01/%Y')], ["widget__publication_date_hour", time.strftime('%H')], ["widget__publication_date_minute", time.strftime('%M')], ["cpsdocument_create_button", "Create"]] self.post("%s/cpsdocument_create" % parent_url, params, description="Creating a news item") last_url = self.getLastUrl() self.assert_('psm_content_created' in last_url, 'Failed to create [%s] in %s/.' % (title, parent_url)) doc_url = self.cpsCleanUrl(self.getLastBaseUrl()) doc_id = doc_url.split('/')[-1] return doc_url, doc_id def cpsChangeUiLanguage(self, lang): """Change the ui language and return the referer page.""" self.get("%s/cpsportlet_change_language" % self.server_url, params=[['lang', lang]], description="Change UI language to %s" % lang) # ------------------------------------------------------------ # helpers # def cpsGetRandomLanguage(self): """Return a random language.""" return random.choice(self._all_langs) def cpsGuessZopeUrl(self, cps_url=None): """Guess a zope url and site_id from a CPS Site url. return a tuple (zope_url, site_id) """ if cps_url is None: cps_url = self.server_url site_id = cps_url.split('/')[-1] zope_url = cps_url[:-(len(site_id)+1)] return zope_url, site_id def cpsSearchDocId(self, doc_id): """Return the list of url that ends with doc_id. Using catalog search.""" params = [["SearchableText", doc_id]] self.post("%s/search_form" % self.server_url, params, description="Searching doc_id %s" % doc_id) ret = self.cpsListDocumentHref(pattern='%s$' % doc_id) self.logd('found %i link ends with %s' % (len(ret), doc_id)) return ret def cpsCleanUrl(self, url_in): """Try to remove server_url and clean ending.""" url = url_in server_url = self.server_url for ending in ('/', '/view', '/folder_contents', '/folder_view', '/cpsdocument_metadata', '/cpsdocument_edit_form'): if url.endswith(ending): url = url[:-len(ending)] if url.startswith(server_url): url = url[len(server_url):] return url def cpsListDocumentHref(self, pattern=None): """Return a clean list of document href that matches pattern. Try to remove server_url and other cps trailings, return a list of uniq url.""" ret = [] for href in [self.cpsCleanUrl(x) for x in self.listHref(pattern)]: if href not in ret: ret.append(href) return ret funkload-1.17.1/src/funkload/demo/cps/CPSTestCase.py000066400000000000000000000015351302537724200221530ustar00rootroot00000000000000# (C) Copyright 2005 Nuxeo SAS # Author: bdelbosc@nuxeo.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """FunkLoad test case for Nuxeo CPS. $Id: CPSTestCase.py 24728 2005-08-31 08:13:54Z bdelbosc $ """ from CPS340TestCase import CPSTestCase funkload-1.17.1/src/funkload/demo/cps/ZopeTestCase.py000066400000000000000000000100341302537724200224350ustar00rootroot00000000000000# (C) Copyright 2005 Nuxeo SAS # Author: bdelbosc@nuxeo.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """FunkLoad test case for Zope. $Id$ """ import time from socket import error as SocketError from FunkLoadTestCase import FunkLoadTestCase class ZopeTestCase(FunkLoadTestCase): """Common zope 2.8 tasks.""" def zopeRestart(self, zope_url, admin_id, admin_pwd, time_out=600): """Stop and Start Zope server.""" self.setBasicAuth(admin_id, admin_pwd) params = {"manage_restart:action": "Restart"} url = "%s/Control_Panel" % zope_url self.post(url, params, description="Restarting Zope server") down = True time_start = time.time() while(down): time.sleep(2) try: self.get(url, description="Checking zope presence") except SocketError: if time.time() - time_start > time_out: self.fail('Zope restart time out %ss' % time_out) else: down = False self.clearBasicAuth() def zopePackZodb(self, zope_url, admin_id, admin_pwd, database="main", days=0): """Pack a zodb database.""" self.setBasicAuth(admin_id, admin_pwd) url = '%s/Control_Panel/Database/%s/manage_pack' % ( zope_url, database) params = {'days:float': str(days)} resp = self.post(url, params, description="Packing %s Zodb, removing previous " "revisions of objects that are older than %s day(s)." % (database, days), ok_codes=[200, 302, 500]) if resp.code == 500: if self.getBody().find( "Error Value: The database has already been packed") == -1: self.fail("Pack_zodb return a code 500.") else: self.logd('Zodb has already been packed.') self.clearBasicAuth() def zopeFlushCache(self, zope_url, admin_id, admin_pwd, database="main"): """Remove all objects from all ZODB in-memory caches.""" self.setBasicAuth(admin_id, admin_pwd) url = "%s/Control_Panel/Database/%s/manage_minimize" % (zope_url, database) self.get(url, description="Flush %s Zodb cache" % database) def zopeAddExternalMethod(self, parent_url, admin_id, admin_pwd, method_id, module, function, run_it=True): """Add an External method an run it.""" self.setBasicAuth(admin_id, admin_pwd) params = [["id", method_id], ["title", ""], ["module", module], ["function", function], ["submit", " Add "]] url = parent_url url += "/manage_addProduct/ExternalMethod/manage_addExternalMethod" resp = self.post(url, params, ok_codes=[200, 302, 400], description="Adding %s external method" % method_id) if resp.code == 400: if self.getBody().find('is invalid - it is already in use.') == -1: self.fail('Error got 400 on manage_addExternalMethod') else: self.logd('External method already exists') if run_it: self.get('%s/%s' % (parent_url, method_id), description="Execute %s external method" % method_id) self.clearBasicAuth() funkload-1.17.1/src/funkload/demo/django_demo/000077500000000000000000000000001302537724200211755ustar00rootroot00000000000000funkload-1.17.1/src/funkload/demo/django_demo/django_test_server/000077500000000000000000000000001302537724200250645ustar00rootroot00000000000000funkload-1.17.1/src/funkload/demo/django_demo/django_test_server/__init__.py000066400000000000000000000000001302537724200271630ustar00rootroot00000000000000funkload-1.17.1/src/funkload/demo/django_demo/django_test_server/manage.py000077500000000000000000000011141302537724200266660ustar00rootroot00000000000000#!/usr/bin/python from __future__ import absolute_import from django.core.management import execute_manager try: from . import settings # Assumed to be in the same directory. except ImportError: import sys sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) sys.exit(1) if __name__ == "__main__": execute_manager(settings) funkload-1.17.1/src/funkload/demo/django_demo/django_test_server/methods/000077500000000000000000000000001302537724200265275ustar00rootroot00000000000000funkload-1.17.1/src/funkload/demo/django_demo/django_test_server/methods/__init__.py000077500000000000000000000000001302537724200306310ustar00rootroot00000000000000funkload-1.17.1/src/funkload/demo/django_demo/django_test_server/methods/models.py000077500000000000000000000000711302537724200303650ustar00rootroot00000000000000from django.db import models # Create your models here. funkload-1.17.1/src/funkload/demo/django_demo/django_test_server/methods/urls.py000066400000000000000000000005431302537724200300700ustar00rootroot00000000000000from __future__ import absolute_import from django.conf.urls.defaults import * from . import views from django.views.generic.simple import direct_to_template from django.http import HttpResponse urlpatterns = patterns('', # Example: (r'get', views.getter), (r'put', views.putter), (r'delete', views.deleter), (r'post', views.poster)) funkload-1.17.1/src/funkload/demo/django_demo/django_test_server/methods/views.py000077500000000000000000000006161302537724200302440ustar00rootroot00000000000000# Create your views here. from django.http import HttpResponse,HttpResponseRedirect import django.http def getter(request): return HttpResponseRedirect("/fltest/_getter/") def poster(request): return HttpResponseRedirect("/fltest/_poster") def putter(request): return HttpResponseRedirect("/fltest/_putter") def deleter(request): return HttpResponseRedirect("/fltest/_deleter") funkload-1.17.1/src/funkload/demo/django_demo/django_test_server/settings.py000066400000000000000000000054031302537724200273000ustar00rootroot00000000000000# Django settings for fltest project. DEBUG = True TEMPLATE_DEBUG = DEBUG ADMINS = ( # ('Your Name', 'your_email@domain.com'), ) MANAGERS = ADMINS DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. DATABASE_NAME = '/home/ali/_dev/eclipse/fltest/sqlite.db' # Or path to database file if using sqlite3. DATABASE_USER = '' # Not used with sqlite3. DATABASE_PASSWORD = '' # Not used with sqlite3. DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3. DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3. # Local time zone for this installation. Choices can be found here: # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # although not all choices may be available on all operating systems. # If running in a Windows environment this must be set to the same as your # system time zone. TIME_ZONE = 'America/Chicago' # Language code for this installation. All choices can be found here: # http://www.i18nguy.com/unicode/language-identifiers.html LANGUAGE_CODE = 'en-us' SITE_ID = 1 # If you set this to False, Django will make some optimizations so as not # to load the internationalization machinery. USE_I18N = True # Absolute path to the directory that holds media. # Example: "/home/media/media.lawrence.com/" MEDIA_ROOT = '' # URL that handles the media served from MEDIA_ROOT. Make sure to use a # trailing slash if there is a path component (optional in other cases). # Examples: "http://media.lawrence.com", "http://example.com/media/" MEDIA_URL = '' # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a # trailing slash. # Examples: "http://foo.com/media/", "/media/". ADMIN_MEDIA_PREFIX = '/media/' # Make this unique, and don't share it with anybody. SECRET_KEY = 'f+q#v(p8q)*14u^i1)0^&a+f0@i7!n_0%lzr4wavocpj2nya71' # List of callables that know how to import templates from various sources. TEMPLATE_LOADERS = ( 'django.template.loaders.filesystem.load_template_source', 'django.template.loaders.app_directories.load_template_source', # 'django.template.loaders.eggs.load_template_source', ) MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', ) ROOT_URLCONF = 'fltest.urls' TEMPLATE_DIRS = ( # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". # Always use forward slashes, even on Windows. # Don't forget to use absolute paths, not relative paths. ) INSTALLED_APPS = ( 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', ) funkload-1.17.1/src/funkload/demo/django_demo/django_test_server/urls.py000066400000000000000000000027241302537724200264300ustar00rootroot00000000000000from django.conf.urls.defaults import * from django.http import HttpResponse,HttpResponseNotAllowed import datetime def _getter( request ): if request.method == "GET": now = datetime.datetime.now() html = "It is now %s." % now return HttpResponse(html) else: return HttpResponseNotAllowed("only gets") def _poster( request ): if request.method == "POST": now = datetime.datetime.now() html = "It is now %s." % now return HttpResponse(html) else: return HttpResponseNotAllowed("only posts") def _deleter( request ): if request.method == "DELETE": now = datetime.datetime.now() html = "It is now %s." % now return HttpResponse(html) else: return HttpResponseNotAllowed("only deletes") def _putter( request ): if request.method == "PUT": now = datetime.datetime.now() html = "It is now %s." % now return HttpResponse(html) else: return HttpResponseNotAllowed("only puts") urlpatterns = patterns('', # Example: (r'^site_media/(?P.*)$', 'django.views.static.serve', {'document_root': '/tmp/snapshots'}), (r'^fltest/methods',include('fltest.methods.urls')), (r'^fltest/_getter', _getter), (r'^fltest/_putter', _putter), (r'^fltest/_deleter',_deleter), (r'^fltest/_poster', _poster), ) funkload-1.17.1/src/funkload/demo/seam-booking-1.1.5/000077500000000000000000000000001302537724200217425ustar00rootroot00000000000000funkload-1.17.1/src/funkload/demo/seam-booking-1.1.5/Makefile000066400000000000000000000023261302537724200234050ustar00rootroot00000000000000# Makefile of FunkLoad xmlrpc demo .PHONY: clean all start stop restart status .PHONY: test credential_test bench credential_bench .PHONY: start_monitor stop_monitor restart_monitor .PHONY: start_credential stop_credential restart_credential MONCTL := fl-monitor-ctl monitor.conf CREDCTL := fl-credential-ctl credential.conf ifdef URL # FunkLoad options FLOPS = -u $(URL) else FLOPS = endif all: test # testing test: fl-run-test test_SeamBooking.py -v $(FLOPS) debug: fl-run-test -d test_SeamBooking.py -v $(FLOPS) # to display donwload of links debug_verbose: fl-run-test -d --debug-level=2 test_SeamBooking.py -v $(FLOPS) # Render each fetched page in a runing firefox debug_firefox: fl-run-test -dV test_SeamBooking.py -v $(FLOPS) # benching bench: @$(MONCTL) restart -fl-run-bench -c 1:20:40:60 -D 30 -m.01 -M2 test_SeamBooking.py SeamBooking.test_seam_booking $(FLOPS) -fl-build-report credential-bench.xml --html @$(MONCTL) stop # misc start_monitor: $(MONCTL) start stop_monitor: -$(MONCTL) stop restart_monitor: $(MONCTL) restart status: $(MONCTL) status; stop: stop_monitor start: start_monitor restart: restart_monitor clean: -find . "(" -name "*~" -or -name ".#*" ")" -print0 | xargs -0 rm -f funkload-1.17.1/src/funkload/demo/seam-booking-1.1.5/README.txt000066400000000000000000000016211302537724200234400ustar00rootroot00000000000000=============================== FunkLoad demo/seam-bookin-1.1.5 =============================== $Id$ Simple test of the Seam Booking application that comes with the JBoss Seam Framework 1.1.5 This script register a new user, search an hotel and book a room. To install seam booking application refer to the http://www.seamframework.org/, the script works with version 1.1.5 along with a jboss 4.0.5. Run test on a local instance: make Run test on a remote instance: make URL=http://another.seam.booking:8080 Run test in debug mode viewing all queries:: make debug Run test and view each fetched page into a running firefox:: make debug_firefox Run a small bench and produce a report:: make bench When you have 2 reports you can generate a differential report: fl-build-report --diff path/to/report/reference path/to/report/challenger More info on the http://funkload.nuxeo.org/ site. funkload-1.17.1/src/funkload/demo/seam-booking-1.1.5/SeamBooking.conf000066400000000000000000000045751302537724200250220ustar00rootroot00000000000000# FunkLoad test configuration file # $Id$ # ------------------------------------------------------------ # Main section # [main] title=SeamBooking description=Seam Booking simple bench # the server url to test url=http://localhost:8080 # the User-Agent header to send default is 'FunkLoad/1.xx' examples: #user_agent = Opera/8.0 (Windows NT 5.1; U; en) #user_agent = Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1) #user_agent = Mozilla/5.0 (X11; U; Linux i686; en; rv:1.7.10) Gecko/20050912 Firefox/1.0.6 # ------------------------------------------------------------ # Test description and configuration # [test_seam_booking] description=Register a new user, search an hotel and book a room. # ------------------------------------------------------------ # Credential access # [credential] host=localhost port=8007 # ------------------------------------------------------------ # Monitoring configuration # [monitor] hosts=localhost # Each host in [monitor] should have a section # with 'port' and 'description' keys [localhost] port=8008 description=The benching machine # ------------------------------------------------------------ # Configuration for unit test mode fl-run-test # [ftest] # log_to destination = # console - to the screen # file - to a file log_to = console file # log_path = full path to the log file (assumes ./ if no leading /) log_path = seam_booking-test.log # result_path = full path to the xml result file result_path = seam_booking-test.xml # ok_codes = list of HTTP response codes to consider "successful" # ok_codes = 200:301:302 # sleeptime_min / sleeptime_max = # minimum / maximum amount of time in seconds to sleep between # requests to the host sleep_time_min = 0 sleep_time_max = 0 # ------------------------------------------------------------ # Configuration for bench mode fl-run-bench # [bench] # cycles = list of cycles with their number of concurrent users cycles = 1:2:3 # duration = duration of a cycle in seconds duration = 30 # startup_delay = time to wait between starting-up threads in seconds startup_delay = 0.2 # sleep_time = time to wait between test in seconds sleep_time = 1 # cycle_time = time to wait between each cycle in seconds cycle_time = 1 # same keys as in [ftest] section log_to = file log_path = seam_booking-bench.log result_path = seam_booking-bench.xml #ok_codes = 200:301:302 sleep_time_min = 0 sleep_time_max = 2 funkload-1.17.1/src/funkload/demo/seam-booking-1.1.5/monitor.conf000066400000000000000000000010631302537724200243000ustar00rootroot00000000000000# default configuration file for the monitor server # note that %(FLOAD_HOME)s is accessible # # $Id$ # ------------------------------------------------------------ [server] # configuration used by monitord host = localhost port = 8008 # sleeptime between monitoring in second # note that load average is updated by the system only every 5s interval = .5 # network interface to monitor lo, eth0 interface = lo # ------------------------------------------------------------ [client] # configuration used by monitorctl host = localhost port = 44402 verbose = 0 funkload-1.17.1/src/funkload/demo/seam-booking-1.1.5/test_SeamBooking.py000066400000000000000000000154121302537724200255540ustar00rootroot00000000000000# -*- coding: iso-8859-15 -*- """seam_booking FunkLoad test $Id$ """ import unittest import random from funkload.FunkLoadTestCase import FunkLoadTestCase from webunit.utility import Upload from funkload.utils import Data from funkload.Lipsum import Lipsum class SeamBooking(FunkLoadTestCase): """Simple test to register a new user and book an hotel. This test uses the configuration file SeamBooking.conf. """ jsf_tag_tree = ' # Author: bdelbosc@nuxeo.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """Simple funkload zope tests $Id$ """ import unittest from funkload.ZopeTestCase import ZopeTestCase from funkload.Lipsum import Lipsum class Zope(ZopeTestCase): """Testing the funkload ZopeTestCase This test uses the configuration file Zope.conf. """ def setUp(self): """Setting up test.""" self.logd("setUp.") self.zope_url = self.conf_get('main', 'url') self.admin_id = self.conf_get('main', 'admin_id') self.admin_pwd = self.conf_get('main', 'admin_pwd') def test_flushCache(self): self.zopeFlushCache(self.zope_url, self.admin_id, self.admin_pwd) def test_restart(self): self.zopeRestart(self.zope_url, self.admin_id, self.admin_pwd, time_out=10) def test_packZodb(self): self.zopePackZodb(self.zope_url, self.admin_id, self.admin_pwd) def test_00_verifyExample(self): if not self.exists(self.zope_url + '/Examples'): self.setBasicAuth(self.admin_id, self.admin_pwd) self.get(self.zope_url + '/manage_importObject?file=Examples.zexp&set_owner:int=1') self.assert_('successfully imported' in self.getBody()) self.clearBasicAuth() self.get(self.zope_url + '/Examples') def test_exampleNavigation(self): server_url = self.zope_url self.get("%s/Examples" % server_url) self.get("%s/Examples/Navigation" % server_url) self.get("%s/Examples/Navigation/Mammals" % server_url) self.get("%s/Examples/Navigation/Mammals/Primates" % server_url) self.get("%s/Examples/Navigation/Mammals/Primates/Monkeys" % server_url) self.get("%s/Examples/Navigation/Mammals/Whales" % server_url) self.get("%s/Examples/Navigation/Mammals/Bats" % server_url) self.get("%s/Examples" % server_url) def test_exampleGuestBook(self): server_url = self.zope_url self.get("%s/Examples/GuestBook" % server_url) server_url = self.zope_url self.setBasicAuth(self.admin_id, self.admin_pwd) lipsum = Lipsum() self.get("%s/Examples/GuestBook/addEntry.html" % server_url) params = [["guest_name", lipsum.getWord().capitalize()], ["comments", lipsum.getParagraph()]] self.post("%s/Examples/GuestBook/addEntry" % server_url, params) self.clearBasicAuth() def test_exampleFileLibrary(self): server_url = self.zope_url self.get("%s/Examples/FileLibrary" % server_url) for sort in ('type', 'size', 'date'): params = [["sort", sort], ["reverse:int", "0"]] self.post("%s/Examples/FileLibrary/index_html" % server_url, params, description="File Library sort by %s" % sort) def test_exampleShoppingCart(self): server_url = self.zope_url self.get("%s/Examples/ShoppingCart" % server_url) params = [["orders.id:records", "510-115"], ["orders.quantity:records", "1"], ["orders.id:records", "510-122"], ["orders.quantity:records", "2"], ["orders.id:records", "510-007"], ["orders.quantity:records", "3"]] self.post("%s/Examples/ShoppingCart/addItems" % server_url, params) def test_anonymous_reader(self): server_url = self.zope_url self.get("%s/Examples/Navigation/Mammals/Whales" % server_url) self.get("%s/Examples/GuestBook" % server_url) self.get("%s/Examples/GuestBook/addEntry.html" % server_url) params = [["sort", 'date'], ["reverse:int", "0"]] self.get("%s/Examples/FileLibrary/index_html" % server_url, params) self.get("%s/Examples/ShoppingCart" % server_url) def tearDown(self): """Setting up test.""" self.logd("tearDown.") if __name__ in ('main', '__main__'): unittest.main() funkload-1.17.1/src/funkload/rtfeedback.py000066400000000000000000000062761302537724200204620ustar00rootroot00000000000000from __future__ import print_function import json import socket try: import gevent import zmq.green as zmq except ImportError: import zmq from multiprocessing import Process from zmq.eventloop import ioloop, zmqstream DEFAULT_ENDPOINT = 'tcp://127.0.0.1:9999' DEFAULT_PUBSUB = 'tcp://127.0.0.1:9998' class FeedbackPublisher(Process): """Publishes all the feedback received from the various nodes. """ def __init__(self, endpoint=DEFAULT_ENDPOINT, pubsub_endpoint=DEFAULT_PUBSUB, context=None, handler=None): Process.__init__(self) self.context = context self.endpoint = endpoint self.pubsub_endpoint = pubsub_endpoint self.daemon = True self.handler = handler def _handler(self, msg): if self.handler is not None: self.handler(msg) self.pub_sock.send_multipart(['feedback', msg[0]]) def run(self): print('publisher running in a thread') self.context = self.context or zmq.Context.instance() self.sock = self.context.socket(zmq.PULL) self.sock.bind(self.endpoint) self.pub_sock = self.context.socket(zmq.PUB) self.pub_sock.bind(self.pubsub_endpoint) self.loop = ioloop.IOLoop.instance() self.stream = zmqstream.ZMQStream(self.sock, self.loop) self.stream.on_recv(self._handler) self.loop.start() def stop(self): self.loop.close() class FeedbackSender(object): """Sends feedback """ def __init__(self, endpoint=DEFAULT_ENDPOINT, server=None, context=None): self.context = context or zmq.Context.instance() self.sock = self.context.socket(zmq.PUSH) self.sock.connect(endpoint) if server is None: server = socket.gethostname() self.server = server def test_done(self, data): data['server'] = self.server self.sock.send(json.dumps(data)) class FeedbackSubscriber(Process): """Subscribes to a published feedback. """ def __init__(self, pubsub_endpoint=DEFAULT_PUBSUB, handler=None, context=None, **kw): Process.__init__(self) self.handler = handler self.context = context self.pubsub_endpoint = pubsub_endpoint self.daemon = True self.kw = kw def _handler(self, msg): topic, msg = msg msg = json.loads(msg) if self.handler is None: print(msg) else: self.handler(msg, **self.kw) def run(self): self.context = self.context or zmq.Context.instance() self.pub_sock = self.context.socket(zmq.SUB) self.pub_sock.connect(self.pubsub_endpoint) self.pub_sock.setsockopt(zmq.SUBSCRIBE, b'') self.loop = ioloop.IOLoop.instance() self.stream = zmqstream.ZMQStream(self.pub_sock, self.loop) self.stream.on_recv(self._handler) self.loop.start() def stop(self): self.loop.close() if __name__ == '__main__': print('Starting subscriber') sub = FeedbackSubscriber() print('Listening to events on %r' % sub.pubsub_endpoint) try: sub.run() except KeyboardInterrupt: sub.stop() print('Bye!') funkload-1.17.1/src/funkload/tests/000077500000000000000000000000001302537724200171455ustar00rootroot00000000000000funkload-1.17.1/src/funkload/tests/__init__.py000066400000000000000000000000441302537724200212540ustar00rootroot00000000000000"""FunkLoad test package. $Id$ """ funkload-1.17.1/src/funkload/tests/doctest_dummy.txt000066400000000000000000000006131302537724200225660ustar00rootroot00000000000000============= Dummy DocTest ============= This is a dummy doc test to check fl-run-test doctest support: >>> 1 + 1 2 Check FunkLoad doctest: >>> from funkload.FunkLoadDocTest import FunkLoadDocTest >>> fl = FunkLoadDocTest() >>> fl.get('http://localhost/') See http://funkload.nuxeo.org/ for more information about FunkLoad funkload-1.17.1/src/funkload/tests/test_Install.py000066400000000000000000000214541302537724200221720ustar00rootroot00000000000000# (C) Copyright 2005 Nuxeo SAS # Author: bdelbosc@nuxeo.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # # """Check an installed FunkLoad. $Id$ """ from __future__ import print_function import os import sys import unittest import commands def winplatform_getstatusoutput(cmd): """A replacement for commands.getstatusoutput on the windows platform. commands.getstatusoutput only works on unix platforms. This only works with python2.6+ as the subprocess module is required. os.system provides the return code value but not the output streams of the commands. os.popen provides the output streams but no reliable easy to get return code. """ try: import subprocess except ImportError: return None # create a new handle for the stdout pipe of cmd, and redirect cmd's stderr to stdout process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, universal_newlines=True) stdoutdata, stderrdata = process.communicate() return (process.returncode, stdoutdata) class TestInstall(unittest.TestCase): """Check installation.""" def setUp(self): self.test_file = 'test_dummy.py' self.doctest_file = 'doctest_dummy.txt' def system(self, cmd, expected_code=0): """Execute a cmd and exit on fail return cmd output.""" if sys.platform.lower().startswith('win'): ret = winplatform_getstatusoutput(cmd) if not ret: self.fail('Cannot run self.system on windows without the subprocess module (python 2.6)') else: ret = commands.getstatusoutput(cmd) if ret[0] != expected_code: self.fail("exec [%s] return code %s != %s output:\n%s" % (cmd, ret[0], expected_code, ret[1])) return ret[1] def test_01_requires(self): try: import webunit except ImportError: self.fail('Missing Required module webunit') try: import funkload except ImportError: self.fail('Unable to import funkload module.') try: import docutils except ImportError: print ("WARNING: missing docutils module, " "no HTML report available.") if sys.platform.lower().startswith('win'): ret = winplatform_getstatusoutput('wgnuplot --version') if not ret: self.fail('Cannot run self.system on windows without the subprocess module (python 2.6)') else: ret = commands.getstatusoutput('gnuplot --version') print(ret[1]) if ret[0]: print ("WARNING: gnuplot is missing, no charts available in " "HTML reports.") from funkload.TestRunner import g_has_doctest if not g_has_doctest: print("WARNING: Python 2.4 is required to support doctest") def test_testloader(self): # check testrunner loader test_file = self.test_file # listing test output = self.system("fl-run-test %s --list" % test_file) self.assert_('test_dummy1_1' in output) self.assert_('test_dummy2_1' in output) self.assert_('test_dummy3_1' in output) # list a test suite output = self.system("fl-run-test %s test_suite --list" % test_file) self.assert_('test_dummy1_1' in output) self.assert_('test_dummy2_1' in output) self.assert_('test_dummy3_1' not in output) # list all test in a test case class output = self.system("fl-run-test %s TestDummy1 --list" % test_file) self.assert_('test_dummy1_1' in output) self.assert_('test_dummy1_2' in output) self.assert_('test_dummy2_1' not in output) # match regex output = self.system("fl-run-test %s --list -e dummy1_1" % test_file) self.assert_('test_dummy1_1' in output) self.assert_('test_dummy2_1' not in output) output = self.system("fl-run-test %s TestDummy1 --list -e dummy1_1" % test_file) self.assert_('test_dummy1_1' in output) self.assert_('test_dummy2_1' not in output) output = self.system("fl-run-test %s --list -e 2$" % test_file) self.assert_('test_dummy1_2' in output) self.assert_('test_dummy2_2' in output) self.assert_('test_dummy1_1' not in output) self.assert_('test_dummy2_1' not in output) output = self.system("fl-run-test %s --list -e '!2$'" % test_file) self.assert_('test_dummy1_1' in output, output) self.assert_('test_dummy2_1' in output) self.assert_('test_dummy1_2' not in output) self.assert_('test_dummy2_2' not in output) def test_doctestloader(self): # check testrunner loader from funkload.TestRunner import g_has_doctest if not g_has_doctest: self.fail('Python 2.4 is required to support doctest') test_file = self.test_file # listing test output = self.system("fl-run-test %s --doctest --list" % test_file) self.assert_('Dummy.double' in output, 'missing doctest') # list a test suite output = self.system("fl-run-test %s --doctest test_suite --list" % test_file) self.assert_('Dummy.double' not in output, 'doctest is not part of the suite') # list all test in a test case class output = self.system("fl-run-test %s --doctest TestDummy1 --list" % test_file) self.assert_('Dummy.double' not in output, 'doctest is not part of the testcase') # pure doctest doctest_file = self.doctest_file output = self.system("fl-run-test %s --doctest --list" % doctest_file) self.assert_(doctest_file.replace('.', '_') in output, 'no %s in output %s' % (doctest_file, output)) # match regex output = self.system("fl-run-test %s --doctest --list -e dummy1_1" % test_file) def test_testrunner(self): # try to launch a test test_file = self.test_file output = self.system('fl-run-test %s TestDummy1 -v' % test_file) self.assert_('Ran 0 tests' not in output, 'not expected output:"""%s"""' % output) output = self.system('fl-run-test %s TestDummy2 -v' % test_file) self.assert_('Ran 0 tests' not in output, 'not expected output:"""%s"""' % output) # doctest from funkload.TestRunner import g_has_doctest if g_has_doctest: output = self.system('fl-run-test %s --doctest -e double -v' % test_file) self.assert_('Ran 0 tests' not in output, 'not expected output:"""%s"""' % output) # failing test output = self.system('fl-run-test %s TestDummy3 -v' % test_file, expected_code=256) self.assert_('Ran 0 tests' not in output, 'not expected output:"""%s"""' % output) self.assert_('FAILED' in output) self.assert_('ERROR' in output) def test_xmlrpc(self): # windows os does not support the monitor server if not sys.platform.lower().startswith('win'): # extract demo example and run the xmlrpc test from tempfile import mkdtemp pwd = os.getcwd() tmp_path = mkdtemp('funkload') os.chdir(tmp_path) self.system('fl-install-demo') os.chdir(os.path.join(tmp_path, 'funkload-demo', 'xmlrpc')) self.system("fl-credential-ctl cred.conf restart") self.system("fl-monitor-ctl monitor.conf restart") self.system("fl-run-test -v test_Credential.py") self.system("fl-run-bench -c 1:10:20 -D 4 " "test_Credential.py Credential.test_credential") self.system("fl-monitor-ctl monitor.conf stop") self.system("fl-credential-ctl cred.conf stop") self.system("fl-build-report credential-bench.xml --html") os.chdir(pwd) def test_suite(): """Return a test suite.""" suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(TestInstall)) return suite if __name__ in ('main', '__main__'): unittest.main() funkload-1.17.1/src/funkload/tests/test_apdex.py000066400000000000000000000023351302537724200216620ustar00rootroot00000000000000#! /usr/bin/env python import os import sys import unittest if os.path.realpath(os.curdir) == os.path.realpath(os.path.dirname(__file__)): sys.path.append('../..') from funkload.apdex import Apdex class TestApdex(unittest.TestCase): def test_sanity(self): self.assertEqual(Apdex.T, 1.5) self.assertTrue(Apdex.satisfying(0.1)) self.assertTrue(Apdex.satisfying(1.49)) self.assertFalse(Apdex.satisfying(1.5)) self.assertTrue(Apdex.tolerable(1.5)) self.assertTrue(Apdex.tolerable(5.99)) self.assertFalse(Apdex.tolerable(6.0)) self.assertTrue(Apdex.frustrating(6.0)) def test_100_percent_satisfied(self): s, t, f = 10, 0, 0 score = Apdex.score(s, t, f) self.assertTrue(score == 1.0) self.assertTrue(score.label == Apdex.Excellent.label) self.assertTrue(Apdex.get_label(score) == Apdex.Excellent.label) def test_unacceptable(self): s, t, f = 0, 0, 10 score = Apdex.score(s, t, f) self.assertTrue(score == 0) self.assertTrue(score.label == Apdex.Unacceptable.label) self.assertTrue(Apdex.get_label(score) == Apdex.Unacceptable.label) if __name__ == '__main__': unittest.main() funkload-1.17.1/src/funkload/tests/test_dummy.py000066400000000000000000000041431302537724200217130ustar00rootroot00000000000000# (C) Copyright 2006 Nuxeo SAS # Author: bdelbosc@nuxeo.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # # """Dummy test used by test_Install.py $Id$ simple doctest in a docstring: >>> 1 + 1 2 """ import unittest class TestDummy1(unittest.TestCase): """Dummy test case.""" def test_dummy1_1(self): self.assertEquals(1+1, 2) def test_dummy1_2(self): self.assertEquals(1+1, 2) class TestDummy2(unittest.TestCase): """Dummy test case.""" def test_dummy2_1(self): self.assertEquals(1+1, 2) def test_dummy2_2(self): self.assertEquals(1+1, 2) class TestDummy3(unittest.TestCase): """Failing test case not part of the test_suite.""" def test_dummy3_1(self): self.assertEquals(1+1, 2) def test_dummy3_2(self): # failing test case self.assertEquals(1+1, 3, 'example of a failing test') def test_dummy3_3(self): # error test case impossible = 1/0 self.assert_(1+1, 2) class Dummy: """Testing docstring.""" def __init__(self, value): self.value = value def double(self): """Return the double of the initial value. >>> d = Dummy(1) >>> d.double() 2 """ return self.value * 2 def test_suite(): """Return a test suite.""" suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(TestDummy1)) suite.addTest(unittest.makeSuite(TestDummy2)) return suite if __name__ in ('main', '__main__'): unittest.main() funkload-1.17.1/src/funkload/tests/test_monitor_plugins.py000066400000000000000000000036241302537724200240130ustar00rootroot00000000000000import unittest import time from ConfigParser import ConfigParser from funkload.MonitorPlugins import MonitorPlugins class TestMonitorPlugins(unittest.TestCase): default_plugins=['MonitorCPU', 'MonitorNetwork', 'MonitorMemFree', 'MonitorCUs'] def test_register_default(self): """ Make sure all default plugins are loaded """ p=MonitorPlugins() p.registerPlugins() plugins_loaded=p.MONITORS.keys() for plugin in self.default_plugins: self.assertTrue(plugin in plugins_loaded) def test_getStat(self): """ Make sure getStat does not raise any exception """ p=MonitorPlugins() p.registerPlugins() for plugin in self.default_plugins: p.MONITORS[plugin].getStat() def test_network(self): """ Make sure self.interface is properly read from config in MonitorNetwork plugin """ conf=ConfigParser() conf.add_section('server') conf.set('server', 'interface', 'eth9') p=MonitorPlugins(conf) p.registerPlugins() self.assertTrue(p.MONITORS['MonitorNetwork'].interface == 'eth9') def test_MonitorInfo(self): """ Make sure Monitor.MonitorInfo still works with plugins """ from funkload.Monitor import MonitorInfo p=MonitorPlugins() p.registerPlugins() m=MonitorInfo('somehost', p) self.assertTrue(m.host=='somehost') def test_MonitorThread(self): """ Make sure Monitor.MonitorThread still works with plugins """ from funkload.Monitor import MonitorThread p=MonitorPlugins() p.registerPlugins() records=[] monitor = MonitorThread(records, p, 'localhost', 1) monitor.start() monitor.startRecord() time.sleep(3) monitor.stopRecord() monitor.stop() self.assertTrue(len(records)>0) if __name__ == '__main__': unittest.main() funkload-1.17.1/src/funkload/tests/test_rtfeedback.py000066400000000000000000000012001302537724200226410ustar00rootroot00000000000000import unittest import time from funkload import rtfeedback import zmq class TestFeedback(unittest.TestCase): def test_feedback(self): context = zmq.Context.instance() pub = rtfeedback.FeedbackPublisher(context=context) pub.start() msgs = [] def _msg(msg): msgs.append(msg) sub = rtfeedback.FeedbackSubscriber(handler=_msg, context=context) sub.start() sender = rtfeedback.FeedbackSender(context=context) for i in range(10): sender.test_done({'result': 'success'}) time.sleep(.1) self.assertEqual(len(msgs), 10) funkload-1.17.1/src/funkload/utils.py000066400000000000000000000265371302537724200175320ustar00rootroot00000000000000# (C) Copyright 2005-2010 Nuxeo SAS # Author: bdelbosc@nuxeo.com # Contributors: Goutham Bhat # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. # """FunkLoad common utils. $Id: utils.py 24649 2005-08-29 14:20:19Z bdelbosc $ """ import os import sys import time import logging from time import sleep from socket import error as SocketError from xmlrpclib import ServerProxy import pkg_resources import tarfile import tempfile def thread_sleep(seconds=0): """Sleep seconds.""" # looks like python >= 2.5 does not need a minimal sleep to let thread # working properly if seconds: sleep(seconds) # ------------------------------------------------------------ # semaphores # g_recording = False def recording(): """A semaphore to tell the running threads when to begin recording.""" global g_recording return g_recording def set_recording_flag(value): """Enable recording.""" global g_recording g_recording = value # ------------------------------------------------------------ # daemon # # See the Chad J. Schroeder example for a full explanation # this version does not chdir to '/' to keep relative path def create_daemon(): """Detach a process from the controlling terminal and run it in the background as a daemon. """ try: pid = os.fork() except OSError as msg: raise Exception("%s [%d]" % (msg.strerror, msg.errno)) if (pid == 0): os.setsid() try: pid = os.fork() except OSError as msg: raise Exception("%s [%d]" % (msg.strerror, msg.errno)) if (pid == 0): os.umask(0) else: os._exit(0) else: sleep(.5) os._exit(0) import resource maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1] if (maxfd == resource.RLIM_INFINITY): maxfd = 1024 for fd in range(0, maxfd): try: os.close(fd) except OSError: pass os.open('/dev/null', os.O_RDWR) os.dup2(0, 1) os.dup2(0, 2) return(0) # ------------------------------------------------------------ # meta method name encodage # MMN_SEP = ':' # meta method name separator def mmn_is_bench(meta_method_name): """Is it a meta method name ?.""" return meta_method_name.count(MMN_SEP) and True or False def mmn_encode(method_name, cycle, cvus, thread_id): """Encode a extra information into a method_name.""" return MMN_SEP.join((method_name, str(cycle), str(cvus), str(thread_id))) def mmn_decode(meta_method_name): """Decode a meta method name.""" if mmn_is_bench(meta_method_name): method_name, cycle, cvus, thread_id = meta_method_name.split(MMN_SEP) return (method_name, int(cycle), int(cvus), int(thread_id)) else: return (meta_method_name, 1, 0, 1) # ------------------------------------------------------------ # logging # def get_default_logger(log_to, log_path=None, level=logging.DEBUG, name='FunkLoad'): """Get a logger.""" logger = logging.getLogger(name) if logger.handlers: # already setup return logger if log_path: log_dir = os.path.dirname(log_path) if log_dir and not os.path.exists(log_dir): try: os.makedirs(log_dir) except Exception as e: raise Exception("%s, (%s) (%s)" % (e, log_dir, log_path)) if log_to.count("console"): hdlr = logging.StreamHandler() logger.addHandler(hdlr) if log_to.count("file") and log_path: formatter = logging.Formatter( '%(asctime)s %(levelname)s %(message)s') hdlr = logging.FileHandler(log_path) hdlr.setFormatter(formatter) logger.addHandler(hdlr) if log_to.count("xml") and log_path: if os.access(log_path, os.F_OK): os.rename(log_path, log_path + '.bak-' + str(int(time.time()))) hdlr = logging.FileHandler(log_path) logger.addHandler(hdlr) logger.setLevel(level) return logger def close_logger(name): """Close the logger.""" logger = logging.getLogger(name) for hdlr in logger.handlers: logger.removeHandler(hdlr) def trace(message): """Simple print to stdout Not thread safe.""" sys.stdout.write(message) sys.stdout.flush() # ------------------------------------------------------------ # xmlrpc # def xmlrpc_get_seq(host, port): """Get credential thru xmlrpc credential_server.""" url = "http://%s:%s" % (host, port) server = ServerProxy(url, allow_none=True) try: return server.getSeq() except SocketError: raise SocketError( 'No Credential server reachable at %s, use fl-credential-ctl ' 'to start the credential server.' % url) def xmlrpc_get_credential(host, port, group=None): """Get credential thru xmlrpc credential_server.""" url = "http://%s:%s" % (host, port) server = ServerProxy(url, allow_none=True) try: return server.getCredential(group) except SocketError: raise SocketError( 'No Credential server reachable at %s, use fl-credential-ctl ' 'to start the credential server.' % url) def xmlrpc_list_groups(host, port): """Get list of groups thru xmlrpc credential_server.""" url = "http://%s:%s" % (host, port) server = ServerProxy(url) try: return server.listGroups() except SocketError: raise SocketError( 'No Credential server reachable at %s, use fl-credential-ctl ' 'to start the credential server.' % url) def xmlrpc_list_credentials(host, port, group=None): """Get list of users thru xmlrpc credential_server.""" url = "http://%s:%s" % (host, port) server = ServerProxy(url, allow_none=True) try: return server.listCredentials(group) except SocketError: raise SocketError( 'No Credential server reachable at %s, use fl-credential-ctl ' 'to start the credential server.' % url) # ------------------------------------------------------------ # misc # def get_version(): """Retrun the FunkLoad package version.""" from pkg_resources import get_distribution return get_distribution('funkload').version _COLOR = {'green': "\x1b[32;01m", 'red': "\x1b[31;01m", 'reset': "\x1b[0m" } def red_str(text): """Return red text.""" global _COLOR return _COLOR['red'] + text + _COLOR['reset'] def green_str(text): """Return green text.""" global _COLOR return _COLOR['green'] + text + _COLOR['reset'] def is_html(text): """Simple check that return True if the text is an html page.""" if text is not None and ' self.length: mid_size = (self.length - 3) / 2 other = other[:mid_size] + self.extra + other[-mid_size:] return other def is_valid_html(html=None, file_path=None, accept_warning=False): """Ask tidy if the html is valid. Return a tuple (status, errors) """ if not file_path: fd, file_path = mkstemp(prefix='fl-tidy', suffix='.html') os.write(fd, html) os.close(fd) tidy_cmd = 'tidy -errors %s' % file_path ret, output = getstatusoutput(tidy_cmd) status = False if ret == 0: status = True elif ret == 256: # got warnings if accept_warning: status = True elif ret > 512: if 'command not found' in output: raise RuntimeError('tidy command not found, please install tidy.') raise RuntimeError('Executing [%s] return: %s ouput: %s' % (tidy_cmd, ret, output)) return status, output class Data: '''Simple "sentinel" class that lets us identify user data and content type in POST''' def __init__(self, content_type, data): self.content_type = content_type self.data = data def __cmp__(self, other): diff = cmp(self.content_type, other.content_type) if not diff: diff = cmp(self.data, other.data) return diff def __repr__(self): return "[User data " + str(self.content_type) + "]" def get_virtualenv_script(): """ returns the path of the virtualenv.py script that is installed in the system. if it doesn't exist returns None. """ try: import virtualenv except ImportError: raise ImportError('No module named virtualenv') pkg = pkg_resources.get_distribution('virtualenv') output = virtualenv.create_bootstrap_script('import os') fpath = os.path.join(os.path.abspath('/tmp'),'tmpvenv.py') f = open(fpath, 'w').write(output) # script_path = os.path.join( pkg.location, 'virtualenv.py') if os.path.isfile( fpath ): return fpath else: return None def package_tests(module_file): """ this function will basically allow you to create a tarball of the current working directory (of tests) for transport over to a remote machine. It uses a few heuristics to avoid packaging log files. """ exclude_func = lambda filename: filename.find(".log")>=0 or\ filename.find(".bak")>=0 or\ filename.find(".pyc")>=0 or\ filename.find(".gplot")>=0 or\ filename.find(".png")>=0 or\ filename.find(".data")>=0 or\ filename.find(".xml")>=0 or\ os.path.split(filename)[1] == "bin" or\ os.path.split(filename)[1] == "lib" _path = tempfile.mktemp(suffix='.tar') import hashlib _targetdir = hashlib.md5(os.path.splitext(module_file)[0]).hexdigest() _directory = os.path.split(os.path.abspath(module_file))[0] _tar = tarfile.TarFile( _path ,'w') _tar.add ( _directory, _targetdir , exclude = exclude_func ) _tar.close() return _path, _targetdir def extract_token(text, tag_start, tag_end): """Extract a token from text, using the first occurence of tag_start and ending with tag_end. Return None if tags are not found.""" start = text.find(tag_start) end = text.find(tag_end, start + len(tag_start)) if start < 0 or end < 0: return None return text[start + len(tag_start):end]