Mercurial > bitten > bitten-test
changeset 502:a3bcc4f98187
Bitten trunk is now trac-0.11 compatible.
line wrap: on
line diff
new file mode 100644 --- /dev/null +++ b/trac-0.11/COPYING @@ -0,0 +1,36 @@ +Copyright (C) 2007 Edgewall Software +Copyright (C) 2005-2007 Christopher Lenz +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + 3. The name of the author may not be used to endorse or promote + products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS +OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +==================================================================== + +This software includes a copy of XML/SWF charts, which is +Copyright (C) 2004-2005 maani.us +See http://www.maani.us/xml_charts/index.php?menu=License for more +information.
new file mode 100644 --- /dev/null +++ b/trac-0.11/ChangeLog @@ -0,0 +1,71 @@ +Version 0.6 +(?, from 0.6.x branch) +http://svn.edgewall.org/repos/bitten/tags/0.6.0 + + * Switch to using HTTP for communication between the build master and build + slaves. This means the `build-master` executable is no longer needed or + installed, the build simply runs in the scope of the Trac site. + * Build recipes now need to include instructions for performing the checkout + from the version control repository. The slave no longer receives a snapshot + archive of the code, but performs the checkout itself based on the + instructions in the build recipe. + * Many fixes for compatibility with more recent versions of Trac. + + +Version 0.5.3 +(18 April 2006, from 0.5.x branch) +http://svn.edgewall.org/repos/bitten/tags/0.5.3 + + * Fix double-escaping of report summaries. + * Fix build master error when build log contains no messages. + + +Version 0.5.2 +(17 January 2006, from 0.5.x branch) +http://svn.edgewall.org/repos/bitten/tags/0.5.2 + + * Fixes the main navigation tab that was broken in 0.5.1. + + +Version 0.5.1 +(10 January 2006, from 0.5.x branch) +http://svn.edgewall.org/repos/bitten/tags/0.5.1 + + * Fixes compatibility with Trac 0.9.3 release, as well as the current trunk. + This also means that Bitten now longer works with versions of Trac earlier + than 0.9.3. + * Improves PostgreSQL compatibility. + * Fixes encoding of non-ASCII characters in command output. + * Fix for missing log output when using <java:ant> on Windows. + + +Version 0.5 +(6 October 2005, from 0.5.x branch) +http://svn.edgewall.org/repos/bitten/tags/0.5 + + * BDB XML is no longer being used for report storage. Instead, + collected metrics data is stored in the Trac database. + * Snapshot archives created by the master are checked for integrity + prior to their transmission to the slaves. + * Improvements to the build status presentation in Trac. + * Changes to the build recipe format. See the documentation on the web + site for details. + * New recipe commands: <sh:pipe>, <c:configure>, <c:cppunit>, + <java:ant>, <java:junit>, and <x:transform>. Various improvements to + the existing commands. + * Recipe commands and command attributes in recipes can now reference + slave configuration values. + * The names of the master and slaves scripts have changed: `bittend` + is now `bitten-master`, `bitten` is now `bitten-slave`. + * The build master can now handle multiple Trac environments. + * The build slave now by default removes any working directories when + done. + * Build configurations can now be completely deleted. + * Build configurations can now have a minimum and maximum revision + specified. Any revisions outside that range will not be built. + * The build configuration editor now validates the supplied values. + * Fix management of target platforms when running under mod_python. + * Improved performance of the build log formatter that is responsible + for linking file references in build logs to the repository browser. + * Add paging to the build configuration view. + * Fix compatibility with PySQLite2.
new file mode 100644 --- /dev/null +++ b/trac-0.11/MANIFEST.in @@ -0,0 +1,2 @@ +include doc/api/*.* +include doc/*.html
new file mode 100644 --- /dev/null +++ b/trac-0.11/README.txt @@ -0,0 +1,141 @@ +About Bitten +============ + +Bitten is a simple distributed continuous integration system that not only +coordinates builds across multiple machines, but also collects software +metrics generated by builds, to enable feedback and reporting about +the progress of a software project. + +The Bitten software consists of three separate parts: + * The build slave, which executes builds on behalf of a local or remote + build master + * The build master, which orchestrates builds for a project across all + connected slaves, and stores the build status and results to the + database + * The web interface, which is implemented as an add-on to Trac + (http://trac.edgewall.com/) and provides a build management interface + as well as presentation of build results. + +Both the build master and the web interface depend on Trac 0.10, and need +to be installed on the same machine, together with the Subversion +repository. The build slave only requires Python (>= 2.3), setuptools +(>= 0.6a2), as well as any tools required by the build process itself. A +build slave may be run on any machine that can connect to the server +running the Bitten build master. + + +Installation +------------ + +Bitten is written in Python, so make sure that you have Python installed. +You'll need Python 2.3 or later. Also, make sure that setuptools +(http://peak.telecommunity.com/DevCenter/setuptools), version 0.6a2 or later, +is installed. + +If that's taken care of, you just need to download and unpack the Bitten +distribution, and execute the command: + + $ python setup.py install + +from the top of the directory where you unpacked (or checked out) the Bitten +code. Note that you may need administrator/root privileges for this step, as +it will by default attempt to install Bitten to the Python site-packages +directory on your system. + +It's also a good idea to run the unit tests at this point, to make sure that +the code works as expected on your platform: + + $ python setup.py test + + +What's left to do now depends on whether you want to use the build master and +web interface, or just the build slave. In the latter case, you're already +done. You might need to install software that the build of your project +requires, but the Bitten build slave itself doesn't require anything extra. + +For the build master and web interface, you'll need to install Trac 0.10 or +later. Please refer to the Trac documentation for information on how it is +installed. + + +Build Master Configuration +-------------------------- + +Once both Bitten and Trac are installed and working, you'll have to introduce +Bitten to your Trac project environment. If you don't have a Trac project +set up yet, you'll need to do so in order to use Bitten. + +If you already have a Trac project environment, the Bitten plugin needs to be +explicitly enabled in the Trac configuration. This is done by adding it to the +[components] section in /path/to/projenv/conf/trac.ini: + + [components] + bitten.* = enabled + +The Trac web interface should now inform you with an error message that the +environment needs to be upgraded. To do this, run: + + $ trac-admin /path/to/projenv upgrade + +This will create the database tables and directories that Bitten requires. +You probably also want to grant permissions to someone (such as yourself) +to manage build configurations, and allow anonymous users to view the +status and results of builds: + + $ trac-admin /path/to/projenv permission add anonymous BUILD_EXEC + $ trac-admin /path/to/projenv permission add anonymous BUILD_VIEW + $ trac-admin /path/to/projenv permission add [yourname] BUILD_ADMIN + +You should now see an additional tab labeled "Build Status" in the Trac +navigation bar. This link will take you to the list of build configurations, +which at this point is of course empty. If you've set up permissions +correctly as described previously, you should see a button for adding new +build configurations. Click that button and fill out the form. Also, add +at least one target platform after saving the configuration. Last but not +least, you'll have to "activate" the build configuration. + + +Running the Build Master +------------------------ + +At this point, you're ready to start the Bitten build master. The +installation of Bitten should have put a `bitten-master` executable on your +path. If the script is not on your path, look for it in the `bin` or +`scripts` subdirectory of your Python installation. + +To find out about the options and arguments of the master, execute it with +the `--help` option as follows: + + $ bitten-master --help + +Most commonly, you'll want to specify the log level and log file, as well as +the path to the Trac environment: + + $ bitten-master --verbose --log=/var/log/bittend /var/trac/myproject + + +Running the Build Slave +----------------------- + +The build slave can be run on any machine that can connect to the machine +on which the build master is running. The installation of Bitten should have put +a `bitten-slave` executable on your path. If the script is not on your path, +look for it in the `bin` or `scripts` subdirectory of your Python installation. + +To get a list of options for the build slave, execute it with the `--help` +option: + + $ bitten-slave --help + +To run the build slave against a Bitten-enabled Trac site installed at +http://myproject.example.org/trac, you'd run: + + $ bitten-slave http://myproject.example.org/trac/builds + + +More Information +---------------- + +For further documentation, please see the Bitten website at: + + <http://bitten.cmlenz.net/>
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/__init__.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +__docformat__ = 'restructuredtext en' +try: + __version__ = __import__('pkg_resources').get_distribution('Bitten').version +except ImportError: + pass
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/admin.py @@ -0,0 +1,343 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +"""Implementation of the web administration interface.""" + +from pkg_resources import require, DistributionNotFound +import re + +from trac.core import * +from trac.admin import IAdminPanelProvider +from trac.web.chrome import add_stylesheet + +from bitten.model import BuildConfig, TargetPlatform +from bitten.recipe import Recipe, InvalidRecipeError +from bitten.util import xmlio + + +class BuildMasterAdminPageProvider(Component): + """Web administration panel for configuring the build master.""" + + implements(IAdminPanelProvider) + + # IAdminPanelProvider methods + + def get_admin_panels(self, req): + if req.perm.has_permission('BUILD_ADMIN'): + yield ('bitten', 'Builds', 'master', 'Master Settings') + + def render_admin_panel(self, req, cat, page, path_info): + from bitten.master import BuildMaster + master = BuildMaster(self.env) + + if req.method == 'POST': + self._save_config_changes(req, master) + req.redirect(req.abs_href.admin(cat, page)) + + data = {'master': master} + add_stylesheet(req, 'bitten/admin.css') + return 'bitten_admin_master.html', data + + # Internal methods + + def _save_config_changes(self, req, master): + changed = False + + build_all = 'build_all' in req.args + if build_all != master.build_all: + self.config['bitten'].set('build_all', + build_all and 'yes' or 'no') + changed = True + + adjust_timestamps = 'adjust_timestamps' in req.args + if adjust_timestamps != master.adjust_timestamps: + self.config['bitten'].set('adjust_timestamps', + adjust_timestamps and 'yes' or 'no') + changed = True + + stabilize_wait = int(req.args.get('stabilize_wait', 0)) + if stabilize_wait != master.stabilize_wait: + self.config['bitten'].set('stabilize_wait', str(stabilize_wait)) + changed = True + + slave_timeout = int(req.args.get('slave_timeout', 0)) + if slave_timeout != master.slave_timeout: + self.config['bitten'].set('slave_timeout', str(slave_timeout)) + changed = True + + if changed: + self.config.save() + + return master + + +class BuildConfigurationsAdminPageProvider(Component): + """Web administration panel for configuring the build master.""" + + implements(IAdminPanelProvider) + + # IAdminPanelProvider methods + + def get_admin_panels(self, req): + if req.perm.has_permission('BUILD_MODIFY'): + yield ('bitten', 'Builds', 'configs', 'Configurations') + + def render_admin_panel(self, req, cat, page, path_info): + data = {} + + # Analyze url + try: + config_name, platform_id = path_info.split('/', 1) + except: + config_name = path_info + platform_id = None + + if config_name: # Existing build config + if platform_id or ( + # Editing or creating one of the config's target platforms + req.method == 'POST' and 'new' in req.args): + + if platform_id: # Editing target platform + platform_id = int(platform_id) + platform = TargetPlatform.fetch(self.env, platform_id) + + if req.method == 'POST': + if 'cancel' in req.args or \ + self._update_platform(req, platform): + req.redirect(req.abs_href.admin(cat, page, + config_name)) + else: # creating target platform + if req.method == 'POST': + if 'add' in req.args: + self._create_platform(req, config_name) + req.redirect(req.abs_href.admin(cat, page, + config_name)) + elif 'cancel' in req.args: + req.redirect(req.abs_href.admin(cat, page, + config_name)) + + platform = TargetPlatform(self.env, config=config_name) + + # Set up template variables + data['platform'] = { + 'id': platform.id, 'name': platform.name, + 'exists': platform.exists, + 'rules': [ + {'property': propname, 'pattern': pattern} + for propname, pattern in platform.rules + ] or [('', '')] + } + + else: # Editing existing build config itself + config = BuildConfig.fetch(self.env, config_name) + platforms = list(TargetPlatform.select(self.env, + config=config.name)) + + if req.method == 'POST': + if 'add' in req.args: # Add target platform + platform = self._create_platform(req, config) + req.redirect(req.abs_href.admin(cat, page, config.name)) + + elif 'remove' in req.args: # Remove selected platforms + self._remove_platforms(req) + req.redirect(req.abs_href.admin(cat, page, config.name)) + + elif 'save' in req.args: # Save this build config + self._update_config(req, config) + + req.redirect(req.abs_href.admin(cat, page)) + + # Prepare template variables + data['config'] = { + 'name': config.name, 'label': config.label or config.name, + 'active': config.active, 'path': config.path, + 'min_rev': config.min_rev, 'max_rev': config.max_rev, + 'description': config.description, + 'recipe': config.recipe, + 'platforms': [{ + 'name': platform.name, + 'id': platform.id, + 'href': req.href.admin('bitten', 'configs', config.name, + platform.id), + 'rules': [{'property': propname, 'pattern': pattern} + for propname, pattern in platform.rules] + } for platform in platforms] + } + + else: # At the top level build config list + if req.method == 'POST': + if 'add' in req.args: # Add build config + config = self._create_config(req) + req.redirect(req.abs_href.admin(cat, page, config.name)) + + elif 'remove' in req.args: # Remove selected build configs + self._remove_configs(req) + + elif 'apply' in req.args: # Update active state of configs + self._activate_configs(req) + req.redirect(req.abs_href.admin(cat, page)) + + # Prepare template variables + configs = [] + for config in BuildConfig.select(self.env, include_inactive=True): + configs.append({ + 'name': config.name, 'label': config.label or config.name, + 'active': config.active, 'path': config.path, + 'min_rev': config.min_rev, 'max_rev': config.max_rev, + 'href': req.href.admin('bitten', 'configs', config.name), + }) + data['configs'] = configs + + add_stylesheet(req, 'bitten/admin.css') + return 'bitten_admin_configs.html', data + + # Internal methods + + def _activate_configs(self, req): + req.perm.assert_permission('BUILD_MODIFY') + + active = req.args.get('active') or [] + active = isinstance(active, list) and active or [active] + + db = self.env.get_db_cnx() + for config in list(BuildConfig.select(self.env, db=db, + include_inactive=True)): + config.active = config.name in active + config.update(db=db) + db.commit() + + def _create_config(self, req): + req.perm.assert_permission('BUILD_CREATE') + + config = BuildConfig(self.env) + self._update_config(req, config) + return config + + def _remove_configs(self, req): + req.perm.assert_permission('BUILD_DELETE') + + sel = req.args.get('sel') + if not sel: + raise TracError('No configuration selected') + sel = isinstance(sel, list) and sel or [sel] + + db = self.env.get_db_cnx() + for name in sel: + config = BuildConfig.fetch(self.env, name, db=db) + if not config: + raise TracError('Configuration %r not found' % name) + config.delete(db=db) + db.commit() + + def _update_config(self, req, config): + req.perm.assert_permission('BUILD_MODIFY') + + name = req.args.get('name') + if not name: + raise TracError('Missing required field "name"', 'Missing Field') + if not re.match(r'^[\w.-]+$', name): + raise TracError('The field "name" may only contain letters, ' + 'digits, periods, or dashes.', 'Invalid Field') + + path = req.args.get('path', '') + repos = self.env.get_repository(req.authname) + max_rev = req.args.get('max_rev') or None + try: + node = repos.get_node(path, max_rev) + assert node.isdir, '%s is not a directory' % node.path + except (AssertionError, TracError), e: + raise TracError(unicode(e), 'Invalid Repository Path') + if req.args.get('min_rev'): + try: + repos.get_node(path, req.args.get('min_rev')) + except TracError, e: + raise TracError(unicode(e), 'Invalid Oldest Revision') + + recipe_xml = req.args.get('recipe', '') + if recipe_xml: + try: + Recipe(xmlio.parse(recipe_xml)).validate() + except xmlio.ParseError, e: + raise TracError('Failure parsing recipe: %s' % e, + 'Invalid Recipe') + except InvalidRecipeError, e: + raise TracError(unicode(e), 'Invalid Recipe') + + config.name = name + config.path = repos.normalize_path(path) + config.recipe = recipe_xml + config.min_rev = req.args.get('min_rev') + config.max_rev = req.args.get('max_rev') + config.label = req.args.get('label', config.name) + config.description = req.args.get('description', '') + + if config.exists: + config.update() + else: + config.insert() + + def _create_platform(self, req, config_name): + req.perm.assert_permission('BUILD_MODIFY') + + name = req.args.get('name') + if not name: + raise TracError('Missing required field "name"', 'Missing field') + + platform = TargetPlatform(self.env, config=config_name, name=name) + self._update_platform(req, platform) + return platform + + def _remove_platforms(self, req): + req.perm.assert_permission('BUILD_MODIFY') + + sel = req.args.get('sel') + if not sel: + raise TracError('No platform selected') + sel = isinstance(sel, list) and sel or [sel] + + db = self.env.get_db_cnx() + for platform_id in sel: + platform = TargetPlatform.fetch(self.env, platform_id, db=db) + if not platform: + raise TracError('Target platform %r not found' % platform_id) + platform.delete(db=db) + db.commit() + + def _update_platform(self, req, platform): + platform.name = req.args.get('name') + + properties = [int(key[9:]) for key in req.args.keys() + if key.startswith('property_')] + properties.sort() + patterns = [int(key[8:]) for key in req.args.keys() + if key.startswith('pattern_')] + patterns.sort() + platform.rules = [(req.args.get('property_%d' % property), + req.args.get('pattern_%d' % pattern)) + for property, pattern in zip(properties, patterns) + if req.args.get('property_%d' % property)] + + if platform.exists: + platform.update() + else: + platform.insert() + + add_rules = [int(key[9:]) for key in req.args.keys() + if key.startswith('add_rule_')] + if add_rules: + platform.rules.insert(add_rules[0] + 1, ('', '')) + return False + rm_rules = [int(key[8:]) for key in req.args.keys() + if key.startswith('rm_rule_')] + if rm_rules: + if rm_rules[0] < len(platform.rules): + del platform.rules[rm_rules[0]] + return False + + return True
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/api.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +"""Interfaces of extension points provided by the Bitten Trac plugin.""" + +from trac.core import * + +__all__ = ['IBuildListener', 'ILogFormatter', 'IReportChartGenerator', + 'IReportSummarizer'] +__docformat__ = 'restructuredtext en' + + +class IBuildListener(Interface): + """Extension point interface for components that need to be notified of + build events. + + Note that these will be notified in the process running the build master, + not the web interface. + """ + + def build_started(build): + """Called when a build slave has accepted a build initiation. + + :param build: the build that was started + :type build: `Build` + """ + + def build_aborted(build): + """Called when a build slave cancels a build or disconnects. + + :param build: the build that was aborted + :type build: `Build` + """ + + def build_completed(build): + """Called when a build slave has completed a build, regardless of the + outcome. + + :param build: the build that was aborted + :type build: `Build` + """ + + +class ILogFormatter(Interface): + """Extension point interface for components that format build log + messages.""" + + def get_formatter(req, build): + """Return a function that gets called for every log message. + + The function must take four positional arguments, ``step``, + ``generator``, ``level`` and ``message``, and return the formatted + message as a string. + + :param req: the request object + :param build: the build to which the logs belong that should be + formatted + :type build: `Build` + :return: the formatted log message + :rtype: `basestring` + """ + + +class IReportSummarizer(Interface): + """Extension point interface for components that render a summary of reports + of some kind.""" + + def get_supported_categories(): + """Return a list of strings identifying the types of reports this + component supports. + """ + + def render_summary(req, config, build, step, category): + """Render a summary for the given report. + + This function should return a tuple of the form `(template, data)`, + where `template` is the name of the template to use and `data` is the + data to be passed to the template. + + :param req: the request object + :param config: the build configuration + :type config: `BuildConfig` + :param build: the build + :type build: `Build` + :param step: the build step + :type step: `BuildStep` + :param category: the category of the report that should be summarized + :type category: `basestring` + """ + + +class IReportChartGenerator(Interface): + """Extension point interface for components that generate a chart for a + set of reports.""" + + def get_supported_categories(): + """Return a list of strings identifying the types of reports this + component supports. + """ + + def generate_chart_data(req, config, category): + """Generate the data for a report chart. + + This function should return a tuple of the form `(template, data)`, + where `template` is the name of the template to use and `data` is the + data to be passed to the template. + + :param req: the request object + :param config: the build configuration + :type config: `BuildConfig` + :param category: the category of reports to include in the chart + :type category: `basestring` + """
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/build/__init__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +from bitten.build.api import * + +__docformat__ = 'restructuredtext en'
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/build/api.py @@ -0,0 +1,290 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +"""Functions and classes used to simplify the implementation recipe commands.""" + +import logging +import fnmatch +import os +import shlex +import time + +log = logging.getLogger('bitten.build.api') + +__docformat__ = 'restructuredtext en' + + +class BuildError(Exception): + """Exception raised when a build fails.""" + + +class TimeoutError(Exception): + """Exception raised when the execution of a command times out.""" + + +def _combine(*iterables): + iterables = [iter(iterable) for iterable in iterables] + size = len(iterables) + while True: + to_yield = [None] * size + for idx, iterable in enumerate(iterables): + if iterable is None: + continue + try: + to_yield[idx] = iterable.next() + except StopIteration: + iterables[idx] = None + if not [iterable for iterable in iterables if iterable is not None]: + break + yield tuple(to_yield) + + +class CommandLine(object): + """Simple helper for executing subprocesses.""" + + def __init__(self, executable, args, input=None, cwd=None): + """Initialize the CommandLine object. + + :param executable: the name of the program to execute + :param args: a list of arguments to pass to the executable + :param input: string or file-like object containing any input data for + the program + :param cwd: the working directory to change to before executing the + command + """ + self.executable = executable + self.arguments = [str(arg) for arg in args] + self.input = input + self.cwd = cwd + if self.cwd: + assert os.path.isdir(self.cwd) + self.returncode = None + + if os.name == 'nt': # windows + + def execute(self, timeout=None): + """Execute the command, and return a generator for iterating over + the output written to the standard output and error streams. + + :param timeout: number of seconds before the external process + should be aborted (not supported on Windows) + """ + args = [self.executable] + self.arguments + for idx, arg in enumerate(args): + if arg.find(' ') >= 0: + args[idx] = '"%s"' % arg + log.debug('Executing %s', args) + + if self.cwd: + old_cwd = os.getcwd() + os.chdir(self.cwd) + + import tempfile + in_name = None + if self.input: + if isinstance(self.input, basestring): + in_file, in_name = tempfile.mkstemp(prefix='bitten_', + suffix='.pipe') + os.write(in_file, self.input) + os.close(in_file) + in_redirect = '< "%s" ' % in_name + else: + in_redirect = '< "%s" ' % self.input.name + else: + in_redirect = '' + + out_file, out_name = tempfile.mkstemp(prefix='bitten_', + suffix='.pipe') + os.close(out_file) + err_file, err_name = tempfile.mkstemp(prefix='bitten_', + suffix='.pipe') + os.close(err_file) + + try: + cmd = '( %s ) > "%s" %s 2> "%s"' % (' '.join(args), out_name, + in_redirect, err_name) + self.returncode = os.system(cmd) + log.debug('Exited with code %s', self.returncode) + + out_file = file(out_name, 'r') + err_file = file(err_name, 'r') + out_lines = out_file.readlines() + err_lines = err_file.readlines() + out_file.close() + err_file.close() + finally: + if in_name: + os.unlink(in_name) + if out_name: + os.unlink(out_name) + if err_name: + os.unlink(err_name) + if self.cwd: + os.chdir(old_cwd) + + for out_line, err_line in _combine(out_lines, err_lines): + yield out_line and out_line.rstrip().replace('\x00', ''), \ + err_line and err_line.rstrip().replace('\x00', '') + + else: # posix + + def execute(self, timeout=None): + """Execute the command, and return a generator for iterating over + the output written to the standard output and error streams. + + :param timeout: number of seconds before the external process + should be aborted (not supported on Windows) + """ + import popen2, select + if self.cwd: + old_cwd = os.getcwd() + os.chdir(self.cwd) + + log.debug('Executing %s', [self.executable] + self.arguments) + pipe = popen2.Popen3([self.executable] + self.arguments, + capturestderr=True) + if self.input: + if isinstance(self.input, basestring): + in_data = self.input + else: + in_data = self.input.read() + else: + pipe.tochild.close() + in_data = '' + + out_data, err_data = [], [] + in_eof = out_eof = err_eof = False + if not in_data: + in_eof = True + while not out_eof or not err_eof: + readable = [pipe.fromchild] * (not out_eof) + \ + [pipe.childerr] * (not err_eof) + writable = [pipe.tochild] * (not in_eof) + ready = select.select(readable, writable, [], timeout) + if not (ready[0] or ready[1]): + raise TimeoutError('Command %s timed out' % self.executable) + if pipe.tochild in ready[1]: + sent = os.write(pipe.tochild.fileno(), in_data) + in_data = in_data[sent:] + if not in_data: + pipe.tochild.close() + in_eof = True + if pipe.fromchild in ready[0]: + data = os.read(pipe.fromchild.fileno(), 1024) + if data: + out_data.append(data) + else: + out_eof = True + if pipe.childerr in ready[0]: + data = os.read(pipe.childerr.fileno(), 1024) + if data: + err_data.append(data) + else: + err_eof = True + out_lines = self._extract_lines(out_data) + err_lines = self._extract_lines(err_data) + for out_line, err_line in _combine(out_lines, err_lines): + yield out_line, err_line + time.sleep(.1) + self.returncode = pipe.wait() + log.debug('%s exited with code %s', self.executable, + self.returncode) + + if self.cwd: + os.chdir(old_cwd) + + def _extract_lines(self, data): + extracted = [] + def _endswith_linesep(string): + for linesep in ('\n', '\r\n', '\r'): + if string.endswith(linesep): + return True + buf = ''.join(data) + lines = buf.splitlines(True) + if len(lines) > 1: + extracted += lines[:-1] + if _endswith_linesep(lines[-1]): + extracted.append(lines[-1]) + buf = '' + else: + buf = lines[-1] + elif _endswith_linesep(buf): + extracted.append(buf) + buf = '' + data[:] = [buf] * bool(buf) + + return [line.rstrip() for line in extracted] + + +class FileSet(object): + """Utility class for collecting a list of files in a directory that match + given name/path patterns.""" + + DEFAULT_EXCLUDES = ['CVS/*', '*/CVS/*', '.svn/*', '*/.svn/*', + '.DS_Store', 'Thumbs.db'] + + def __init__(self, basedir, include=None, exclude=None): + """Create a file set. + + :param basedir: the base directory for all files in the set + :param include: a list of patterns that define which files should be + included in the set + :param exclude: a list of patterns that define which files should be + excluded from the set + """ + self.files = [] + self.basedir = basedir + + self.include = [] + if include is not None: + self.include = shlex.split(include) + + self.exclude = self.DEFAULT_EXCLUDES[:] + if exclude is not None: + self.exclude += shlex.split(exclude) + + for dirpath, dirnames, filenames in os.walk(self.basedir): + dirpath = dirpath[len(self.basedir) + 1:] + + for filename in filenames: + filepath = nfilepath = os.path.join(dirpath, filename) + if os.sep != '/': + nfilepath = nfilepath.replace(os.sep, '/') + + if self.include: + included = False + for pattern in self.include: + if fnmatch.fnmatchcase(nfilepath, pattern) or \ + fnmatch.fnmatchcase(filename, pattern): + included = True + break + if not included: + continue + + excluded = False + for pattern in self.exclude: + if fnmatch.fnmatchcase(nfilepath, pattern) or \ + fnmatch.fnmatchcase(filename, pattern): + excluded = True + break + if not excluded: + self.files.append(filepath) + + def __iter__(self): + """Iterate over the names of all files in the set.""" + for filename in self.files: + yield filename + + def __contains__(self, filename): + """Return whether the given file name is in the set. + + :param filename: the name of the file to check + """ + return filename in self.files
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/build/config.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +"""Support for build slave configuration.""" + +from ConfigParser import SafeConfigParser +import logging +import os +import platform +import re + +log = logging.getLogger('bitten.config') + +__docformat__ = 'restructuredtext en' + + +class Configuration(object): + """Encapsulates the configuration of a build machine. + + Configuration values can be provided through a configuration file (in INI + format) or through command-line parameters (properties). In addition to + explicitly defined properties, this class automatically collects platform + information and stores them as properties. These defaults can be + overridden (useful for cross-compilation). + """ + # TODO: document mapping from config file to property names + + def __init__(self, filename=None, properties=None): + """Create the configuration object. + + :param filename: the path to the configuration file, if any + :param properties: a dictionary of the configuration properties + provided on the command-line + """ + self.properties = {} + self.packages = {} + parser = SafeConfigParser() + if filename: + parser.read(filename) + self._merge_sysinfo(parser, properties) + self._merge_packages(parser, properties) + + def _merge_sysinfo(self, parser, properties): + """Merge the platform information properties into the configuration.""" + system, _, release, version, machine, processor = platform.uname() + system, release, version = platform.system_alias(system, release, + version) + self.properties['machine'] = machine + self.properties['processor'] = processor + self.properties['os'] = system + self.properties['family'] = os.name + self.properties['version'] = release + + mapping = {'machine': ('machine', 'name'), + 'processor': ('machine', 'processor'), + 'os': ('os', 'name'), + 'family': ('os', 'family'), + 'version': ('os', 'version')} + for key, (section, option) in mapping.items(): + if parser.has_section(section): + value = parser.get(section, option) + if value is not None: + self.properties[key] = value + + if properties: + for key, value in properties.items(): + if key in mapping: + self.properties[key] = value + + def _merge_packages(self, parser, properties): + """Merge package information into the configuration.""" + for section in parser.sections(): + if section in ('os', 'machine', 'maintainer'): + continue + package = {} + for option in parser.options(section): + package[option] = parser.get(section, option) + self.packages[section] = package + + if properties: + for key, value in properties.items(): + if '.' in key: + package, propname = key.split('.', 1) + if package not in self.packages: + self.packages[package] = {} + self.packages[package][propname] = value + + def __contains__(self, key): + """Return whether the configuration contains a value for the specified + key. + + :param key: name of the configuration option using dotted notation + (for example, "python.path") + """ + if '.' in key: + package, propname = key.split('.', 1) + return propname in self.packages.get(package, {}) + return key in self.properties + + def __getitem__(self, key): + """Return the value for the specified configuration key. + + :param key: name of the configuration option using dotted notation + (for example, "python.path") + """ + if '.' in key: + package, propname = key.split('.', 1) + return self.packages.get(package, {}).get(propname) + return self.properties.get(key) + + def __str__(self): + return str({'properties': self.properties, 'packages': self.packages}) + + def get_dirpath(self, key): + """Return the value of the specified configuration key, but verify that + the value refers to the path of an existing directory. + + If the value does not exist, or is not a directory path, return `None`. + + :param key: name of the configuration option using dotted notation + (for example, "ant.home") + """ + dirpath = self[key] + if dirpath: + if os.path.isdir(dirpath): + return dirpath + log.warning('Invalid %s: %s is not a directory', key, dirpath) + return None + + def get_filepath(self, key): + """Return the value of the specified configuration key, but verify that + the value refers to the path of an existing file. + + If the value does not exist, or is not a file path, return `None`. + + :param key: name of the configuration option using dotted notation + (for example, "python.path") + """ + filepath = self[key] + if filepath: + if os.path.isfile(filepath): + return filepath + log.warning('Invalid %s: %s is not a file', key, filepath) + return None + + _VAR_RE = re.compile(r'\$\{(?P<ref>\w[\w.]*?\w)(?:\:(?P<def>.+))?\}') + + def interpolate(self, text, **vars): + """Interpolate configuration properties into a string. + + Properties can be referenced in the text using the notation + ``${property.name}``. A default value can be provided by appending it to + the property name separated by a colon, for example + ``${property.name:defaultvalue}``. This value will be used when there's + no such property in the configuration. Otherwise, if no default is + provided, the reference is not replaced at all. + + :param text: the string containing variable references + :param vars: extra variables to use for the interpolation + """ + def _replace(m): + refname = m.group('ref') + if refname in self: + return self[refname] + elif refname in vars: + return vars[refname] + elif m.group('def'): + return m.group('def') + else: + return m.group(0) + return self._VAR_RE.sub(_replace, text)
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/build/ctools.py @@ -0,0 +1,365 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +"""Recipe commands for build tasks commonly used for C/C++ projects.""" + +import logging +import re +import os +import posixpath +import shlex + +from bitten.build import CommandLine, FileSet +from bitten.util import xmlio + +log = logging.getLogger('bitten.build.ctools') + +__docformat__ = 'restructuredtext en' + +def configure(ctxt, file_='configure', enable=None, disable=None, with=None, + without=None, cflags=None, cxxflags=None): + """Run a ``configure`` script. + + :param ctxt: the build context + :type ctxt: `Context` + :param file\_: name of the configure script + :param enable: names of the features to enable, seperated by spaces + :param disable: names of the features to disable, separated by spaces + :param with: names of external packages to include + :param without: names of external packages to exclude + :param cflags: ``CFLAGS`` to pass to the configure script + :param cxxflags: ``CXXFLAGS`` to pass to the configure script + """ + args = [] + if enable: + args += ['--enable-%s' % feature for feature in enable.split()] + if disable: + args += ['--disable-%s' % feature for feature in disable.split()] + if with: + for pkg in with.split(): + pkg_path = pkg + '.path' + if pkg_path in ctxt.config: + args.append('--with-%s=%s' % (pkg, ctxt.config[pkg_path])) + else: + args.append('--with-%s' % pkg) + if without: + args += ['--without-%s' % pkg for pkg in without.split()] + if cflags: + args.append('CFLAGS=%s' % cflags) + if cxxflags: + args.append('CXXFLAGS=%s' % cxxflags) + + from bitten.build import shtools + returncode = shtools.execute(ctxt, file_=file_, args=args) + if returncode != 0: + ctxt.error('configure failed (%s)' % returncode) + +def autoreconf(ctxt, file_='configure', force=None, install=None, symlink=None, + warnings=None, prepend_include=None, include =None): + """Run the autotoll ``autoreconf``. + + :param ctxt: the build context + :type ctxt: `Context` + :param force: consider all files obsolete + :param install: copy missing auxiliary files + :param symlink: install symbolic links instead of copies + :param warnings: report the warnings falling in CATEGORY + :prepend_include: prepend directories to search path + :include: append directories to search path + + """ + args = [] + if install: + args.append('--install') + if symlink: + args.append('--symlink') + if force: + args.append('--force') + if warnings: + args.append('--warnings=%s' % warnings) + + if include: + args += ['--include=%s' % inc for inc in include.split()] + if prepend_include: + args += ['--prepend-include=%s' % pinc for pinc in prepend_include.split()] + + from bitten.build import shtools + returncode = shtools.execute(ctxt, 'autoreconf', args=args) + if returncode != 0: + ctxt.error('autoreconf failed (%s)' % returncode) + +def make(ctxt, target=None, file_=None, keep_going=False, directory=None, jobs=None, args=None): + """Execute a Makefile target. + + :param ctxt: the build context + :type ctxt: `Context` + :param file\_: name of the Makefile + :param keep_going: whether make should keep going when errors are + encountered + :param directory: directory in which to build; defaults to project source directory + :param jobs: number of concurrent jobs to run + :param args: command-line arguments to pass to the script + """ + executable = ctxt.config.get_filepath('make.path') or 'make' + + if directory is None: + directory = ctxt.basedir + + margs = ['--directory', directory] + + if file_: + margs += ['--file', ctxt.resolve(file_)] + if keep_going: + margs.append('--keep-going') + if target: + margs.append(target) + if jobs: + margs += ['--jobs', jobs] + + if args: + if isinstance(args, basestring): + margs += shlex.split(args) + + from bitten.build import shtools + returncode = shtools.execute(ctxt, executable=executable, args=margs) + if returncode != 0: + ctxt.error('make failed (%s)' % returncode) + +def cppunit(ctxt, file_=None, srcdir=None): + """Collect CppUnit XML data. + + :param ctxt: the build context + :type ctxt: `Context` + :param file\_: path of the file containing the CppUnit results; may contain + globbing wildcards to match multiple files + :param srcdir: name of the directory containing the source files, used to + link the test results to the corresponding files + """ + assert file_, 'Missing required attribute "file"' + + try: + fileobj = file(ctxt.resolve(file_), 'r') + try: + total, failed = 0, 0 + results = xmlio.Fragment() + for group in xmlio.parse(fileobj): + if group.name not in ('FailedTests', 'SuccessfulTests'): + continue + for child in group.children(): + test = xmlio.Element('test') + name = child.children('Name').next().gettext() + if '::' in name: + parts = name.split('::') + test.attr['fixture'] = '::'.join(parts[:-1]) + name = parts[-1] + test.attr['name'] = name + + for location in child.children('Location'): + for file_elem in location.children('File'): + filepath = file_elem.gettext() + if srcdir is not None: + filepath = posixpath.join(srcdir, filepath) + test.attr['file'] = filepath + break + for line_elem in location.children('Line'): + test.attr['line'] = line_elem.gettext() + break + break + + if child.name == 'FailedTest': + for message in child.children('Message'): + test.append(xmlio.Element('traceback')[ + message.gettext() + ]) + test.attr['status'] = 'failure' + failed += 1 + else: + test.attr['status'] = 'success' + + results.append(test) + total += 1 + + if failed: + ctxt.error('%d of %d test%s failed' % (failed, total, + total != 1 and 's' or '')) + + ctxt.report('test', results) + + finally: + fileobj.close() + + except IOError, e: + log.warning('Error opening CppUnit results file (%s)', e) + except xmlio.ParseError, e: + print e + log.warning('Error parsing CppUnit results file (%s)', e) + +def cunit (ctxt, file_=None, srcdir=None): + """Collect CUnit XML data. + + :param ctxt: the build context + :type ctxt: `Context` + :param file\_: path of the file containing the CUnit results; may contain + globbing wildcards to match multiple files + :param srcdir: name of the directory containing the source files, used to + link the test results to the corresponding files + """ + assert file_, 'Missing required attribute "file"' + + try: + fileobj = file(ctxt.resolve(file_), 'r') + try: + total, failed = 0, 0 + results = xmlio.Fragment() + log_elem = xmlio.Fragment() + def info (msg): + log.info (msg) + log_elem.append (xmlio.Element ('message', level='info')[msg]) + def warning (msg): + log.warning (msg) + log_elem.append (xmlio.Element ('message', level='warning')[msg]) + def error (msg): + log.error (msg) + log_elem.append (xmlio.Element ('message', level='error')[msg]) + for node in xmlio.parse(fileobj): + if node.name != 'CUNIT_RESULT_LISTING': + continue + for suiteRun in node.children ('CUNIT_RUN_SUITE'): + for suite in suiteRun.children(): + if suite.name not in ('CUNIT_RUN_SUITE_SUCCESS', 'CUNIT_RUN_SUITE_FAILURE'): + warning ("Unknown node: %s" % suite.name) + continue + suiteName = suite.children ('SUITE_NAME').next().gettext() + info ("%s [%s]" % ("*" * (57 - len (suiteName)), suiteName)) + for record in suite.children ('CUNIT_RUN_TEST_RECORD'): + for result in record.children(): + if result.name not in ('CUNIT_RUN_TEST_SUCCESS', 'CUNIT_RUN_TEST_FAILURE'): + continue + testName = result.children ('TEST_NAME').next().gettext() + info ("Running %s..." % testName); + test = xmlio.Element('test') + test.attr['fixture'] = suiteName + test.attr['name'] = testName + if result.name == 'CUNIT_RUN_TEST_FAILURE': + error ("%s(%d): %s" + % (result.children ('FILE_NAME').next().gettext(), + int (result.children ('LINE_NUMBER').next().gettext()), + result.children ('CONDITION').next().gettext())) + test.attr['status'] = 'failure' + failed += 1 + else: + test.attr['status'] = 'success' + + results.append(test) + total += 1 + + if failed: + ctxt.error('%d of %d test%s failed' % (failed, total, + total != 1 and 's' or '')) + + ctxt.report('test', results) + ctxt.log (log_elem) + + finally: + fileobj.close() + + except IOError, e: + log.warning('Error opening CUnit results file (%s)', e) + except xmlio.ParseError, e: + print e + log.warning('Error parsing CUnit results file (%s)', e) + +def gcov(ctxt, include=None, exclude=None, prefix=None, root=""): + """Run ``gcov`` to extract coverage data where available. + + :param ctxt: the build context + :type ctxt: `Context` + :param include: patterns of files and directories to include + :param exclude: patterns of files and directories that should be excluded + :param prefix: optional prefix name that is added to object files by the + build system + :param root: optional root path in which the build system puts the object + files + """ + file_re = re.compile(r'^File (?:\'|\`)(?P<file>[^\']+)\'\s*$') + lines_re = re.compile(r'^Lines executed:(?P<cov>\d+\.\d+)\% of (?P<num>\d+)\s*$') + + files = [] + for filename in FileSet(ctxt.basedir, include, exclude): + if os.path.splitext(filename)[1] in ('.c', '.cpp', '.cc', '.cxx'): + files.append(filename) + + coverage = xmlio.Fragment() + log_elem = xmlio.Fragment() + def info (msg): + log.info (msg) + log_elem.append (xmlio.Element ('message', level='info')[msg]) + def warning (msg): + log.warning (msg) + log_elem.append (xmlio.Element ('message', level='warning')[msg]) + def error (msg): + log.error (msg) + log_elem.append (xmlio.Element ('message', level='error')[msg]) + + for srcfile in files: + # Determine the coverage for each source file by looking for a .gcno + # and .gcda pair + info ("Getting coverage info for %s" % srcfile) + filepath, filename = os.path.split(srcfile) + stem = os.path.splitext(filename)[0] + if prefix is not None: + stem = prefix + '-' + stem + + objfile = os.path.join (root, filepath, stem + '.o') + if not os.path.isfile(ctxt.resolve(objfile)): + warning ('No object file found for %s at %s' % (srcfile, objfile)) + continue + if not os.path.isfile (ctxt.resolve (os.path.join (root, filepath, stem + '.gcno'))): + warning ('No .gcno file found for %s at %s' % (srcfile, os.path.join (root, filepath, stem + '.gcno'))) + continue + if not os.path.isfile (ctxt.resolve (os.path.join (root, filepath, stem + '.gcda'))): + warning ('No .gcda file found for %s at %s' % (srcfile, os.path.join (root, filepath, stem + '.gcda'))) + continue + + num_lines, num_covered = 0, 0 + skip_block = False + cmd = CommandLine('gcov', ['-b', '-n', '-o', objfile, srcfile], + cwd=ctxt.basedir) + for out, err in cmd.execute(): + if out == '': # catch blank lines, reset the block state... + skip_block = False + elif out and not skip_block: + # Check for a file name + match = file_re.match(out) + if match: + if os.path.isabs(match.group('file')): + skip_block = True + continue + else: + # check for a "Lines executed" message + match = lines_re.match(out) + if match: + lines = float(match.group('num')) + cov = float(match.group('cov')) + num_covered += int(lines * cov / 100) + num_lines += int(lines) + if cmd.returncode != 0: + continue + + module = xmlio.Element('coverage', name=os.path.basename(srcfile), + file=srcfile.replace(os.sep, '/'), + lines=num_lines, percentage=0) + if num_lines: + percent = int(round(num_covered * 100 / num_lines)) + module.attr['percentage'] = percent + coverage.append(module) + + ctxt.report('coverage', coverage) + ctxt.log (log_elem)
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/build/javatools.py @@ -0,0 +1,225 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2006 Matthew Good <matt@matt-good.net> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +"""Recipe commands for tools commonly used in Java projects.""" + +from glob import glob +import logging +import os +import posixpath +import shlex +import tempfile + +from bitten.build import CommandLine +from bitten.util import xmlio + +log = logging.getLogger('bitten.build.javatools') + +__docformat__ = 'restructuredtext en' + +def ant(ctxt, file_=None, target=None, keep_going=False, args=None): + """Run an Ant build. + + :param ctxt: the build context + :type ctxt: `Context` + :param file\_: name of the Ant build file + :param target: name of the target that should be executed (optional) + :param keep_going: whether Ant should keep going when errors are encountered + :param args: additional arguments to pass to Ant + """ + executable = 'ant' + ant_home = ctxt.config.get_dirpath('ant.home') + if ant_home: + executable = os.path.join(ant_home, 'bin', 'ant') + + java_home = ctxt.config.get_dirpath('java.home') + if java_home: + os.environ['JAVA_HOME'] = java_home + + logfile = tempfile.NamedTemporaryFile(prefix='ant_log', suffix='.xml') + logfile.close() + if args: + args = shlex.split(args) + else: + args = [] + args += ['-noinput', '-listener', 'org.apache.tools.ant.XmlLogger', + '-Dant.XmlLogger.stylesheet.uri', '""', + '-DXmlLogger.file', logfile.name] + if file_: + args += ['-buildfile', ctxt.resolve(file_)] + if keep_going: + args.append('-keep-going') + if target: + args.append(target) + + cmdline = CommandLine(executable, args, cwd=ctxt.basedir) + for out, err in cmdline.execute(): + if out is not None: + log.info(out) + if err is not None: + log.error(err) + + error_logged = False + log_elem = xmlio.Fragment() + try: + xml_log = xmlio.parse(file(logfile.name, 'r')) + def collect_log_messages(node): + for child in node.children(): + if child.name == 'message': + if child.attr['priority'] == 'debug': + continue + log_elem.append(xmlio.Element('message', + level=child.attr['priority'])[ + child.gettext().replace(ctxt.basedir + os.sep, '') + .replace(ctxt.basedir, '') + ]) + else: + collect_log_messages(child) + collect_log_messages(xml_log) + + if 'error' in xml_log.attr: + ctxt.error(xml_log.attr['error']) + error_logged = True + + except xmlio.ParseError, e: + log.warning('Error parsing Ant XML log file (%s)', e) + ctxt.log(log_elem) + + if not error_logged and cmdline.returncode != 0: + ctxt.error('Ant failed (%s)' % cmdline.returncode) + +def junit(ctxt, file_=None, srcdir=None): + """Extract test results from a JUnit XML report. + + :param ctxt: the build context + :type ctxt: `Context` + :param file\_: path to the JUnit XML test results; may contain globbing + wildcards for matching multiple results files + :param srcdir: name of the directory containing the test sources, used to + link test results to the corresponding source files + """ + assert file_, 'Missing required attribute "file"' + try: + total, failed = 0, 0 + results = xmlio.Fragment() + for path in glob(ctxt.resolve(file_)): + fileobj = file(path, 'r') + try: + for testcase in xmlio.parse(fileobj).children('testcase'): + test = xmlio.Element('test') + test.attr['fixture'] = testcase.attr['classname'] + if 'time' in testcase.attr: + test.attr['duration'] = testcase.attr['time'] + if srcdir is not None: + cls = testcase.attr['classname'].split('.') + test.attr['file'] = posixpath.join(srcdir, *cls) + \ + '.java' + + result = list(testcase.children()) + if result: + test.attr['status'] = result[0].name + test.append(xmlio.Element('traceback')[ + result[0].gettext() + ]) + failed += 1 + else: + test.attr['status'] = 'success' + + results.append(test) + total += 1 + finally: + fileobj.close() + if failed: + ctxt.error('%d of %d test%s failed' % (failed, total, + total != 1 and 's' or '')) + ctxt.report('test', results) + except IOError, e: + log.warning('Error opening JUnit results file (%s)', e) + except xmlio.ParseError, e: + log.warning('Error parsing JUnit results file (%s)', e) + + +class _LineCounter(object): + def __init__(self): + self.lines = [] + self.covered = 0 + self.num_lines = 0 + + def __getitem__(self, idx): + if idx >= len(self.lines): + return 0 + return self.lines[idx] + + def __setitem__(self, idx, val): + idx = int(idx) - 1 # 1-indexed to 0-indexed + from itertools import repeat + if idx >= len(self.lines): + self.lines.extend(repeat('-', idx - len(self.lines) + 1)) + self.lines[idx] = val + self.num_lines += 1 + if val != '0': + self.covered += 1 + + def line_hits(self): + return ' '.join(self.lines) + line_hits = property(line_hits) + + def percentage(self): + if self.num_lines == 0: + return 0 + return int(round(self.covered * 100. / self.num_lines)) + percentage = property(percentage) + + +def cobertura(ctxt, file_=None): + """Extract test coverage information from a Cobertura XML report. + + :param ctxt: the build context + :type ctxt: `Context` + :param file\_: path to the Cobertura XML output + """ + assert file_, 'Missing required attribute "file"' + + coverage = xmlio.Fragment() + doc = xmlio.parse(open(ctxt.resolve(file_))) + srcdir = [s.gettext().strip() for ss in doc.children('sources') + for s in ss.children('source')][0] + + classes = [cls for pkgs in doc.children('packages') + for pkg in pkgs.children('package') + for clss in pkg.children('classes') + for cls in clss.children('class')] + + counters = {} + class_names = {} + + for cls in classes: + filename = cls.attr['filename'].replace(os.sep, '/') + name = cls.attr['name'] + if not '$' in name: # ignore internal classes + class_names[filename] = name + counter = counters.get(filename) + if counter is None: + counter = counters[filename] = _LineCounter() + lines = [l for ls in cls.children('lines') + for l in ls.children('line')] + for line in lines: + counter[line.attr['number']] = line.attr['hits'] + + for filename, name in class_names.iteritems(): + counter = counters[filename] + module = xmlio.Element('coverage', name=name, + file=posixpath.join(srcdir, filename), + lines=counter.num_lines, + percentage=counter.percentage) + module.append(xmlio.Element('line_hits')[counter.line_hits]) + coverage.append(module) + ctxt.report('coverage', coverage)
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/build/phptools.py @@ -0,0 +1,113 @@ +# -*- coding: UTF-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# Copyright (C) 2007 Wei Zhuo <weizhuo@gmail.com> +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.cmlenz.net/wiki/License. + +import logging +import os +import shlex + +from bitten.util import xmlio +from bitten.build import shtools + +log = logging.getLogger('bitten.build.phptools') + +def phing(ctxt, file_=None, target=None, executable=None, args=None): + """Run a phing build""" + if args: + args = shlex.split(args) + else: + args = [] + args += ['-logger', 'phing.listener.DefaultLogger', + '-buildfile', ctxt.resolve(file_ or 'build.xml')] + if target: + args.append(target) + + returncode = shtools.execute(ctxt, file_=executable or 'phing', args=args) + if returncode != 0: + ctxt.error('Phing failed (%s)' % returncode) + +def phpunit(ctxt, file_=None): + """Extract test results from a PHPUnit XML report.""" + assert file_, 'Missing required attribute "file"' + try: + total, failed = 0, 0 + results = xmlio.Fragment() + fileobj = file(ctxt.resolve(file_), 'r') + try: + for testsuit in xmlio.parse(fileobj).children('testsuite'): + total += int(testsuit.attr['tests']) + failed += int(testsuit.attr['failures']) + \ + int(testsuit.attr['errors']) + + for testcase in testsuit.children(): + test = xmlio.Element('test') + test.attr['fixture'] = testcase.attr['class'] + test.attr['name'] = testcase.attr['name'] + test.attr['duration'] = testcase.attr['time'] + result = list(testcase.children()) + if result: + test.append(xmlio.Element('traceback')[ + result[0].gettext() + ]) + test.attr['status'] = result[0].name + else: + test.attr['status'] = 'success' + if 'file' in testsuit.attr: + testfile = os.path.realpath(testsuit.attr['file']) + if testfile.startswith(ctxt.basedir): + testfile = testfile[len(ctxt.basedir) + 1:] + testfile = testfile.replace(os.sep, '/') + test.attr['file'] = testfile + results.append(test) + finally: + fileobj.close() + if failed: + ctxt.error('%d of %d test%s failed' % (failed, total, + total != 1 and 's' or '')) + ctxt.report('test', results) + except IOError, e: + ctxt.log('Error opening PHPUnit results file (%s)' % e) + except xmlio.ParseError, e: + ctxt.log('Error parsing PHPUnit results file (%s)' % e) + +def coverage(ctxt, file_=None): + """Extract data from a Phing code coverage report.""" + assert file_, 'Missing required attribute "file"' + try: + summary_file = file(ctxt.resolve(file_), 'r') + try: + coverage = xmlio.Fragment() + for package in xmlio.parse(summary_file).children('package'): + for cls in package.children('class'): + statements = float(cls.attr['statementcount']) + covered = float(cls.attr['statementscovered']) + if statements: + percentage = covered / statements * 100 + else: + percentage = 100 + class_coverage = xmlio.Element('coverage', + name=cls.attr['name'], + lines=int(statements), + percentage=percentage + ) + source = list(cls.children())[0] + if 'sourcefile' in source.attr: + sourcefile = os.path.realpath(source.attr['sourcefile']) + if sourcefile.startswith(ctxt.basedir): + sourcefile = sourcefile[len(ctxt.basedir) + 1:] + sourcefile = sourcefile.replace(os.sep, '/') + class_coverage.attr['file'] = sourcefile + coverage.append(class_coverage) + finally: + summary_file.close() + ctxt.report('coverage', coverage) + except IOError, e: + ctxt.log('Error opening coverage summary file (%s)' % e) + except xmlio.ParseError, e: + ctxt.log('Error parsing coverage summary file (%s)' % e)
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/build/pythontools.py @@ -0,0 +1,466 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2008 Matt Good <matt@matt-good.net> +# Copyright (C) 2008 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +"""Recipe commands for tools commonly used by Python projects.""" + +from __future__ import division + +import logging +import os +import cPickle as pickle +import re +try: + set +except NameError: + from sets import Set as set +import shlex +import sys + +from bitten.build import CommandLine, FileSet +from bitten.util import loc, xmlio + +log = logging.getLogger('bitten.build.pythontools') + +__docformat__ = 'restructuredtext en' + +def _python_path(ctxt): + """Return the path to the Python interpreter. + + If the configuration has a ``python.path`` property, the value of that + option is returned; otherwise the path to the current Python interpreter is + returned. + """ + python_path = ctxt.config.get_filepath('python.path') + if python_path: + return python_path + return sys.executable + +def distutils(ctxt, file_='setup.py', command='build', options=None): + """Execute a ``distutils`` command. + + :param ctxt: the build context + :type ctxt: `Context` + :param file\_: name of the file defining the distutils setup + :param command: the setup command to execute + :param options: additional options to pass to the command + """ + if options: + if isinstance(options, basestring): + options = shlex.split(options) + else: + options = [] + + cmdline = CommandLine(_python_path(ctxt), + [ctxt.resolve(file_), command] + options, + cwd=ctxt.basedir) + log_elem = xmlio.Fragment() + error_logged = False + for out, err in cmdline.execute(): + if out is not None: + log.info(out) + log_elem.append(xmlio.Element('message', level='info')[out]) + if err is not None: + level = 'error' + if err.startswith('warning: '): + err = err[9:] + level = 'warning' + log.warning(err) + elif err.startswith('error: '): + ctxt.error(err[7:]) + error_logged = True + else: + log.error(err) + log_elem.append(xmlio.Element('message', level=level)[err]) + ctxt.log(log_elem) + + if not error_logged and cmdline.returncode != 0: + ctxt.error('distutils failed (%s)' % cmdline.returncode) + +def exec_(ctxt, file_=None, module=None, function=None, output=None, args=None): + """Execute a Python script. + + Either the `file_` or the `module` parameter must be provided. If + specified using the `file_` parameter, the file must be inside the project + directory. If specified as a module, the module must either be resolvable + to a file, or the `function` parameter must be provided + + :param ctxt: the build context + :type ctxt: `Context` + :param file\_: name of the script file to execute + :param module: name of the Python module to execute + :param function: name of the Python function to run + :param output: name of the file to which output should be written + :param args: extra arguments to pass to the script + """ + assert file_ or module, 'Either "file" or "module" attribute required' + if function: + assert module and not file_, '"module" attribute required for use of ' \ + '"function" attribute' + + if module: + # Script specified as module name, need to resolve that to a file, + # or use the function name if provided + if function: + args = '-c "import sys; from %s import %s; %s(sys.argv)" %s' % ( + module, function, function, args) + else: + try: + mod = __import__(module, globals(), locals(), []) + components = module.split('.') + for comp in components[1:]: + mod = getattr(mod, comp) + file_ = mod.__file__.replace('\\', '/') + except ImportError, e: + ctxt.error('Cannot execute Python module %s: %s' % (module, e)) + return + + from bitten.build import shtools + returncode = shtools.execute(ctxt, executable=_python_path(ctxt), + file_=file_, output=output, args=args) + if returncode != 0: + ctxt.error('Executing %s failed (error code %s)' % (file_, returncode)) + +def pylint(ctxt, file_=None): + """Extract data from a ``pylint`` run written to a file. + + :param ctxt: the build context + :type ctxt: `Context` + :param file\_: name of the file containing the Pylint output + """ + assert file_, 'Missing required attribute "file"' + msg_re = re.compile(r'^(?P<file>.+):(?P<line>\d+): ' + r'\[(?P<type>[A-Z]\d*)(?:, (?P<tag>[\w\.]+))?\] ' + r'(?P<msg>.*)$') + msg_categories = dict(W='warning', E='error', C='convention', R='refactor') + + problems = xmlio.Fragment() + try: + fd = open(ctxt.resolve(file_), 'r') + try: + for line in fd: + match = msg_re.search(line) + if match: + msg_type = match.group('type') + category = msg_categories.get(msg_type[0]) + if len(msg_type) == 1: + msg_type = None + filename = os.path.realpath(match.group('file')) + if filename.startswith(ctxt.basedir): + filename = filename[len(ctxt.basedir) + 1:] + filename = filename.replace(os.sep, '/') + lineno = int(match.group('line')) + tag = match.group('tag') + problems.append(xmlio.Element('problem', category=category, + type=msg_type, tag=tag, + line=lineno, file=filename)[ + match.group('msg') or '' + ]) + ctxt.report('lint', problems) + finally: + fd.close() + except IOError, e: + log.warning('Error opening pylint results file (%s)', e) + +def coverage(ctxt, summary=None, coverdir=None, include=None, exclude=None): + """Extract data from a ``coverage.py`` run. + + :param ctxt: the build context + :type ctxt: `Context` + :param summary: path to the file containing the coverage summary + :param coverdir: name of the directory containing the per-module coverage + details + :param include: patterns of files or directories to include in the report + :param exclude: patterns of files or directories to exclude from the report + """ + assert summary, 'Missing required attribute "summary"' + + summary_line_re = re.compile(r'^(?P<module>.*?)\s+(?P<stmts>\d+)\s+' + r'(?P<exec>\d+)\s+(?P<cov>\d+)%\s+' + r'(?:(?P<missing>(?:\d+(?:-\d+)?(?:, )?)*)\s+)?' + r'(?P<file>.+)$') + + fileset = FileSet(ctxt.basedir, include, exclude) + missing_files = [] + for filename in fileset: + if os.path.splitext(filename)[1] != '.py': + continue + missing_files.append(filename) + covered_modules = set() + + try: + summary_file = open(ctxt.resolve(summary), 'r') + try: + coverage = xmlio.Fragment() + for summary_line in summary_file: + match = summary_line_re.search(summary_line) + if match: + modname = match.group(1) + filename = match.group(6) + if not os.path.isabs(filename): + filename = os.path.normpath(os.path.join(ctxt.basedir, + filename)) + else: + filename = os.path.realpath(filename) + if not filename.startswith(ctxt.basedir): + continue + filename = filename[len(ctxt.basedir) + 1:] + if not filename in fileset: + continue + + percentage = int(match.group(4).rstrip('%')) + num_lines = int(match.group(2)) + + missing_files.remove(filename) + covered_modules.add(modname) + module = xmlio.Element('coverage', name=modname, + file=filename.replace(os.sep, '/'), + percentage=percentage, + lines=num_lines) + coverage.append(module) + + for filename in missing_files: + modname = os.path.splitext(filename.replace(os.sep, '.'))[0] + if modname in covered_modules: + continue + covered_modules.add(modname) + module = xmlio.Element('coverage', name=modname, + file=filename.replace(os.sep, '/'), + percentage=0) + coverage.append(module) + + ctxt.report('coverage', coverage) + finally: + summary_file.close() + except IOError, e: + log.warning('Error opening coverage summary file (%s)', e) + +def trace(ctxt, summary=None, coverdir=None, include=None, exclude=None): + """Extract data from a ``trace.py`` run. + + :param ctxt: the build context + :type ctxt: `Context` + :param summary: path to the file containing the coverage summary + :param coverdir: name of the directory containing the per-module coverage + details + :param include: patterns of files or directories to include in the report + :param exclude: patterns of files or directories to exclude from the report + """ + assert summary, 'Missing required attribute "summary"' + assert coverdir, 'Missing required attribute "coverdir"' + + summary_line_re = re.compile(r'^\s*(?P<lines>\d+)\s+(?P<cov>\d+)%\s+' + r'(?P<module>.*?)\s+\((?P<filename>.*?)\)') + coverage_line_re = re.compile(r'\s*(?:(?P<hits>\d+): )?(?P<line>.*)') + + fileset = FileSet(ctxt.basedir, include, exclude) + missing_files = [] + for filename in fileset: + if os.path.splitext(filename)[1] != '.py': + continue + missing_files.append(filename) + covered_modules = set() + + def handle_file(elem, sourcefile, coverfile=None): + code_lines = set() + for lineno, linetype, line in loc.count(sourcefile): + if linetype == loc.CODE: + code_lines.add(lineno) + num_covered = 0 + lines = [] + + if coverfile: + prev_hits = '0' + for idx, coverline in enumerate(coverfile): + match = coverage_line_re.search(coverline) + if match: + hits = match.group(1) + if hits: # Line covered + if hits != '0': + num_covered += 1 + lines.append(hits) + prev_hits = hits + elif coverline.startswith('>'): # Line not covered + lines.append('0') + prev_hits = '0' + elif idx not in code_lines: # Not a code line + lines.append('-') + prev_hits = '0' + else: # A code line not flagged by trace.py + if prev_hits != '0': + num_covered += 1 + lines.append(prev_hits) + + elem.append(xmlio.Element('line_hits')[' '.join(lines)]) + + num_lines = len(code_lines) + if num_lines: + percentage = int(round(num_covered * 100 / num_lines)) + else: + percentage = 0 + elem.attr['percentage'] = percentage + elem.attr['lines'] = num_lines + + try: + summary_file = open(ctxt.resolve(summary), 'r') + try: + coverage = xmlio.Fragment() + for summary_line in summary_file: + match = summary_line_re.search(summary_line) + if match: + modname = match.group(3) + filename = match.group(4) + if not os.path.isabs(filename): + filename = os.path.normpath(os.path.join(ctxt.basedir, + filename)) + else: + filename = os.path.realpath(filename) + if not filename.startswith(ctxt.basedir): + continue + filename = filename[len(ctxt.basedir) + 1:] + if not filename in fileset: + continue + + missing_files.remove(filename) + covered_modules.add(modname) + module = xmlio.Element('coverage', name=modname, + file=filename.replace(os.sep, '/')) + sourcefile = file(ctxt.resolve(filename)) + try: + coverpath = ctxt.resolve(coverdir, modname + '.cover') + if os.path.isfile(coverpath): + coverfile = file(coverpath, 'r') + else: + log.warning('No coverage file for module %s at %s', + modname, coverpath) + coverfile = None + try: + handle_file(module, sourcefile, coverfile) + finally: + if coverfile: + coverfile.close() + finally: + sourcefile.close() + coverage.append(module) + + for filename in missing_files: + modname = os.path.splitext(filename.replace(os.sep, '.'))[0] + if modname in covered_modules: + continue + covered_modules.add(modname) + module = xmlio.Element('coverage', name=modname, + file=filename.replace(os.sep, '/'), + percentage=0) + filepath = ctxt.resolve(filename) + fileobj = file(filepath, 'r') + try: + handle_file(module, fileobj) + finally: + fileobj.close() + coverage.append(module) + + ctxt.report('coverage', coverage) + finally: + summary_file.close() + except IOError, e: + log.warning('Error opening coverage summary file (%s)', e) + +def figleaf(ctxt, summary=None, include=None, exclude=None): + from figleaf import get_lines + coverage = xmlio.Fragment() + try: + fileobj = open(ctxt.resolve(summary)) + except IOError, e: + log.warning('Error opening coverage summary file (%s)', e) + return + coverage_data = pickle.load(fileobj) + fileset = FileSet(ctxt.basedir, include, exclude) + for filename in fileset: + base, ext = os.path.splitext(filename) + if ext != '.py': + continue + modname = base.replace(os.path.sep, '.') + realfilename = ctxt.resolve(filename) + interesting_lines = get_lines(open(realfilename)) + covered_lines = coverage_data.get(realfilename, set()) + percentage = int(round(len(covered_lines) * 100 / len(interesting_lines))) + line_hits = [] + for lineno in xrange(1, max(interesting_lines)+1): + if lineno not in interesting_lines: + line_hits.append('-') + elif lineno in covered_lines: + line_hits.append('1') + else: + line_hits.append('0') + module = xmlio.Element('coverage', name=modname, + file=filename, + percentage=percentage, + lines=len(interesting_lines), + line_hits=' '.join(line_hits)) + coverage.append(module) + ctxt.report('coverage', coverage) + +def _normalize_filenames(ctxt, filenames, fileset): + for filename in filenames: + if not os.path.isabs(filename): + filename = os.path.normpath(os.path.join(ctxt.basedir, + filename)) + else: + filename = os.path.realpath(filename) + if not filename.startswith(ctxt.basedir): + continue + filename = filename[len(ctxt.basedir) + 1:] + if filename not in fileset: + continue + yield filename.replace(os.sep, '/') + +def unittest(ctxt, file_=None): + """Extract data from a unittest results file in XML format. + + :param ctxt: the build context + :type ctxt: `Context` + :param file\_: name of the file containing the test results + """ + assert file_, 'Missing required attribute "file"' + + try: + fileobj = file(ctxt.resolve(file_), 'r') + try: + total, failed = 0, 0 + results = xmlio.Fragment() + for child in xmlio.parse(fileobj).children(): + test = xmlio.Element('test') + for name, value in child.attr.items(): + if name == 'file': + value = os.path.realpath(value) + if value.startswith(ctxt.basedir): + value = value[len(ctxt.basedir) + 1:] + value = value.replace(os.sep, '/') + else: + continue + test.attr[name] = value + if name == 'status' and value in ('error', 'failure'): + failed += 1 + for grandchild in child.children(): + test.append(xmlio.Element(grandchild.name)[ + grandchild.gettext() + ]) + results.append(test) + total += 1 + if failed: + ctxt.error('%d of %d test%s failed' % (failed, total, + total != 1 and 's' or '')) + ctxt.report('test', results) + finally: + fileobj.close() + except IOError, e: + log.warning('Error opening unittest results file (%s)', e) + except xmlio.ParseError, e: + log.warning('Error parsing unittest results file (%s)', e)
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/build/shtools.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +"""Generic recipe commands for executing external processes.""" + +import logging +import os +import shlex + +from bitten.build import CommandLine +from bitten.util import xmlio + +log = logging.getLogger('bitten.build.shtools') + +__docformat__ = 'restructuredtext en' + +def exec_(ctxt, executable=None, file_=None, output=None, args=None, dir_=None): + """Execute a program or shell script. + + :param ctxt: the build context + :type ctxt: `Context` + :param executable: name of the executable to run + :param file\_: name of the script file, relative to the project directory, + that should be run + :param output: name of the file to which the output of the script should be + written + :param args: command-line arguments to pass to the script + """ + assert executable or file_, \ + 'Either "executable" or "file" attribute required' + + returncode = execute(ctxt, executable=executable, file_=file_, + output=output, args=args, dir_=dir_) + if returncode != 0: + ctxt.error('Executing %s failed (error code %s)' % (executable or file_, + returncode)) + +def pipe(ctxt, executable=None, file_=None, input_=None, output=None, + args=None, dir_=None): + """Pipe the contents of a file through a program or shell script. + + :param ctxt: the build context + :type ctxt: `Context` + :param executable: name of the executable to run + :param file\_: name of the script file, relative to the project directory, + that should be run + :param input\_: name of the file containing the data that should be passed + to the shell script on its standard input stream + :param output: name of the file to which the output of the script should be + written + :param args: command-line arguments to pass to the script + """ + assert executable or file_, \ + 'Either "executable" or "file" attribute required' + assert input_, 'Missing required attribute "input"' + + returncode = execute(ctxt, executable=executable, file_=file_, + input_=input_, output=output, args=args, dir_=dir_) + if returncode != 0: + ctxt.error('Piping through %s failed (error code %s)' + % (executable or file_, returncode)) + +def execute(ctxt, executable=None, file_=None, input_=None, output=None, + args=None, dir_=None, filter_=None): + """Generic external program execution. + + This function is not itself bound to a recipe command, but rather used from + other commands. + + :param ctxt: the build context + :type ctxt: `Context` + :param executable: name of the executable to run + :param file\_: name of the script file, relative to the project directory, + that should be run + :param input\_: name of the file containing the data that should be passed + to the shell script on its standard input stream + :param output: name of the file to which the output of the script should be + written + :param args: command-line arguments to pass to the script + :param dirs: + :param filter\_: function to filter out messages from the executable stdout + """ + if args: + if isinstance(args, basestring): + args = shlex.split(args) + else: + args = [] + + if dir_: + def resolve(*args): + return ctxt.resolve(dir_, *args) + else: + resolve = ctxt.resolve + + if file_ and os.path.isfile(resolve(file_)): + file_ = resolve(file_) + + if executable is None: + executable = file_ + elif file_: + args[:0] = [file_] + + if input_: + input_file = file(resolve(input_), 'r') + else: + input_file = None + + if output: + output_file = file(resolve(output), 'w') + else: + output_file = None + + if dir_ and os.path.isdir(ctxt.resolve(dir_)): + dir_ = ctxt.resolve(dir_) + else: + dir_ = ctxt.basedir + + if not filter_: + filter_=lambda s: s + + try: + cmdline = CommandLine(executable, args, input=input_file, + cwd=dir_) + log_elem = xmlio.Fragment() + for out, err in cmdline.execute(): + if out is not None: + log.info(out) + info = filter_(out) + if info: + log_elem.append(xmlio.Element('message', level='info')[ + info.replace(ctxt.basedir + os.sep, '') + .replace(ctxt.basedir, '') + ]) + if output: + output_file.write(out + os.linesep) + if err is not None: + log.error(err) + log_elem.append(xmlio.Element('message', level='error')[ + err.replace(ctxt.basedir + os.sep, '') + .replace(ctxt.basedir, '') + ]) + if output: + output_file.write(err + os.linesep) + ctxt.log(log_elem) + finally: + if input_: + input_file.close() + if output: + output_file.close() + + return cmdline.returncode
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/build/svntools.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +"""Recipe commands for Subversion.""" + +import logging +import posixpath +import re + +log = logging.getLogger('bitten.build.svntools') + +__docformat__ = 'restructuredtext en' + +def checkout(ctxt, url, path=None, revision=None, dir_='.', verbose=False): + """Perform a checkout from a Subversion repository. + + :param ctxt: the build context + :type ctxt: `Context` + :param url: the URL of the repository + :param path: the path inside the repository + :param revision: the revision to check out + :param dir_: the name of a local subdirectory to check out into + :param verbose: whether to log the list of checked out files + """ + args = ['checkout'] + if revision: + args += ['-r', revision] + if path: + url = posixpath.join(url, path.lstrip('/')) + args += [url, dir_] + + cofilter = None + if not verbose: + cre = re.compile(r'^[AU]\s.*$') + cofilter = lambda s: cre.sub('', s) + from bitten.build import shtools + returncode = shtools.execute(ctxt, file_='svn', args=args, + filter_=cofilter) + if returncode != 0: + ctxt.error('svn checkout failed (%s)' % returncode) + +def export(ctxt, url, path=None, revision=None, dir_='.'): + """Perform an export from a Subversion repository. + + :param ctxt: the build context + :type ctxt: `Context` + :param url: the URL of the repository + :param path: the path inside the repository + :param revision: the revision to check out + :param dir_: the name of a local subdirectory to export out into + """ + args = ['export', '--force'] + if revision: + args += ['-r', revision] + if path: + url = posixpath.join(url, path) + args += [url, dir_] + + from bitten.build import shtools + returncode = shtools.execute(ctxt, file_='svn', args=args) + if returncode != 0: + ctxt.error('svn export failed (%s)' % returncode) + +def update(ctxt, revision=None, dir_='.'): + """Update the local working copy from the Subversion repository. + + :param ctxt: the build context + :type ctxt: `Context` + :param revision: the revision to check out + :param dir_: the name of a local subdirectory containing the working copy + """ + args = ['update'] + if revision: + args += ['-r', revision] + args += [dir_] + + from bitten.build import shtools + returncode = shtools.execute(ctxt, file_='svn', args=args) + if returncode != 0: + ctxt.error('svn update failed (%s)' % returncode)
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/build/tests/__init__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +import unittest + +from bitten.build.tests import api, config, ctools, phptools, pythontools, \ + xmltools + +def suite(): + suite = unittest.TestSuite() + suite.addTest(api.suite()) + suite.addTest(config.suite()) + suite.addTest(ctools.suite()) + suite.addTest(phptools.suite()) + suite.addTest(pythontools.suite()) + suite.addTest(xmltools.suite()) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/build/tests/api.py @@ -0,0 +1,234 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +import os +import shutil +import sys +import tempfile +import unittest + +from bitten.build import CommandLine, FileSet, TimeoutError +from bitten.build.api import _combine + + +class CommandLineTestCase(unittest.TestCase): + + def setUp(self): + self.basedir = os.path.realpath(tempfile.mkdtemp(suffix='bitten_test')) + + def tearDown(self): + shutil.rmtree(self.basedir) + + def _create_file(self, name, content=None): + filename = os.path.join(self.basedir, name) + fd = file(filename, 'w') + if content: + fd.write(content) + fd.close() + return filename + + def test_extract_lines(self): + cmdline = CommandLine('test', []) + data = ['foo\n', 'bar\n'] + lines = cmdline._extract_lines(data) + self.assertEqual(['foo', 'bar'], lines) + self.assertEqual([], data) + + def test_extract_lines_spanned(self): + cmdline = CommandLine('test', []) + data = ['foo ', 'bar\n'] + lines = cmdline._extract_lines(data) + self.assertEqual(['foo bar'], lines) + self.assertEqual([], data) + + def test_extract_lines_trailing(self): + cmdline = CommandLine('test', []) + data = ['foo\n', 'bar'] + lines = cmdline._extract_lines(data) + self.assertEqual(['foo'], lines) + self.assertEqual(['bar'], data) + + def test_combine(self): + list1 = ['foo', 'bar'] + list2 = ['baz'] + combined = list(_combine(list1, list2)) + self.assertEqual([('foo', 'baz'), ('bar', None)], combined) + + def test_single_argument(self): + cmdline = CommandLine(sys.executable, ['-V']) + stdout = [] + stderr = [] + for out, err in cmdline.execute(timeout=5.0): + if out is not None: + stdout.append(out) + if err is not None: + stderr.append(err) + py_version = '.'.join([str(v) for (v) in sys.version_info[:3]]) + self.assertEqual(['Python %s' % py_version], stderr) + self.assertEqual([], stdout) + self.assertEqual(0, cmdline.returncode) + + def test_multiple_arguments(self): + script_file = self._create_file('test.py', content=""" +import sys +for arg in sys.argv[1:]: + print arg +""") + cmdline = CommandLine('python', [script_file, 'foo', 'bar', 'baz']) + stdout = [] + stderr = [] + for out, err in cmdline.execute(timeout=5.0): + stdout.append(out) + stderr.append(err) + py_version = '.'.join([str(v) for (v) in sys.version_info[:3]]) + self.assertEqual(['foo', 'bar', 'baz'], stdout) + self.assertEqual([None, None, None], stderr) + self.assertEqual(0, cmdline.returncode) + + def test_output_error_streams(self): + script_file = self._create_file('test.py', content=""" +import sys, time +print>>sys.stdout, 'Hello' +print>>sys.stdout, 'world!' +sys.stdout.flush() +time.sleep(.1) +print>>sys.stderr, 'Oops' +sys.stderr.flush() +""") + cmdline = CommandLine('python', [script_file]) + stdout = [] + stderr = [] + for out, err in cmdline.execute(timeout=5.0): + stdout.append(out) + stderr.append(err) + py_version = '.'.join([str(v) for (v) in sys.version_info[:3]]) + # nt doesn't properly split stderr and stdout. See ticket #256. + if os.name != "nt": + self.assertEqual(['Hello', 'world!', None], stdout) + self.assertEqual(0, cmdline.returncode) + + def test_input_stream_as_fileobj(self): + script_file = self._create_file('test.py', content=""" +import sys +data = sys.stdin.read() +if data == 'abcd': + print>>sys.stdout, 'Thanks' +""") + input_file = self._create_file('input.txt', content='abcd') + input_fileobj = file(input_file, 'r') + try: + cmdline = CommandLine('python', [script_file], input=input_fileobj) + stdout = [] + stderr = [] + for out, err in cmdline.execute(timeout=5.0): + stdout.append(out) + stderr.append(err) + py_version = '.'.join([str(v) for (v) in sys.version_info[:3]]) + self.assertEqual(['Thanks'], stdout) + self.assertEqual([None], stderr) + self.assertEqual(0, cmdline.returncode) + finally: + input_fileobj.close() + + def test_input_stream_as_string(self): + script_file = self._create_file('test.py', content=""" +import sys +data = sys.stdin.read() +if data == 'abcd': + print>>sys.stdout, 'Thanks' +""") + cmdline = CommandLine('python', [script_file], input='abcd') + stdout = [] + stderr = [] + for out, err in cmdline.execute(timeout=5.0): + stdout.append(out) + stderr.append(err) + py_version = '.'.join([str(v) for (v) in sys.version_info[:3]]) + self.assertEqual(['Thanks'], stdout) + self.assertEqual([None], stderr) + self.assertEqual(0, cmdline.returncode) + + def test_timeout(self): + script_file = self._create_file('test.py', content=""" +import time +time.sleep(2.0) +print 'Done' +""") + cmdline = CommandLine('python', [script_file]) + iterable = iter(cmdline.execute(timeout=.5)) + if os.name != "nt": + # commandline timeout not implemented on windows. See #257 + self.assertRaises(TimeoutError, iterable.next) + +class FileSetTestCase(unittest.TestCase): + + def setUp(self): + self.basedir = os.path.realpath(tempfile.mkdtemp(suffix='bitten_test')) + + def tearDown(self): + shutil.rmtree(self.basedir) + + # Convenience methods + + def _create_dir(self, *path): + cur = self.basedir + for part in path: + cur = os.path.join(cur, part) + os.mkdir(cur) + return cur[len(self.basedir) + 1:] + + def _create_file(self, *path): + filename = os.path.join(self.basedir, *path) + fd = file(filename, 'w') + fd.close() + return filename[len(self.basedir) + 1:] + + # Test methods + + def test_empty(self): + fileset = FileSet(self.basedir) + self.assertRaises(StopIteration, iter(fileset).next) + + def test_top_level_files(self): + foo_txt = self._create_file('foo.txt') + bar_txt = self._create_file('bar.txt') + fileset = FileSet(self.basedir) + assert foo_txt in fileset and bar_txt in fileset + + def test_files_in_subdir(self): + self._create_dir('tests') + foo_txt = self._create_file('tests', 'foo.txt') + bar_txt = self._create_file('tests', 'bar.txt') + fileset = FileSet(self.basedir) + assert foo_txt in fileset and bar_txt in fileset + + def test_files_in_subdir_with_include(self): + self._create_dir('tests') + foo_txt = self._create_file('tests', 'foo.txt') + bar_txt = self._create_file('tests', 'bar.txt') + fileset = FileSet(self.basedir, include='tests/*.txt') + assert foo_txt in fileset and bar_txt in fileset + + def test_files_in_subdir_with_exclude(self): + self._create_dir('tests') + foo_txt = self._create_file('tests', 'foo.txt') + bar_txt = self._create_file('tests', 'bar.txt') + fileset = FileSet(self.basedir, include='tests/*.txt', exclude='bar.*') + assert foo_txt in fileset and bar_txt not in fileset + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(CommandLineTestCase, 'test')) + suite.addTest(unittest.makeSuite(FileSetTestCase, 'test')) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/build/tests/config.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +import platform +import os +import tempfile +import unittest + +from bitten.build.config import Configuration + + +class ConfigurationTestCase(unittest.TestCase): + + def test_sysinfo_defaults(self): + config = Configuration() + + self.assertEqual(platform.machine(), config['machine']) + self.assertEqual(platform.processor(), config['processor']) + system, release, version = platform.system_alias(platform.system(), + platform.release(), + platform.version()) + self.assertEqual(system, config['os']) + self.assertEqual(os.name, config['family']) + self.assertEqual(release, config['version']) + + def test_sysinfo_properties_override(self): + config = Configuration(properties={ + 'machine': 'MACHINE', + 'processor': 'PROCESSOR', + 'os': 'OS', + 'family': 'FAMILY', + 'version': 'VERSION' + }) + self.assertEqual('MACHINE', config['machine']) + self.assertEqual('PROCESSOR', config['processor']) + self.assertEqual('OS', config['os']) + self.assertEqual('FAMILY', config['family']) + self.assertEqual('VERSION', config['version']) + + def test_sysinfo_configfile_override(self): + inifd, ininame = tempfile.mkstemp(prefix='bitten_test') + try: + os.write(inifd, """ +[machine] +name = MACHINE +processor = PROCESSOR + +[os] +name = OS +family = FAMILY +version = VERSION +""") + os.close(inifd) + config = Configuration(ininame) + + self.assertEqual('MACHINE', config['machine']) + self.assertEqual('PROCESSOR', config['processor']) + self.assertEqual('OS', config['os']) + self.assertEqual('FAMILY', config['family']) + self.assertEqual('VERSION', config['version']) + finally: + os.remove(ininame) + + def test_package_properties(self): + config = Configuration(properties={ + 'python.version': '2.3.5', + 'python.path': '/usr/local/bin/python2.3' + }) + self.assertEqual(True, 'python' in config.packages) + self.assertEqual('/usr/local/bin/python2.3', config['python.path']) + self.assertEqual('2.3.5', config['python.version']) + + def test_package_configfile(self): + inifd, ininame = tempfile.mkstemp(prefix='bitten_test') + try: + os.write(inifd, """ +[python] +path = /usr/local/bin/python2.3 +version = 2.3.5 +""") + os.close(inifd) + config = Configuration(ininame) + + self.assertEqual(True, 'python' in config.packages) + self.assertEqual('/usr/local/bin/python2.3', config['python.path']) + self.assertEqual('2.3.5', config['python.version']) + finally: + os.remove(ininame) + + def test_get_dirpath_non_existant(self): + tempdir = tempfile.mkdtemp() + os.rmdir(tempdir) + config = Configuration(properties={'somepkg.home': tempdir}) + self.assertEqual(None, config.get_dirpath('somepkg.home')) + + def test_get_dirpath(self): + tempdir = tempfile.mkdtemp() + try: + config = Configuration(properties={'somepkg.home': tempdir}) + self.assertEqual(tempdir, config.get_dirpath('somepkg.home')) + finally: + os.rmdir(tempdir) + + def test_get_filepath_non_existant(self): + testfile, testname = tempfile.mkstemp(prefix='bitten_test') + os.close(testfile) + os.remove(testname) + config = Configuration(properties={'somepkg.path': testname}) + self.assertEqual(None, config.get_filepath('somepkg.path')) + + def test_get_filepath(self): + testfile = tempfile.NamedTemporaryFile(prefix='bitten_test') + config = Configuration(properties={'somepkg.path': testfile.name}) + self.assertEqual(testfile.name, config.get_filepath('somepkg.path')) + + def test_interpolate(self): + config = Configuration(properties={ + 'python.version': '2.3.5', + 'python.path': '/usr/local/bin/python2.3' + }) + self.assertEqual('/usr/local/bin/python2.3', + config.interpolate('${python.path}')) + self.assertEqual('foo /usr/local/bin/python2.3 bar', + config.interpolate('foo ${python.path} bar')) + + def test_interpolate_default(self): + config = Configuration() + self.assertEqual('python2.3', + config.interpolate('${python.path:python2.3}')) + self.assertEqual('foo python2.3 bar', + config.interpolate('foo ${python.path:python2.3} bar')) + + def test_interpolate_missing(self): + config = Configuration() + self.assertEqual('${python.path}', + config.interpolate('${python.path}')) + self.assertEqual('foo ${python.path} bar', + config.interpolate('foo ${python.path} bar')) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(ConfigurationTestCase, 'test')) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/build/tests/ctools.py @@ -0,0 +1,156 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +import os +import shutil +import tempfile +import unittest + +from bitten.build import ctools +from bitten.build.tests import dummy +from bitten.recipe import Context, Recipe + + +class CppUnitTestCase(unittest.TestCase): + + def setUp(self): + self.basedir = os.path.realpath(tempfile.mkdtemp()) + self.ctxt = Context(self.basedir) + + def tearDown(self): + shutil.rmtree(self.basedir) + + def test_missing_param_file(self): + self.assertRaises(AssertionError, ctools.cppunit, self.ctxt) + + def test_empty_summary(self): + cppunit_xml = file(self.ctxt.resolve('cppunit.xml'), 'w') + cppunit_xml.write("""<?xml version="1.0" encoding='utf-8' ?> +<TestRun> + <FailedTests> + <FailedTest id="2"> + <Name>HelloTest::secondTest</Name> + <FailureType>Assertion</FailureType> + <Location> + <File>HelloTest.cxx</File> + <Line>95</Line> + </Location> + <Message>assertion failed +- Expression: 2 == 3 +</Message> + </FailedTest> + </FailedTests> + <SuccessfulTests> + <Test id="1"> + <Name>HelloTest::firstTest</Name> + </Test> + <Test id="3"> + <Name>HelloTest::thirdTest</Name> + </Test> + </SuccessfulTests> + <Statistics> + <Tests>3</Tests> + <FailuresTotal>1</FailuresTotal> + <Errors>0</Errors> + <Failures>1</Failures> + </Statistics> +</TestRun>""") + cppunit_xml.close() + ctools.cppunit(self.ctxt, file_='cppunit.xml') + type, category, generator, xml = self.ctxt.output.pop() + self.assertEqual(Recipe.REPORT, type) + self.assertEqual('test', category) + + tests = list(xml.children) + self.assertEqual(3, len(tests)) + self.assertEqual('HelloTest', tests[0].attr['fixture']) + self.assertEqual('secondTest', tests[0].attr['name']) + self.assertEqual('failure', tests[0].attr['status']) + self.assertEqual('HelloTest.cxx', tests[0].attr['file']) + self.assertEqual('95', tests[0].attr['line']) + + self.assertEqual('HelloTest', tests[1].attr['fixture']) + self.assertEqual('firstTest', tests[1].attr['name']) + self.assertEqual('success', tests[1].attr['status']) + + self.assertEqual('HelloTest', tests[2].attr['fixture']) + self.assertEqual('thirdTest', tests[2].attr['name']) + self.assertEqual('success', tests[2].attr['status']) + + +class GCovTestCase(unittest.TestCase): + + def setUp(self): + self.basedir = os.path.realpath(tempfile.mkdtemp()) + self.ctxt = Context(self.basedir) + + def tearDown(self): + shutil.rmtree(self.basedir) + + def _create_file(self, *path): + filename = os.path.join(self.basedir, *path) + dirname = os.path.dirname(filename) + if not os.path.isdir(dirname): + os.makedirs(dirname) + fd = file(filename, 'w') + fd.close() + return filename[len(self.basedir) + 1:] + + def test_no_file(self): + ctools.CommandLine = dummy.CommandLine() + ctools.gcov(self.ctxt) + type, category, generator, xml = self.ctxt.output.pop() + self.assertEqual('log', type) + type, category, generator, xml = self.ctxt.output.pop() + self.assertEqual('report', type) + self.assertEqual('coverage', category) + self.assertEqual(0, len(xml.children)) + + def test_single_file(self): + self._create_file('foo.c') + self._create_file('foo.o') + self._create_file('foo.gcno') + self._create_file('foo.gcda') + + ctools.CommandLine = dummy.CommandLine(stdout=""" +File `foo.c' +Lines executed:45.81% of 884 +Branches executed:54.27% of 398 +Taken at least once:36.68% of 398 +Calls executed:48.19% of 249 + +File `foo.h' +Lines executed:50.00% of 4 +No branches +Calls executed:100.00% of 1 +""") + ctools.gcov(self.ctxt) + type, category, generator, xml = self.ctxt.output.pop() + self.assertEqual('log', type) + type, category, generator, xml = self.ctxt.output.pop() + self.assertEqual('report', type) + self.assertEqual('coverage', category) + self.assertEqual(1, len(xml.children)) + elem = xml.children[0] + self.assertEqual('coverage', elem.name) + self.assertEqual('foo.c', elem.attr['file']) + self.assertEqual('foo.c', elem.attr['name']) + self.assertEqual(888, elem.attr['lines']) + self.assertEqual(45, elem.attr['percentage']) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(CppUnitTestCase, 'test')) + suite.addTest(unittest.makeSuite(GCovTestCase, 'test')) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/build/tests/dummy.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +from StringIO import StringIO + +from bitten.build import api + + +class CommandLine(api.CommandLine): + + def __init__(self, returncode=0, stdout='', stderr=''): + self.returncode = returncode + self.stdout = StringIO(stdout) + self.stderr = StringIO(stderr) + + def __call__(self, executable, args, input=None, cwd=None): + return self + + def execute(self): + return api._combine(self.stdout.readlines(), self.stderr.readlines())
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/build/tests/javatools.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2006 Matthew Good <matt@matt-good.net> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +import os.path +import shutil +import tempfile +import unittest + +from bitten.build import javatools +from bitten.recipe import Context + +class CoberturaTestCase(unittest.TestCase): + xml_template="""<?xml version="1.0"?> +<!DOCTYPE coverage SYSTEM "http://cobertura.sourceforge.net/xml/coverage-02.dtd"> + +<coverage timestamp="1148533713840"> + <sources> + <source>src</source> + </sources> + <packages> + <package name="test"> + <classes>%s + </classes> + </package> + </packages> +</coverage>""" + + def setUp(self): + self.basedir = os.path.realpath(tempfile.mkdtemp()) + self.ctxt = Context(self.basedir) + + def tearDown(self): + shutil.rmtree(self.basedir) + + def _create_file(self, *path, **kw): + filename = os.path.join(self.basedir, *path) + dirname = os.path.dirname(filename) + if not os.path.exists(dirname): + os.makedirs(dirname) + fd = file(filename, 'w') + content = kw.get('content') + if content is not None: + fd.write(content) + fd.close() + return filename[len(self.basedir) + 1:] + + def test_basic(self): + filename = self._create_file('coverage.xml', content=self.xml_template % """ + <class name="test.TestClass" filename="test/TestClass.java"> + <lines> + <line number="1" hits="0" branch="false"/> + <line number="2" hits="1" branch="false"/> + <line number="3" hits="0" branch="false"/> + <line number="4" hits="2" branch="false"/> + </lines> + </class>""") + javatools.cobertura(self.ctxt, file_=filename) + type, category, generator, xml = self.ctxt.output.pop() + self.assertEqual('report', type) + self.assertEqual('coverage', category) + self.assertEqual(1, len(xml.children)) + + elem = xml.children[0] + self.assertEqual('coverage', elem.name) + self.assertEqual('src/test/TestClass.java', elem.attr['file']) + self.assertEqual('test.TestClass', elem.attr['name']) + self.assertEqual(4, elem.attr['lines']) + self.assertEqual(50, elem.attr['percentage']) + + def test_skipped_lines(self): + filename = self._create_file('coverage.xml', content=self.xml_template % """ + <class name="test.TestClass" filename="test/TestClass.java"> + <lines> + <line number="1" hits="0" branch="false"/> + <line number="3" hits="1" branch="false"/> + </lines> + </class>""") + javatools.cobertura(self.ctxt, file_=filename) + type, category, generator, xml = self.ctxt.output.pop() + self.assertEqual('report', type) + self.assertEqual('coverage', category) + self.assertEqual(1, len(xml.children)) + + elem = xml.children[0] + self.assertEqual('coverage', elem.name) + self.assertEqual('src/test/TestClass.java', elem.attr['file']) + self.assertEqual('test.TestClass', elem.attr['name']) + self.assertEqual(2, elem.attr['lines']) + self.assertEqual(50, elem.attr['percentage']) + + line_hits = elem.children[0] + self.assertEqual('line_hits', line_hits.name) + self.assertEqual('0 - 1', line_hits.children[0]) + + def test_interface(self): + filename = self._create_file('coverage.xml', content=self.xml_template % """ + <class name="test.TestInterface" filename="test/TestInterface.java"> + <lines> + </lines> + </class>""") + javatools.cobertura(self.ctxt, file_=filename) + type, category, generator, xml = self.ctxt.output.pop() + self.assertEqual('report', type) + self.assertEqual('coverage', category) + self.assertEqual(1, len(xml.children)) + + elem = xml.children[0] + self.assertEqual('coverage', elem.name) + self.assertEqual('src/test/TestInterface.java', elem.attr['file']) + self.assertEqual('test.TestInterface', elem.attr['name']) + self.assertEqual(0, elem.attr['lines']) + self.assertEqual(0, elem.attr['percentage']) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(CoberturaTestCase, 'test')) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/build/tests/phptools.py @@ -0,0 +1,131 @@ +# -*- coding: UTF-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# Copyright (C) 2007 Wei Zhuo <weizhuo@gmail.com> +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.cmlenz.net/wiki/License. + +import os +import shutil +import tempfile +import unittest + +from bitten.build import phptools +from bitten.recipe import Context, Recipe + +class PhpUnitTestCase(unittest.TestCase): + + def setUp(self): + self.basedir = os.path.realpath(tempfile.mkdtemp()) + self.ctxt = Context(self.basedir) + + def tearDown(self): + shutil.rmtree(self.basedir) + + def test_missing_param_file(self): + self.assertRaises(AssertionError, phptools.phpunit, self.ctxt) + + def test_sample_unit_test_result(self): + phpunit_xml = file(self.ctxt.resolve('phpunit.xml'), 'w') + phpunit_xml.write("""<?xml version="1.0" encoding="UTF-8"?> +<testsuites> + <testsuite name="FooTest" file="FooTest.php" tests="2" failures="1" errors="0" time="0.147397"> + <testcase name="testBar" class="FooTest" time="0.122265"> + <failure message="expected same: <1> was not: <2>" type="PHPUnit2_Framework_AssertionFailedError"> + ... +</failure> + </testcase> + <testcase name="testBar2" class="FooTest" time="0.025132"/> + </testsuite> + <testsuite name="BarTest" file="BarTest.php" tests="1" failures="0" errors="0" time="0.050713"> + <testcase name="testFoo" class="BarTest" time="0.026046"/> + </testsuite> +</testsuites>""") + phpunit_xml.close() + phptools.phpunit(self.ctxt, file_='phpunit.xml') + type, category, generator, xml = self.ctxt.output.pop() + self.assertEqual(Recipe.REPORT, type) + self.assertEqual('test', category) + + tests = list(xml.children) + self.assertEqual(3, len(tests)) + self.assertEqual('FooTest', tests[0].attr['fixture']) + self.assertEqual('testBar', tests[0].attr['name']) + self.assertEqual('failure', tests[0].attr['status']) + self.assert_('FooTest.php' in tests[0].attr['file']) + + self.assertEqual('FooTest', tests[1].attr['fixture']) + self.assertEqual('testBar2', tests[1].attr['name']) + self.assertEqual('success', tests[1].attr['status']) + + self.assertEqual('BarTest', tests[2].attr['fixture']) + self.assertEqual('testFoo', tests[2].attr['name']) + self.assertEqual('success', tests[2].attr['status']) + +class PhpCodeCoverageTestCase(unittest.TestCase): + + def setUp(self): + self.basedir = os.path.realpath(tempfile.mkdtemp()) + self.ctxt = Context(self.basedir) + + def tearDown(self): + shutil.rmtree(self.basedir) + + def test_missing_param_file(self): + self.assertRaises(AssertionError, phptools.coverage, self.ctxt) + + def test_sample_code_coverage(self): + coverage_xml = file(self.ctxt.resolve('phpcoverage.xml'), 'w') + coverage_xml.write("""<?xml version="1.0" encoding="UTF-8"?> +<snapshot methodcount="4" methodscovered="2" statementcount="11" statementscovered="5" totalcount="15" totalcovered="7"> + <package name="default" methodcount="4" methodscovered="2" statementcount="11" statementscovered="5" totalcount="15" totalcovered="7"> + <class name="Foo" methodcount="1" methodscovered="1" statementcount="7" statementscovered="3" totalcount="8" totalcovered="4"> + <sourcefile name="Foo.php" sourcefile="xxxx/Foo.php"> + ... + </sourcefile> + </class> + <class name="Foo2" methodcount="2" methodscovered="1" statementcount="4" statementscovered="2" totalcount="6" totalcovered="3"> + <sourcefile name="Foo.php" sourcefile="xxxx/Foo.php"> + ... + </sourcefile> + </class> + <class name="Bar" methodcount="1" methodscovered="0" statementcount="0" statementscovered="0" totalcount="1" totalcovered="0"> + <sourcefile name="Bar.php" sourcefile="xxxx/Bar.php"> + ... + </sourcefile> + </class> + </package> +</snapshot>""") + coverage_xml.close() + phptools.coverage(self.ctxt, file_='phpcoverage.xml') + type, category, generator, xml = self.ctxt.output.pop() + self.assertEqual(Recipe.REPORT, type) + self.assertEqual('coverage', category) + + coverage = list(xml.children) + self.assertEqual(3, len(coverage)) + self.assertEqual(7, coverage[0].attr['lines']) + self.assertEqual('Foo', coverage[0].attr['name']) + self.assert_('xxxx/Foo.php' in coverage[0].attr['file']) + + self.assertEqual(4, coverage[1].attr['lines']) + self.assertEqual(50.0, coverage[1].attr['percentage']) + self.assertEqual('Foo2', coverage[1].attr['name']) + self.assert_('xxxx/Foo.php' in coverage[1].attr['file']) + + self.assertEqual(0, coverage[2].attr['lines']) + self.assertEqual(100.0, coverage[2].attr['percentage']) + self.assertEqual('Bar', coverage[2].attr['name']) + self.assert_('xxxx/Bar.php' in coverage[2].attr['file']) + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(PhpUnitTestCase, 'test')) + suite.addTest(unittest.makeSuite(PhpCodeCoverageTestCase, 'test')) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/build/tests/pythontools.py @@ -0,0 +1,413 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2008 Matt Good <matt@matt-good.net> +# Copyright (C) 2008 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +import os +import cPickle as pickle +import shutil +import tempfile +import unittest + +from bitten.build import pythontools +from bitten.build import FileSet +from bitten.recipe import Context, Recipe + + +class CoverageTestCase(unittest.TestCase): + + def setUp(self): + self.basedir = os.path.realpath(tempfile.mkdtemp()) + self.ctxt = Context(self.basedir) + self.summary = open(os.path.join(self.basedir, 'test-coverage.txt'), + 'w') + self.coverdir = os.path.join(self.basedir, 'coverage') + os.mkdir(self.coverdir) + + def tearDown(self): + shutil.rmtree(self.basedir) + + def _create_file(self, *path): + filename = os.path.join(self.basedir, *path) + dirname = os.path.dirname(filename) + os.makedirs(dirname) + fd = file(filename, 'w') + fd.close() + return filename[len(self.basedir) + 1:] + + def test_missing_param_summary(self): + self.summary.close() + self.assertRaises(AssertionError, pythontools.coverage, self.ctxt, + coverdir=self.coverdir) + + def test_empty_summary(self): + self.summary.write(""" +Name Stmts Exec Cover Missing +------------------------------------------- +""") + self.summary.close() + pythontools.coverage(self.ctxt, summary=self.summary.name, include='*.py') + type, category, generator, xml = self.ctxt.output.pop() + self.assertEqual(Recipe.REPORT, type) + self.assertEqual('coverage', category) + self.assertEqual(0, len(xml.children)) + + def test_summary_with_absolute_path(self): + self.summary.write(""" +Name Stmts Exec Cover Missing +------------------------------------------- +test.module 60 60 100%% %s/test/module.py +""" % self.ctxt.basedir) + self.summary.close() + self._create_file('test', 'module.py') + pythontools.coverage(self.ctxt, summary=self.summary.name, + include='test/*') + type, category, generator, xml = self.ctxt.output.pop() + self.assertEqual(Recipe.REPORT, type) + self.assertEqual('coverage', category) + self.assertEqual(1, len(xml.children)) + child = xml.children[0] + self.assertEqual('coverage', child.name) + self.assertEqual('test.module', child.attr['name']) + self.assertEqual('test/module.py', child.attr['file']) + self.assertEqual(100, child.attr['percentage']) + self.assertEqual(60, child.attr['lines']) + + def test_summary_with_relative_path(self): + self.summary.write(""" +Name Stmts Exec Cover Missing +------------------------------------------- +test.module 60 60 100% ./test/module.py +""") + self.summary.close() + self._create_file('test', 'module.py') + pythontools.coverage(self.ctxt, summary=self.summary.name, + include='test/*') + type, category, generator, xml = self.ctxt.output.pop() + self.assertEqual(Recipe.REPORT, type) + self.assertEqual('coverage', category) + self.assertEqual(1, len(xml.children)) + child = xml.children[0] + self.assertEqual('coverage', child.name) + self.assertEqual('test.module', child.attr['name']) + self.assertEqual('test/module.py', child.attr['file']) + self.assertEqual(100, child.attr['percentage']) + self.assertEqual(60, child.attr['lines']) + + def test_summary_with_missing_lines(self): + self.summary.write(""" +Name Stmts Exec Cover Missing +------------------------------------------- +test.module 28 26 92% 13-14 ./test/module.py +""") + self.summary.close() + self._create_file('test', 'module.py') + pythontools.coverage(self.ctxt, summary=self.summary.name, + include='test/*') + type, category, generator, xml = self.ctxt.output.pop() + self.assertEqual(Recipe.REPORT, type) + self.assertEqual('coverage', category) + self.assertEqual(1, len(xml.children)) + child = xml.children[0] + self.assertEqual('coverage', child.name) + self.assertEqual('test.module', child.attr['name']) + self.assertEqual('test/module.py', child.attr['file']) + self.assertEqual(92, child.attr['percentage']) + self.assertEqual(28, child.attr['lines']) + + +class TraceTestCase(unittest.TestCase): + + def setUp(self): + self.basedir = os.path.realpath(tempfile.mkdtemp()) + self.ctxt = Context(self.basedir) + self.summary = open(os.path.join(self.basedir, 'test-coverage.txt'), + 'w') + self.coverdir = os.path.join(self.basedir, 'coverage') + os.mkdir(self.coverdir) + + def tearDown(self): + shutil.rmtree(self.basedir) + + def _create_file(self, *path): + filename = os.path.join(self.basedir, *path) + dirname = os.path.dirname(filename) + os.makedirs(dirname) + fd = file(filename, 'w') + fd.close() + return filename[len(self.basedir) + 1:] + + def test_missing_param_summary(self): + self.summary.close() + self.assertRaises(AssertionError, pythontools.trace, self.ctxt, + coverdir='coverage') + + def test_missing_param_coverdir(self): + self.summary.close() + self.assertRaises(AssertionError, pythontools.trace, self.ctxt, + summary='test-coverage.txt') + + def test_empty_summary(self): + self.summary.write('line cov% module (path)') + self.summary.close() + pythontools.trace(self.ctxt, summary=self.summary.name, include='*.py', + coverdir=self.coverdir) + type, category, generator, xml = self.ctxt.output.pop() + self.assertEqual(Recipe.REPORT, type) + self.assertEqual('coverage', category) + self.assertEqual(0, len(xml.children)) + + def test_summary_with_absolute_path(self): + self.summary.write(""" +lines cov%% module (path) + 60 100%% test.module (%s/test/module.py) +""" % self.ctxt.basedir) + self.summary.close() + self._create_file('test', 'module.py') + pythontools.trace(self.ctxt, summary=self.summary.name, + include='test/*', coverdir=self.coverdir) + type, category, generator, xml = self.ctxt.output.pop() + self.assertEqual(Recipe.REPORT, type) + self.assertEqual('coverage', category) + self.assertEqual(1, len(xml.children)) + child = xml.children[0] + self.assertEqual('coverage', child.name) + self.assertEqual('test.module', child.attr['name']) + self.assertEqual('test/module.py', child.attr['file']) + + def test_summary_with_relative_path(self): + self.summary.write(""" +lines cov% module (path) + 60 100% test.module (./test/module.py) +""") + self.summary.close() + self._create_file('test', 'module.py') + pythontools.trace(self.ctxt, summary=self.summary.name, + include='test/*', coverdir=self.coverdir) + type, category, generator, xml = self.ctxt.output.pop() + self.assertEqual(Recipe.REPORT, type) + self.assertEqual('coverage', category) + self.assertEqual(1, len(xml.children)) + child = xml.children[0] + self.assertEqual('coverage', child.name) + self.assertEqual('test.module', child.attr['name']) + self.assertEqual('test/module.py', child.attr['file']) + + +class FigleafTestCase(unittest.TestCase): + + def setUp(self): + self.basedir = os.path.realpath(tempfile.mkdtemp()) + self.ctxt = Context(self.basedir) + self.summary = open(os.path.join(self.basedir, '.figleaf'), 'w') + + def tearDown(self): + shutil.rmtree(self.basedir) + + def _create_file(self, *path): + filename = os.path.join(self.basedir, *path) + dirname = os.path.dirname(filename) + os.makedirs(dirname) + fd = file(filename, 'w') + fd.close() + return filename[len(self.basedir) + 1:] + + def test_missing_param_summary(self): + self.summary.close() + self.assertRaises(AssertionError, pythontools.coverage, self.ctxt) + + def test_empty_summary(self): + pickle.dump({}, self.summary) + self.summary.close() + pythontools.figleaf(self.ctxt, summary=self.summary.name, include='*.py') + type, category, generator, xml = self.ctxt.output.pop() + self.assertEqual(Recipe.REPORT, type) + self.assertEqual('coverage', category) + self.assertEqual(0, len(xml.children)) + + def test_missing_coverage_file(self): + self.summary.close() + pythontools.figleaf(self.ctxt, summary='non-existant-file', include='*.py') + self.assertEqual([], self.ctxt.output) + + def test_summary_with_absolute_path(self): + filename = os.sep.join([self.ctxt.basedir, 'test', 'module.py']) + pickle.dump({ + filename: set([1, 4, 5]), + }, self.summary) + self.summary.close() + sourcefile = self.ctxt.resolve(self._create_file('test', 'module.py')) + open(sourcefile, 'w').write( + "if foo: # line 1\n" + " print 'uncovered' # line 2\n" + "else: # line 3 (uninteresting)\n" + " print 'covered' # line 4\n" + "print 'covered' # line 6\n" + ) + pythontools.figleaf(self.ctxt, summary=self.summary.name, + include='test/*') + type, category, generator, xml = self.ctxt.output.pop() + self.assertEqual(Recipe.REPORT, type) + self.assertEqual('coverage', category) + self.assertEqual(1, len(xml.children)) + child = xml.children[0] + self.assertEqual('coverage', child.name) + self.assertEqual('test.module', child.attr['name']) + self.assertEqual(os.path.join('test', 'module.py'), child.attr['file']) + self.assertEqual(75, child.attr['percentage']) + self.assertEqual(4, child.attr['lines']) + self.assertEqual('1 0 - 1 1', child.attr['line_hits']) + + def test_summary_with_non_covered_file(self): + pickle.dump({}, self.summary) + self.summary.close() + sourcefile = self.ctxt.resolve(self._create_file('test', 'module.py')) + open(sourcefile, 'w').write( + "print 'line 1'\n" + "print 'line 2'\n" + "print 'line 3'\n" + "print 'line 4'\n" + "print 'line 5'\n" + ) + pythontools.figleaf(self.ctxt, summary=self.summary.name, + include='test/*') + type, category, generator, xml = self.ctxt.output.pop() + self.assertEqual(Recipe.REPORT, type) + self.assertEqual('coverage', category) + self.assertEqual(1, len(xml.children)) + child = xml.children[0] + self.assertEqual('coverage', child.name) + self.assertEqual('test.module', child.attr['name']) + self.assertEqual(os.path.join('test', 'module.py'), child.attr['file']) + self.assertEqual(0, child.attr['percentage']) + self.assertEqual(5, child.attr['lines']) + + def test_summary_with_non_python_files(self): + "Figleaf coverage reports should not include files that do not end in .py" + pickle.dump({}, self.summary) + self.summary.close() + sourcefile = self.ctxt.resolve(self._create_file('test', 'document.txt')) + open(sourcefile, 'w').write("\n") + pythontools.figleaf(self.ctxt, summary=self.summary.name, + include='test/*') + type, category, generator, xml = self.ctxt.output.pop() + self.assertEqual(Recipe.REPORT, type) + self.assertEqual('coverage', category) + self.assertEqual(0, len(xml.children)) + + +class FilenameNormalizationTestCase(unittest.TestCase): + + def setUp(self): + self.basedir = os.path.realpath(tempfile.mkdtemp()) + self.ctxt = Context(self.basedir) + + def tearDown(self): + shutil.rmtree(self.basedir) + + def _create_file(self, *path): + filename = os.path.join(self.basedir, *path) + dirname = os.path.dirname(filename) + os.makedirs(dirname) + fd = file(filename, 'w') + fd.close() + return filename[len(self.basedir) + 1:] + + def test_absolute_path(self): + filename = os.sep.join([self.ctxt.basedir, 'test', 'module.py']) + self._create_file('test', 'module.py') + filenames = pythontools._normalize_filenames( + self.ctxt, [filename], + FileSet(self.ctxt.basedir, '**/*.py', None)) + self.assertEqual(['test/module.py'], list(filenames)) + + +class UnittestTestCase(unittest.TestCase): + + def setUp(self): + self.basedir = os.path.realpath(tempfile.mkdtemp()) + self.ctxt = Context(self.basedir) + self.results_xml = open(os.path.join(self.basedir, 'test-results.xml'), + 'w') + + def tearDown(self): + shutil.rmtree(self.basedir) + + def test_missing_file_param(self): + self.results_xml.close() + self.assertRaises(AssertionError, pythontools.unittest, self.ctxt) + + def test_empty_results(self): + self.results_xml.write('<?xml version="1.0"?>' + '<unittest-results>' + '</unittest-results>') + self.results_xml.close() + pythontools.unittest(self.ctxt, self.results_xml.name) + type, category, generator, xml = self.ctxt.output.pop() + self.assertEqual(Recipe.REPORT, type) + self.assertEqual('test', category) + self.assertEqual(0, len(xml.children)) + + def test_successful_test(self): + self.results_xml.write('<?xml version="1.0"?>' + '<unittest-results>' + '<test duration="0.12" status="success"' + ' file="%s"' + ' name="test_foo (pkg.BarTestCase)"/>' + '</unittest-results>' + % os.path.join(self.ctxt.basedir, 'bar_test.py')) + self.results_xml.close() + pythontools.unittest(self.ctxt, self.results_xml.name) + type, category, generator, xml = self.ctxt.output.pop() + self.assertEqual(1, len(xml.children)) + test_elem = xml.children[0] + self.assertEqual('test', test_elem.name) + self.assertEqual('0.12', test_elem.attr['duration']) + self.assertEqual('success', test_elem.attr['status']) + self.assertEqual('bar_test.py', test_elem.attr['file']) + self.assertEqual('test_foo (pkg.BarTestCase)', test_elem.attr['name']) + + def test_file_path_normalization(self): + self.results_xml.write('<?xml version="1.0"?>' + '<unittest-results>' + '<test duration="0.12" status="success"' + ' file="%s"' + ' name="test_foo (pkg.BarTestCase)"/>' + '</unittest-results>' + % os.path.join(self.ctxt.basedir, 'bar_test.py')) + self.results_xml.close() + pythontools.unittest(self.ctxt, self.results_xml.name) + type, category, generator, xml = self.ctxt.output.pop() + self.assertEqual(1, len(xml.children)) + self.assertEqual('bar_test.py', xml.children[0].attr['file']) + + def test_missing_file_attribute(self): + self.results_xml.write('<?xml version="1.0"?>' + '<unittest-results>' + '<test duration="0.12" status="success"' + ' name="test_foo (pkg.BarTestCase)"/>' + '</unittest-results>') + self.results_xml.close() + pythontools.unittest(self.ctxt, self.results_xml.name) + type, category, generator, xml = self.ctxt.output.pop() + self.assertEqual(1, len(xml.children)) + self.assertEqual(None, xml.children[0].attr.get('file')) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(CoverageTestCase, 'test')) + suite.addTest(unittest.makeSuite(TraceTestCase, 'test')) + suite.addTest(unittest.makeSuite(FigleafTestCase, 'test')) + suite.addTest(unittest.makeSuite(FilenameNormalizationTestCase, 'test')) + suite.addTest(unittest.makeSuite(UnittestTestCase, 'test')) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/build/tests/xmltools.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +import os +import shutil +import tempfile +import unittest + +from bitten.build import xmltools +from bitten.recipe import Context +from bitten.util import xmlio + + +class TransformTestCase(unittest.TestCase): + + def setUp(self): + self.basedir = os.path.realpath(tempfile.mkdtemp()) + self.ctxt = Context(self.basedir) + + def tearDown(self): + shutil.rmtree(self.basedir) + + def test_transform_no_src(self): + self.assertRaises(AssertionError, xmltools.transform, self.ctxt) + + def test_transform_no_dest(self): + self.assertRaises(AssertionError, xmltools.transform, self.ctxt, + src='src.xml') + + def test_transform_no_stylesheet(self): + self.assertRaises(AssertionError, xmltools.transform, self.ctxt, + src='src.xml', dest='dest.xml') + + def test_transform(self): + src_file = file(self.ctxt.resolve('src.xml'), 'w') + try: + src_file.write("""<doc> +<title>Document Title</title> +<section> +<title>Section Title</title> +<para>This is a test.</para> +<note>This is a note.</note> +</section> +</doc> +""") + finally: + src_file.close() + + style_file = file(self.ctxt.resolve('style.xsl'), 'w') + try: + style_file.write("""<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns="http://www.w3.org/TR/xhtml1/strict"> + <xsl:template match="doc"> + <html> + <head> + <title><xsl:value-of select="title"/></title> + </head> + <body> + <xsl:apply-templates/> + </body> + </html> + </xsl:template> + <xsl:template match="doc/title"> + <h1><xsl:apply-templates/></h1> + </xsl:template> + <xsl:template match="section/title"> + <h2><xsl:apply-templates/></h2> + </xsl:template> + <xsl:template match="para"> + <p><xsl:apply-templates/></p> + </xsl:template> + <xsl:template match="note"> + <p class="note"><b>NOTE: </b><xsl:apply-templates/></p> + </xsl:template> +</xsl:stylesheet> +""") + finally: + style_file.close() + + xmltools.transform(self.ctxt, src='src.xml', dest='dest.xml', + stylesheet='style.xsl') + + dest_file = file(self.ctxt.resolve('dest.xml')) + try: + dest = xmlio.parse(dest_file) + finally: + dest_file.close() + + self.assertEqual('html', dest.name) + self.assertEqual('http://www.w3.org/TR/xhtml1/strict', dest.namespace) + children = list(dest.children()) + self.assertEqual(2, len(children)) + self.assertEqual('head', children[0].name) + head_children = list(children[0].children()) + self.assertEqual(1, len(head_children)) + self.assertEqual('title', head_children[0].name) + self.assertEqual('Document Title', head_children[0].gettext()) + self.assertEqual('body', children[1].name) + body_children = list(children[1].children()) + self.assertEqual(4, len(body_children)) + self.assertEqual('h1', body_children[0].name) + self.assertEqual('Document Title', body_children[0].gettext()) + self.assertEqual('h2', body_children[1].name) + self.assertEqual('Section Title', body_children[1].gettext()) + self.assertEqual('p', body_children[2].name) + self.assertEqual('This is a test.', body_children[2].gettext()) + self.assertEqual('p', body_children[3].name) + self.assertEqual('note', body_children[3].attr['class']) + self.assertEqual('This is a note.', body_children[3].gettext()) + + +def suite(): + suite = unittest.TestSuite() + if xmltools.have_libxslt or xmltools.have_msxml: + suite.addTest(unittest.makeSuite(TransformTestCase, 'test')) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/build/xmltools.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +"""Recipe commands for XML processing.""" + +import logging +import os + +try: + import libxml2 + import libxslt + have_libxslt = True +except ImportError: + have_libxslt = False + +if not have_libxslt and os.name == 'nt': + try: + import win32com.client + have_msxml = True + except ImportError: + have_msxml = False +else: + have_msxml = False + +log = logging.getLogger('bitten.build.xmltools') + +__docformat__ = 'restructuredtext en' + +def transform(ctxt, src=None, dest=None, stylesheet=None): + """Apply an XSLT stylesheet to a source XML document. + + This command requires either libxslt (with Python bindings), or MSXML to + be installed. + + :param ctxt: the build context + :type ctxt: `Context` + :param src: name of the XML input file + :param dest: name of the XML output file + :param stylesheet: name of the file containing the XSLT stylesheet + """ + assert src, 'Missing required attribute "src"' + assert dest, 'Missing required attribute "dest"' + assert stylesheet, 'Missing required attribute "stylesheet"' + + if have_libxslt: + log.debug('Using libxslt for XSLT transformation') + srcdoc, styledoc, result = None, None, None + try: + srcdoc = libxml2.parseFile(ctxt.resolve(src)) + styledoc = libxslt.parseStylesheetFile(ctxt.resolve(stylesheet)) + result = styledoc.applyStylesheet(srcdoc, None) + styledoc.saveResultToFilename(ctxt.resolve(dest), result, 0) + finally: + if styledoc: + styledoc.freeStylesheet() + if srcdoc: + srcdoc.freeDoc() + if result: + result.freeDoc() + + elif have_msxml: + log.debug('Using MSXML for XSLT transformation') + srcdoc = win32com.client.Dispatch('MSXML2.DOMDocument.3.0') + if not srcdoc.load(ctxt.resolve(src)): + err = styledoc.parseError + ctxt.error('Failed to parse XML source %s: %s', src, err.reason) + return + styledoc = win32com.client.Dispatch('MSXML2.DOMDocument.3.0') + if not styledoc.load(ctxt.resolve(stylesheet)): + err = styledoc.parseError + ctxt.error('Failed to parse XSLT stylesheet %s: %s', stylesheet, + err.reason) + return + result = srcdoc.transformNode(styledoc) + + # MSXML seems to always write produce the resulting XML document using + # UTF-16 encoding, regardless of the encoding specified in the + # stylesheet. For better interoperability, recode to UTF-8 here. + result = result.encode('utf-8').replace(' encoding="UTF-16"?>', '?>') + + dest_file = file(ctxt.resolve(dest), 'w') + try: + dest_file.write(result) + finally: + dest_file.close() + + else: + ctxt.error('No usable XSLT implementation found') + + # TODO: as a last resort, try to invoke 'xsltproc' to do the + # transformation?
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/htdocs/admin.css @@ -0,0 +1,6 @@ +table.form th { text-align: right; } +div.platforms h3 { margin-top: 3em; } +table#platformlist td ul { list-style: none; margin: 0; padding: 0; } + +dl.help { color: #666; font-size: 90%; margin: 1em .5em; } +dl.help dt { font-weight: bold; }
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/htdocs/bitten.css @@ -0,0 +1,183 @@ +/* Timeline styles */ +#content.timeline dt.successbuild, #content.timeline dt.successbuild a { + background-image: url(bitten_build.png) !important; +} +#content.timeline dt.failedbuild, #content.timeline dt.failedbuild a { + background-image: url(bitten_buildf.png) !important; +} + +#content.build h2.config, #content.build h2.step { background: #f7f7f7; + border-bottom: 1px solid #d7d7d7; margin: 2em 0 0; +} +#content.build h2.config :link, #content.build h2.config :visited { + color: #b00; display: block; border-bottom: none; +} +#content.build h2.deactivated { text-decoration: line-through; } +#content.build #prefs { line-height: 1.4em; } + +#content.build h3.builds { font-weight: bold; text-align: left; + margin: 2em 0 0 2em; +} +#content.build table.builds { border-collapse: separate; + border-top: 1px solid #666; margin-left: 2em; table-layout: fixed; +} +#content.build table.builds th { padding: 0 1em 0 .25em; text-align: left; + vertical-align: top; +} +#content.build table.builds th p { color: #666; font-size: smaller; + margin-top: 0; +} +#content.build table.builds th p.message { font-style: italic; } +#content.build table.builds td { color: #999; border: 1px solid; + padding: .25em .5em; vertical-align: top; +} +#content.build table.builds td :link, #content.build table.builds td :visited { + font-weight: bold; +} +#content.build table.builds td.completed { background: #9d9; border-color: #696; + color: #393; +} +#content.build table.builds td.failed { background: #d99; border-color: #966; + color: #933; +} +#content.build table.builds td.in-progress { background: #dd9; + border-color: #996; color: #993; +} +#content.build table.builds td p { font-size: smaller; margin-top: 0; } +#content.build table.builds .status { color: #000; } +#content.build table.builds .system { font-size: smaller; line-height: 1.2em; + margin: .5em 0; +} + +#content.build form.config { margin-top: 1em; } +#content.build form.config th { text-align: left; } +#content.build form.config fieldset { margin-bottom: 1em; } +#content.build div.platforms { margin-top: 2em; } +#content.build form.platforms ul { list-style-type: none; padding-left: 1em; } + +#content.build p.path { color: #999; font-size: smaller; margin-top: 0; } + +#content.build #charts { clear: right; float: right; width: 44%; } + +#content.build #builds { clear: none; margin-top: 2em; table-layout: fixed; + width: 54%; +} +#content.build #builds tbody th, #content.build #builds tbody td { + background: #fff; +} +#content.build #builds th.chgset { width: 6em; } +#content.build #builds td :link, #content.build #builds td :visited { + font-weight: bold; +} +#content.build #builds tbody td { background-position: 2px .5em; + background-repeat: no-repeat; +} +#content.build #builds td.completed { + background-color: #e8f6e8; background-image: url(bitten_build.png); +} +#content.build #builds td.failed { + background-color: #fbe8e7; background-image: url(bitten_buildf.png); +} +#content.build #builds td.in-progress { + background-color: #f6fae0; background-image: url(bitten_build.png); +} +#content.build #builds .info { margin-left: 16px; } +#content.build #builds :link, #content.build #builds :visited { + text-decoration: none; +} +#content.build #builds .info .status { color: #000; } +#content.build #builds .info .system { color: #999; font-size: smaller; + line-height: 1.2em; margin-top: .5em; +} +#content.build #builds ul.steps { + list-style-type: none; margin: .5em 0 0; padding: 0; +} +#content.build #builds ul.steps li.success, +#content.build #builds ul.steps li.failed { + border: 1px solid; margin: 1px 0; padding: 0 2px 0 12px; +} +#content.build #builds ul.steps li.success { + background: #9d9; border-color: #696; color: #393; +} +#content.build #builds ul.steps li.failed { + background: #d99 url(failure.png) 2px .3em no-repeat; border-color: #966; + color: #933; +} +#content.build #builds ul.steps li :link, +#content.build #builds ul.steps li :visited { border: none; color: inherit; + font-weight: bold; text-decoration: none; +} +#content.build #builds ul.steps li .duration { float: right; + font-size: smaller; +} +#content.build #builds ul.steps li.success .duration { color: #696; } +#content.build #builds ul.steps li.failed .duration { color: #966; } +#content.build #builds ul.steps li.failed ul { font-size: smaller; + line-height: 1.2em; list-style-type: square; margin: 0; + padding: 0 0 .5em 1.5em; +} + +#content.build #overview { line-height: 130%; margin-top: 1em; padding: .5em; } +#content.build #overview dt { font-weight: bold; padding-right: .25em; + position: absolute; left: 0; text-align: right; width: 11.5em; +} +#content.build #overview dd { margin-left: 12em; } +#content.build #overview .slave { margin-top: 1em; } +#content.build #overview .time { margin-top: 1em; } + +#content.build div.errors { background: #d99; border: 1px solid #966; + color: #933; float: right; margin: 1em; +} +#content.build div.errors h3 { background: #966; color: #fff; margin: 0; + padding: 0 .3em; +} +#content.build div.errors ul { list-style-image: url(failure.png); margin: 0; + padding: .5em 1.75em; +} + +#content.build .tabs { clear: right; list-style: none; float: left; width: 100%; + margin: 0 1em; padding: 0; +} +#content.build .tabs li { cursor: pointer; float: left; } +#content.build .tabs li a { background: #b9b9b9; color: #666; display: block; + margin: 2px 2px 0; padding: 3px 2em 0; +} +#content.build .tabs li a:hover { color: #333; text-decoration: none; } +#content.build .tabs li.active a { background: #d7d7d7; border: 1px outset; + border-bottom: none; color: #333; font-weight: bold; margin-top: 0; + padding-bottom: 1px; +} +#content.build .tab-content { background: #f4f4f4; border: 1px outset; + clear: both; margin: 0 2em 0 1em; padding: 5px; +} +#content.build .tab-content table { margin: 0; } + +#content.build tbody.totals td, #content.build tbody.totals th { + font-weight: bold; +} +#content.build table.tests tr.failed { background: #d99; } +#content.build table.tests tr.failed td { font-weight: bold; } +#content.build table.tests tr.failed:hover th, +#content.build table.tests tr.failed:hover td, +#content.build table.tests tr.failed:hover tr { background: #966; } +#content.build table.tests tr.failed :link, +#content.build table.tests tr.failed :visited { color: #933 } +#content.build table.tests tr.failed :link:hover, +#content.build table.tests tr.failed :visited:hover { color: #fff; } + +#content.build .log { background: #fff; border: 1px inset; font-size: 90%; + overflow: auto; max-height: 20em; width: 100%; white-space: pre; +} +#content.build .log code { padding: 0 5px; } +#content.build .log .warning { color: #660; font-weight: bold; } +#content.build .log .error { color: #900; font-weight: bold; } + +#content.build table.listing th, #content.build table.listing td { + font-size: 95%; +} +#content.build table.listing tbody th, #content.build table.listing tbody td { + background: #fff; padding: .1em .3em; +} +#content.build table.listing :link, #content.build table.listing :visited { + border: none; +}
new file mode 100755 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..1e2f5669121b121d8119ca1ca05ff66663a41075 GIT binary patch literal 300 zc%17D@N?(olHy`uVBq!ia0vp^JRr=%3?!FCJ6-`&%*9TgAsieWw;%dH0CH6Vd_r9R z|NsB&*|Vd^PoG~B(Kl^QOH0e_8mF|hwBjhusHi9_D=SMa-YEH<-$7=T1o;Is{6_%J zBLQkaan1sd$YKTtZXpn6ymYtj4^U9C#5JNMI6tkVJh3R1!8b9vC_gtfB{NaMEwd=K zJijQrSiwZk;FX$sDNwN(NU?KKYGO%dex5=|W^O8jfw{hcp}v8s@ER``pb8657srr_ zImrnLAwek&Oo7(}7}?m4P0(;<6-c-A$O#MC$vB}aC1N7aBcp=tk_|6qZ|sy!=t^W} ZIP#C{{ok5>vOqH!JYD@<);T3K0RZWzW`O_z
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..a5b902f0caf9aa53a97b38b3cc0436f483f24d92 GIT binary patch literal 289 zc%17D@N?(olHy`uVBq!ia0vp^JRr=$3?vg*uel1OSkfJR9T^y|-MHc(VFct$mbgZg z1m~xflqVLYGB~E>C#5QQ<|d}62BjvZR2H60wE-$J4)6(ajf#pYj?%Q$;+<XN^m*E} z^GhQ7rp>XkvO0SF^s{Hr?q_Cp^Ygc~wA^4|`2YWZT3Q;AJlGg74b(1J666>B9}O_5 zuAP|#lnnQDaSW-rWpXB3=zxHLOJK{4^e_LyXB!GGJ!LDumLWmuQAOaQw`RtEZ{{7@ zGI8&O<x?d09%JvmDl+jiqr%--{zdac4hcVYkJ;;%cV&0>YE#Cm5hi<>co>u_*nX^- SuDb?k1%s!npUXO@geCxb9(I)g
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/htdocs/bitten_coverage.css @@ -0,0 +1,4 @@ +/* Code coverage file annotations */ +table.code th.coverage { width: 4em; } +table.code th.covered { background-color: #0f0; } +table.code th.uncovered { background-color: #f00; }
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..e2821facdb1d55065baf6a435dafe72de5d8e777 GIT binary patch literal 29521 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 AdH?_b
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..cbd15765f453d2ac3725c7740c87be1593c3f5db GIT binary patch literal 6346 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 Mv=aaRKU-6&zehYvYXATM
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..268f8e3042f54d4fccbc9161c338c9f465412609 GIT binary patch literal 6619 zc$@*-86@UIS5pQZeE<M>oZLNWcwE(WcSah`lV@zpLPji4U=b&=35yYH09UqU7>U@= zVr&OYD3fQ+(`fK$MwuCH4m%_)Negv|fKV_bETIq=LJ72W5^5VaO&3awflz3JF-tH3 zzI<((<ZG<nd+uBA_GU(!vEoEO-n@6ua?d&UoO9RrG=}Q;A{2QBp=bbg1t%vbQA20l z|6YO6O&`-fM2UPZS464hW9dR-#LNs^XfT~c<_fc5p;Xs^nJ*^tR$mc~qX8?O8YrSc zvp9gvjRkx&hVr@LY!aDW!}<IMjGoIB(DHoV97kzno=;$AF`dhz0!qbmqXoN~KC@`0 za`|!0A)msPjbTc=Xzh^Mm(Hfp&~O1)vdSE{@+g%zhXxX-Bv5LlHH7P4o=j2zh<*|g zD4!l%H$G&cjkz&2iU!bVI*AeUwgOnJTz;jQ--I$&HiZF9G0LQ~mYGKzEi6eaZzj`L zws?LzlL5fm;&=udBsXHM%OTUITWJ<ew3=9<ACNF;eB7}WHQ_)yX(cvW`5YR`71CgW zKFk-(Y;MHNaBZ2~D3+$bi1J|6zTtcU3zJ%B4GvK)`!eYvRJ6v536y|eu7(7Z3?$4< zI+aBu_U~XinZ)v%nV|s_^?`bY^kxgUQjxYpe=b|ZM#?17%Dg$cj_}x+D;9HusF=gG z5fc&<3>Q!aVGyp8%9TtPie|RYB1|#2QIuMh&TaxjSlJ|)Yhx~-wDO5!x^Ghf6>^z0 ztpnp(sAI{J)5+yDayb>DI5u`JyI{F<vjX*@p}f_f9z#j1FFj~x5`9=#=K=GxhRnQK z#FDJVZJkXex^jbqCMpc~6PiNN8Y&RC#h#VvBYsJ5y6|OnI=dSCDK%OKtAV|WTpb^z zWvR7RK5Z2eDRT(dRfOM-W*&Z1E5JhQ2C!yXt59&-fhug<FHv;C{h1tT$yK;<JsQ%~ z3{EmVg8C?z6v|pD+&Uvxg0>j-828bnZfY)vMs<O-ja^duNRc`a@pUWPZH-Y{Ti{ea zolFcMPYT@Tj7%)n2x(U8nk>fZ^q7^zfsA4#BvE11q|REfL$R1AWHiXBBMh3C=kg2- z+j#+&&K7W(pxkf~erbSMIGe03VnL939_B^qsU>g&bG)+HeK%l_9L7FPVe~#t_-t^C zzye-+qmhGC3ywjQ%ad@Yv5`!hsazJj-tdrjpUjQo@Urq)VhVBZ0b)W3(U|YeWz77# zTyGB7j|v01(F94Pd5dI^)wz+h)kPxOT)PQhUGJloFg-^x*WtTWG))y)R0LUg^b|DO zflZapIPF!;n^`->WOF#yu)h!^^pOC;2K2xu;vmLxiL*~~I8Rjuds!sTGB{<Y&hK4| zGBo9$H)iz>7cG<+Nf*)^Gfr0k>QcWiK*ZR|8LjiAfT*`p;0g4Xm64GL2L*=(3fd1B zK?1iQs74N223oE^ZQ(3N+LEG@L~KU^-1fQ2oefQ#sB9#-rAS2JIIi2wrm&(Lhcg+g z2$aCSoA!FxF4$4s=69qi^jdjHarQI0FQ9>5Gnq`1er%wJ7#KSU8ga`;+%l7(+wGOk z_u<-!EM4@Lh7@g&1nwf&^!MY`bTLYdJNFx~x8Tg4>CF}FJHtvLb2aI|@r9&6e1o$e zxxkY(HiUfyS2u)%7{?jOo6v<@$>k)O&=Nx2VS_u+-Lvd)M`+t&6G5aQom>M3xdx4i zM$~}pzwYMddxGdhG*OSVIvcMZ@Ir=1NlO}xSleAeG*KzCqUJCP2XL)g7>c#Hh|*}t zM3LDt?`T9|H7njmq>9*hQLn1m%i8e0HV2;C2yl+>F^n4B!~n-3@(|HIaGkz}Fr0%Q zdx_T1O-{^4+FaLgz&C=lCbtB5H!-~DQQl_+$irOS21gLfV@-M4-a%ebGw6SqLCOt8 zBx>=vQIz>cWFmyZL0lpkuEh}Rm~TX5(5lP)xmC!7n5#u=(aCQJ+s~1r%WVqB2EgF( zcq?aMG)W$tFc==6I8fHQqyoY>0%kg*X`%sXt=bU;%Z3oCC;?`Dq)m9F(R(8XlIF#w ztvV4e=xKZumFs8>iRrgvLFRJJ=Wv$VQA3)pwP6bgwgPJiv4YEv5j>id9m|z1FeaLi z*4}XQ_VBXjpZWUp;RZ0~VJJL_Z`{g3_7xLzQ8+=KF7b+7QypiB#-k-4Gdh`gTPR^I z^sqt>(?+CyJRBlQe`0Ct#BrV+@=dsopnVdfDgAf#wEWlcCBc0=r{caU9(MfO%}X&o z*I0y=C>yAIBWgtt#XwzbmmKdjy5rY2jnPIOa<T4O>r>U{Y_J<<D0M$NqEX`HUm!SE zj}UcR)i5`(OX%R{2V)|Df<2_Bw-CuHC-7P21f7j=+vlN0&p{I81f8*Miw;~v2n#wF z1U&&GK>m7g(vLJGI@gIyTlw7SL*<)KdBsU$B?Nl8YC<Jaw320I=8k#hUKAiTorG&@ zo08o>DsT7F);&p2H4#R4`ZAh{Rsh}AY}z<y`RIUW-je}i2@_PlG0H|#Pg8Rx6woqY znwJ~II!}RXNB=!A#!~`M1z<o8JdFUi2GzjR0a&L7o<V@$u2%!k1Yii8X<-Ag+F9CJ zI`rUexX;DUpVrRCs=;XUoS2|@%xzL!@*|Q9Sl6co&A6)#--gk(2KsZZEyPk^D@Kv_ zM_Py0sV&o%a}qCyetZR2wgSq!xUwz-ci7rWFNvSWfzJby^SQF~!S&`iuE&g6V)0gN zoN%bN#v!JF4yvwdD|H__R(hkZwpJp>iB)Bso<qktyMk32NAQ@M1=<;hR-oM*sLD#z zV`^rc(+@4<T#Lv==mPCC+A6yvT}{-)zwNgEvmE^mTYtBpyvDeof$MuI!5XK>CDTY- zfL?x}gMTk1V-)-w=caM^CZu4wRyjuv{WpCS#w_kY)0r7aBiuFeIAjhx4pBkqM1>uj z5IHU+a@;Y=O+VLFGCTPhI^@RvpO98v#2LUEtZaKZtQfyibKn5Ckg)qhSxjCOEXj?` zcxSzVXZ<5!t<Sne6Z28H1F@4^uuYvV>f2(7B@5_Em=YD+xY2XEi{h<!&xHlW1gza+ zNT}1JI^J<NcGyRxzZ!8!hcm8mv9>|GMEhL09zU#D!=W8bbqKE~m(P$(3!%8w2bVZv zI881`lFK53`^P*kw%9yDRt6OHK#T$c9yJc6OP@)*v)(6BNUJPmDHe$9EoCVh#8sOT z6idlyyAB#?snbAs=Dw6)He%6R6pJX%8ezK^-0h4f*=vmH0LKWByz0A<43*Ze6!fz^ zlpqLknJ2_$a)#NXsK%hU-jwiue<Tk;`<jFymV90+F{P4?(#(mG0ykH^7jJ#>m`I&8 zXg0YFqeJ>=o}`^uJAJ&0nW@oiV!7!bjL?cW1Z*@5fNMm$pl6fQcp5xY4!VZt>98pM zp#aH>El38)l{9*l93SB9!Y*_<t;j5xFcF#})T^ZG8mGeRe-?ezx%kpscSMQkvFdoW zZ2PctTkMU^QI)o0Vn13kZKJ<wQ+}pR5z{u}PK+HQ&^l96FW4`YsCr6;8-?lUMjKe# zCJJDY{u8hyp@TLkD1#bpP$LH6%#)TjFXr$IGPHNS9Ip0C%wn)bS{K2%^W}TpAHe>| zrfFBPsiG=e1v6l@!O{($fd(H`HMmQUT`TE0c`HwgV|NgM5?!_&T~?%<=Pn%esHMzG zQexF}Hp&px`URKAY@y%D#~GpmM^vnUDCn(MPmtA25OF#3Sk4zaN+CE2`^aqQB8F)f zft%~xDKH4I41$icAoadYgv_@_#626A;|LkQ#T&f>TIr~l3fmC}#N0+X)G<SqtvOFC zoqAHu^I%=Ot{fYJbutV!#{~&s9KM+={Gvh3db|RCRE2&8Exc0OtUI%u2v_?(PO4)N za0OAsVnIWU>(q8c+!evnTU~Urb&S~H()O0`mT^dr(2cLmjlbzM@BhUs_MC4n-S`S$ zgV-Ie&A{MFcPV=X8Q7e9>i*7C@gEVKpQT<ExAm#fhL)>~JE0UnEewd=O_~x1p@hsR z*r}n+OKsU1;VNlMcnPyrvcDTrj@cDeu(ZN#QxSzyz^RW9w26gdhUenhpfdPs5=~WY z6FYGf(naw@M(u(|FpDaH1e8CpV3N%{eX>fzK_nYxeDP+5FV-rv&rlv;a82{FXKx4} zQzMrgE$@<}HE_wwT60lV9`pN^wLP+{f$SQ2niviGj``E<n15e@I+-)EQ047uCwIjv zY>$WLj-^@In`AlNVW~uWtw1}^Jp*t6i8i``5qiWBq06O`6;jDHQVW)hxQZOcFFQ_= zv$T52P1ov}rIPFX9OiXqRl+?pLjY|s%)MToCA7I`Ej*zAO!Imj{B0yqflm~Ded2oE z-y3f5^@d*{(ExtlaGm50*Gb-R-ITncbpI!w2`bOihpw~-j2@V%L)w>uWhZm%6$kLn zaxuL+=P~zDTW%*_;b9Z|;Lya{xJ>qCH<R)5Fmt{m#WP~-q2e3kt>u?uW2ge|jW+k2 zFo&4#@Maep@0+YFlYY73U}sWVb~BXS!aKVN{)z!dOU??40D6++tK=j{CsG6dgaC^^ zz&|CxM@UdL2FN<ot>ToRz#OK7!O?A^+%`1oRZyVknV&{VjuK&nrHpWu6ud<i!qKXp zV|lTWxI-1DD^eVTKQlyo-cp0^<Tkz9o&#TR)m7<3kammgZ$7<rcax7ZeilxAe9c?K z<aRu37b!R~-0mfY+jUW#uNx`o*mlE!e!?CyeodE*<e<BP&ZZ2bF=^83YxN1)>LDX_ zXvXH2>Bi=kn#X2k9LtMcQEc8J#pagEV^h(<Q;$twChFM4D&3C#5W1(Me#`ar=C9eq z1#+fw^h*h=<xV--Xp@R>F%%~URDF(*N81M-KhAW>1<xrcXQv5cYbxt*e;A&b?)FZ@ z;qR^l91Tu&mxw^je<cT^btI%(KQEP-QVHEX^|ULoVM+;-MyyRN`vzR^CVAmfqz3*u z0bUhQ1MeZg$AW6$y#)AF9oG8U`iWUc+p1ll-6yYM%%kVg!Y3lcEf(A#w-MBV?(Sw( zx;h!8*;{dZ>n`0pzUG~P3^+r5=7d(j8SFDBv}jouyaVE$&;oTpw(~eV{-_o$6OL*z zquE%TIL*D)n6EZgD_sj&(=21xp6S~ahx<yXxCW+NTUWxA?#aknSj;{zs62A&7^b6^ z4tGiI9PY$m2`kmmI3>NNqXu4cNo~BQEil7gQ#PEIP2Vu;gQU|p4uX!ucg2P>`KCd; z_@l_6&a3@sZL2-2{+6&JvFKN%1s`C#`LgFGS2LTqzLVewcsZpJiDDijkTf^V6Nw(A z6PParRp|5uI;`76(wGSsJ}e6C^mc9R=CLzJB$nw`Fx#SHwxuMqJ19v7zS&mlHMU`0 z@k(4wY$37)B`r}@I-N<Bn$BEJrZZ28Q-z1w@p4X>73BDN#+Z7Zk(1{_59=&bm_M9T zVa*Cs->Jtq6g;HUoItMjRH20_y45XG4*ws(ZrIyqyQ5n(UAO%<onOvHVuZ2r_l^Ji zoy`*-oXR{Q=QV0jF;AFM0=cchCw><}Q*E`e6~|)j(Y<EleeQP`TFs@#^(+v^^?lN! zJ~OUAGN=Z^xc-A6dDXs-9^rq49^o%P6m|A1%MB-)$xDu|krO24SZb?sklC#06`K{k zVsnLq_hv=+*j%=Ig!e3X8#xx3vVNymr=L8Cp1w-kf}Bcz@1#w$5tr4f5v2j~^flfd zP-z^it~IMrR36+;U$>v4(ycjV-8!oCRQ33D`uaKV8T|#-k4c<v|L!SU@4ag<AI#i9 z-=Bb2lCXQlJ@*PLYUEx<s=5!p=+_>Za^urJl8$K}715-lr1a&WJPg@S=4jmgN`D$r zjvM^scuYj|;rSg#j>jsL<2$k(Uy$W^rSyvKkmGtXC!w#c;+O~id29-+yE4N^^+>5L z7v<>&qWpKKr~I`QC|{kX)v2ZO@oT5#<L(KK%6(knTPhz{o%Qzj@p5t~d|VOp(`@Eq z<vy-xRM`PSZllVmWt&-%LZ%tIH`G|I1#(oD(o{k1r#u`qI7??v7;FO?dfMbK>F&W3 z^`uXDoaqyeK-v@9ceN*}qx_XPU7$RkVkG&V5f%JuRy^Wvz>??3u^}3f_Ovzvy9Q!E z_Ke$)xjaXp>-}r;e$By@XMLsetoP*c+Yh3Me*?7ku23`}0_>e3+60gElmy?`180Vw zK?lnO^~3~<>Ys*$QQ9)~WI0Vq8`E7=vBQf!%&{*j@ZZ99d%ydgH5vE_0q!P?F4qOh zAF;U;m46xWQ{=3x_I;|<50<u)r?b7Mdgc|s?eL1<<JeT9a8H&qbsKkPXU!y8NDGt~ zjw(=E$?^)g^;WBZYY9-`#tN)^*}+i2731bfI>2U6ZT*lvfBLMP-(Cxl&%ZsP{hfdU z-v1<5h>bE^5naztHM$_P?5H%e?3h+&36;()J76K{1vKNj&uB_{?ZDlKv)?NXINzDB z4^U>Uv;)43{r(0VtaCTdUlmNWO=Y5OrA$;+@@>^s@>OUmU+rvL+20JXzu<~c!(3a& zdcX6QJA7ZB+sVCrzmpkv`Mu`8lY4h%r!%lAOR<2XDE;#NPVVLVo%YzvmjcJvF4gOl z&$$W8ZKYD*-+azKv2}W2;v~9Hw}V|(4SBZ<!WRc&&Ef>QW<lwXcb%JC*j22%?FLMK zHDk9kE@f6TXqoS71_kr08MLhAZX5M7XUpvGf#64spYNip3dcHqHx8s-3IaZ6pE$r8 zQd;|dju{Bh_WTD!Ecvli@{dx<KS?G3%#`eqpbb*V0;wb<mCy*7<yq}tU=8RVX5dck z{5)L$wa5E-AoaXmt|{q8W&JW}IMIkN7||Gc1O1E=q`&I)Cl$r{H@N;gh;z8Z6FdBi z^pga*rf)Jez36L~U55Li9(jt()cPX-ErfY;l^iY?X&8ULLYU{+Pl)XC9E#y1&vyCL zAuA(55A(J(V<7mMymvC$sx+}wFY9i53{{}vb}K86A&Jva4NpnR-?z|HIUM+S3oLxT z<c+P!(L|X3zvNr-i=1n)$H64aMN}0>=sxbtoP)f?eV}xYO3GI_DK~+XuT0%}DwKB* zCoj(6KT$9v*H?6ST|#@6AE1g{Qr>67rLJER2sWliD@88vH=K$}vv`~AG5>dioLmAi z_CiS2xG6i4tHwlX$zO${m&s{utn}biNK1!?x?Okis=DQDcl00P%$w`02ykHl%UTE$ zzi3zaMWgpBW|TR;Q2J#`W{qv3AzFQ*J;`A|KuuFdyG~6;H&xnoZad5#G`k;Q)x0`h zqr>Ib-2pG##1UG*_g)koJ^!b@cPgvQG{}D$&?l$;&BGn&c!+2K!<~1br>3WRAJVy= zF#_}FhwO1Dvpl&&T%IgvI`%+EJA0s`U3aDzQLg=BDtzT>j~bcM>v$xa&JP>}A5sg! zxo`z2u92v9gbwInIvPE}GbQ#vWxhtrB!@T1$E?_0N^U>$Wf;jfs4Rh}7Z-Zdi#Yq% z47n-0NBJsFgLIGbRi?dOH0XCz`0UHP!9ybBY*cCQ(}dW}@Dn}8Z*w00o4c2?|4*pF z`R^I_f7^h6vcdez4SKUOJ!oYMWRc;fdF7YE?g$WJ_cFrb$2a-IL8TYT9_K&Z7nHx5 zMzFqE3QPK+%wjOiUTv@LPX)cxLO^^|d$S}Ww^DZ|e1mBppuVX?JH(M)ByPOW7Z&yr z_Uvc7pm8{m`*kp=>#a}#to1fjvX{QVdkhe~O>~k7-XR3+`eE+@ABkGQx%TlJ<4k?x z$D}C!+dTd|HvU^6-#a?E?M-AQDijbZ?QZUO#t#_ASJ2b%Y`ICGZUTCMtcITg4)!h| z4g1{Brl#z#8G$XmV%ibCtG!Fj@#9GsLEfHpAo%fJa?%1A(B&bD^s~6rEHF^ril`0X zVGHsV@0XL4lif2f9zvdfzD>W*-)y^%CjdPlce$70$hXQA9w2*BcXN}Bm{K67n(2$+ zy8YEv>dgD-R^NL%91*<)d2TK#uf?4yF+*<?w%dgYA_t2g?AI34@9ql?v|n~~`UO!1 zfL67ifNbMM`PSP#=_1H`&JLtR>w!WePW7^1vsEh{XZlq~xiF@OK)dS>k|WdyRQMQh zIr5#Lyhn5260nZ*Q9RGT>HT6oDtrL<`;#L6+d3IoTwzBd^`-rG735n;RSB@aVgdF$ zHYnln3*a46FGXYhB0l(3dwKPTdg%L8Apwf_u)gH+eZ9n_(Kd;RyrTDMo;ilM64TD! zSBxuf#IO3;@L?p_XTOtEqMQ;=Bg?v$SFR@QO=GK>=_7X^tk^o&@hf(Cc&(TAS#O^q z?eBsw-OfZ{lOH;CbKmI{7u)4;(wdM7iIk@V%g5xB05zp+M9$NE!pz6@r~>TH?mqgT z)_y(}M&2$P%L#`<v{dtYQw^fkr+MqMUrhOV$p(LP(wC67{Oh*sEoLIoKT&;vB~yg1 zilC(P(#<M1F@10_64O^EX=F2j{YypPR+GS9t(aQ$c*7#rBk!``0`p0Y82sA`hFjN2 zeAO(c;Hs4d3(Uwil*#UnWd7ORrx&QebEca2woh^?o{pcE1mza4i7U+9Fln%YQOb7Y z)2Qq}w)BZ$bp}*zL)(g-DJka6253pd)-S^A_dOr|{(rp;R48nySbwpAj<<dV9+YAN Z8NdQMxcV7<Jj>aRoxuS}0|4K2cKHYC%l`lX
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..7b61a52c4cb18bafc176d2fc35def4b617037cbc GIT binary patch literal 6472 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
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..30d4aa2dea3c059fdc08b7900e756018f8fe1c01 GIT binary patch literal 6508 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 SOJZ#oG@zyU_x}J8wfM^g_{1{+
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..c67ab32fafc00d2c3a3f90df799452111eec4d39 GIT binary patch literal 6462 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 Y1GVJS;YYokTkDbtT8e-F7h~FU^*S0y&j0`b
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..1c2f7ffbc7eef1402abcc7a381ad11e42c292cca GIT binary patch literal 9346 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
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..3d3da8e80ce449c0d0c9b2853073942e598dc209 GIT binary patch literal 6404 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 STm0(0pcyU4fBzTIz#+=UR%}rK
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..edc00972484cc0591683f7f0d9d55b423d81c92c GIT binary patch literal 6487 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
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..a5e6b7973a525c8738b733036c3dda2330ea4b02 GIT binary patch literal 9304 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 GZAkrbjtgx7
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..866116d4de72f211e1ff7a0bece0bd0f70222934 GIT binary patch literal 9338 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
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..811e3d26d9e3056b2145b0bed6eabb16b5651ada GIT binary patch literal 6487 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
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..a740afab2e9dcb9b94738b57cc63388e35f1cb60 GIT binary patch literal 6727 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¬iv1C`*)+(Ottn(&2366nq1v}So;G|4<uHRt1=TL%v7l-c)nD@Gfo<4&r z?F*+NHc$P;i1VA@+dtU7jQuxsP0l}3vj6cC{J$mUzc107)tLb+S0qbMpO{V(#xLxI zfp6dPZiAOYZwS#f&R-aQi>jUc$IvwS1AFA*u;CAt24&?z1&4iLVm1+Rn{1AaMXTKJ znGvO(#8tVUO#{uo9S+4RVf#n`+tkLidq3jKHdG!_u<%R5g1vrBpx-i}&)<2BTVqbc zMEGrh_@x0YZYL%QhXieOwDmdM;JPFHxxGGTpH!;qAXN{OEsS$Q#980ro%OK$jaug4 zHXLS(X|MH;{tjjLrEwQP-cxpJ@Z&q=cntmEsOACvs@t&+jB4*kR8R157xHEAKgP$$ zJB~kl^sF~UIATCzY_pxgV}PD&w7djFzU4+8OZvu^XwYdgU|Io~ZliDS8_;-<?X!pJ z{_ML3oa^0$Ja3kS*SnqhGlOp<&a(>@NDdSMII8y;F@=GS3L?QFWuFS#)KOApD=*4h zZ~wRpARiQPY67eW0u4LO%YMx^t#q8)o2J7!CyeRI-HwJya)ic^3LaN%M!vI|cj*@= z&Iau`9}V>Uo8I>r(Kx?O_w8{J{)j<FA=lcGNPX$3-30j*)r1&0S}_Ah932EW{3-C_ z?_n=CduYt~_<D~aFBMS_eXkl6z<3wSOCH}d0%n-DN=)Pxy+`xRTwY6zojZ3qpZu%b zh`0IZa1RpnbJWQxQBDZQ$g-^Eg{zCbX>7GIedPYhimeMBzhXykH+pHG_4eZ={Vw=^ z@$m?3@<Yct?u)qM%Dnug;maWt5-!it%E#o{A*xDOiJYhTgqe@)Q32TP_`?QB`F=Ho zysc4|5)OrEka@kC0Fmm`y!P48+`J!X@O|U|Gn^VBF@G1D^pcQrBJP6fGtgyNvMx3a zcoQ#VW(&Ds`b>+0srx@9XCr0oZ&>O!s$^_m2243mM6(DvMb3n7-NR8d0;T^F(cEgl zFc-U#BqUX0GYhz*5lNtQhq(-Q>5iioAR^_ETJG)nk5a@#+D`^dm@!P1q(HxBQZ5m# zlsybMA{F@xVjMXbdkKhb&Kl}5a}1BcdIJy<Zm9pbQT?|KhyQvRs8HBYvHoHK9sm6v dJZ8iMGJpkiu=i{D;Fq%<JA(s|1^`&GkYfS`76SkP
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..4d7dd27b1e90089a091d707f7ce747634ce2e7a2 GIT binary patch literal 7016 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 GD?V&pkAuMg
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..f682a88233e0675c5561588423ed6cc00e9d2004 GIT binary patch literal 8566 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
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..7d450c40f902be61743defaae782269e2984bf14 GIT binary patch literal 8140 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
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..f2f2d537b1fd0f96654f8f1005dc2ad0206f2eb9 GIT binary patch literal 5498 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
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..c452702490bd5c175c0716acc03c79bb5c0ede47 GIT binary patch literal 6848 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
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..702258c3d72fb128148646894daed3c9facc0513 GIT binary patch literal 206 zc%17D@N?(olHy`uVBq!ia0vp^93afW3?x5a^xFxfSkfJR9T^y|-MHc(VFct$mbgZg z1m~xflqVLYGB~E>C#5QQ<|d}62BjvZR2H60wE-#;3h)VW{XA{j{mjg6e*XXe|KDI> z0Fnm{m_SCdl?3?({|5nv&HI<^2Z|VYx;TbZ+;TZ($ala%fXPwe&Hw#xK5I_OEaN(2 t-}tumYMQ~{bx-Un99R8yFnY>iz@R9@@I-illP6FwgQu&X%Q~loCIE_oN?HH_
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/htdocs/tabset.js @@ -0,0 +1,54 @@ +function makeTabSet(parentElement) { + var tabList = document.createElement("ul"); + tabList.className = "tabs"; + var contentDivs = document.createElement("div"); + + function makeTab(div) { + var title = div.firstChild; + while (title.nodeType != 1) title = title.nextSibling; + var tabItem = document.createElement("li"); + if (!tabList.childNodes.length) tabItem.className = "active"; + var link = document.createElement("a"); + link.href = "#"; + link.appendChild(title.firstChild); + tabItem.appendChild(link); + + var contentDiv = document.createElement("div"); + contentDiv.className = "tab-content"; + while (div.childNodes.length) contentDiv.appendChild(div.firstChild); + if (tabList.childNodes.length) contentDiv.style.display = "none"; + + link.onclick = function() { + var child = contentDivs.firstChild; + while (child) { + if (child != contentDiv && child.nodeType == 1) { + child.style.display = "none"; + } + child = child.nextSibling; + } + var item = tabList.firstChild; + while (item) { + if (item.nodeType == 1) { + item.className = item != tabItem ? "" : "active"; + } + item = item.nextSibling; + } + contentDiv.style.display = "block"; + return false; + } + contentDivs.appendChild(contentDiv); + tabList.appendChild(tabItem); + } + + var divs = parentElement.getElementsByTagName("div"); + for (var i = 0; i < divs.length; i++) { + var div = divs[i]; + if (!/\btab\b/.test(div.className)) { + continue; + } + makeTab(div); + } + + parentElement.appendChild(tabList); + parentElement.appendChild(contentDivs); +}
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/main.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +import inspect +import os +import textwrap + +from trac.core import * +from trac.db import DatabaseManager +from trac.env import IEnvironmentSetupParticipant +from trac.perm import IPermissionRequestor +from trac.wiki import IWikiSyntaxProvider +from bitten.api import IBuildListener +from bitten.model import schema, schema_version, Build, BuildConfig + +__all__ = ['BuildSystem'] +__docformat__ = 'restructuredtext en' + + +class BuildSystem(Component): + + implements(IEnvironmentSetupParticipant, IPermissionRequestor, + IWikiSyntaxProvider) + + listeners = ExtensionPoint(IBuildListener) + + # IEnvironmentSetupParticipant methods + + def environment_created(self): + # Create the required tables + db = self.env.get_db_cnx() + connector, _ = DatabaseManager(self.env)._get_connector() + cursor = db.cursor() + for table in schema: + for stmt in connector.to_sql(table): + cursor.execute(stmt) + + # Insert a global version flag + cursor.execute("INSERT INTO system (name,value) " + "VALUES ('bitten_version',%s)", (schema_version,)) + + # Create the directory for storing snapshot archives + snapshots_dir = os.path.join(self.env.path, 'snapshots') + os.mkdir(snapshots_dir) + + db.commit() + + def environment_needs_upgrade(self, db): + cursor = db.cursor() + cursor.execute("SELECT value FROM system WHERE name='bitten_version'") + row = cursor.fetchone() + if not row or int(row[0]) < schema_version: + return True + + def upgrade_environment(self, db): + cursor = db.cursor() + cursor.execute("SELECT value FROM system WHERE name='bitten_version'") + row = cursor.fetchone() + if not row: + self.environment_created() + else: + current_version = int(row[0]) + from bitten import upgrades + for version in range(current_version + 1, schema_version + 1): + for function in upgrades.map.get(version): + print textwrap.fill(inspect.getdoc(function)) + function(self.env, db) + print 'Done.' + cursor.execute("UPDATE system SET value=%s WHERE " + "name='bitten_version'", (schema_version,)) + self.log.info('Upgraded Bitten tables from version %d to %d', + current_version, schema_version) + + # IPermissionRequestor methods + + def get_permission_actions(self): + actions = ['BUILD_VIEW', 'BUILD_CREATE', 'BUILD_MODIFY', 'BUILD_DELETE', + 'BUILD_EXEC'] + return actions + [('BUILD_ADMIN', actions)] + + # IWikiSyntaxProvider methods + + def get_wiki_syntax(self): + return [] + + def get_link_resolvers(self): + def _format_link(formatter, ns, name, label): + build = Build.fetch(self.env, int(name)) + if build: + config = BuildConfig.fetch(self.env, build.config) + title = 'Build %d ([%s] of %s) by %s' % (build.id, build.rev, + config.label, build.slave) + return '<a class="build" href="%s" title="%s">%s</a>' \ + % (formatter.href.build(build.config, build.id), title, + label) + return label + yield 'build', _format_link
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/master.py @@ -0,0 +1,314 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +"""Build master implementation.""" + +import calendar +import re +import time + +from trac.config import BoolOption, IntOption +from trac.core import * +from trac.web import IRequestHandler, HTTPBadRequest, HTTPConflict, \ + HTTPForbidden, HTTPMethodNotAllowed, HTTPNotFound, \ + RequestDone + +from bitten.model import BuildConfig, Build, BuildStep, BuildLog, Report +from bitten.main import BuildSystem +from bitten.queue import BuildQueue +from bitten.recipe import Recipe +from bitten.util import xmlio + +__all__ = ['BuildMaster'] +__docformat__ = 'restructuredtext en' + + +class BuildMaster(Component): + """BEEP listener implementation for the build master.""" + + implements(IRequestHandler) + + # Configuration options + + adjust_timestamps = BoolOption('bitten', 'adjust_timestamps', False, doc= + """Whether the timestamps of builds should be adjusted to be close + to the timestamps of the corresponding changesets.""") + + build_all = BoolOption('bitten', 'build_all', False, doc= + """Whether to request builds of older revisions even if a younger + revision has already been built.""") + + stabilize_wait = IntOption('bitten', 'stabilize_wait', 0, doc= + """The time in seconds to wait for the repository to stabilize before + queuing up a new build. This allows time for developers to check in + a group of related changes back to back without spawning multiple + builds.""") + + slave_timeout = IntOption('bitten', 'slave_timeout', 3600, doc= + """The time in seconds after which a build is cancelled if the slave + does not report progress.""") + + # IRequestHandler methods + + def match_request(self, req): + match = re.match(r'/builds(?:/(\d+)(?:/(\w+)/([^/]+)?)?)?$', + req.path_info) + if match: + if match.group(1): + req.args['id'] = match.group(1) + req.args['collection'] = match.group(2) + req.args['member'] = match.group(3) + return True + + def process_request(self, req): + req.perm.assert_permission('BUILD_EXEC') + + if 'id' not in req.args: + if req.method != 'POST': + raise HTTPMethodNotAllowed('Method not allowed') + return self._process_build_creation(req) + + build = Build.fetch(self.env, req.args['id']) + if not build: + raise HTTPNotFound('No such build') + config = BuildConfig.fetch(self.env, build.config) + + if not req.args['collection']: + if req.method == 'DELETE': + return self._process_build_cancellation(req, config, build) + else: + return self._process_build_initiation(req, config, build) + + if req.method != 'POST': + raise HTTPMethodNotAllowed('Method not allowed') + + if req.args['collection'] == 'steps': + return self._process_build_step(req, config, build) + else: + raise HTTPNotFound('No such collection') + + def _process_build_creation(self, req): + queue = BuildQueue(self.env, build_all=self.build_all, + stabilize_wait=self.stabilize_wait, + timeout=self.slave_timeout) + queue.populate() + + try: + elem = xmlio.parse(req.read()) + except xmlio.ParseError, e: + self.log.error('Error parsing build initialization request: %s', e, + exc_info=True) + raise HTTPBadRequest('XML parser error') + + slavename = elem.attr['name'] + properties = {'name': slavename, Build.IP_ADDRESS: req.remote_addr} + self.log.info('Build slave %r connected from %s', slavename, + req.remote_addr) + + for child in elem.children(): + if child.name == 'platform': + properties[Build.MACHINE] = child.gettext() + properties[Build.PROCESSOR] = child.attr.get('processor') + elif child.name == 'os': + properties[Build.OS_NAME] = child.gettext() + properties[Build.OS_FAMILY] = child.attr.get('family') + properties[Build.OS_VERSION] = child.attr.get('version') + elif child.name == 'package': + for name, value in child.attr.items(): + if name == 'name': + continue + properties[child.attr['name'] + '.' + name] = value + + self.log.debug('Build slave configuration: %r', properties) + + build = queue.get_build_for_slave(slavename, properties) + if not build: + req.send_response(204) + req.write('') + raise RequestDone + + req.send_response(201) + req.send_header('Content-Type', 'text/plain') + req.send_header('Location', req.abs_href.builds(build.id)) + req.write('Build pending') + raise RequestDone + + def _process_build_cancellation(self, req, config, build): + self.log.info('Build slave %r cancelled build %d', build.slave, + build.id) + build.status = Build.PENDING + build.slave = None + build.slave_info = {} + build.started = 0 + db = self.env.get_db_cnx() + for step in list(BuildStep.select(self.env, build=build.id, db=db)): + step.delete(db=db) + build.update(db=db) + db.commit() + + for listener in BuildSystem(self.env).listeners: + listener.build_aborted(build) + + req.send_response(204) + req.write('') + raise RequestDone + + def _process_build_initiation(self, req, config, build): + self.log.info('Build slave %r initiated build %d', build.slave, + build.id) + build.started = int(time.time()) + build.update() + + for listener in BuildSystem(self.env).listeners: + listener.build_started(build) + + xml = xmlio.parse(config.recipe) + xml.attr['path'] = config.path + xml.attr['revision'] = build.rev + xml.attr['config'] = config.name + xml.attr['build'] = str(build.id) + body = str(xml) + + self.log.info('Build slave %r initiated build %d', build.slave, + build.id) + + req.send_response(200) + req.send_header('Content-Type', 'application/x-bitten+xml') + req.send_header('Content-Length', str(len(body))) + req.send_header('Content-Disposition', + 'attachment; filename=recipe_%s_r%s.xml' % + (config.name, build.rev)) + req.write(body) + raise RequestDone + + def _process_build_step(self, req, config, build): + try: + elem = xmlio.parse(req.read()) + except xmlio.ParseError, e: + self.log.error('Error parsing build step result: %s', e, + exc_info=True) + raise HTTPBadRequest('XML parser error') + stepname = elem.attr['step'] + + # make sure it's the right slave. + if build.status != Build.IN_PROGRESS or \ + build.slave_info.get(Build.IP_ADDRESS) != req.remote_addr: + raise HTTPForbidden('Build %s has been invalidated for host %s.' + % (build.id, req.remote_addr)) + + step = BuildStep.fetch(self.env, build=build.id, name=stepname) + if step: + raise HTTPConflict('Build step already exists') + + recipe = Recipe(xmlio.parse(config.recipe)) + index = None + current_step = None + for num, recipe_step in enumerate(recipe): + if recipe_step.id == stepname: + index = num + current_step = recipe_step + if index is None: + raise HTTPForbidden('No such build step') + last_step = index == num + + self.log.debug('Slave %s (build %d) completed step %d (%s) with ' + 'status %s', build.slave, build.id, index, stepname, + elem.attr['status']) + + db = self.env.get_db_cnx() + + step = BuildStep(self.env, build=build.id, name=stepname) + try: + step.started = int(_parse_iso_datetime(elem.attr['time'])) + step.stopped = step.started + float(elem.attr['duration']) + except ValueError, e: + self.log.error('Error parsing build step timestamp: %s', e, + exc_info=True) + raise HTTPBadRequest(e.args[0]) + if elem.attr['status'] == 'failure': + self.log.warning('Build %s step %s failed', build.id, stepname) + step.status = BuildStep.FAILURE + if current_step.onerror == 'fail': + last_step = True + else: + step.status = BuildStep.SUCCESS + step.errors += [error.gettext() for error in elem.children('error')] + step.insert(db=db) + + # Collect log messages from the request body + for idx, log_elem in enumerate(elem.children('log')): + build_log = BuildLog(self.env, build=build.id, step=stepname, + generator=log_elem.attr.get('generator'), + orderno=idx) + for message_elem in log_elem.children('message'): + build_log.messages.append((message_elem.attr['level'], + message_elem.gettext())) + build_log.insert(db=db) + + # Collect report data from the request body + for report_elem in elem.children('report'): + report = Report(self.env, build=build.id, step=stepname, + category=report_elem.attr.get('category'), + generator=report_elem.attr.get('generator')) + for item_elem in report_elem.children(): + item = {'type': item_elem.name} + item.update(item_elem.attr) + for child_elem in item_elem.children(): + item[child_elem.name] = child_elem.gettext() + report.items.append(item) + report.insert(db=db) + + # If this was the last step in the recipe we mark the build as + # completed + if last_step: + self.log.info('Slave %s completed build %d ("%s" as of [%s])', + build.slave, build.id, build.config, build.rev) + build.stopped = step.stopped + + # Determine overall outcome of the build by checking the outcome + # of the individual steps against the "onerror" specification of + # each step in the recipe + for num, recipe_step in enumerate(recipe): + step = BuildStep.fetch(self.env, build.id, recipe_step.id) + if step.status == BuildStep.FAILURE: + if recipe_step.onerror != 'ignore': + build.status = Build.FAILURE + break + else: + build.status = Build.SUCCESS + + build.update(db=db) + + db.commit() + + if last_step: + for listener in BuildSystem(self.env).listeners: + listener.build_completed(build) + + body = 'Build step processed' + req.send_response(201) + req.send_header('Content-Type', 'text/plain') + req.send_header('Content-Length', str(len(body))) + req.send_header('Location', req.abs_href.builds(build.id, 'steps', + stepname)) + req.write(body) + raise RequestDone + + +def _parse_iso_datetime(string): + """Minimal parser for ISO date-time strings. + + Return the time as floating point number. Only handles UTC timestamps + without time zone information.""" + try: + string = string.split('.', 1)[0] # strip out microseconds + return calendar.timegm(time.strptime(string, '%Y-%m-%dT%H:%M:%S')) + except ValueError, e: + raise ValueError('Invalid ISO date/time %r' % string)
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/model.py @@ -0,0 +1,940 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +"""Model classes for objects persisted in the database.""" + +from trac.db import Table, Column, Index + +__docformat__ = 'restructuredtext en' + + +class BuildConfig(object): + """Representation of a build configuration.""" + + _schema = [ + Table('bitten_config', key='name')[ + Column('name'), Column('path'), Column('active', type='int'), + Column('recipe'), Column('min_rev'), Column('max_rev'), + Column('label'), Column('description') + ] + ] + + def __init__(self, env, name=None, path=None, active=False, recipe=None, + min_rev=None, max_rev=None, label=None, description=None): + """Initialize a new build configuration with the specified attributes. + + To actually create this configuration in the database, the `insert` + method needs to be called. + """ + self.env = env + self._old_name = None + self.name = name + self.path = path or '' + self.active = bool(active) + self.recipe = recipe or '' + self.min_rev = min_rev or None + self.max_rev = max_rev or None + self.label = label or '' + self.description = description or '' + + def __repr__(self): + return '<%s %r>' % (type(self).__name__, self.name) + + exists = property(fget=lambda self: self._old_name is not None, + doc='Whether this configuration exists in the database') + + def delete(self, db=None): + """Remove a build configuration and all dependent objects from the + database.""" + assert self.exists, 'Cannot delete non-existing configuration' + if not db: + db = self.env.get_db_cnx() + handle_ta = True + else: + handle_ta = False + + for platform in list(TargetPlatform.select(self.env, self.name, db=db)): + platform.delete(db=db) + + for build in list(Build.select(self.env, config=self.name, db=db)): + build.delete(db=db) + + cursor = db.cursor() + cursor.execute("DELETE FROM bitten_config WHERE name=%s", (self.name,)) + + if handle_ta: + db.commit() + self._old_name = None + + def insert(self, db=None): + """Insert a new configuration into the database.""" + assert not self.exists, 'Cannot insert existing configuration' + assert self.name, 'Configuration requires a name' + if not db: + db = self.env.get_db_cnx() + handle_ta = True + else: + handle_ta = False + + cursor = db.cursor() + cursor.execute("INSERT INTO bitten_config (name,path,active," + "recipe,min_rev,max_rev,label,description) " + "VALUES (%s,%s,%s,%s,%s,%s,%s,%s)", + (self.name, self.path, int(self.active or 0), + self.recipe or '', self.min_rev, self.max_rev, + self.label or '', self.description or '')) + + if handle_ta: + db.commit() + self._old_name = self.name + + def update(self, db=None): + """Save changes to an existing build configuration.""" + assert self.exists, 'Cannot update a non-existing configuration' + assert self.name, 'Configuration requires a name' + if not db: + db = self.env.get_db_cnx() + handle_ta = True + else: + handle_ta = False + + cursor = db.cursor() + cursor.execute("UPDATE bitten_config SET name=%s,path=%s,active=%s," + "recipe=%s,min_rev=%s,max_rev=%s,label=%s," + "description=%s WHERE name=%s", + (self.name, self.path, int(self.active or 0), + self.recipe, self.min_rev, self.max_rev, + self.label, self.description, self._old_name)) + if self.name != self._old_name: + cursor.execute("UPDATE bitten_platform SET config=%s " + "WHERE config=%s", (self.name, self._old_name)) + cursor.execute("UPDATE bitten_build SET config=%s " + "WHERE config=%s", (self.name, self._old_name)) + + if handle_ta: + db.commit() + self._old_name = self.name + + def fetch(cls, env, name, db=None): + """Retrieve an existing build configuration from the database by + name. + """ + if not db: + db = env.get_db_cnx() + + cursor = db.cursor() + cursor.execute("SELECT path,active,recipe,min_rev,max_rev,label," + "description FROM bitten_config WHERE name=%s", (name,)) + row = cursor.fetchone() + if not row: + return None + + config = BuildConfig(env) + config.name = config._old_name = name + config.path = row[0] or '' + config.active = bool(row[1]) + config.recipe = row[2] or '' + config.min_rev = row[3] or None + config.max_rev = row[4] or None + config.label = row[5] or '' + config.description = row[6] or '' + return config + + fetch = classmethod(fetch) + + def select(cls, env, include_inactive=False, db=None): + """Retrieve existing build configurations from the database that match + the specified criteria. + """ + if not db: + db = env.get_db_cnx() + + cursor = db.cursor() + if include_inactive: + cursor.execute("SELECT name,path,active,recipe,min_rev,max_rev," + "label,description FROM bitten_config ORDER BY name") + else: + cursor.execute("SELECT name,path,active,recipe,min_rev,max_rev," + "label,description FROM bitten_config " + "WHERE active=1 ORDER BY name") + for name, path, active, recipe, min_rev, max_rev, label, description \ + in cursor: + config = BuildConfig(env, name=name, path=path or '', + active=bool(active), recipe=recipe or '', + min_rev=min_rev or None, + max_rev=max_rev or None, label=label or '', + description=description or '') + config._old_name = name + yield config + + select = classmethod(select) + + +class TargetPlatform(object): + """Target platform for a build configuration.""" + + _schema = [ + Table('bitten_platform', key='id')[ + Column('id', auto_increment=True), Column('config'), Column('name') + ], + Table('bitten_rule', key=('id', 'propname'))[ + Column('id'), Column('propname'), Column('pattern'), + Column('orderno', type='int') + ] + ] + + def __init__(self, env, config=None, name=None): + """Initialize a new target platform with the specified attributes. + + To actually create this platform in the database, the `insert` method + needs to be called. + """ + self.env = env + self.id = None + self.config = config + self.name = name + self.rules = [] + + def __repr__(self): + return '<%s %r>' % (type(self).__name__, self.id) + + exists = property(fget=lambda self: self.id is not None, + doc='Whether this target platform exists in the database') + + def delete(self, db=None): + """Remove the target platform from the database.""" + if not db: + db = self.env.get_db_cnx() + handle_ta = True + else: + handle_ta = False + + cursor = db.cursor() + cursor.execute("DELETE FROM bitten_rule WHERE id=%s", (self.id,)) + cursor.execute("DELETE FROM bitten_platform WHERE id=%s", (self.id,)) + if handle_ta: + db.commit() + + def insert(self, db=None): + """Insert a new target platform into the database.""" + if not db: + db = self.env.get_db_cnx() + handle_ta = True + else: + handle_ta = False + + assert not self.exists, 'Cannot insert existing target platform' + assert self.config, 'Target platform needs to be associated with a ' \ + 'configuration' + assert self.name, 'Target platform requires a name' + + cursor = db.cursor() + cursor.execute("INSERT INTO bitten_platform (config,name) " + "VALUES (%s,%s)", (self.config, self.name)) + self.id = db.get_last_id(cursor, 'bitten_platform') + if self.rules: + cursor.executemany("INSERT INTO bitten_rule VALUES (%s,%s,%s,%s)", + [(self.id, propname, pattern, idx) + for idx, (propname, pattern) + in enumerate(self.rules)]) + + if handle_ta: + db.commit() + + def update(self, db=None): + """Save changes to an existing target platform.""" + assert self.exists, 'Cannot update a non-existing platform' + assert self.config, 'Target platform needs to be associated with a ' \ + 'configuration' + assert self.name, 'Target platform requires a name' + if not db: + db = self.env.get_db_cnx() + handle_ta = True + else: + handle_ta = False + + cursor = db.cursor() + cursor.execute("UPDATE bitten_platform SET name=%s WHERE id=%s", + (self.name, self.id)) + cursor.execute("DELETE FROM bitten_rule WHERE id=%s", (self.id,)) + if self.rules: + cursor.executemany("INSERT INTO bitten_rule VALUES (%s,%s,%s,%s)", + [(self.id, propname, pattern, idx) + for idx, (propname, pattern) + in enumerate(self.rules)]) + + if handle_ta: + db.commit() + + def fetch(cls, env, id, db=None): + """Retrieve an existing target platform from the database by ID.""" + if not db: + db = env.get_db_cnx() + + cursor = db.cursor() + cursor.execute("SELECT config,name FROM bitten_platform " + "WHERE id=%s", (id,)) + row = cursor.fetchone() + if not row: + return None + + platform = TargetPlatform(env, config=row[0], name=row[1]) + platform.id = id + cursor.execute("SELECT propname,pattern FROM bitten_rule " + "WHERE id=%s ORDER BY orderno", (id,)) + for propname, pattern in cursor: + platform.rules.append((propname, pattern)) + return platform + + fetch = classmethod(fetch) + + def select(cls, env, config=None, db=None): + """Retrieve existing target platforms from the database that match the + specified criteria. + """ + if not db: + db = env.get_db_cnx() + + where_clauses = [] + if config is not None: + where_clauses.append(("config=%s", config)) + if where_clauses: + where = "WHERE " + " AND ".join([wc[0] for wc in where_clauses]) + else: + where = "" + + cursor = db.cursor() + cursor.execute("SELECT id FROM bitten_platform %s ORDER BY name" + % where, [wc[1] for wc in where_clauses]) + for (id,) in cursor: + yield TargetPlatform.fetch(env, id) + + select = classmethod(select) + + +class Build(object): + """Representation of a build.""" + + _schema = [ + Table('bitten_build', key='id')[ + Column('id', auto_increment=True), Column('config'), Column('rev'), + Column('rev_time', type='int'), Column('platform', type='int'), + Column('slave'), Column('started', type='int'), + Column('stopped', type='int'), Column('status', size=1), + Index(['config', 'rev', 'slave']) + ], + Table('bitten_slave', key=('build', 'propname'))[ + Column('build', type='int'), Column('propname'), Column('propvalue') + ] + ] + + # Build status codes + PENDING = 'P' + IN_PROGRESS = 'I' + SUCCESS = 'S' + FAILURE = 'F' + + # Standard slave properties + IP_ADDRESS = 'ipnr' + MAINTAINER = 'owner' + OS_NAME = 'os' + OS_FAMILY = 'family' + OS_VERSION = 'version' + MACHINE = 'machine' + PROCESSOR = 'processor' + + def __init__(self, env, config=None, rev=None, platform=None, slave=None, + started=0, stopped=0, rev_time=0, status=PENDING): + """Initialize a new build with the specified attributes. + + To actually create this build in the database, the `insert` method needs + to be called. + """ + self.env = env + self.id = None + self.config = config + self.rev = rev and str(rev) or None + self.platform = platform + self.slave = slave + self.started = started or 0 + self.stopped = stopped or 0 + self.rev_time = rev_time + self.status = status + self.slave_info = {} + + def __repr__(self): + return '<%s %r>' % (type(self).__name__, self.id) + + exists = property(fget=lambda self: self.id is not None, + doc='Whether this build exists in the database') + completed = property(fget=lambda self: self.status != Build.IN_PROGRESS, + doc='Whether the build has been completed') + successful = property(fget=lambda self: self.status == Build.SUCCESS, + doc='Whether the build was successful') + + def delete(self, db=None): + """Remove the build from the database.""" + assert self.exists, 'Cannot delete a non-existing build' + if not db: + db = self.env.get_db_cnx() + handle_ta = True + else: + handle_ta = False + + for step in list(BuildStep.select(self.env, build=self.id)): + step.delete(db=db) + + cursor = db.cursor() + cursor.execute("DELETE FROM bitten_slave WHERE build=%s", (self.id,)) + cursor.execute("DELETE FROM bitten_build WHERE id=%s", (self.id,)) + + if handle_ta: + db.commit() + + def insert(self, db=None): + """Insert a new build into the database.""" + assert not self.exists, 'Cannot insert an existing build' + if not db: + db = self.env.get_db_cnx() + handle_ta = True + else: + handle_ta = False + + assert self.config and self.rev and self.rev_time and self.platform + assert self.status in (self.PENDING, self.IN_PROGRESS, self.SUCCESS, + self.FAILURE) + if not self.slave: + assert self.status == self.PENDING + + cursor = db.cursor() + cursor.execute("INSERT INTO bitten_build (config,rev,rev_time,platform," + "slave,started,stopped,status) " + "VALUES (%s,%s,%s,%s,%s,%s,%s,%s)", + (self.config, self.rev, int(self.rev_time), + self.platform, self.slave or '', self.started or 0, + self.stopped or 0, self.status)) + self.id = db.get_last_id(cursor, 'bitten_build') + if self.slave_info: + cursor.executemany("INSERT INTO bitten_slave VALUES (%s,%s,%s)", + [(self.id, name, value) for name, value + in self.slave_info.items()]) + + if handle_ta: + db.commit() + + def update(self, db=None): + """Save changes to an existing build.""" + assert self.exists, 'Cannot update a non-existing build' + if not db: + db = self.env.get_db_cnx() + handle_ta = True + else: + handle_ta = False + + assert self.config and self.rev + assert self.status in (self.PENDING, self.IN_PROGRESS, self.SUCCESS, + self.FAILURE) + if not self.slave: + assert self.status == self.PENDING + + cursor = db.cursor() + cursor.execute("UPDATE bitten_build SET slave=%s,started=%s," + "stopped=%s,status=%s WHERE id=%s", + (self.slave or '', self.started or 0, + self.stopped or 0, self.status, self.id)) + cursor.execute("DELETE FROM bitten_slave WHERE build=%s", (self.id,)) + if self.slave_info: + cursor.executemany("INSERT INTO bitten_slave VALUES (%s,%s,%s)", + [(self.id, name, value) for name, value + in self.slave_info.items()]) + if handle_ta: + db.commit() + + def fetch(cls, env, id, db=None): + """Retrieve an existing build from the database by ID.""" + if not db: + db = env.get_db_cnx() + + cursor = db.cursor() + cursor.execute("SELECT config,rev,rev_time,platform,slave,started," + "stopped,status FROM bitten_build WHERE id=%s", (id,)) + row = cursor.fetchone() + if not row: + return None + + build = Build(env, config=row[0], rev=row[1], rev_time=int(row[2]), + platform=int(row[3]), slave=row[4], + started=row[5] and int(row[5]) or 0, + stopped=row[6] and int(row[6]) or 0, status=row[7]) + build.id = int(id) + cursor.execute("SELECT propname,propvalue FROM bitten_slave " + "WHERE build=%s", (id,)) + for propname, propvalue in cursor: + build.slave_info[propname] = propvalue + return build + + fetch = classmethod(fetch) + + def select(cls, env, config=None, rev=None, platform=None, slave=None, + status=None, db=None): + """Retrieve existing builds from the database that match the specified + criteria. + """ + if not db: + db = env.get_db_cnx() + + where_clauses = [] + if config is not None: + where_clauses.append(("config=%s", config)) + if rev is not None: + where_clauses.append(("rev=%s", rev)) + if platform is not None: + where_clauses.append(("platform=%s", platform)) + if slave is not None: + where_clauses.append(("slave=%s", slave)) + if status is not None: + where_clauses.append(("status=%s", status)) + if where_clauses: + where = "WHERE " + " AND ".join([wc[0] for wc in where_clauses]) + else: + where = "" + + cursor = db.cursor() + cursor.execute("SELECT id FROM bitten_build %s " + "ORDER BY config,rev_time DESC,slave" + % where, [wc[1] for wc in where_clauses]) + for (id,) in cursor: + yield Build.fetch(env, id) + select = classmethod(select) + + +class BuildStep(object): + """Represents an individual step of an executed build.""" + + _schema = [ + Table('bitten_step', key=('build', 'name'))[ + Column('build', type='int'), Column('name'), Column('description'), + Column('status', size=1), Column('started', type='int'), + Column('stopped', type='int') + ], + Table('bitten_error', key=('build', 'step', 'orderno'))[ + Column('build', type='int'), Column('step'), Column('message'), + Column('orderno', type='int') + ] + ] + + # Step status codes + SUCCESS = 'S' + FAILURE = 'F' + + def __init__(self, env, build=None, name=None, description=None, + status=None, started=None, stopped=None): + """Initialize a new build step with the specified attributes. + + To actually create this build step in the database, the `insert` method + needs to be called. + """ + self.env = env + self.build = build + self.name = name + self.description = description + self.status = status + self.started = started + self.stopped = stopped + self.errors = [] + self._exists = False + + exists = property(fget=lambda self: self._exists, + doc='Whether this build step exists in the database') + successful = property(fget=lambda self: self.status == BuildStep.SUCCESS, + doc='Whether the build step was successful') + + def delete(self, db=None): + """Remove the build step from the database.""" + if not db: + db = self.env.get_db_cnx() + handle_ta = True + else: + handle_ta = False + + for log in list(BuildLog.select(self.env, build=self.build, + step=self.name, db=db)): + log.delete(db=db) + for report in list(Report.select(self.env, build=self.build, + step=self.name, db=db)): + report.delete(db=db) + + cursor = db.cursor() + cursor.execute("DELETE FROM bitten_step WHERE build=%s AND name=%s", + (self.build, self.name)) + cursor.execute("DELETE FROM bitten_error WHERE build=%s AND step=%s", + (self.build, self.name)) + + if handle_ta: + db.commit() + self._exists = False + + def insert(self, db=None): + """Insert a new build step into the database.""" + if not db: + db = self.env.get_db_cnx() + handle_ta = True + else: + handle_ta = False + + assert self.build and self.name + assert self.status in (self.SUCCESS, self.FAILURE) + + cursor = db.cursor() + cursor.execute("INSERT INTO bitten_step (build,name,description,status," + "started,stopped) VALUES (%s,%s,%s,%s,%s,%s)", + (self.build, self.name, self.description or '', + self.status, self.started or 0, self.stopped or 0)) + if self.errors: + cursor.executemany("INSERT INTO bitten_error (build,step,message," + "orderno) VALUES (%s,%s,%s,%s)", + [(self.build, self.name, message, idx) + for idx, message in enumerate(self.errors)]) + + if handle_ta: + db.commit() + self._exists = True + + def fetch(cls, env, build, name, db=None): + """Retrieve an existing build from the database by build ID and step + name.""" + if not db: + db = env.get_db_cnx() + + cursor = db.cursor() + cursor.execute("SELECT description,status,started,stopped " + "FROM bitten_step WHERE build=%s AND name=%s", + (build, name)) + row = cursor.fetchone() + if not row: + return None + step = BuildStep(env, build, name, row[0] or '', row[1], + row[2] and int(row[2]), row[3] and int(row[3])) + step._exists = True + + cursor.execute("SELECT message FROM bitten_error WHERE build=%s " + "AND step=%s ORDER BY orderno", (build, name)) + for row in cursor: + step.errors.append(row[0] or '') + return step + + fetch = classmethod(fetch) + + def select(cls, env, build=None, name=None, status=None, db=None): + """Retrieve existing build steps from the database that match the + specified criteria. + """ + if not db: + db = env.get_db_cnx() + + assert status in (None, BuildStep.SUCCESS, BuildStep.FAILURE) + + where_clauses = [] + if build is not None: + where_clauses.append(("build=%s", build)) + if name is not None: + where_clauses.append(("name=%s", name)) + if status is not None: + where_clauses.append(("status=%s", status)) + if where_clauses: + where = "WHERE " + " AND ".join([wc[0] for wc in where_clauses]) + else: + where = "" + + cursor = db.cursor() + cursor.execute("SELECT build,name FROM bitten_step %s ORDER BY stopped" + % where, [wc[1] for wc in where_clauses]) + for build, name in cursor: + yield BuildStep.fetch(env, build, name, db=db) + + select = classmethod(select) + + +class BuildLog(object): + """Represents a build log.""" + + _schema = [ + Table('bitten_log', key='id')[ + Column('id', auto_increment=True), Column('build', type='int'), + Column('step'), Column('generator'), Column('orderno', type='int'), + Index(['build', 'step']) + ], + Table('bitten_log_message', key=('log', 'line'))[ + Column('log', type='int'), Column('line', type='int'), + Column('level', size=1), Column('message') + ] + ] + + # Message levels + DEBUG = 'D' + INFO = 'I' + WARNING = 'W' + ERROR = 'E' + + def __init__(self, env, build=None, step=None, generator=None, + orderno=None): + """Initialize a new build log with the specified attributes. + + To actually create this build log in the database, the `insert` method + needs to be called. + """ + self.env = env + self.id = None + self.build = build + self.step = step + self.generator = generator or '' + self.orderno = orderno and int(orderno) or 0 + self.messages = [] + + exists = property(fget=lambda self: self.id is not None, + doc='Whether this build log exists in the database') + + def delete(self, db=None): + """Remove the build log from the database.""" + assert self.exists, 'Cannot delete a non-existing build log' + if not db: + db = self.env.get_db_cnx() + handle_ta = True + else: + handle_ta = False + + cursor = db.cursor() + cursor.execute("DELETE FROM bitten_log_message WHERE log=%s", + (self.id,)) + cursor.execute("DELETE FROM bitten_log WHERE id=%s", (self.id,)) + + if handle_ta: + db.commit() + self.id = None + + def insert(self, db=None): + """Insert a new build log into the database.""" + if not db: + db = self.env.get_db_cnx() + handle_ta = True + else: + handle_ta = False + + assert self.build and self.step + + cursor = db.cursor() + cursor.execute("INSERT INTO bitten_log (build,step,generator,orderno) " + "VALUES (%s,%s,%s,%s)", (self.build, self.step, + self.generator, self.orderno)) + id = db.get_last_id(cursor, 'bitten_log') + if self.messages: + cursor.executemany("INSERT INTO bitten_log_message " + "(log,line,level,message) VALUES (%s,%s,%s,%s)", + [(id, idx, msg[0], msg[1]) for idx, msg in + enumerate(self.messages)]) + + if handle_ta: + db.commit() + self.id = id + + def fetch(cls, env, id, db=None): + """Retrieve an existing build from the database by ID.""" + if not db: + db = env.get_db_cnx() + + cursor = db.cursor() + cursor.execute("SELECT build,step,generator,orderno FROM bitten_log " + "WHERE id=%s", (id,)) + row = cursor.fetchone() + if not row: + return None + log = BuildLog(env, int(row[0]), row[1], row[2], row[3]) + log.id = id + cursor.execute("SELECT level,message FROM bitten_log_message " + "WHERE log=%s ORDER BY line", (id,)) + log.messages = cursor.fetchall() or [] + + return log + + fetch = classmethod(fetch) + + def select(cls, env, build=None, step=None, generator=None, db=None): + """Retrieve existing build logs from the database that match the + specified criteria. + """ + if not db: + db = env.get_db_cnx() + + where_clauses = [] + if build is not None: + where_clauses.append(("build=%s", build)) + if step is not None: + where_clauses.append(("step=%s", step)) + if generator is not None: + where_clauses.append(("generator=%s", generator)) + if where_clauses: + where = "WHERE " + " AND ".join([wc[0] for wc in where_clauses]) + else: + where = "" + + cursor = db.cursor() + cursor.execute("SELECT id FROM bitten_log %s ORDER BY orderno" + % where, [wc[1] for wc in where_clauses]) + for (id, ) in cursor: + yield BuildLog.fetch(env, id, db=db) + + select = classmethod(select) + + +class Report(object): + """Represents a generated report.""" + + _schema = [ + Table('bitten_report', key='id')[ + Column('id', auto_increment=True), Column('build', type='int'), + Column('step'), Column('category'), Column('generator'), + Index(['build', 'step', 'category']) + ], + Table('bitten_report_item', key=('report', 'item', 'name'))[ + Column('report', type='int'), Column('item', type='int'), + Column('name'), Column('value') + ] + ] + + def __init__(self, env, build=None, step=None, category=None, + generator=None): + """Initialize a new report with the specified attributes. + + To actually create this build log in the database, the `insert` method + needs to be called. + """ + self.env = env + self.id = None + self.build = build + self.step = step + self.category = category + self.generator = generator or '' + self.items = [] + + exists = property(fget=lambda self: self.id is not None, + doc='Whether this report exists in the database') + + def delete(self, db=None): + """Remove the report from the database.""" + assert self.exists, 'Cannot delete a non-existing report' + if not db: + db = self.env.get_db_cnx() + handle_ta = True + else: + handle_ta = False + + cursor = db.cursor() + cursor.execute("DELETE FROM bitten_report_item WHERE report=%s", + (self.id,)) + cursor.execute("DELETE FROM bitten_report WHERE id=%s", (self.id,)) + + if handle_ta: + db.commit() + self.id = None + + def insert(self, db=None): + """Insert a new build log into the database.""" + if not db: + db = self.env.get_db_cnx() + handle_ta = True + else: + handle_ta = False + + assert self.build and self.step and self.category + + # Enforce uniqueness of build-step-category. + # This should be done by the database, but the DB schema helpers in Trac + # currently don't support UNIQUE() constraints + assert not list(Report.select(self.env, build=self.build, + step=self.step, category=self.category, + db=db)), 'Report already exists' + + cursor = db.cursor() + cursor.execute("INSERT INTO bitten_report " + "(build,step,category,generator) VALUES (%s,%s,%s,%s)", + (self.build, self.step, self.category, self.generator)) + id = db.get_last_id(cursor, 'bitten_report') + for idx, item in enumerate([item for item in self.items if item]): + cursor.executemany("INSERT INTO bitten_report_item " + "(report,item,name,value) VALUES (%s,%s,%s,%s)", + [(id, idx, key, value) for key, value + in item.items()]) + + if handle_ta: + db.commit() + self.id = id + + def fetch(cls, env, id, db=None): + """Retrieve an existing build from the database by ID.""" + if not db: + db = env.get_db_cnx() + + cursor = db.cursor() + cursor.execute("SELECT build,step,category,generator " + "FROM bitten_report WHERE id=%s", (id,)) + row = cursor.fetchone() + if not row: + return None + report = Report(env, int(row[0]), row[1], row[2] or '', row[3] or '') + report.id = id + + cursor.execute("SELECT item,name,value FROM bitten_report_item " + "WHERE report=%s ORDER BY item", (id,)) + items = {} + for item, name, value in cursor: + items.setdefault(item, {})[name] = value + report.items = items.values() + + return report + + fetch = classmethod(fetch) + + def select(cls, env, config=None, build=None, step=None, category=None, + db=None): + """Retrieve existing reports from the database that match the specified + criteria. + """ + where_clauses = [] + joins = [] + if config is not None: + where_clauses.append(("config=%s", config)) + joins.append("INNER JOIN bitten_build ON (bitten_build.id=build)") + if build is not None: + where_clauses.append(("build=%s", build)) + if step is not None: + where_clauses.append(("step=%s", step)) + if category is not None: + where_clauses.append(("category=%s", category)) + + if where_clauses: + where = "WHERE " + " AND ".join([wc[0] for wc in where_clauses]) + else: + where = "" + + if not db: + db = env.get_db_cnx() + cursor = db.cursor() + cursor.execute("SELECT bitten_report.id FROM bitten_report %s %s " + "ORDER BY category" % (' '.join(joins), where), + [wc[1] for wc in where_clauses]) + for (id, ) in cursor: + yield Report.fetch(env, id, db=db) + + select = classmethod(select) + + +schema = BuildConfig._schema + TargetPlatform._schema + Build._schema + \ + BuildStep._schema + BuildLog._schema + Report._schema +schema_version = 7
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/queue.py @@ -0,0 +1,314 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +"""Implements the scheduling of builds for a project. + +This module provides the functionality for scheduling builds for a specific +Trac environment. It is used by both the build master and the web interface to +get the list of required builds (revisions not built yet). + +Furthermore, the `BuildQueue` class is used by the build master to determine +the next pending build, and to match build slaves against configured target +platforms. +""" + +from datetime import datetime +from itertools import ifilter +import logging +import re +import time + +from trac.versioncontrol import NoSuchNode +from bitten.model import BuildConfig, TargetPlatform, Build, BuildStep + +__docformat__ = 'restructuredtext en' + +log = logging.getLogger('bitten.queue') + + +def collect_changes(repos, config, db=None): + """Collect all changes for a build configuration that either have already + been built, or still need to be built. + + This function is a generator that yields ``(platform, rev, build)`` tuples, + where ``platform`` is a `TargetPlatform` object, ``rev`` is the identifier + of the changeset, and ``build`` is a `Build` object or `None`. + + :param repos: the version control repository + :param config: the build configuration + :param db: a database connection (optional) + """ + env = config.env + if not db: + db = env.get_db_cnx() + try: + node = repos.get_node(config.path) + except NoSuchNode, e: + env.log.warn('Node for configuration %r not found', config.name, + exc_info=True) + return + + for path, rev, chg in node.get_history(): + + # Don't follow moves/copies + if path != repos.normalize_path(config.path): + break + + # Stay within the limits of the build config + if config.min_rev and repos.rev_older_than(rev, config.min_rev): + break + if config.max_rev and repos.rev_older_than(config.max_rev, rev): + continue + + # Make sure the repository directory isn't empty at this + # revision + old_node = repos.get_node(path, rev) + is_empty = True + for entry in old_node.get_entries(): + is_empty = False + break + if is_empty: + continue + + # For every target platform, check whether there's a build + # of this revision + for platform in TargetPlatform.select(env, config.name, db=db): + builds = list(Build.select(env, config.name, rev, platform.id, + db=db)) + if builds: + build = builds[0] + else: + build = None + + yield platform, rev, build + + +class BuildQueue(object): + """Enapsulates the build queue of an environment. + + A build queue manages the the registration of build slaves and detection of + repository revisions that need to be built. + """ + + def __init__(self, env, build_all=False, stabilize_wait=0, timeout=0): + """Create the build queue. + + :param env: the Trac environment + :param build_all: whether older revisions should be built + :param stabilize_wait: The time in seconds to wait before considering + the repository stable to create a build in the queue. + :param timeout: the time in seconds after which an in-progress build + should be considered orphaned, and reset to pending + state + """ + self.env = env + self.log = env.log + self.build_all = build_all + self.stabilize_wait = stabilize_wait + self.timeout = timeout + + # Build scheduling + + def get_build_for_slave(self, name, properties): + """Check whether one of the pending builds can be built by the build + slave. + + :param name: the name of the slave + :type name: `basestring` + :param properties: the slave configuration + :type properties: `dict` + :return: the allocated build, or `None` if no build was found + :rtype: `Build` + """ + log.debug('Checking for pending builds...') + + db = self.env.get_db_cnx() + repos = self.env.get_repository() + + self.reset_orphaned_builds() + + # Iterate through pending builds by descending revision timestamp, to + # avoid the first configuration/platform getting all the builds + platforms = [p.id for p in self.match_slave(name, properties)] + build = None + builds_to_delete = [] + for build in Build.select(self.env, status=Build.PENDING, db=db): + if self.should_delete_build(build, repos): + self.log.info('Scheduling build %d for deletion', build.id) + builds_to_delete.append(build) + elif build.platform in platforms: + break + else: + self.log.debug('No pending builds.') + build = None + + # delete any obsolete builds + for build_to_delete in builds_to_delete: + build_to_delete.delete(db=db) + + if build: + build.slave = name + build.slave_info.update(properties) + build.status = Build.IN_PROGRESS + build.update(db=db) + + if build or builds_to_delete: + db.commit() + + return build + + def match_slave(self, name, properties): + """Match a build slave against available target platforms. + + :param name: the name of the slave + :type name: `basestring` + :param properties: the slave configuration + :type properties: `dict` + :return: the list of platforms the slave matched + """ + platforms = [] + + for config in BuildConfig.select(self.env): + for platform in TargetPlatform.select(self.env, config=config.name): + match = True + for propname, pattern in ifilter(None, platform.rules): + try: + propvalue = properties.get(propname) + if not propvalue or not re.match(pattern, propvalue): + match = False + break + except re.error: + self.log.error('Invalid platform matching pattern "%s"', + pattern, exc_info=True) + match = False + break + if match: + self.log.debug('Slave %r matched target platform %r of ' + 'build configuration %r', name, + platform.name, config.name) + platforms.append(platform) + + if not platforms: + self.log.warning('Slave %r matched none of the target platforms', + name) + + return platforms + + def populate(self): + """Add a build for the next change on each build configuration to the + queue. + + The next change is the latest repository check-in for which there isn't + a corresponding build on each target platform. Repeatedly calling this + method will eventually result in the entire change history of the build + configuration being in the build queue. + """ + repos = self.env.get_repository() + if hasattr(repos, 'sync'): + repos.sync() + + db = self.env.get_db_cnx() + builds = [] + + for config in BuildConfig.select(self.env, db=db): + platforms = [] + for platform, rev, build in collect_changes(repos, config, db): + + if not self.build_all and platform.id in platforms: + # We've seen this platform already, so these are older + # builds that should only be built if built_all=True + self.log.debug('Ignoring older revisions for configuration ' + '%r on %r', config.name, platform.name) + break + + platforms.append(platform.id) + + if build is None: + self.log.info('Enqueuing build of configuration "%s" at ' + 'revision [%s] on %s', config.name, rev, + platform.name) + + rev_time = repos.get_changeset(rev).date + if isinstance(rev_time, datetime): # Trac>=0.11 + from trac.util.datefmt import to_timestamp + rev_time = to_timestamp(rev_time) + age = int(time.time()) - rev_time + if self.stabilize_wait and age < self.stabilize_wait: + self.log.info('Delaying build of revision %s until %s ' + 'seconds pass. Current age is: %s ' + 'seconds' % (rev, self.stabilize_wait, + age)) + continue + + build = Build(self.env, config=config.name, + platform=platform.id, rev=str(rev), + rev_time=rev_time) + builds.append(build) + + for build in builds: + build.insert(db=db) + + db.commit() + + def reset_orphaned_builds(self): + """Reset all in-progress builds to ``PENDING`` state if they've been + running so long that the configured timeout has been reached. + + This is used to cleanup after slaves that have unexpectedly cancelled + a build without notifying the master, or are for some other reason not + reporting back status updates. + """ + if not self.timeout: + # If no timeout is set, none of the in-progress builds can be + # considered orphaned + return + + db = self.env.get_db_cnx() + now = int(time.time()) + for build in Build.select(self.env, status=Build.IN_PROGRESS, db=db): + if now - build.started < self.timeout: + # This build has not reached the timeout yet, assume it's still + # being executed + # FIXME: ideally, we'd base this check on the last activity on + # the build, not the start time + continue + build.status = Build.PENDING + build.slave = None + build.slave_info = {} + build.started = 0 + for step in list(BuildStep.select(self.env, build=build.id, db=db)): + step.delete(db=db) + build.update(db=db) + db.commit() + + def should_delete_build(self, build, repos): + # Ignore pending builds for deactived build configs + config = BuildConfig.fetch(self.env, build.config) + if not config.active: + log.info('Dropping build of configuration "%s" at ' + 'revision [%s] on "%s" because the configuration is ' + 'deactivated', config.name, build.rev, + TargetPlatform.fetch(self.env, build.platform).name) + return True + + # Stay within the revision limits of the build config + if (config.min_rev and repos.rev_older_than(build.rev, + config.min_rev)) \ + or (config.max_rev and repos.rev_older_than(config.max_rev, + build.rev)): + # This minimum and/or maximum revision has changed since + # this build was enqueued, so drop it + log.info('Dropping build of configuration "%s" at revision [%s] on ' + '"%s" because it is outside of the revision range of the ' + 'configuration', config.name, build.rev, + TargetPlatform.fetch(self.env, build.platform).name) + return True + + return False
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/recipe.py @@ -0,0 +1,278 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +"""Execution of build recipes. + +This module provides various classes that can be used to process build recipes, +most importantly the `Recipe` class. +""" + +import keyword +import logging +import os +try: + set +except NameError: + from sets import Set as set + +from pkg_resources import WorkingSet +from bitten.build import BuildError +from bitten.build.config import Configuration +from bitten.util import xmlio + +__all__ = ['Context', 'Recipe', 'Step', 'InvalidRecipeError'] +__docformat__ = 'restructuredtext en' + +log = logging.getLogger('bitten.recipe') + + +class InvalidRecipeError(Exception): + """Exception raised when a recipe is not valid.""" + + +class Context(object): + """The context in which a build is executed.""" + + step = None # The current step + generator = None # The current generator (namespace#name) + + def __init__(self, basedir, config=None, vars=None): + """Initialize the context. + + :param basedir: a string containing the working directory for the build. + (may be a pattern for replacement ex: 'build_${build}' + :param config: the build slave configuration + :type config: `Configuration` + """ + self.config = config or Configuration() + self.vars = vars or {} + self.output = [] + self.basedir = os.path.realpath(self.config.interpolate(basedir, + **self.vars)) + + def run(self, step, namespace, name, attr): + """Run the specified recipe command. + + :param step: the build step that the command belongs to + :param namespace: the namespace URI of the command + :param name: the local tag name of the command + :param attr: a dictionary containing the attributes defined on the + command element + """ + self.step = step + + try: + function = None + qname = '#'.join(filter(None, [namespace, name])) + if namespace: + group = 'bitten.recipe_commands' + for entry_point in WorkingSet().iter_entry_points(group, qname): + function = entry_point.load() + break + elif name == 'report': + function = Context.report_file + if not function: + raise InvalidRecipeError('Unknown recipe command %s' % qname) + + def escape(name): + name = name.replace('-', '_') + if keyword.iskeyword(name) or name in __builtins__: + name = name + '_' + return name + args = dict([(escape(name), + self.config.interpolate(attr[name], **self.vars)) + for name in attr]) + + self.generator = qname + log.debug('Executing %s with arguments: %s', function, args) + function(self, **args) + + finally: + self.generator = None + self.step = None + + def error(self, message): + """Record an error message. + + :param message: a string containing the error message. + """ + self.output.append((Recipe.ERROR, None, self.generator, message)) + + def log(self, xml): + """Record log output. + + :param xml: an XML fragment containing the log messages + """ + self.output.append((Recipe.LOG, None, self.generator, xml)) + + def report(self, category, xml): + """Record report data. + + :param category: the name of category of the report + :param xml: an XML fragment containing the report data + """ + self.output.append((Recipe.REPORT, category, self.generator, xml)) + + def report_file(self, category=None, file_=None): + """Read report data from a file and record it. + + :param category: the name of the category of the report + :param file\_: the path to the file containing the report data, relative + to the base directory + """ + filename = self.resolve(file_) + try: + fileobj = file(filename, 'r') + try: + xml_elem = xmlio.Fragment() + for child in xmlio.parse(fileobj).children(): + child_elem = xmlio.Element(child.name, **dict([ + (name, value) for name, value in child.attr.items() + if value is not None + ])) + xml_elem.append(child_elem[ + [xmlio.Element(grandchild.name)[grandchild.gettext()] + for grandchild in child.children()] + ]) + self.output.append((Recipe.REPORT, category, None, xml_elem)) + finally: + fileobj.close() + except xmlio.ParseError, e: + self.error('Failed to parse %s report at %s: %s' + % (category, filename, e)) + except IOError, e: + self.error('Failed to read %s report at %s: %s' + % (category, filename, e)) + + def resolve(self, *path): + """Return the path of a file relative to the base directory. + + Accepts any number of positional arguments, which are joined using the + system path separator to form the path. + """ + return os.path.normpath(os.path.join(self.basedir, *path)) + + +class Step(object): + """Represents a single step of a build recipe. + + Iterate over an object of this class to get the commands to execute, and + their keyword arguments. + """ + + def __init__(self, elem): + """Create the step. + + :param elem: the XML element representing the step + :type elem: `ParsedElement` + """ + self._elem = elem + self.id = elem.attr['id'] + self.description = elem.attr.get('description') + self.onerror = elem.attr.get('onerror', 'fail') + + def __repr__(self): + return '<%s %r>' % (type(self).__name__, self.id) + + def execute(self, ctxt): + """Execute this step in the given context. + + :param ctxt: the build context + :type ctxt: `Context` + """ + for child in self._elem: + ctxt.run(self, child.namespace, child.name, child.attr) + + errors = [] + while ctxt.output: + type, category, generator, output = ctxt.output.pop(0) + yield type, category, generator, output + if type == Recipe.ERROR: + errors.append((generator, output)) + if errors: + if self.onerror != 'ignore': + raise BuildError('Build step %s failed' % self.id) + log.warning('Continuing despite errors in step %s (%s)', self.id, + ', '.join([error[1] for error in errors])) + + +class Recipe(object): + """A build recipe. + + Iterate over this object to get the individual build steps in the order + they have been defined in the recipe file. + """ + + ERROR = 'error' + LOG = 'log' + REPORT = 'report' + + def __init__(self, xml, basedir=os.getcwd(), config=None): + """Create the recipe. + + :param xml: the XML document representing the recipe + :type xml: `ParsedElement` + :param basedir: the base directory for the build + :param config: the slave configuration (optional) + :type config: `Configuration` + """ + assert isinstance(xml, xmlio.ParsedElement) + vars = dict([(name, value) for name, value in xml.attr.items() + if not name.startswith('xmlns')]) + self.ctxt = Context(basedir, config, vars) + self._root = xml + + def __iter__(self): + """Iterate over the individual steps of the recipe.""" + for child in self._root.children('step'): + yield Step(child) + + def validate(self): + """Validate the recipe. + + This method checks a number of constraints: + - the name of the root element must be "build" + - the only permitted child elements or the root element with the name + "step" + - the recipe must contain at least one step + - step elements must have a unique "id" attribute + - a step must contain at least one nested command + - commands must not have nested content + + :raise InvalidRecipeError: in case any of the above contraints is + violated + """ + if self._root.name != 'build': + raise InvalidRecipeError('Root element must be <build>') + steps = list(self._root.children()) + if not steps: + raise InvalidRecipeError('Recipe defines no build steps') + + step_ids = set() + for step in steps: + if step.name != 'step': + raise InvalidRecipeError('Only <step> elements allowed at ' + 'top level of recipe') + if not step.attr.get('id'): + raise InvalidRecipeError('Steps must have an "id" attribute') + + if step.attr['id'] in step_ids: + raise InvalidRecipeError('Duplicate step ID "%s"' % + step.attr['id']) + step_ids.add(step.attr['id']) + + cmds = list(step.children()) + if not cmds: + raise InvalidRecipeError('Step "%s" has no recipe commands' % + step.attr['id']) + for cmd in cmds: + if len(list(cmd.children())): + raise InvalidRecipeError('Recipe command <%s> has nested ' + 'content' % cmd.name)
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/report/__init__.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +__docformat__ = 'restructuredtext en'
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/report/coverage.py @@ -0,0 +1,213 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +from trac.core import * +from trac.mimeview.api import IHTMLPreviewAnnotator +from trac.web.chrome import Chrome, add_stylesheet +from trac.web.clearsilver import HDFWrapper +from bitten.api import IReportChartGenerator, IReportSummarizer +from bitten.model import BuildConfig, Build, Report + +__docformat__ = 'restructuredtext en' + + +class TestCoverageChartGenerator(Component): + implements(IReportChartGenerator) + + # IReportChartGenerator methods + + def get_supported_categories(self): + return ['coverage'] + + def generate_chart_data(self, req, config, category): + assert category == 'coverage' + + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute(""" +SELECT build.rev, SUM(%s) AS loc, SUM(%s * %s / 100) AS cov +FROM bitten_build AS build + LEFT OUTER JOIN bitten_report AS report ON (report.build=build.id) + LEFT OUTER JOIN bitten_report_item AS item_lines + ON (item_lines.report=report.id AND item_lines.name='lines') + LEFT OUTER JOIN bitten_report_item AS item_percentage + ON (item_percentage.report=report.id AND item_percentage.name='percentage' AND + item_percentage.item=item_lines.item) +WHERE build.config=%%s AND report.category='coverage' +GROUP BY build.rev_time, build.rev, build.platform +ORDER BY build.rev_time""" % (db.cast('item_lines.value', 'int'), + db.cast('item_lines.value', 'int'), + db.cast('item_percentage.value', 'int')), + (config.name,)) + + prev_rev = None + coverage = [] + for rev, loc, cov in cursor: + if rev != prev_rev: + coverage.append([rev, 0, 0]) + if loc > coverage[-1][1]: + coverage[-1][1] = int(loc) + if cov > coverage[-1][2]: + coverage[-1][2] = int(cov) + prev_rev = rev + + data = {'title': 'Test Coverage', + 'data': [ + [''] + ['[%s]' % item[0] for item in coverage], + ['Lines of code'] + [item[1] for item in coverage], + ['Coverage'] + [int(item[2]) for item in coverage] + ]} + + return 'bitten_chart_coverage.html', data + + +class TestCoverageSummarizer(Component): + implements(IReportSummarizer) + + # IReportSummarizer methods + + def get_supported_categories(self): + return ['coverage'] + + def render_summary(self, req, config, build, step, category): + assert category == 'coverage' + + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute(""" +SELECT item_name.value AS unit, item_file.value AS file, + max(item_lines.value) AS loc, max(item_percentage.value) AS cov +FROM bitten_report AS report + LEFT OUTER JOIN bitten_report_item AS item_name + ON (item_name.report=report.id AND item_name.name='name') + LEFT OUTER JOIN bitten_report_item AS item_file + ON (item_file.report=report.id AND item_file.item=item_name.item AND + item_file.name='file') + LEFT OUTER JOIN bitten_report_item AS item_lines + ON (item_lines.report=report.id AND item_lines.item=item_name.item AND + item_lines.name='lines') + LEFT OUTER JOIN bitten_report_item AS item_percentage + ON (item_percentage.report=report.id AND + item_percentage.item=item_name.item AND + item_percentage.name='percentage') +WHERE category='coverage' AND build=%s AND step=%s +GROUP BY file, item_name.value +ORDER BY item_name.value""", (build.id, step.name)) + + units = [] + total_loc, total_cov = 0, 0 + for unit, file, loc, cov in cursor: + try: + loc, cov = int(loc), float(cov) + except TypeError: + continue # no rows + if loc: + d = {'name': unit, 'loc': loc, 'cov': int(cov)} + if file: + d['href'] = req.href.browser(config.path, file, rev=build.rev, annotate='coverage') + units.append(d) + total_loc += loc + total_cov += loc * cov + + coverage = 0 + if total_loc != 0: + coverage = total_cov // total_loc + + return 'bitten_summary_coverage.html', { + 'units': units, + 'totals': {'loc': total_loc, 'cov': int(coverage)} + } + + +# Coverage annotation requires the new interface from 0.11 +if hasattr(IHTMLPreviewAnnotator, 'get_annotation_data'): + class TestCoverageAnnotator(Component): + """ + >>> from genshi.builder import tag + >>> from trac.test import Mock, MockPerm + >>> from trac.mimeview import Context + >>> from trac.web.href import Href + >>> from bitten.model import BuildConfig, Build, Report + >>> from bitten.report.tests.coverage import env_stub_with_tables + >>> env = env_stub_with_tables() + + >>> BuildConfig(env, name='trunk', path='trunk').insert() + >>> Build(env, rev=123, config='trunk', rev_time=12345, platform=1).insert() + >>> rpt = Report(env, build=1, step='test', category='coverage') + >>> rpt.items.append({'file': 'foo.py', 'line_hits': '5 - 0'}) + >>> rpt.insert() + + >>> ann = TestCoverageAnnotator(env) + >>> req = Mock(href=Href('/'), perm=MockPerm(), chrome={}) + + Version in the branch should not match: + >>> context = Context.from_request(req, 'source', 'branches/blah/foo.py', 123) + >>> ann.get_annotation_data(context) + [] + + Version in the trunk should match: + >>> context = Context.from_request(req, 'source', 'trunk/foo.py', 123) + >>> data = ann.get_annotation_data(context) + >>> print data + [u'5', u'-', u'0'] + + >>> def annotate_row(lineno, line): + ... row = tag.tr() + ... ann.annotate_row(context, row, lineno, line, data) + ... return row.generate().render('html') + + >>> annotate_row(1, 'x = 1') + '<tr><th class="covered">5</th></tr>' + >>> annotate_row(2, '') + '<tr><th></th></tr>' + >>> annotate_row(3, 'y = x') + '<tr><th class="uncovered">0</th></tr>' + """ + implements(IHTMLPreviewAnnotator) + + # IHTMLPreviewAnnotator methods + + def get_annotation_type(self): + return 'coverage', 'Cov', 'Code coverage' + + def get_annotation_data(self, context): + add_stylesheet(context.req, 'bitten/bitten_coverage.css') + + resource = context.resource + builds = Build.select(self.env, rev=resource.version) + reports = [] + for build in builds: + config = BuildConfig.fetch(self.env, build.config) + if not resource.id.startswith(config.path): + continue + reports = Report.select(self.env, build=build.id, + category='coverage') + path_in_config = resource.id[len(config.path):].lstrip('/') + for report in reports: + for item in report.items: + if item.get('file') == path_in_config: + # TODO should aggregate coverage across builds + return item.get('line_hits', '').split() + return [] + + def annotate_row(self, context, row, lineno, line, data): + self.log.debug('%s', data) + from genshi.builder import tag + lineno -= 1 # 0-based index for data + if lineno >= len(data): + row.append(tag.th()) + return + row_data = data[lineno] + if row_data == '-': + row.append(tag.th()) + elif row_data == '0': + row.append(tag.th(row_data, class_='uncovered')) + else: + row.append(tag.th(row_data, class_='covered'))
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/report/testing.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +from trac.core import * +from trac.web.chrome import Chrome +from trac.web.clearsilver import HDFWrapper +from bitten.api import IReportChartGenerator, IReportSummarizer + +__docformat__ = 'restructuredtext en' + + +class TestResultsChartGenerator(Component): + implements(IReportChartGenerator) + + # IReportChartGenerator methods + + def get_supported_categories(self): + return ['test'] + + def generate_chart_data(self, req, config, category): + assert category == 'test' + + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute(""" +SELECT build.rev, build.platform, item_status.value AS status, COUNT(*) AS num +FROM bitten_build AS build + LEFT OUTER JOIN bitten_report AS report ON (report.build=build.id) + LEFT OUTER JOIN bitten_report_item AS item_status + ON (item_status.report=report.id AND item_status.name='status') +WHERE build.config=%s AND report.category='test' +GROUP BY build.rev_time, build.rev, build.platform, item_status.value +ORDER BY build.rev_time, build.platform""", (config.name,)) + + prev_rev = None + prev_platform, platform_total = None, 0 + tests = [] + for rev, platform, status, num in cursor: + if rev != prev_rev: + tests.append([rev, 0, 0]) + prev_rev = rev + platform_total = 0 + if platform != prev_platform: + prev_platform = platform + platform_total = 0 + + platform_total += num + tests[-1][1] = max(platform_total, tests[-1][1]) + if status != 'success': + tests[-1][2] = max(num, tests[-1][2]) + + data = {'title': 'Unit Tests', + 'data': [ + [''] + ['[%s]' % item[0] for item in tests], + ['Total'] + [item[1] for item in tests], + ['Failures'] + [item[2] for item in tests] + ]} + + return 'bitten_chart_tests.html', data + + +class TestResultsSummarizer(Component): + implements(IReportSummarizer) + + # IReportSummarizer methods + + def get_supported_categories(self): + return ['test'] + + def render_summary(self, req, config, build, step, category): + assert category == 'test' + + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute(""" +SELECT item_fixture.value AS fixture, item_file.value AS file, + COUNT(item_success.value) AS num_success, + COUNT(item_failure.value) AS num_failure, + COUNT(item_error.value) AS num_error +FROM bitten_report AS report + LEFT OUTER JOIN bitten_report_item AS item_fixture + ON (item_fixture.report=report.id AND item_fixture.name='fixture') + LEFT OUTER JOIN bitten_report_item AS item_file + ON (item_file.report=report.id AND item_file.item=item_fixture.item AND + item_file.name='file') + LEFT OUTER JOIN bitten_report_item AS item_success + ON (item_success.report=report.id AND item_success.item=item_fixture.item AND + item_success.name='status' AND item_success.value='success') + LEFT OUTER JOIN bitten_report_item AS item_failure + ON (item_failure.report=report.id AND item_failure.item=item_fixture.item AND + item_failure.name='status' AND item_failure.value='failure') + LEFT OUTER JOIN bitten_report_item AS item_error + ON (item_error.report=report.id AND item_error.item=item_fixture.item AND + item_error.name='status' AND item_error.value='error') +WHERE category='test' AND build=%s AND step=%s +GROUP BY file, fixture +ORDER BY fixture""", (build.id, step.name)) + + fixtures = [] + total_success, total_failure, total_error = 0, 0, 0 + for fixture, file, num_success, num_failure, num_error in cursor: + fixtures.append({'name': fixture, 'num_success': num_success, + 'num_error': num_error, + 'num_failure': num_failure}) + total_success += num_success + total_failure += num_failure + total_error += num_error + if file: + fixtures[-1]['href'] = req.href.browser(config.path, file) + + data = {'fixtures': fixtures, + 'totals': {'success': total_success, 'failure': total_failure, + 'error': total_error} + } + return 'bitten_summary_tests.html', data
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/report/tests/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +import unittest + +from bitten.report.tests import coverage, testing + +def suite(): + suite = unittest.TestSuite() + suite.addTest(coverage.suite()) + suite.addTest(testing.suite()) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/report/tests/coverage.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +import doctest +import unittest + +from trac.db import DatabaseManager +from trac.test import EnvironmentStub, Mock +from bitten.model import * +from bitten.report import coverage +from bitten.report.coverage import TestCoverageChartGenerator + +def env_stub_with_tables(): + env = EnvironmentStub() + db = env.get_db_cnx() + cursor = db.cursor() + connector, _ = DatabaseManager(env)._get_connector() + for table in schema: + for stmt in connector.to_sql(table): + cursor.execute(stmt) + return env + +class TestCoverageChartGeneratorTestCase(unittest.TestCase): + + def setUp(self): + self.env = env_stub_with_tables() + self.env.path = '' + + def test_supported_categories(self): + generator = TestCoverageChartGenerator(self.env) + self.assertEqual(['coverage'], generator.get_supported_categories()) + + def test_no_reports(self): + req = Mock() + config = Mock(name='trunk') + generator = TestCoverageChartGenerator(self.env) + template, data = generator.generate_chart_data(req, config, 'coverage') + self.assertEqual('bitten_chart_coverage.html', template) + self.assertEqual('Test Coverage', data['title']) + self.assertEqual('', data['data'][0][0]) + self.assertEqual('Lines of code', data['data'][1][0]) + self.assertEqual('Coverage', data['data'][2][0]) + + def test_single_platform(self): + config = Mock(name='trunk') + build = Build(self.env, config='trunk', platform=1, rev=123, + rev_time=42) + build.insert() + report = Report(self.env, build=build.id, step='foo', + category='coverage') + report.items += [{'lines': '12', 'percentage': '25'}] + report.insert() + + req = Mock() + generator = TestCoverageChartGenerator(self.env) + template, data = generator.generate_chart_data(req, config, 'coverage') + self.assertEqual('bitten_chart_coverage.html', template) + self.assertEqual('Test Coverage', data['title']) + self.assertEqual('', data['data'][0][0]) + self.assertEqual('[123]', data['data'][0][1]) + self.assertEqual('Lines of code', data['data'][1][0]) + self.assertEqual(12, data['data'][1][1]) + self.assertEqual('Coverage', data['data'][2][0]) + self.assertEqual(3, data['data'][2][1]) + + def test_multi_platform(self): + config = Mock(name='trunk') + build = Build(self.env, config='trunk', platform=1, rev=123, + rev_time=42) + build.insert() + report = Report(self.env, build=build.id, step='foo', + category='coverage') + report.items += [{'lines': '12', 'percentage': '25'}] + report.insert() + build = Build(self.env, config='trunk', platform=2, rev=123, + rev_time=42) + build.insert() + report = Report(self.env, build=build.id, step='foo', + category='coverage') + report.items += [{'lines': '12', 'percentage': '50'}] + report.insert() + + req = Mock() + generator = TestCoverageChartGenerator(self.env) + template, data = generator.generate_chart_data(req, config, 'coverage') + self.assertEqual('bitten_chart_coverage.html', template) + self.assertEqual('Test Coverage', data['title']) + self.assertEqual('', data['data'][0][0]) + self.assertEqual('[123]', data['data'][0][1]) + self.assertEqual('Lines of code', data['data'][1][0]) + self.assertEqual(12, data['data'][1][1]) + self.assertEqual('Coverage', data['data'][2][0]) + self.assertEqual(6, data['data'][2][1]) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TestCoverageChartGeneratorTestCase)) + suite.addTest(doctest.DocTestSuite(coverage)) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/report/tests/testing.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +import unittest + +from trac.db import DatabaseManager +from trac.test import EnvironmentStub, Mock +from bitten.model import * +from bitten.report.testing import TestResultsChartGenerator + + +class TestResultsChartGeneratorTestCase(unittest.TestCase): + + def setUp(self): + self.env = EnvironmentStub() + self.env.path = '' + + db = self.env.get_db_cnx() + cursor = db.cursor() + connector, _ = DatabaseManager(self.env)._get_connector() + for table in schema: + for stmt in connector.to_sql(table): + cursor.execute(stmt) + + def test_supported_categories(self): + generator = TestResultsChartGenerator(self.env) + self.assertEqual(['test'], generator.get_supported_categories()) + + def test_no_reports(self): + req = Mock() + config = Mock(name='trunk') + generator = TestResultsChartGenerator(self.env) + template, data = generator.generate_chart_data(req, config, 'test') + self.assertEqual('bitten_chart_tests.html', template) + self.assertEqual('Unit Tests', data['title']) + self.assertEqual('', data['data'][0][0]) + self.assertEqual('Total', data['data'][1][0]) + self.assertEqual('Failures', data['data'][2][0]) + + def test_single_platform(self): + config = Mock(name='trunk') + build = Build(self.env, config='trunk', platform=1, rev=123, + rev_time=42) + build.insert() + report = Report(self.env, build=build.id, step='foo', category='test') + report.items += [{'status': 'success'}, {'status': 'failure'}, + {'status': 'success'}] + report.insert() + + req = Mock() + generator = TestResultsChartGenerator(self.env) + template, data = generator.generate_chart_data(req, config, 'test') + self.assertEqual('bitten_chart_tests.html', template) + self.assertEqual('Unit Tests', data['title']) + self.assertEqual('', data['data'][0][0]) + self.assertEqual('[123]', data['data'][0][1]) + self.assertEqual('Total', data['data'][1][0]) + self.assertEqual(3, data['data'][1][1]) + self.assertEqual('Failures', data['data'][2][0]) + self.assertEqual(1, data['data'][2][1]) + + def test_multi_platform(self): + config = Mock(name='trunk') + + build = Build(self.env, config='trunk', platform=1, rev=123, + rev_time=42) + build.insert() + report = Report(self.env, build=build.id, step='foo', category='test') + report.items += [{'status': 'success'}, {'status': 'failure'}, + {'status': 'success'}] + report.insert() + + build = Build(self.env, config='trunk', platform=2, rev=123, + rev_time=42) + build.insert() + report = Report(self.env, build=build.id, step='foo', category='test') + report.items += [{'status': 'success'}, {'status': 'failure'}, + {'status': 'failure'}] + report.insert() + + req = Mock() + generator = TestResultsChartGenerator(self.env) + template, data = generator.generate_chart_data(req, config, 'test') + self.assertEqual('bitten_chart_tests.html', template) + self.assertEqual('Unit Tests', data['title']) + self.assertEqual('', data['data'][0][0]) + self.assertEqual('[123]', data['data'][0][1]) + self.assertEqual('Total', data['data'][1][0]) + self.assertEqual(3, data['data'][1][1]) + self.assertEqual('Failures', data['data'][2][0]) + self.assertEqual(2, data['data'][2][1]) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TestResultsChartGeneratorTestCase)) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100755 --- /dev/null +++ b/trac-0.11/bitten/slave.py @@ -0,0 +1,410 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +"""Implementation of the build slave.""" + +from datetime import datetime +import errno +import urllib2 +import logging +import os +import platform +import shutil +import socket +import tempfile +import time + +from bitten.build import BuildError +from bitten.build.config import Configuration +from bitten.recipe import Recipe +from bitten.util import xmlio + +EX_OK = getattr(os, "EX_OK", 0) +EX_UNAVAILABLE = getattr(os, "EX_UNAVAILABLE", 69) +EX_PROTOCOL = getattr(os, "EX_PROTOCOL", 76) + +__all__ = ['BuildSlave', 'ExitSlave'] +__docformat__ = 'restructuredtext en' + +log = logging.getLogger('bitten.slave') + +# List of network errors which are usually temporary and non critical. +temp_net_errors = [errno.ENETUNREACH, errno.ENETDOWN, errno.ETIMEDOUT, + errno.ECONNREFUSED] + +def _rmtree(root): + """Catch shutil.rmtree failures on Windows when files are read-only.""" + def _handle_error(fn, path, excinfo): + os.chmod(path, 0666) + fn(path) + return shutil.rmtree(root, onerror=_handle_error) + +class SaneHTTPErrorProcessor(urllib2.HTTPErrorProcessor): + "The HTTPErrorProcessor defined in urllib needs some love." + + def http_response(self, request, response): + code, msg, hdrs = response.code, response.msg, response.info() + if code >= 300: + response = self.parent.error( + 'http', request, response, code, msg, hdrs) + return response + + +class SaneHTTPRequest(urllib2.Request): + + def __init__(self, method, url, data=None, headers={}): + urllib2.Request.__init__(self, url, data, headers) + self.method = method + + def get_method(self): + if self.method is None: + self.method = self.has_data() and 'POST' or 'GET' + return self.method + + +class BuildSlave(object): + """BEEP initiator implementation for the build slave.""" + + def __init__(self, urls, name=None, config=None, dry_run=False, + work_dir=None, build_dir="build_${build}", + keep_files=False, single_build=False, + poll_interval=300, username=None, password=None, + dump_reports=False): + """Create the build slave instance. + + :param urls: a list of URLs of the build masters to connect to, or a + single-element list containing the path to a build recipe + file + :param name: the name with which this slave should identify itself + :param config: the path to the slave configuration file + :param dry_run: wether the build outcome should not be reported back + to the master + :param work_dir: the working directory to use for build execution + :param build_dir: the pattern to use for naming the build subdir + :param keep_files: whether files and directories created for build + execution should be kept when done + :param single_build: whether this slave should exit after completing a + single build, or continue processing builds forever + :param poll_interval: the time in seconds to wait between requesting + builds from the build master (default is five + minutes) + :param username: the username to use when authentication against the + build master is requested + :param password: the password to use when authentication is needed + :param dump_reports: whether report data should be written to the + standard output, in addition to being transmitted + to the build master + """ + self.urls = urls + self.local = len(urls) == 1 and not urls[0].startswith('http://') \ + and not urls[0].startswith('https://') + if name is None: + name = platform.node().split('.', 1)[0].lower() + self.name = name + self.config = Configuration(config) + self.dry_run = dry_run + if not work_dir: + work_dir = tempfile.mkdtemp(prefix='bitten') + elif not os.path.exists(work_dir): + os.makedirs(work_dir) + self.work_dir = work_dir + self.build_dir = build_dir + self.keep_files = keep_files + self.single_build = single_build + self.poll_interval = poll_interval + self.dump_reports = dump_reports + + if not self.local: + self.opener = urllib2.build_opener(SaneHTTPErrorProcessor) + password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm() + if username and password: + log.debug('Enabling authentication with username %r', username) + password_mgr.add_password(None, urls, username, password) + self.opener.add_handler(urllib2.HTTPBasicAuthHandler(password_mgr)) + self.opener.add_handler(urllib2.HTTPDigestAuthHandler(password_mgr)) + + def request(self, method, url, body=None, headers=None): + log.debug('Sending %s request to %r', method, url) + req = SaneHTTPRequest(method, url, body, headers or {}) + try: + return self.opener.open(req) + except urllib2.HTTPError, e: + if e.code >= 300: + log.warning('Server returned error %d: %s', e.code, e.msg) + raise + return e + + def run(self): + if self.local: + fileobj = open(self.urls[0]) + try: + self._execute_build(None, fileobj) + finally: + fileobj.close() + return EX_OK + + urls = [] + while True: + if not urls: + urls[:] = self.urls + url = urls.pop(0) + try: + try: + job_done = self._create_build(url) + if job_done: + continue + except urllib2.HTTPError, e: + # HTTPError doesn't have the "reason" attribute of URLError + log.error(e) + raise ExitSlave(EX_UNAVAILABLE) + except urllib2.URLError, e: + # Is this a temporary network glitch or something a bit + # more severe? + if isinstance(e.reason, socket.error) and \ + e.reason.args[0] in temp_net_errors: + log.warning(e) + else: + log.error(e) + raise ExitSlave(EX_UNAVAILABLE) + except ExitSlave, e: + return e.exit_code + time.sleep(self.poll_interval) + + def quit(self): + log.info('Shutting down') + raise ExitSlave(EX_OK) + + def _create_build(self, url): + xml = xmlio.Element('slave', name=self.name)[ + xmlio.Element('platform', processor=self.config['processor'])[ + self.config['machine'] + ], + xmlio.Element('os', family=self.config['family'], + version=self.config['version'])[ + self.config['os'] + ], + ] + + log.debug('Configured packages: %s', self.config.packages) + for package, properties in self.config.packages.items(): + xml.append(xmlio.Element('package', name=package, **properties)) + + body = str(xml) + log.debug('Sending slave configuration: %s', body) + resp = self.request('POST', url, body, { + 'Content-Length': len(body), + 'Content-Type': 'application/x-bitten+xml' + }) + + if resp.code == 201: + self._initiate_build(resp.info().get('location')) + return True + elif resp.code == 204: + log.info('No pending builds') + return False + else: + log.error('Unexpected response (%d %s)', resp.code, resp.msg) + raise ExitSlave(EX_PROTOCOL) + + def _initiate_build(self, build_url): + log.info('Build pending at %s', build_url) + try: + resp = self.request('GET', build_url) + if resp.code == 200: + self._execute_build(build_url, resp) + else: + log.error('Unexpected response (%d): %s', resp.code, resp.msg) + self._cancel_build(build_url, exit_code=EX_PROTOCOL) + except KeyboardInterrupt: + log.warning('Build interrupted') + self._cancel_build(build_url) + + def _execute_build(self, build_url, fileobj): + build_id = build_url and int(build_url.split('/')[-1]) or 0 + xml = xmlio.parse(fileobj) + try: + recipe = Recipe(xml, os.path.join(self.work_dir, self.build_dir), + self.config) + basedir = recipe.ctxt.basedir + log.debug('Running build in directory %s' % basedir) + if not os.path.exists(basedir): + os.mkdir(basedir) + + for step in recipe: + log.info('Executing build step %r', step.id) + if not self._execute_step(build_url, recipe, step): + log.warning('Stopping build due to failure') + break + else: + log.info('Build completed') + if self.dry_run: + self._cancel_build(build_url) + finally: + if not self.keep_files: + log.debug('Removing build directory %s' % basedir) + _rmtree(basedir) + if self.single_build: + log.info('Exiting after single build completed.') + raise ExitSlave(EX_OK) + + def _execute_step(self, build_url, recipe, step): + failed = False + started = datetime.utcnow() + xml = xmlio.Element('result', step=step.id, time=started.isoformat()) + try: + for type, category, generator, output in \ + step.execute(recipe.ctxt): + if type == Recipe.ERROR: + failed = True + if type == Recipe.REPORT and self.dump_reports: + print output + xml.append(xmlio.Element(type, category=category, + generator=generator)[ + output + ]) + except KeyboardInterrupt: + log.warning('Build interrupted') + self._cancel_build(build_url) + except BuildError, e: + log.error('Build step %r failed (%s)', step.id, e) + failed = True + except Exception, e: + log.error('Internal error in build step %r', step.id, exc_info=True) + failed = True + xml.attr['duration'] = (datetime.utcnow() - started).seconds + if failed: + xml.attr['status'] = 'failure' + log.warning('Build step %r failed', step.id) + else: + xml.attr['status'] = 'success' + log.info('Build step %s completed successfully', step.id) + + if not self.local and not self.dry_run: + try: + resp = self.request('POST', build_url + '/steps/', str(xml), { + 'Content-Type': 'application/x-bitten+xml' + }) + if resp.code != 201: + log.error('Unexpected response (%d): %s', resp.code, + resp.msg) + except KeyboardInterrupt: + log.warning('Build interrupted') + self._cancel_build(build_url) + + return not failed or step.onerror != 'fail' + + def _cancel_build(self, build_url, exit_code=EX_OK): + log.info('Cancelling build at %s', build_url) + if not self.local: + resp = self.request('DELETE', build_url) + if resp.code not in (200, 204): + log.error('Unexpected response (%d): %s', resp.code, resp.msg) + raise ExitSlave(exit_code) + + +class ExitSlave(Exception): + """Exception used internally by the slave to signal that the slave process + should be stopped. + """ + def __init__(self, exit_code): + self.exit_code = exit_code + Exception.__init__(self) + + +def main(): + """Main entry point for running the build slave.""" + from bitten import __version__ as VERSION + from optparse import OptionParser + + parser = OptionParser(usage='usage: %prog [options] url1 [url2] ...', + version='%%prog %s' % VERSION) + parser.add_option('--name', action='store', dest='name', + help='name of this slave (defaults to host name)') + parser.add_option('-f', '--config', action='store', dest='config', + metavar='FILE', help='path to configuration file') + parser.add_option('-u', '--user', dest='username', + help='the username to use for authentication') + parser.add_option('-p', '--password', dest='password', + help='the password to use when authenticating') + + group = parser.add_option_group('building') + group.add_option('-d', '--work-dir', action='store', dest='work_dir', + metavar='DIR', help='working directory for builds') + group.add_option('--build-dir', action='store', dest='build_dir', + default = 'build_${config}_${build}', + help='name pattern for the build dir to use inside the ' + 'working dir ["%default"]') + group.add_option('-k', '--keep-files', action='store_true', + dest='keep_files', + help='don\'t delete files after builds') + group.add_option('-s', '--single', action='store_true', + dest='single_build', + help='exit after completing a single build') + group.add_option('-n', '--dry-run', action='store_true', dest='dry_run', + help='don\'t report results back to master') + group.add_option('-i', '--interval', dest='interval', metavar='SECONDS', + type='int', help='time to wait between requesting builds') + group = parser.add_option_group('logging') + group.add_option('-l', '--log', dest='logfile', metavar='FILENAME', + help='write log messages to FILENAME') + group.add_option('-v', '--verbose', action='store_const', dest='loglevel', + const=logging.DEBUG, help='print as much as possible') + group.add_option('-q', '--quiet', action='store_const', dest='loglevel', + const=logging.WARN, help='print as little as possible') + group.add_option('--dump-reports', action='store_true', dest='dump_reports', + help='whether report data should be printed') + + parser.set_defaults(dry_run=False, keep_files=False, + loglevel=logging.INFO, single_build=False, + dump_reports=False, interval=300) + options, args = parser.parse_args() + + if len(args) < 1: + parser.error('incorrect number of arguments') + urls = args + + logger = logging.getLogger('bitten') + logger.setLevel(options.loglevel) + handler = logging.StreamHandler() + handler.setLevel(options.loglevel) + formatter = logging.Formatter('[%(levelname)-8s] %(message)s') + handler.setFormatter(formatter) + logger.addHandler(handler) + if options.logfile: + handler = logging.FileHandler(options.logfile) + handler.setLevel(options.loglevel) + formatter = logging.Formatter('%(asctime)s [%(name)s] %(levelname)s: ' + '%(message)s') + handler.setFormatter(formatter) + logger.addHandler(handler) + + slave = BuildSlave(urls, name=options.name, config=options.config, + dry_run=options.dry_run, work_dir=options.work_dir, + build_dir=options.build_dir, + keep_files=options.keep_files, + single_build=options.single_build, + poll_interval=options.interval, + username=options.username, password=options.password, + dump_reports=options.dump_reports) + try: + try: + exit_code = slave.run() + except KeyboardInterrupt: + slave.quit() + except ExitSlave, e: + exit_code = e.exit_code + + if not options.work_dir: + log.debug('Removing temporary directory %s' % slave.work_dir) + _rmtree(slave.work_dir) + return exit_code + +if __name__ == '__main__': + sys.exit(main())
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/templates/bitten_admin_configs.html @@ -0,0 +1,240 @@ +<!DOCTYPE html + PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" + xmlns:xi="http://www.w3.org/2001/XInclude" + xmlns:py="http://genshi.edgewall.org/"> + <xi:include href="admin.html" /> + <head> + <title>Manage Build Configurations</title> + </head> + <body> + <h2>Manage Build Configurations</h2> + <py:choose><form py:when="config" class="mod" + id="modconfig" method="post" action=""> + <table class="form" summary=""><tr> + <td class="name"><label>Name:<br /> + <input type="text" name="name" + value="$config.name" /> + </label></td> + <td class="label"><label>Label (for display):<br /> + <input type="text" name="label" size="32" + value="$config.label" /> + </label></td> + </tr><tr> + <td colspan="2"><fieldset class="iefix"> + <label for="description"> + Description (you may use + <a tabindex="42" + href="${href.wiki('WikiFormatting')}"> + WikiFormatting</a> here): + </label> + <p> + <textarea id="description" name="description" + class="wikitext" rows="3" cols="65">$config.description</textarea> + </p> + <script type="text/javascript" src="${chrome.htdocs_location}js/wikitoolbar.js"></script> + </fieldset></td> + </tr><tr> + <td colspan="2"><fieldset class="iefix"> + <label for="recipe">Recipe:</label> + <p> + <textarea id="recipe" name="recipe" rows="8" cols="78">$config.recipe</textarea> + </p> + </fieldset></td> + </tr></table> + <fieldset id="repos"> + <legend>Repository Mapping</legend> + <table class="form" summary=""><tr> + <th><label for="path">Path:</label></th> + <td colspan="3"><input id="path" type="text" name="path" + size="48" value="$config.path" /></td> + </tr><tr> + <th><label for="min_rev"> + Oldest revision: + </label></th> + <td><input id="min_rev" type="text" name="min_rev" size="8" + value="$config.min_rev" /></td> + <th><label for="max_rev"> + Youngest revision: + </label></th> + <td><input id="max_rev" type="text" name="max_rev" size="8" + value="$config.max_rev" /></td> + </tr></table> + </fieldset> + <div class="buttons"> + <input type="submit" name="cancel" + value="Cancel" /> + <input type="submit" name="save" value="Save" /> + </div> + <div class="platforms"> + <h3>Target Platforms</h3> + <table class="listing" id="platformlist"> + <thead> + <tr><th class="sel"> </th> + <th>Name</th><th>Rules</th></tr> + </thead> + <tbody> + <tr py:if="not config.platforms"> + <td colspan="3"><em>(No Platforms)</em></td> + </tr> + <tr py:for="platform in config.platforms"> + <td class="sel"> + <input type="checkbox" name="sel" + value="$platform.id" /> + </td> + <td class="name"><a href="$platform.href"> + $platform.name + </a></td> + <td class="rules"> + <ul py:if="len(platform.rules)"> + <li py:for="rule in platform.rules"> + <code> + <strong>$rule.property</strong> ~= + $rule.pattern + </code> + </li> + </ul> + </td> + </tr> + </tbody> + </table> + <div class="buttons"> + <input type="submit" name="new" + value="Add platform" /> + <input type="submit" name="remove" + value="Delete selected platforms" /> + </div> + </div> + </form> + + <form py:when="platform" class="mod" id="modplatform" + method="post" action=""> + <div class="field"><label>Target Platform: + <input type="text" name="name" + value="$platform.name" /> + </label></div> + <fieldset> + <legend>Rules</legend> + <table><thead><tr> + <th>Property name</th><th>Match pattern</th> + </tr></thead><tbody> + <tr py:for="idx, rule in enumerate(platform.rules)"> + <td><input type="text" name="property_${idx}" + value="$rule.property" /></td> + <td><input type="text" name="pattern_${idx}" + value="$rule.pattern" /></td> + <td><input type="submit" + name="add_rule_${idx}" value="+" /> + <input type="submit" name="rm_rule_${idx}" + value="-" /> + </td> + </tr> + </tbody></table> + </fieldset> + <p class="help"> + The property name can be any of a set of standard + default properties, or custom properties defined + in slave configuration files. The default + properties are: + </p> + <dl class="help"> + <dt><code>os</code>:</dt> + <dd>The name of the operating system (for example + "Darwin")</dd> + <dt><code>family</code>:</dt> + <dd>The type of operating system (for example + "posix" or "nt")</dd> + <dt><code>version</code>:</dt> + <dd>The operating system version (for example + "8.10.1)</dd> + <dt><code>machine</code>:</dt> + <dd>The hardware architecture (for example "i386"</dd> + <dt><code>processor</code>:</dt> + <dd>The CPU model (for example "i386", this may be + empty or the same as for <code>machine</code> + </dd> + <dt><code>name</code>:</dt> + <dd>The name of the slave</dd> + <dt><code>ipnr</code>:</dt> + <dd>The IP address of the slave</dd> + </dl> + <p class="help"> + The match pattern is a regular expression. + </p> + <div class="buttons"> + <form method="get" action=""><div> + <input type="hidden" + name="${platform.exists and 'edit' + or 'new'}" value="" /> + <input type="hidden" name="platform" + value="$platform.id" /> + <input type="submit" name="cancel" + value="Cancel" /><py:choose> + <input py:when="platform.exists" type="submit" + name="save" value="Save" /> + <input py:otherwise="" type="submit" + name="add" value="Add" /></py:choose> + </div></form> + </div> + </form> + + <py:otherwise><form class="addnew" id="addcomp" + method="post" action=""><fieldset> + <legend>Add Configuration:</legend> + <table summary=""><tr> + <td class="name"><div + class="field"><label>Name:<br /> + <input type="text" name="name" size="12" /> + </label></div></td> + <td class="label"><div + class="field"><label>Label:<br /> + <input type="text" name="label" size="22" /> + </label></div></td> + </tr><tr> + <td class="path" colspan="2"><div class="field"> + <label>Path:<br /> + <input type="text" name="path" size="32" /> + </label> + </div></td> + </tr></table> + <div class="buttons"> + <input type="submit" name="add" value="Add" /> + </div> + </fieldset></form> + + <form method="post" action=""> + <table class="listing" id="configlist"> + <thead> + <tr><th class="sel"> </th><th>Name</th> + <th>Path</th><th>Active</th></tr> + </thead><tbody> + <tr py:if="not configs"> + <td colspan="4"><em>(No Build Configurations)</em></td> + </tr> + <tr py:for="config in configs"> + <td class="sel"> + <input type="checkbox" name="sel" + value="$config.name" /> + </td> + <td class="name"> + <a href="$config.href">$config.label</a> + </td> + <td class="path"><code>$config.path</code></td> + <td class="active"> + <input type="checkbox" name="active" + value="$config.name" + checked="${config.active and 'checked' + or None}" /> + </td> + </tr> + </tbody></table> + <div class="buttons"> + <input type="submit" name="remove" + value="Remove selected items" /> + <input type="submit" name="apply" + value="Apply changes" /> + </div> + </form></py:otherwise></py:choose> + </body> +</html>
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/templates/bitten_admin_master.html @@ -0,0 +1,73 @@ +<!DOCTYPE html + PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" + xmlns:xi="http://www.w3.org/2001/XInclude" + xmlns:py="http://genshi.edgewall.org/"> + <xi:include href="admin.html" /> + <head> + <title>Manage Build Master</title> + </head> + <body> + <h2>Manage Build Master</h2> + + <form class="mod" id="bitten" method="post" action=""> + <fieldset id="config"> + <legend>Configuration Options</legend> + <div class="field"> + <label> + <input type="checkbox" id="build_all" name="build_all" + checked="${master.build_all and 'checked' or None}" /> + Build all revisions + </label> + </div> + <p class="hint"> + Whether to build older revisions even when a more recent + revision has already been built. + </p> + <div class="field"> + <label> + <input type="checkbox" id="adjust_timestamps" + name="adjust_timestamps" + checked="${master.adjust_timestamps and 'checked' + or None}" /> + Adjust build timestamps + </label> + </div> + <p class="hint"> + Whether the timestamps of builds should be adjusted to be + close to the timestamps of the corresponding changesets. + </p> + <hr /> + <div class="field"> + <label> + Time to wait for stabilization: + <input type="text" id="stabilize_wait" name="stabilize_wait" + value="$master.stabilize_wait" size="5" /> + </label> + </div> + <p class="hint"> + The time in seconds to wait for the repository to stabilize + after a check-in before initiating a build. + </p> + <hr /> + <div class="field"> + <label> + Connection timeout for build slaves: + <input type="text" id="slave_timeout" name="slave_timeout" + value="$master.slave_timeout" size="5" /> + </label> + </div> + <p class="hint"> + The timeout in seconds after which a build started by a slave + is considered aborted, in case there has been no activity from + that slave in that time. + </p> + </fieldset> + + <div class="buttons"> + <input type="submit" value="Apply changes"/> + </div> + </form> + </body> +</html>
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/templates/bitten_build.html @@ -0,0 +1,73 @@ +<!DOCTYPE html + PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" + xmlns:xi="http://www.w3.org/2001/XInclude" + xmlns:py="http://genshi.edgewall.org/"> + <xi:include href="layout.html" /> + <head> + <title>$title</title> + </head> + <body> + <div id="content" class="build"> + <h1>$title</h1> + <dl id="overview" py:with="slave = build.slave"> + <dt class="config">Configuration:</dt> + <dd class="config"> + <a href="$build.config.href">$build.config.name</a> + </dd> + <dt class="trigger">Triggered by:</dt> + <dd class="trigger"> + Changeset <a href="$build.chgset_href">[$build.rev]</a> by + $build.chgset_author + </dd> + <dt class="slave">Built by:</dt> + <dd class="slave"> + <code>$slave.name</code> ($slave.ipnr) + </dd> + <dt class="os">Operating system:</dt> + <dd>$slave.os_name $slave.os_version ($slave.os_family)</dd> + <py:if test="slave.machine"><dt class="machine">Hardware:</dt> + <dd class="machine"> + $slave.machine + <py:if test="slave.processor"> ($slave.processor)</py:if> + </dd></py:if> + <dt class="time"> + ${build.stopped and 'Started:' or 'Building since:'} + </dt> + <dd class="time">$build.started ($build.started_delta ago)</dd> + <py:if test="build.stopped"><dt class="time">Stopped:</dt> + <dd class="time">$build.stopped ($build.stopped_delta ago)</dd></py:if> + <dt class="duration">Duration:</dt> + <dd class="duration">$build.duration</dd> + </dl> + <div py:if="build.can_delete" class="buttons"> + <form method="post" action=""><div> + <input type="hidden" name="action" value="invalidate" /> + <input type="submit" value="Invalidate build" /> + </div></form> + </div><py:for each="step in build.steps"> + <h2 class="step" id="step_${step.name}">$step.name ($step.duration)</h2> + <div py:if="step.errors" class="errors"> + <h3>Errors</h3> + <ul> + <li py:for="error in step.errors">$error</li> + </ul> + </div> + <p>$step.description</p> + <div id="${step.name}_tabs"> + <div class="tab"> + <h3>Log</h3> + <div class="log"><py:for each="item in step.log"><code class="$item.level">$item.message</code><br /></py:for></div> + </div> + <div py:for="report in [r for r in step.reports if r.template]" + class="tab report $report.category"> + <xi:include href="$report.template" py:with="data = report.data" /> + </div> + </div> + <script type="text/javascript"> + makeTabSet(document.getElementById("${step.name}_tabs")); + </script></py:for> + </div> + </body> +</html>
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/templates/bitten_chart_coverage.html @@ -0,0 +1,38 @@ +<chart xmlns:py="http://genshi.edgewall.org/"> + <chart_type> + <value>area</value> + <value>area</value> + </chart_type> + + <axis_category size="10" orientation="diagonal_up" + skip="${len(data[0]) / 6}"/> + <axis_ticks value_ticks="false" category_ticks="true" major_thickness="1" + minor_thickness="0" major_color="000000" position="outside"/> + + <chart_data> + <row py:for="idx, row in enumerate(data)"> + <py:choose py:for="jdx, value in enumerate(row)"> + <string py:when="not idx or not jdx">$value</string> + <number py:otherwise="">$value</number> + </py:choose> + </row> + </chart_data> + + <chart_border color="999999" left_thickness="1" bottom_thickness="1"/> + <chart_grid_h alpha="5" color="666666" thickness="3"/> + <chart_pref line_thickness="2" point_shape="none"/> + <chart_value position="cursor"/> + <series_color> + <color>bbbbbb</color> + <color>9999ff</color> + </series_color> + + <legend_label layout="vertical" alpha="60"/> + <legend_rect x="60" y="50" width="10"/> + + <draw> + <text width="320" height="40" h_align="center" v_align="bottom" + size="12">$title</text> + </draw> + +</chart>
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/templates/bitten_chart_tests.html @@ -0,0 +1,38 @@ +<chart xmlns:py="http://genshi.edgewall.org/"> + <chart_type> + <value>area</value> + <value>column</value> + </chart_type> + + <axis_category size="10" orientation="diagonal_up" + skip="${len(data[0]) / 6}"/> + <axis_ticks value_ticks="false" category_ticks="true" major_thickness="2" + minor_thickness="0" major_color="000000" position="outside"/> + + <chart_data> + <row py:for="idx, row in enumerate(data)"> + <py:choose py:for="jdx, value in enumerate(row)"> + <string py:when="not idx or not jdx">$value</string> + <number py:otherwise="">$value</number> + </py:choose> + </row> + </chart_data> + + <chart_border color="999999" left_thickness="1" bottom_thickness="1"/> + <chart_grid_h alpha="5" color="666666" thickness="3"/> + <chart_pref line_thickness="2" point_shape="none"/> + <chart_value position="cursor"/> + <series_color> + <color>99dd99</color> + <color>ff0000</color> + </series_color> + + <legend_label layout="vertical" alpha="60"/> + <legend_rect x="60" y="50" width="10"/> + + <draw> + <text width="320" height="40" h_align="center" v_align="bottom" + size="12">$title</text> + </draw> + +</chart>
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/templates/bitten_config.html @@ -0,0 +1,178 @@ +<!DOCTYPE html + PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" + xmlns:xi="http://www.w3.org/2001/XInclude" + xmlns:py="http://genshi.edgewall.org/"> + <xi:include href="layout.html" /> + <head> + <title>$title</title> + </head> + <body> + <strong py:def="build_status(status)" class="status"> + <py:choose test="status"> + <py:when test="'completed'">Success</py:when> + <py:when test="'failed'">Failed</py:when> + <py:when test="'in progress'">In-progress</py:when> + </py:choose> + </strong> + + <div py:def="slave_info(slave)" class="system"> + <strong>$slave.name</strong> ($slave.ipnr)<br /> + $slave.os_name $slave.os_version + <py:if test="slave.machine or slave.processor"> / </py:if> + ${slave.processor or slave.machine or ''} + </div> + + <ul py:def="build_steps(steps)" py:if="steps" class="steps"> + <li py:for="step in steps" + class="${step.failed and 'failed' or 'success'}"> + <span class="duration">$step.duration</span> + <a href="$step.href" title="${step.description or None}"> + $step.name + </a> + <ul py:if="step.failed and step.errors"> + <li py:for="error in step.errors">$error</li> + </ul> + </li> + </ul> + + <div id="content" class="build"> + <h1>$title</h1><py:choose test="page_mode"><py:when test="'overview'"> + <form id="prefs" method="get" action=""> + <div> + <input type="checkbox" id="showall" name="show" value="all" + checked="${show_all and 'checked' or None}" /> + <label for="showall">Show deactivated configurations</label> + </div> + <div class="buttons"> + <input type="submit" value="Update" /> + </div> + </form><py:for each="config in configs"> + <h2 class="config ${not config.active and 'deactivated' or ''}"> + <a href="$config.href">$config.label</a> + </h2> + <div py:if="config.description" class="description"> + $config.description + </div><py:if test="len(config.builds)"> + <h3 class="builds"><a href="$config.href">Latest builds</a></h3> + <table class="builds"><tbody><tr> + <th py:with="youngest_rev = config.youngest_rev"> + <a href="$youngest_rev.href">[$youngest_rev.id]</a> + by $youngest_rev.author<p class="date">$youngest_rev.date</p> + <p class="message">$youngest_rev.message</p> + </th> + <td py:for="build in config.builds" class="$build.cls"><py:choose> + <py:when test="build.status != 'pending'"> + <a href="$build.href">$build.platform</a> + <p class="date">$build.stopped</p> + ${slave_info(build.slave)} + ${build_status(build.status)} + </py:when><py:otherwise> + <strong>$build.platform</strong> + <p class="nobuild">No build yet</p> + </py:otherwise></py:choose> + </td> + </tr></tbody></table></py:if></py:for> + + </py:when><py:when test="'view_config'"> + <form py:if="config.can_modify" id="prefs" method="post" + class="activation"><py:choose> + <div py:when="not config.active" class="help"> + This build configuration is currently inactive.<br /> + No builds will be initiated for this configuration<br /> + until it is activated. + </div> + <div py:otherwise="" class="help"> + This configuration is currently active. + </div></py:choose> + <div class="buttons" py:choose=""> + <input type="hidden" name="action" value="edit" /> + <input py:when="config.active" type="submit" name="deactivate" + value="Deactivate" /> + <input py:otherwise="" type="submit" name="activate" + value="Activate" /> + </div> + </form> + <p class="path"> + Repository path: + <a py:if="config.path" href="$config.browser_href">$config.path</a> + ${not config.path and '—' or ''} + <py:if test="config.min_rev or config.max_rev"> + (<py:if test="config.min_rev">starting at + <a href="$config.min_rev_href">[$config.min_rev]</a></py:if> + <py:if test="config.min_rev and config.max_rev">, </py:if> + <py:if test="config.max_rev">up to + <a href="$config.max_rev_href">[$config.max_rev]</a></py:if>) + </py:if> + </p> + <div py:if="config.description" class="description"> + $config.description + </div> + <div id="charts"><py:for each="chart in config.charts"> + <object type="application/x-shockwave-flash" + width="320" height="240" + data="${href.chrome('bitten', 'charts.swf')}"> + <param name="movie" value="${href.chrome('bitten', 'charts.swf')}" /> + <param name="FlashVars" + value="library_path=${href.chrome('bitten')}&xml_source=$chart.href${config.charts_license and '&license='+config.charts_license or ''}" /> + <param name="wmode" value="transparent" /> + </object><br /></py:for> + </div> + + <table py:if="config.platforms and config.builds" + class="listing" id="builds"> + <thead><tr> + <th class="chgset"><abbr title="Changeset">Chgset</abbr></th> + <th py:for="platform in config.platforms">$platform.name</th> + </tr></thead> + <tbody py:if="config.builds"> + <tr py:for="rev_num in sorted(config.builds, reverse=True)" + py:with="rev = config.builds[rev_num]"> + <th class="chgset" scope="row"> + <a href="$rev.href" title="View Changeset">[$rev_num]</a> + </th><py:for each="platform in config.platforms"><py:choose> + <td py:when="platform.id in rev" py:with="build = rev[platform.id]" + class="$build.cls"> + <div class="info"> + <a href="$build.href" title="View build results"> + $build.id + ${build_status(build.status)} + </a> + ${slave_info(build.slave)} + </div> + ${build_steps(build.steps)} + </td> + <td py:otherwise="">—</td></py:choose></py:for> + </tr> + </tbody> + </table> + <br style="clear: right"/> + + </py:when><py:when test="'view-inprogress'"> + <py:for each="config in configs"> + <h2 class="config ${not config.active and 'deactivated' or ''}"> + <a href="$config.href">$config.label</a> + </h2> + <table class="listing" id="builds"> + <thead><tr> + <th class="chgset" abbrev="Changeset">Chgset</th><th>Build</th> + </tr></thead><tbody> + <tr py:for="build in config.builds"> + <th class="chgset" scope="row"> + <a href="$build.rev_href" title="View Changeset">[$build.rev]</a> + </th> + <td class="$build.cls"> + <div class="info"> + <a href="$build.href" title="View build results"> + $build.id: <strong class="status">$build.platform</strong> + </a> + ${slave_info(build.slave)} + </div> + ${build_steps(build.steps)} + </td> + </tr></tbody> + </table></py:for></py:when> + </py:choose></div> + </body> +</html>
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/templates/bitten_summary_coverage.html @@ -0,0 +1,27 @@ +<!DOCTYPE html + PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" + xmlns:py="http://genshi.edgewall.org/" py:strip=""> + <body py:strip=""> + <h3>Code Coverage</h3> + <table class="listing coverage"> + <thead><tr> + <th class="name">Unit</th><th class="loc">Lines of Code</th> + <th class="cov">Coverage</th> + </tr></thead> + <tbody><tr py:for="item in data.units"> + <td class="name" py:choose=""> + <a py:when="item.href" href="$item.href">$item.name</a> + <py:otherwise>$item.name</py:otherwise> + </td> + <td class="loc">$item.loc</td> + <td class="cov">${item.cov}%</td> + </tr></tbody> + <tbody class="totals"><tr py:with="totals = data.totals"> + <th>Total</th><td>$totals.loc</td> + <td>${totals.cov}%</td> + </tr></tbody> + </table> + </body> +</html>
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/templates/bitten_summary_tests.html @@ -0,0 +1,31 @@ +<!DOCTYPE html + PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" + xmlns:py="http://genshi.edgewall.org/" py:strip=""> + <body py:strip=""> + <h3>Test Results</h3> + <table class="listing tests"> + <thead><tr> + <th>Test Fixture</th><th>Total</th> + <th>Failures</th><th>Errors</th> + </tr></thead> + <tbody><tr py:for="item in data.fixtures" + class="${item.num_failure or item.num_error and 'failed' or None}"> + <th py:choose=""> + <a py:when="item.href" href="$item.href">$item.name</a> + <py:otherwise>$item.name</py:otherwise> + </th> + <td>${item.num_success + item.num_failure + item.num_error}</td> + <td>$item.num_failure</td> + <td>$item.num_error</td> + </tr></tbody> + <tbody class="totals"><tr py:with="totals = data.totals"> + <th>Total</th> + <td>$totals.success</td> + <td>$totals.failure</td> + <td>$totals.error</td> + </tr></tbody> + </table> + </body> +</html>
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/tests/__init__.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +import unittest + +from bitten.tests import admin, master, model, recipe, queue, slave, web_ui +from bitten.build import tests as build +from bitten.report import tests as report +from bitten.util import tests as util + +def suite(): + suite = unittest.TestSuite() + suite.addTest(admin.suite()) + suite.addTest(master.suite()) + suite.addTest(model.suite()) + suite.addTest(recipe.suite()) + suite.addTest(queue.suite()) + suite.addTest(slave.suite()) + suite.addTest(web_ui.suite()) + suite.addTest(build.suite()) + suite.addTest(report.suite()) + suite.addTest(util.suite()) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/tests/admin.py @@ -0,0 +1,810 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +import shutil +import tempfile +import unittest + +from trac.core import TracError +from trac.db import DatabaseManager +from trac.perm import PermissionCache, PermissionError, PermissionSystem +from trac.test import EnvironmentStub, Mock +from trac.web.href import Href +from trac.web.main import RequestDone +from bitten.main import BuildSystem +from bitten.model import BuildConfig, TargetPlatform, schema +from bitten.admin import BuildMasterAdminPageProvider, \ + BuildConfigurationsAdminPageProvider + +try: + from trac.perm import DefaultPermissionPolicy +except ImportError: + DefaultPermissionPolicy = None + +class BuildMasterAdminPageProviderTestCase(unittest.TestCase): + + def setUp(self): + self.env = EnvironmentStub(enable=['trac.*', 'bitten.*']) + self.env.path = tempfile.mkdtemp() + + # Create tables + db = self.env.get_db_cnx() + cursor = db.cursor() + connector, _ = DatabaseManager(self.env)._get_connector() + for table in schema: + for stmt in connector.to_sql(table): + cursor.execute(stmt) + + # Set up permissions + self.env.config.set('trac', 'permission_store', + 'DefaultPermissionStore') + PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN') + if DefaultPermissionPolicy is not None and hasattr(DefaultPermissionPolicy, "CACHE_EXPIRY"): + self.old_perm_cache_expiry = DefaultPermissionPolicy.CACHE_EXPIRY + DefaultPermissionPolicy.CACHE_EXPIRY = -1 + + # Hook up a dummy repository + self.repos = Mock( + get_node=lambda path, rev=None: Mock(get_history=lambda: [], + isdir=True), + normalize_path=lambda path: path, + sync=lambda: None + ) + self.env.get_repository = lambda authname=None: self.repos + + def tearDown(self): + if DefaultPermissionPolicy is not None and hasattr(DefaultPermissionPolicy, "CACHE_EXPIRY"): + DefaultPermissionPolicy.CACHE_EXPIRY = self.old_perm_cache_expiry + shutil.rmtree(self.env.path) + + def test_get_admin_panels(self): + provider = BuildMasterAdminPageProvider(self.env) + + req = Mock(perm=PermissionCache(self.env, 'joe')) + self.assertEqual([('bitten', 'Builds', 'master', 'Master Settings')], + list(provider.get_admin_panels(req))) + + PermissionSystem(self.env).revoke_permission('joe', 'BUILD_ADMIN') + req = Mock(perm=PermissionCache(self.env, 'joe')) + self.assertEqual([], list(provider.get_admin_panels(req))) + + def test_process_get_request(self): + req = Mock(method='GET', chrome={}, href=Href('/'), + perm=PermissionCache(self.env, 'joe')) + + provider = BuildMasterAdminPageProvider(self.env) + template_name, data = provider.render_admin_panel( + req, 'bitten', 'master', '' + ) + + self.assertEqual('bitten_admin_master.html', template_name) + assert 'master' in data + master = data['master'] + self.assertEqual(3600, master.slave_timeout) + self.assertEqual(0, master.stabilize_wait) + assert not master.adjust_timestamps + assert not master.build_all + + def test_process_config_changes(self): + redirected_to = [] + def redirect(url): + redirected_to.append(url) + raise RequestDone + req = Mock(method='POST', perm=PermissionCache(self.env, 'joe'), + abs_href=Href('http://example.org/'), redirect=redirect, + args={'slave_timeout': '60', 'adjust_timestamps': ''}) + + provider = BuildMasterAdminPageProvider(self.env) + try: + provider.render_admin_panel(req, 'bitten', 'master', '') + self.fail('Expected RequestDone') + + except RequestDone: + self.assertEqual('http://example.org/admin/bitten/master', + redirected_to[0]) + section = self.env.config['bitten'] + self.assertEqual(60, section.getint('slave_timeout')) + self.assertEqual(True, section.getbool('adjust_timestamps')) + self.assertEqual(False, section.getbool('build_all')) + + +class BuildConfigurationsAdminPageProviderTestCase(unittest.TestCase): + + def setUp(self): + self.env = EnvironmentStub(enable=['trac.*', 'bitten.*']) + self.env.path = tempfile.mkdtemp() + + # Create tables + db = self.env.get_db_cnx() + cursor = db.cursor() + connector, _ = DatabaseManager(self.env)._get_connector() + for table in schema: + for stmt in connector.to_sql(table): + cursor.execute(stmt) + + # Set up permissions + self.env.config.set('trac', 'permission_store', + 'DefaultPermissionStore') + PermissionSystem(self.env).grant_permission('joe', 'BUILD_CREATE') + PermissionSystem(self.env).grant_permission('joe', 'BUILD_DELETE') + PermissionSystem(self.env).grant_permission('joe', 'BUILD_MODIFY') + if DefaultPermissionPolicy is not None and hasattr(DefaultPermissionPolicy, "CACHE_EXPIRY"): + self.old_perm_cache_expiry = DefaultPermissionPolicy.CACHE_EXPIRY + DefaultPermissionPolicy.CACHE_EXPIRY = -1 + + # Hook up a dummy repository + self.repos = Mock( + get_node=lambda path, rev=None: Mock(get_history=lambda: [], + isdir=True), + normalize_path=lambda path: path, + sync=lambda: None + ) + self.env.get_repository = lambda authname=None: self.repos + + def tearDown(self): + if DefaultPermissionPolicy is not None and hasattr(DefaultPermissionPolicy, "CACHE_EXPIRY"): + DefaultPermissionPolicy.CACHE_EXPIRY = self.old_perm_cache_expiry + shutil.rmtree(self.env.path) + + def test_get_admin_panels(self): + provider = BuildConfigurationsAdminPageProvider(self.env) + + req = Mock(perm=PermissionCache(self.env, 'joe')) + self.assertEqual([('bitten', 'Builds', 'configs', 'Configurations')], + list(provider.get_admin_panels(req))) + + PermissionSystem(self.env).revoke_permission('joe', 'BUILD_MODIFY') + req = Mock(perm=PermissionCache(self.env, 'joe')) + self.assertEqual([], list(provider.get_admin_panels(req))) + + def test_process_view_configs_empty(self): + req = Mock(method='GET', chrome={}, href=Href('/'), + perm=PermissionCache(self.env, 'joe')) + + provider = BuildConfigurationsAdminPageProvider(self.env) + template_name, data = provider.render_admin_panel( + req, 'bitten', 'configs', '' + ) + + self.assertEqual('bitten_admin_configs.html', template_name) + self.assertEqual([], data['configs']) + + def test_process_view_configs(self): + BuildConfig(self.env, name='foo', label='Foo', path='branches/foo', + active=True).insert() + BuildConfig(self.env, name='bar', label='Bar', path='branches/bar', + min_rev='123', max_rev='456').insert() + + req = Mock(method='GET', chrome={}, href=Href('/'), + perm=PermissionCache(self.env, 'joe')) + + provider = BuildConfigurationsAdminPageProvider(self.env) + template_name, data = provider.render_admin_panel( + req, 'bitten', 'configs', '' + ) + + self.assertEqual('bitten_admin_configs.html', template_name) + assert 'configs' in data + configs = data['configs'] + self.assertEqual(2, len(configs)) + self.assertEqual({ + 'name': 'bar', 'href': '/admin/bitten/configs/bar', + 'label': 'Bar', 'min_rev': '123', 'max_rev': '456', + 'path': 'branches/bar', 'active': False + }, configs[0]) + self.assertEqual({ + 'name': 'foo', 'href': '/admin/bitten/configs/foo', + 'label': 'Foo', 'min_rev': None, 'max_rev': None, + 'path': 'branches/foo', 'active': True + }, configs[1]) + + def test_process_view_config(self): + BuildConfig(self.env, name='foo', label='Foo', path='branches/foo', + active=True).insert() + TargetPlatform(self.env, config='foo', name='any').insert() + + req = Mock(method='GET', chrome={}, href=Href('/'), + perm=PermissionCache(self.env, 'joe')) + + provider = BuildConfigurationsAdminPageProvider(self.env) + template_name, data = provider.render_admin_panel( + req, 'bitten', 'configs', 'foo' + ) + + self.assertEqual('bitten_admin_configs.html', template_name) + assert 'config' in data + config = data['config'] + self.assertEqual({ + 'name': 'foo', 'label': 'Foo', 'description': '', 'recipe': '', + 'path': 'branches/foo', 'min_rev': None, 'max_rev': None, + 'active': True, 'platforms': [{ + 'href': '/admin/bitten/configs/foo/1', + 'name': 'any', 'id': 1, 'rules': [] + }] + }, config) + + def test_process_activate_config(self): + BuildConfig(self.env, name='foo', path='branches/foo').insert() + BuildConfig(self.env, name='bar', path='branches/bar').insert() + + redirected_to = [] + def redirect(url): + redirected_to.append(url) + raise RequestDone + req = Mock(method='POST', perm=PermissionCache(self.env, 'joe'), + abs_href=Href('http://example.org/'), redirect=redirect, + authname='joe', + args={'apply': '', 'active': ['foo']}) + + provider = BuildConfigurationsAdminPageProvider(self.env) + try: + provider.render_admin_panel(req, 'bitten', 'configs', '') + self.fail('Expected RequestDone') + + except RequestDone: + self.assertEqual('http://example.org/admin/bitten/configs', + redirected_to[0]) + config = BuildConfig.fetch(self.env, name='foo') + self.assertEqual(True, config.active) + + def test_process_deactivate_config(self): + BuildConfig(self.env, name='foo', path='branches/foo', + active=True).insert() + BuildConfig(self.env, name='bar', path='branches/bar', + active=True).insert() + + redirected_to = [] + def redirect(url): + redirected_to.append(url) + raise RequestDone + req = Mock(method='POST', perm=PermissionCache(self.env, 'joe'), + abs_href=Href('http://example.org/'), redirect=redirect, + authname='joe', + args={'apply': ''}) + + provider = BuildConfigurationsAdminPageProvider(self.env) + try: + provider.render_admin_panel(req, 'bitten', 'configs', '') + self.fail('Expected RequestDone') + + except RequestDone: + self.assertEqual('http://example.org/admin/bitten/configs', + redirected_to[0]) + config = BuildConfig.fetch(self.env, name='foo') + self.assertEqual(False, config.active) + config = BuildConfig.fetch(self.env, name='bar') + self.assertEqual(False, config.active) + + def test_process_add_config(self): + BuildConfig(self.env, name='foo', label='Foo', path='branches/foo', + active=True).insert() + + redirected_to = [] + def redirect(url): + redirected_to.append(url) + raise RequestDone + req = Mock(method='POST', perm=PermissionCache(self.env, 'joe'), + abs_href=Href('http://example.org/'), redirect=redirect, + authname='joe', + args={'add': '', 'name': 'bar', 'label': 'Bar'}) + + provider = BuildConfigurationsAdminPageProvider(self.env) + try: + provider.render_admin_panel(req, 'bitten', 'configs', '') + self.fail('Expected RequestDone') + + except RequestDone: + self.assertEqual('http://example.org/admin/bitten/configs/bar', + redirected_to[0]) + config = BuildConfig.fetch(self.env, name='bar') + self.assertEqual('Bar', config.label) + + def test_process_add_config_cancel(self): + redirected_to = [] + def redirect(url): + redirected_to.append(url) + raise RequestDone + req = Mock(method='POST', perm=PermissionCache(self.env, 'joe'), + abs_href=Href('http://example.org/'), redirect=redirect, + args={'cancel': '', 'name': 'bar', 'label': 'Bar'}) + + provider = BuildConfigurationsAdminPageProvider(self.env) + try: + provider.render_admin_panel(req, 'bitten', 'configs', '') + self.fail('Expected RequestDone') + + except RequestDone: + self.assertEqual('http://example.org/admin/bitten/configs', + redirected_to[0]) + configs = list(BuildConfig.select(self.env, include_inactive=True)) + self.assertEqual(0, len(configs)) + + def test_process_add_config_no_name(self): + req = Mock(method='POST', perm=PermissionCache(self.env, 'joe'), + args={'add': ''}) + + provider = BuildConfigurationsAdminPageProvider(self.env) + try: + provider.render_admin_panel(req, 'bitten', 'configs', '') + self.fail('Expected TracError') + + except TracError, e: + self.assertEqual('Missing required field "name"', e.message) + self.assertEqual('Missing Field', e.title) + + def test_process_add_config_invalid_name(self): + req = Mock(method='POST', perm=PermissionCache(self.env, 'joe'), + args={'add': '', 'name': 'no spaces allowed'}) + + provider = BuildConfigurationsAdminPageProvider(self.env) + try: + provider.render_admin_panel(req, 'bitten', 'configs', '') + self.fail('Expected TracError') + + except TracError, e: + self.assertEqual('The field "name" may only contain letters, ' + 'digits, periods, or dashes.', e.message) + self.assertEqual('Invalid Field', e.title) + + def test_new_config_submit_with_invalid_path(self): + req = Mock(method='POST', perm=PermissionCache(self.env, 'joe'), + authname='joe', + args={'add': '', 'name': 'foo', 'path': 'invalid/path'}) + + def get_node(path, rev=None): + raise TracError('No such node') + self.repos = Mock(get_node=get_node) + + provider = BuildConfigurationsAdminPageProvider(self.env) + try: + provider.render_admin_panel(req, 'bitten', 'configs', '') + self.fail('Expected TracError') + + except TracError, e: + self.assertEqual('No such node', e.message) + self.assertEqual('Invalid Repository Path', e.title) + + def test_process_add_config_no_perms(self): + BuildConfig(self.env, name='foo', label='Foo', path='branches/foo', + active=True).insert() + PermissionSystem(self.env).revoke_permission('joe', 'BUILD_CREATE') + + req = Mock(method='POST', + perm=PermissionCache(self.env, 'joe'), + args={'add': '', 'name': 'bar', 'label': 'Bar'}) + + provider = BuildConfigurationsAdminPageProvider(self.env) + self.assertRaises(PermissionError, provider.render_admin_panel, req, + 'bitten', 'configs', '') + + def test_process_remove_config(self): + BuildConfig(self.env, name='foo', label='Foo', path='branches/foo', + active=True).insert() + BuildConfig(self.env, name='bar', label='Bar', path='branches/bar', + min_rev='123', max_rev='456').insert() + + redirected_to = [] + def redirect(url): + redirected_to.append(url) + raise RequestDone + req = Mock(method='POST', perm=PermissionCache(self.env, 'joe'), + abs_href=Href('http://example.org/'), redirect=redirect, + args={'remove': '', 'sel': 'bar'}) + + provider = BuildConfigurationsAdminPageProvider(self.env) + try: + provider.render_admin_panel(req, 'bitten', 'configs', '') + self.fail('Expected RequestDone') + + except RequestDone: + self.assertEqual('http://example.org/admin/bitten/configs', + redirected_to[0]) + assert not BuildConfig.fetch(self.env, name='bar') + + def test_process_remove_config_cancel(self): + BuildConfig(self.env, name='foo', label='Foo', path='branches/foo', + active=True).insert() + BuildConfig(self.env, name='bar', label='Bar', path='branches/bar', + min_rev='123', max_rev='456').insert() + + redirected_to = [] + def redirect(url): + redirected_to.append(url) + raise RequestDone + req = Mock(method='POST', perm=PermissionCache(self.env, 'joe'), + abs_href=Href('http://example.org/'), redirect=redirect, + args={'cancel': '', 'sel': 'bar'}) + + provider = BuildConfigurationsAdminPageProvider(self.env) + try: + provider.render_admin_panel(req, 'bitten', 'configs', '') + self.fail('Expected RequestDone') + + except RequestDone: + self.assertEqual('http://example.org/admin/bitten/configs', + redirected_to[0]) + configs = list(BuildConfig.select(self.env, include_inactive=True)) + self.assertEqual(2, len(configs)) + + def test_process_remove_config_no_selection(self): + BuildConfig(self.env, name='foo', label='Foo', path='branches/foo', + active=True).insert() + + req = Mock(method='POST', perm=PermissionCache(self.env, 'joe'), + args={'remove': ''}) + + provider = BuildConfigurationsAdminPageProvider(self.env) + try: + provider.render_admin_panel(req, 'bitten', 'configs', '') + self.fail('Expected TracError') + + except TracError, e: + self.assertEqual('No configuration selected', e.message) + + def test_process_remove_config_bad_selection(self): + BuildConfig(self.env, name='foo', label='Foo', path='branches/foo', + active=True).insert() + + req = Mock(method='POST', perm=PermissionCache(self.env, 'joe'), + args={'remove': '', 'sel': 'baz'}) + + provider = BuildConfigurationsAdminPageProvider(self.env) + try: + provider.render_admin_panel(req, 'bitten', 'configs', '') + self.fail('Expected TracError') + + except TracError, e: + self.assertEqual("Configuration 'baz' not found", e.message) + + def test_process_remove_config_no_perms(self): + BuildConfig(self.env, name='foo', label='Foo', path='branches/foo', + active=True).insert() + PermissionSystem(self.env).revoke_permission('joe', 'BUILD_DELETE') + + req = Mock(method='POST', + perm=PermissionCache(self.env, 'joe'), + args={'remove': '', 'sel': 'bar'}) + + provider = BuildConfigurationsAdminPageProvider(self.env) + self.assertRaises(PermissionError, provider.render_admin_panel, req, + 'bitten', 'configs', '') + + def test_process_update_config(self): + BuildConfig(self.env, name='foo', label='Foo', path='branches/foo', + active=True).insert() + + redirected_to = [] + def redirect(url): + redirected_to.append(url) + raise RequestDone + req = Mock(method='POST', perm=PermissionCache(self.env, 'joe'), + abs_href=Href('http://example.org/'), redirect=redirect, + authname='joe', args={ + 'save': '', 'name': 'foo', 'label': 'Foobar', + 'description': 'Thanks for all the fish!' + }) + + provider = BuildConfigurationsAdminPageProvider(self.env) + try: + provider.render_admin_panel(req, 'bitten', 'configs', 'foo') + self.fail('Expected RequestDone') + + except RequestDone: + self.assertEqual('http://example.org/admin/bitten/configs', + redirected_to[0]) + config = BuildConfig.fetch(self.env, name='foo') + self.assertEqual('Foobar', config.label) + self.assertEqual('Thanks for all the fish!', config.description) + + def test_process_update_config_no_name(self): + BuildConfig(self.env, name='foo', label='Foo', path='branches/foo', + active=True).insert() + + req = Mock(method='POST', perm=PermissionCache(self.env, 'joe'), + args={'save': ''}) + + provider = BuildConfigurationsAdminPageProvider(self.env) + try: + provider.render_admin_panel(req, 'bitten', 'configs', 'foo') + self.fail('Expected TracError') + + except TracError, e: + self.assertEqual('Missing required field "name"', e.message) + self.assertEqual('Missing Field', e.title) + + def test_process_update_config_invalid_name(self): + BuildConfig(self.env, name='foo', label='Foo', path='branches/foo', + active=True).insert() + + req = Mock(method='POST', perm=PermissionCache(self.env, 'joe'), + args={'save': '', 'name': 'no spaces allowed'}) + + provider = BuildConfigurationsAdminPageProvider(self.env) + try: + provider.render_admin_panel(req, 'bitten', 'configs', 'foo') + self.fail('Expected TracError') + + except TracError, e: + self.assertEqual('The field "name" may only contain letters, ' + 'digits, periods, or dashes.', e.message) + self.assertEqual('Invalid Field', e.title) + + def test_process_update_config_invalid_path(self): + BuildConfig(self.env, name='foo', label='Foo', path='branches/foo', + active=True).insert() + + req = Mock(method='POST', perm=PermissionCache(self.env, 'joe'), + authname='joe', + args={'save': '', 'name': 'foo', 'path': 'invalid/path'}) + + def get_node(path, rev=None): + raise TracError('No such node') + self.repos = Mock(get_node=get_node) + + provider = BuildConfigurationsAdminPageProvider(self.env) + try: + provider.render_admin_panel(req, 'bitten', 'configs', 'foo') + self.fail('Expected TracError') + + except TracError, e: + self.assertEqual('No such node', e.message) + self.assertEqual('Invalid Repository Path', e.title) + + def test_process_update_config_non_wellformed_recipe(self): + BuildConfig(self.env, name='foo', label='Foo', path='branches/foo', + active=True).insert() + + req = Mock(method='POST', perm=PermissionCache(self.env, 'joe'), + authname='joe', + args={'save': '', 'name': 'foo', 'recipe': 'not_xml'}) + + provider = BuildConfigurationsAdminPageProvider(self.env) + try: + provider.render_admin_panel(req, 'bitten', 'configs', 'foo') + self.fail('Expected TracError') + + except TracError, e: + self.assertEqual('Failure parsing recipe: syntax error: line 1, ' + 'column 0', e.message) + self.assertEqual('Invalid Recipe', e.title) + + def test_process_update_config_invalid_recipe(self): + BuildConfig(self.env, name='foo', label='Foo', path='branches/foo', + active=True).insert() + + req = Mock(method='POST', perm=PermissionCache(self.env, 'joe'), + authname='joe', + args={'save': '', 'name': 'foo', + 'recipe': '<build><step /></build>'}) + + provider = BuildConfigurationsAdminPageProvider(self.env) + try: + provider.render_admin_panel(req, 'bitten', 'configs', 'foo') + self.fail('Expected TracError') + + except TracError, e: + self.assertEqual('Steps must have an "id" attribute', e.message) + self.assertEqual('Invalid Recipe', e.title) + + def test_process_new_platform(self): + BuildConfig(self.env, name='foo', label='Foo', path='branches/foo', + active=True).insert() + + data = {} + req = Mock(method='POST', chrome={}, hdf=data, href=Href('/'), + perm=PermissionCache(self.env, 'joe'), + args={'new': ''}) + + provider = BuildConfigurationsAdminPageProvider(self.env) + template_name, data = provider.render_admin_panel( + req, 'bitten', 'configs', 'foo' + ) + + self.assertEqual('bitten_admin_configs.html', template_name) + assert 'platform' in data + platform = data['platform'] + self.assertEqual({ + 'id': None, 'exists': False, 'name': None, 'rules': [('', '')], + }, platform) + + def test_process_add_platform(self): + BuildConfig(self.env, name='foo', label='Foo', path='branches/foo', + active=True).insert() + + redirected_to = [] + def redirect(url): + redirected_to.append(url) + raise RequestDone + req = Mock(method='POST', perm=PermissionCache(self.env, 'joe'), + abs_href=Href('http://example.org/'), redirect=redirect, + authname='joe', + args={'add': '', 'new': '', 'name': 'Test', + 'property_0': 'family', 'pattern_0': 'posix'}) + + provider = BuildConfigurationsAdminPageProvider(self.env) + try: + provider.render_admin_panel(req, 'bitten', 'configs', 'foo') + self.fail('Expected RequestDone') + + except RequestDone: + self.assertEqual('http://example.org/admin/bitten/configs/foo', + redirected_to[0]) + platforms = list(TargetPlatform.select(self.env, config='foo')) + self.assertEqual(1, len(platforms)) + self.assertEqual('Test', platforms[0].name) + self.assertEqual([('family', 'posix')], platforms[0].rules) + + def test_process_add_platform_cancel(self): + BuildConfig(self.env, name='foo', label='Foo', path='branches/foo', + active=True).insert() + + redirected_to = [] + def redirect(url): + redirected_to.append(url) + raise RequestDone + req = Mock(method='POST', perm=PermissionCache(self.env, 'joe'), + abs_href=Href('http://example.org/'), redirect=redirect, + authname='joe', + args={'cancel': '', 'new': '', 'name': 'Test', + 'property_0': 'family', 'pattern_0': 'posix'}) + + provider = BuildConfigurationsAdminPageProvider(self.env) + try: + provider.render_admin_panel(req, 'bitten', 'configs', 'foo') + self.fail('Expected RequestDone') + + except RequestDone: + self.assertEqual('http://example.org/admin/bitten/configs/foo', + redirected_to[0]) + platforms = list(TargetPlatform.select(self.env, config='foo')) + self.assertEqual(0, len(platforms)) + + def test_process_remove_platforms(self): + BuildConfig(self.env, name='foo', label='Foo', path='branches/foo', + active=True).insert() + platform = TargetPlatform(self.env, config='foo', name='any') + platform.insert() + + redirected_to = [] + def redirect(url): + redirected_to.append(url) + raise RequestDone + req = Mock(method='POST', perm=PermissionCache(self.env, 'joe'), + abs_href=Href('http://example.org/'), redirect=redirect, + authname='joe', + args={'remove': '', 'sel': str(platform.id)}) + + provider = BuildConfigurationsAdminPageProvider(self.env) + try: + provider.render_admin_panel(req, 'bitten', 'configs', 'foo') + self.fail('Expected RequestDone') + + except RequestDone: + self.assertEqual('http://example.org/admin/bitten/configs/foo', + redirected_to[0]) + platforms = list(TargetPlatform.select(self.env, config='foo')) + self.assertEqual(0, len(platforms)) + + def test_process_remove_platforms_no_selection(self): + BuildConfig(self.env, name='foo', label='Foo', path='branches/foo', + active=True).insert() + platform = TargetPlatform(self.env, config='foo', name='any') + platform.insert() + + redirected_to = [] + def redirect(url): + redirected_to.append(url) + raise RequestDone + req = Mock(method='POST', perm=PermissionCache(self.env, 'joe'), + abs_href=Href('http://example.org/'), redirect=redirect, + authname='joe', + args={'remove': ''}) + + provider = BuildConfigurationsAdminPageProvider(self.env) + try: + provider.render_admin_panel(req, 'bitten', 'configs', 'foo') + self.fail('Expected TracError') + + except TracError, e: + self.assertEqual('No platform selected', e.message) + + def test_process_edit_platform(self): + BuildConfig(self.env, name='foo', label='Foo', path='branches/foo', + active=True).insert() + platform = TargetPlatform(self.env, config='foo', name='any') + platform.insert() + + req = Mock(method='GET', chrome={}, href=Href('/'), + perm=PermissionCache(self.env, 'joe'), args={}) + + provider = BuildConfigurationsAdminPageProvider(self.env) + template_name, data = provider.render_admin_panel( + req, 'bitten', 'configs', 'foo/%d' % platform.id + ) + + self.assertEqual('bitten_admin_configs.html', template_name) + assert 'platform' in data + platform = data['platform'] + self.assertEqual({ + 'id': 1, 'exists': True, 'name': 'any', 'rules': [('', '')], + }, platform) + + def test_process_update_platform(self): + BuildConfig(self.env, name='foo', label='Foo', path='branches/foo', + active=True).insert() + platform = TargetPlatform(self.env, config='foo', name='any') + platform.insert() + + redirected_to = [] + def redirect(url): + redirected_to.append(url) + raise RequestDone + req = Mock(method='POST', perm=PermissionCache(self.env, 'joe'), + abs_href=Href('http://example.org/'), redirect=redirect, + authname='joe', + args={'save': '', 'edit': '', 'name': 'Test', + 'property_0': 'family', 'pattern_0': 'posix'}) + + provider = BuildConfigurationsAdminPageProvider(self.env) + try: + provider.render_admin_panel(req, 'bitten', 'configs', + 'foo/%d' % platform.id) + self.fail('Expected RequestDone') + + except RequestDone: + self.assertEqual('http://example.org/admin/bitten/configs/foo', + redirected_to[0]) + platforms = list(TargetPlatform.select(self.env, config='foo')) + self.assertEqual(1, len(platforms)) + self.assertEqual('Test', platforms[0].name) + self.assertEqual([('family', 'posix')], platforms[0].rules) + + def test_process_update_platform_cancel(self): + BuildConfig(self.env, name='foo', label='Foo', path='branches/foo', + active=True).insert() + platform = TargetPlatform(self.env, config='foo', name='any') + platform.insert() + + redirected_to = [] + def redirect(url): + redirected_to.append(url) + raise RequestDone + req = Mock(method='POST', perm=PermissionCache(self.env, 'joe'), + abs_href=Href('http://example.org/'), redirect=redirect, + authname='joe', + args={'cancel': '', 'edit': '', 'name': 'Changed', + 'property_0': 'family', 'pattern_0': 'posix'}) + + provider = BuildConfigurationsAdminPageProvider(self.env) + try: + provider.render_admin_panel(req, 'bitten', 'configs', + 'foo/%d' % platform.id) + self.fail('Expected RequestDone') + + except RequestDone: + self.assertEqual('http://example.org/admin/bitten/configs/foo', + redirected_to[0]) + platforms = list(TargetPlatform.select(self.env, config='foo')) + self.assertEqual(1, len(platforms)) + self.assertEqual('any', platforms[0].name) + self.assertEqual([], platforms[0].rules) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite( + BuildMasterAdminPageProviderTestCase, 'test' + )) + suite.addTest(unittest.makeSuite( + BuildConfigurationsAdminPageProviderTestCase, 'test' + )) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/tests/master.py @@ -0,0 +1,748 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +import re +import shutil +from StringIO import StringIO +import tempfile +import unittest + +from trac.db import DatabaseManager +from trac.perm import PermissionCache, PermissionSystem +from trac.test import EnvironmentStub, Mock +from trac.web.api import HTTPBadRequest, HTTPMethodNotAllowed, HTTPNotFound, \ + HTTPForbidden, RequestDone +from trac.web.href import Href + +from bitten.master import BuildMaster +from bitten.model import BuildConfig, TargetPlatform, Build, BuildStep, \ + BuildLog, Report, schema + + +class BuildMasterTestCase(unittest.TestCase): + + def setUp(self): + self.env = EnvironmentStub(enable=['trac.*', 'bitten.*']) + self.env.path = tempfile.mkdtemp() + + PermissionSystem(self.env).grant_permission('hal', 'BUILD_EXEC') + + # Create tables + db = self.env.get_db_cnx() + cursor = db.cursor() + connector, _ = DatabaseManager(self.env)._get_connector() + for table in schema: + for stmt in connector.to_sql(table): + cursor.execute(stmt) + + self.repos = Mock() + self.env.get_repository = lambda authname=None: self.repos + + def tearDown(self): + shutil.rmtree(self.env.path) + + def test_create_build(self): + BuildConfig(self.env, 'test', path='somepath', active=True).insert() + platform = TargetPlatform(self.env, config='test', name="Unix") + platform.rules.append(('family', 'posix')) + platform.insert() + + self.repos = Mock( + get_node=lambda path, rev=None: Mock( + get_entries=lambda: [Mock(), Mock()], + get_history=lambda: [('somepath', 123, 'edit'), + ('somepath', 121, 'edit'), + ('somepath', 120, 'edit')] + ), + get_changeset=lambda rev: Mock(date=42), + normalize_path=lambda path: path, + rev_older_than=lambda rev1, rev2: rev1 < rev2 + ) + + inheaders = {'Content-Type': 'application/x-bitten+xml'} + inbody = StringIO("""<slave name="hal"> + <platform>Power Macintosh</platform> + <os family="posix" version="8.1.0">Darwin</os> + <package name="java" version="2.4.3"/> +</slave>""") + outheaders = {} + outbody = StringIO() + req = Mock(method='POST', base_path='', path_info='/builds', + href=Href('/trac'), abs_href=Href('http://example.org/trac'), + remote_addr='127.0.0.1', args={}, + perm=PermissionCache(self.env, 'hal'), + get_header=lambda x: inheaders.get(x), read=inbody.read, + send_response=lambda x: outheaders.setdefault('Status', x), + send_header=lambda x, y: outheaders.setdefault(x, y), + write=outbody.write) + + module = BuildMaster(self.env) + assert module.match_request(req) + try: + module.process_request(req) + self.fail('Expected RequestDone') + except RequestDone: + self.assertEqual(201, outheaders['Status']) + self.assertEqual('text/plain', outheaders['Content-Type']) + location = outheaders['Location'] + mo = re.match('http://example.org/trac/builds/(\d+)', location) + assert mo, 'Location was %r' % location + self.assertEqual('Build pending', outbody.getvalue()) + build = Build.fetch(self.env, int(mo.group(1))) + self.assertEqual(Build.IN_PROGRESS, build.status) + self.assertEqual('hal', build.slave) + + def test_create_build_invalid_xml(self): + inheaders = {'Content-Type': 'application/x-bitten+xml'} + inbody = StringIO('<slave></salve>') + req = Mock(method='POST', base_path='', path_info='/builds', + href=Href('/trac'), remote_addr='127.0.0.1', args={}, + perm=PermissionCache(self.env, 'hal'), + get_header=lambda x: inheaders.get(x), read=inbody.read) + + module = BuildMaster(self.env) + assert module.match_request(req) + try: + module.process_request(req) + self.fail('Expected HTTPBadRequest') + except HTTPBadRequest, e: + self.assertEqual('XML parser error', e.detail) + + def test_create_build_no_post(self): + req = Mock(method='GET', base_path='', path_info='/builds', + href=Href('/trac'), remote_addr='127.0.0.1', args={}, + perm=PermissionCache(self.env, 'hal')) + module = BuildMaster(self.env) + assert module.match_request(req) + try: + module.process_request(req) + self.fail('Expected HTTPMethodNotAllowed') + except HTTPMethodNotAllowed, e: + self.assertEqual('Method not allowed', e.detail) + + def test_create_build_no_match(self): + inheaders = {'Content-Type': 'application/x-bitten+xml'} + inbody = StringIO("""<slave name="hal"> + <platform>Power Macintosh</platform> + <os family="posix" version="8.1.0">Darwin</os> +</slave>""") + outheaders = {} + outbody = StringIO() + req = Mock(method='POST', base_path='', path_info='/builds', + href=Href('/trac'), remote_addr='127.0.0.1', args={}, + perm=PermissionCache(self.env, 'hal'), + get_header=lambda x: inheaders.get(x), read=inbody.read, + send_response=lambda x: outheaders.setdefault('Status', x), + send_header=lambda x, y: outheaders.setdefault(x, y), + write=outbody.write) + + module = BuildMaster(self.env) + assert module.match_request(req) + try: + module.process_request(req) + self.fail('Expected RequestDone') + except RequestDone: + self.assertEqual(204, outheaders['Status']) + self.assertEqual('', outbody.getvalue()) + + def test_cancel_build(self): + config = BuildConfig(self.env, 'test', path='somepath', active=True, + recipe='<build></build>') + config.insert() + build = Build(self.env, 'test', '123', 1, slave='hal', rev_time=42, + status=Build.IN_PROGRESS, started=42) + build.insert() + + outheaders = {} + outbody = StringIO() + req = Mock(method='DELETE', base_path='', + path_info='/builds/%d' % build.id, + href=Href('/trac'), remote_addr='127.0.0.1', args={}, + perm=PermissionCache(self.env, 'hal'), + send_response=lambda x: outheaders.setdefault('Status', x), + send_header=lambda x, y: outheaders.setdefault(x, y), + write=outbody.write) + + module = BuildMaster(self.env) + assert module.match_request(req) + try: + module.process_request(req) + self.fail('Expected RequestDone') + except RequestDone: + self.assertEqual(204, outheaders['Status']) + self.assertEqual('', outbody.getvalue()) + + # Make sure the started timestamp has been set + build = Build.fetch(self.env, build.id) + self.assertEqual(Build.PENDING, build.status) + assert not build.started + + def test_initiate_build(self): + config = BuildConfig(self.env, 'test', path='somepath', active=True, + recipe='<build></build>') + config.insert() + platform = TargetPlatform(self.env, config='test', name="Unix") + platform.rules.append(('family', 'posix')) + platform.insert() + build = Build(self.env, 'test', '123', platform.id, slave='hal', + rev_time=42) + build.insert() + + outheaders = {} + outbody = StringIO() + + req = Mock(method='GET', base_path='', + path_info='/builds/%d' % build.id, + href=Href('/trac'), remote_addr='127.0.0.1', args={}, + perm=PermissionCache(self.env, 'hal'), + send_response=lambda x: outheaders.setdefault('Status', x), + send_header=lambda x, y: outheaders.setdefault(x, y), + write=outbody.write) + + module = BuildMaster(self.env) + assert module.match_request(req) + try: + module.process_request(req) + self.fail('Expected RequestDone') + except RequestDone: + self.assertEqual(200, outheaders['Status']) + self.assertEqual('63', outheaders['Content-Length']) + self.assertEqual('application/x-bitten+xml', + outheaders['Content-Type']) + self.assertEqual('attachment; filename=recipe_test_r123.xml', + outheaders['Content-Disposition']) + self.assertEqual('<build build="1" config="test"' + ' path="somepath" revision="123"/>', + outbody.getvalue()) + + # Make sure the started timestamp has been set + build = Build.fetch(self.env, build.id) + assert build.started + + def test_initiate_build_no_such_build(self): + req = Mock(method='GET', base_path='', + path_info='/builds/123', href=Href('/trac'), + remote_addr='127.0.0.1', args={}, + perm=PermissionCache(self.env, 'hal')) + + module = BuildMaster(self.env) + assert module.match_request(req) + try: + module.process_request(req) + self.fail('Expected HTTPNotFound') + except HTTPNotFound, e: + self.assertEqual('No such build', e.detail) + + def test_process_unknown_collection(self): + BuildConfig(self.env, 'test', path='somepath', active=True, + recipe='<build></build>').insert() + build = Build(self.env, 'test', '123', 1, slave='hal', rev_time=42) + build.insert() + + req = Mock(method='POST', base_path='', + path_info='/builds/%d/files/' % build.id, + href=Href('/trac'), remote_addr='127.0.0.1', args={}, + perm=PermissionCache(self.env, 'hal')) + + module = BuildMaster(self.env) + assert module.match_request(req) + try: + module.process_request(req) + self.fail('Expected HTTPNotFound') + except HTTPNotFound, e: + self.assertEqual('No such collection', e.detail) + + def test_process_build_step_success(self): + recipe = """<build> + <step id="foo"> + </step> +</build>""" + BuildConfig(self.env, 'test', path='somepath', active=True, + recipe=recipe).insert() + build = Build(self.env, 'test', '123', 1, slave='hal', rev_time=42, + started=42, status=Build.IN_PROGRESS) + build.slave_info[Build.IP_ADDRESS] = '127.0.0.1'; + build.insert() + + inbody = StringIO("""<result step="foo" status="success" + time="2007-04-01T15:30:00.0000" + duration="3.45"> +</result>""") + outheaders = {} + outbody = StringIO() + req = Mock(method='POST', base_path='', + path_info='/builds/%d/steps/' % build.id, + href=Href('/trac'), abs_href=Href('http://example.org/trac'), + remote_addr='127.0.0.1', args={}, + perm=PermissionCache(self.env, 'hal'), + read=inbody.read, + send_response=lambda x: outheaders.setdefault('Status', x), + send_header=lambda x, y: outheaders.setdefault(x, y), + write=outbody.write) + module = BuildMaster(self.env) + assert module.match_request(req) + try: + module.process_request(req) + self.fail('Expected RequestDone') + except RequestDone: + self.assertEqual(201, outheaders['Status']) + self.assertEqual('20', outheaders['Content-Length']) + self.assertEqual('text/plain', outheaders['Content-Type']) + self.assertEqual('Build step processed', outbody.getvalue()) + + build = Build.fetch(self.env, build.id) + self.assertEqual(Build.SUCCESS, build.status) + assert build.stopped + assert build.stopped > build.started + + steps = list(BuildStep.select(self.env, build.id)) + self.assertEqual(1, len(steps)) + self.assertEqual('foo', steps[0].name) + self.assertEqual(BuildStep.SUCCESS, steps[0].status) + + def test_process_build_step_success_with_log(self): + recipe = """<build> + <step id="foo"> + </step> +</build>""" + BuildConfig(self.env, 'test', path='somepath', active=True, + recipe=recipe).insert() + build = Build(self.env, 'test', '123', 1, slave='hal', rev_time=42, + started=42, status=Build.IN_PROGRESS) + build.slave_info[Build.IP_ADDRESS] = '127.0.0.1'; + build.insert() + + inbody = StringIO("""<result step="foo" status="success" + time="2007-04-01T15:30:00.0000" + duration="3.45"> + <log generator="http://bitten.cmlenz.net/tools/python#unittest"> + <message level="info">Doing stuff</message> + <message level="error">Ouch that hurt</message> + </log> +</result>""") + outheaders = {} + outbody = StringIO() + req = Mock(method='POST', base_path='', + path_info='/builds/%d/steps/' % build.id, + href=Href('/trac'), abs_href=Href('http://example.org/trac'), + remote_addr='127.0.0.1', args={}, + perm=PermissionCache(self.env, 'hal'), + read=inbody.read, + send_response=lambda x: outheaders.setdefault('Status', x), + send_header=lambda x, y: outheaders.setdefault(x, y), + write=outbody.write) + module = BuildMaster(self.env) + assert module.match_request(req) + try: + module.process_request(req) + self.fail('Expected RequestDone') + except RequestDone: + self.assertEqual(201, outheaders['Status']) + self.assertEqual('20', outheaders['Content-Length']) + self.assertEqual('text/plain', outheaders['Content-Type']) + self.assertEqual('Build step processed', outbody.getvalue()) + + build = Build.fetch(self.env, build.id) + self.assertEqual(Build.SUCCESS, build.status) + assert build.stopped + assert build.stopped > build.started + + steps = list(BuildStep.select(self.env, build.id)) + self.assertEqual(1, len(steps)) + self.assertEqual('foo', steps[0].name) + self.assertEqual(BuildStep.SUCCESS, steps[0].status) + + logs = list(BuildLog.select(self.env, build=build.id, step='foo')) + self.assertEqual(1, len(logs)) + self.assertEqual('http://bitten.cmlenz.net/tools/python#unittest', + logs[0].generator) + self.assertEqual(2, len(logs[0].messages)) + self.assertEqual((u'info', u'Doing stuff'), logs[0].messages[0]) + self.assertEqual((u'error', u'Ouch that hurt'), logs[0].messages[1]) + + def test_process_build_step_success_with_report(self): + recipe = """<build> + <step id="foo"> + </step> +</build>""" + BuildConfig(self.env, 'test', path='somepath', active=True, + recipe=recipe).insert() + build = Build(self.env, 'test', '123', 1, slave='hal', rev_time=42, + started=42, status=Build.IN_PROGRESS) + build.slave_info[Build.IP_ADDRESS] = '127.0.0.1'; + build.insert() + + inbody = StringIO("""<result step="foo" status="success" + time="2007-04-01T15:30:00.0000" + duration="3.45"> + <report category="test" + generator="http://bitten.cmlenz.net/tools/python#unittest"> + <test fixture="my.Fixture" file="my/test/file.py"> + <stdout>Doing my thing</stdout> + </test> + </report> +</result>""") + outheaders = {} + outbody = StringIO() + req = Mock(method='POST', base_path='', + path_info='/builds/%d/steps/' % build.id, + href=Href('/trac'), abs_href=Href('http://example.org/trac'), + remote_addr='127.0.0.1', args={}, + perm=PermissionCache(self.env, 'hal'), + read=inbody.read, + send_response=lambda x: outheaders.setdefault('Status', x), + send_header=lambda x, y: outheaders.setdefault(x, y), + write=outbody.write) + module = BuildMaster(self.env) + assert module.match_request(req) + try: + module.process_request(req) + self.fail('Expected RequestDone') + except RequestDone: + self.assertEqual(201, outheaders['Status']) + self.assertEqual('20', outheaders['Content-Length']) + self.assertEqual('text/plain', outheaders['Content-Type']) + self.assertEqual('Build step processed', outbody.getvalue()) + + build = Build.fetch(self.env, build.id) + self.assertEqual(Build.SUCCESS, build.status) + assert build.stopped + assert build.stopped > build.started + + steps = list(BuildStep.select(self.env, build.id)) + self.assertEqual(1, len(steps)) + self.assertEqual('foo', steps[0].name) + self.assertEqual(BuildStep.SUCCESS, steps[0].status) + + reports = list(Report.select(self.env, build=build.id, step='foo')) + self.assertEqual(1, len(reports)) + self.assertEqual('test', reports[0].category) + self.assertEqual('http://bitten.cmlenz.net/tools/python#unittest', + reports[0].generator) + self.assertEqual(1, len(reports[0].items)) + self.assertEqual({ + 'fixture': 'my.Fixture', + 'file': 'my/test/file.py', + 'stdout': 'Doing my thing', + 'type': 'test', + }, reports[0].items[0]) + + def test_process_build_step_wrong_slave(self): + recipe = """<build> + <step id="foo"> + </step> +</build>""" + BuildConfig(self.env, 'test', path='somepath', active=True, + recipe=recipe).insert() + build = Build(self.env, 'test', '123', 1, slave='hal', rev_time=42, + started=42, status=Build.IN_PROGRESS) + build.slave_info[Build.IP_ADDRESS] = '192.168.1.1'; + build.insert() + + inbody = StringIO("""<result step="foo" status="success" + time="2007-04-01T15:30:00.0000" + duration="3.45"> + <log generator="http://bitten.cmlenz.net/tools/python#unittest"> + <message level="info">Doing stuff</message> + <message level="error">Ouch that hurt</message> + </log> +</result>""") + outheaders = {} + outbody = StringIO() + req = Mock(method='POST', base_path='', + path_info='/builds/%d/steps/' % build.id, + href=Href('/trac'), abs_href=Href('http://example.org/trac'), + remote_addr='127.0.0.1', args={}, + perm=PermissionCache(self.env, 'hal'), + read=inbody.read, + send_response=lambda x: outheaders.setdefault('Status', x), + send_header=lambda x, y: outheaders.setdefault(x, y), + write=outbody.write) + module = BuildMaster(self.env) + assert module.match_request(req) + try: + module.process_request(req) + self.fail('Expected HTTPForbidden') + except HTTPForbidden, e: + self.assertEqual('Build 1 has been invalidated for host 127.0.0.1.', e.detail) + + build = Build.fetch(self.env, build.id) + self.assertEqual(Build.IN_PROGRESS, build.status) + assert not build.stopped + + steps = list(BuildStep.select(self.env, build.id)) + self.assertEqual(0, len(steps)) + + + def test_process_build_step_invalidated_build(self): + recipe = """<build> + <step id="foo"> + </step> + <step id="foo2"> + </step> +</build>""" + BuildConfig(self.env, 'test', path='somepath', active=True, + recipe=recipe).insert() + build = Build(self.env, 'test', '123', 1, slave='hal', rev_time=42, + started=42, status=Build.IN_PROGRESS) + build.slave_info[Build.IP_ADDRESS] = '127.0.0.1'; + build.insert() + + inbody = StringIO("""<result step="foo" status="success" + time="2007-04-01T15:30:00.0000" + duration="3.45"> + <log generator="http://bitten.cmlenz.net/tools/python#unittest"> + <message level="info">Doing stuff</message> + <message level="error">Ouch that hurt</message> + </log> +</result>""") + outheaders = {} + outbody = StringIO() + req = Mock(method='POST', base_path='', + path_info='/builds/%d/steps/' % build.id, + href=Href('/trac'), abs_href=Href('http://example.org/trac'), + remote_addr='127.0.0.1', args={}, + perm=PermissionCache(self.env, 'hal'), + read=inbody.read, + send_response=lambda x: outheaders.setdefault('Status', x), + send_header=lambda x, y: outheaders.setdefault(x, y), + write=outbody.write) + module = BuildMaster(self.env) + assert module.match_request(req) + try: + module.process_request(req) + self.fail('Expected RequestDone') + except RequestDone: + build = Build.fetch(self.env, build.id) + self.assertEqual(Build.IN_PROGRESS, build.status) + assert not build.stopped + + steps = list(BuildStep.select(self.env, build.id)) + self.assertEqual(1, len(steps)) + + # invalidate the build. + + build = Build.fetch(self.env, build.id) + build.slave = None + build.status = Build.PENDING + build.update() + + # have this slave submit more data. + inbody = StringIO("""<result step="foo2" status="success" + time="2007-04-01T15:45:00.0000" + duration="4"> + <log generator="http://bitten.cmlenz.net/tools/python#unittest"> + <message level="info">This is a step after invalidation</message> + </log> +</result>""") + outheaders = {} + outbody = StringIO() + req = Mock(method='POST', base_path='', + path_info='/builds/%d/steps/' % build.id, + href=Href('/trac'), abs_href=Href('http://example.org/trac'), + remote_addr='127.0.0.1', args={}, + perm=PermissionCache(self.env, 'hal'), + read=inbody.read, + send_response=lambda x: outheaders.setdefault('Status', x), + send_header=lambda x, y: outheaders.setdefault(x, y), + write=outbody.write) + module = BuildMaster(self.env) + assert module.match_request(req) + try: + module.process_request(req) + self.fail('Build was invalidated. Should fail.'); + except HTTPForbidden, e: + self.assertEqual('Build 1 has been invalidated for host 127.0.0.1.', e.detail) + + build = Build.fetch(self.env, build.id) + self.assertEqual(Build.PENDING, build.status) + + def test_process_build_step_failure(self): + recipe = """<build> + <step id="foo"> + </step> +</build>""" + BuildConfig(self.env, 'test', path='somepath', active=True, + recipe=recipe).insert() + build = Build(self.env, 'test', '123', 1, slave='hal', rev_time=42, + started=42, status=Build.IN_PROGRESS) + build.slave_info[Build.IP_ADDRESS] = '127.0.0.1'; + build.insert() + + inbody = StringIO("""<result step="foo" status="failure" + time="2007-04-01T15:30:00.0000" + duration="3.45"> +</result>""") + outheaders = {} + outbody = StringIO() + req = Mock(method='POST', base_path='', + path_info='/builds/%d/steps/' % build.id, + href=Href('/trac'), abs_href=Href('http://example.org/trac'), + remote_addr='127.0.0.1', args={}, + perm=PermissionCache(self.env, 'hal'), + read=inbody.read, + send_response=lambda x: outheaders.setdefault('Status', x), + send_header=lambda x, y: outheaders.setdefault(x, y), + write=outbody.write) + module = BuildMaster(self.env) + assert module.match_request(req) + try: + module.process_request(req) + self.fail('Expected RequestDone') + except RequestDone: + self.assertEqual(201, outheaders['Status']) + self.assertEqual('20', outheaders['Content-Length']) + self.assertEqual('text/plain', outheaders['Content-Type']) + self.assertEqual('Build step processed', outbody.getvalue()) + + build = Build.fetch(self.env, build.id) + self.assertEqual(Build.FAILURE, build.status) + assert build.stopped + assert build.stopped > build.started + + steps = list(BuildStep.select(self.env, build.id)) + self.assertEqual(1, len(steps)) + self.assertEqual('foo', steps[0].name) + self.assertEqual(BuildStep.FAILURE, steps[0].status) + + def test_process_build_step_failure_ignored(self): + recipe = """<build> + <step id="foo" onerror="ignore"> + </step> +</build>""" + BuildConfig(self.env, 'test', path='somepath', active=True, + recipe=recipe).insert() + build = Build(self.env, 'test', '123', 1, slave='hal', rev_time=42, + started=42, status=Build.IN_PROGRESS) + build.slave_info[Build.IP_ADDRESS] = '127.0.0.1'; + + build.insert() + + inbody = StringIO("""<result step="foo" status="failure" + time="2007-04-01T15:30:00.0000" + duration="3.45"> +</result>""") + outheaders = {} + outbody = StringIO() + req = Mock(method='POST', base_path='', + path_info='/builds/%d/steps/' % build.id, + href=Href('/trac'), abs_href=Href('http://example.org/trac'), + remote_addr='127.0.0.1', args={}, + perm=PermissionCache(self.env, 'hal'), + read=inbody.read, + send_response=lambda x: outheaders.setdefault('Status', x), + send_header=lambda x, y: outheaders.setdefault(x, y), + write=outbody.write) + module = BuildMaster(self.env) + assert module.match_request(req) + try: + module.process_request(req) + self.fail('Expected RequestDone') + except RequestDone: + self.assertEqual(201, outheaders['Status']) + self.assertEqual('20', outheaders['Content-Length']) + self.assertEqual('text/plain', outheaders['Content-Type']) + self.assertEqual('Build step processed', outbody.getvalue()) + + build = Build.fetch(self.env, build.id) + self.assertEqual(Build.SUCCESS, build.status) + assert build.stopped + assert build.stopped > build.started + + steps = list(BuildStep.select(self.env, build.id)) + self.assertEqual(1, len(steps)) + self.assertEqual('foo', steps[0].name) + self.assertEqual(BuildStep.FAILURE, steps[0].status) + + def test_process_build_step_invalid_xml(self): + recipe = """<build> + <step id="foo"> + </step> +</build>""" + BuildConfig(self.env, 'test', path='somepath', active=True, + recipe=recipe).insert() + build = Build(self.env, 'test', '123', 1, slave='hal', rev_time=42, + started=42) + build.insert() + + inbody = StringIO("""<result></rsleut>""") + req = Mock(method='POST', base_path='', + path_info='/builds/%d/steps/' % build.id, + href=Href('/trac'), remote_addr='127.0.0.1', args={}, + perm=PermissionCache(self.env, 'hal'), + read=inbody.read) + + module = BuildMaster(self.env) + assert module.match_request(req) + try: + module.process_request(req) + self.fail('Expected HTTPBadRequest') + except HTTPBadRequest, e: + self.assertEqual('XML parser error', e.detail) + + def test_process_build_step_invalid_datetime(self): + recipe = """<build> + <step id="foo"> + </step> +</build>""" + BuildConfig(self.env, 'test', path='somepath', active=True, + recipe=recipe).insert() + build = Build(self.env, 'test', '123', 1, slave='hal', rev_time=42, + started=42, status=Build.IN_PROGRESS) + build.slave_info[Build.IP_ADDRESS] = '127.0.0.1'; + build.insert() + + inbody = StringIO("""<result step="foo" status="success" + time="sometime tomorrow maybe" + duration="3.45"> +</result>""") + req = Mock(method='POST', base_path='', + path_info='/builds/%d/steps/' % build.id, + href=Href('/trac'), remote_addr='127.0.0.1', args={}, + perm=PermissionCache(self.env, 'hal'), + read=inbody.read) + + module = BuildMaster(self.env) + assert module.match_request(req) + try: + module.process_request(req) + self.fail('Expected HTTPBadRequest') + except HTTPBadRequest, e: + self.assertEqual("Invalid ISO date/time 'sometime tomorrow maybe'", + e.detail) + + def test_process_build_step_no_post(self): + BuildConfig(self.env, 'test', path='somepath', active=True, + recipe='<build></build>').insert() + build = Build(self.env, 'test', '123', 1, slave='hal', rev_time=42, + started=42) + build.insert() + + req = Mock(method='GET', base_path='', + path_info='/builds/%d/steps/' % build.id, + href=Href('/trac'), remote_addr='127.0.0.1', args={}, + perm=PermissionCache(self.env, 'hal')) + + module = BuildMaster(self.env) + assert module.match_request(req) + try: + module.process_request(req) + self.fail('Expected HTTPMethodNotAllowed') + except HTTPMethodNotAllowed, e: + self.assertEqual('Method not allowed', e.detail) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(BuildMasterTestCase, 'test')) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/tests/model.py @@ -0,0 +1,733 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +import unittest + +from trac.db import DatabaseManager +from trac.test import EnvironmentStub +from bitten.model import BuildConfig, TargetPlatform, Build, BuildStep, \ + BuildLog, Report, schema + + +class BuildConfigTestCase(unittest.TestCase): + + def setUp(self): + self.env = EnvironmentStub() + self.env.path = '' + + db = self.env.get_db_cnx() + cursor = db.cursor() + connector, _ = DatabaseManager(self.env)._get_connector() + for table in schema: + for stmt in connector.to_sql(table): + cursor.execute(stmt) + db.commit() + + def test_new(self): + config = BuildConfig(self.env, name='test') + assert not config.exists + + def test_fetch(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("INSERT INTO bitten_config (name,path,label,active) " + "VALUES (%s,%s,%s,%s)", ('test', 'trunk', 'Test', 0)) + config = BuildConfig.fetch(self.env, name='test') + assert config.exists + self.assertEqual('test', config.name) + self.assertEqual('trunk', config.path) + self.assertEqual('Test', config.label) + self.assertEqual(False, config.active) + + def test_fetch_none(self): + config = BuildConfig.fetch(self.env, name='test') + self.assertEqual(None, config) + + def test_select_none(self): + configs = BuildConfig.select(self.env) + self.assertRaises(StopIteration, configs.next) + + def test_select_none(self): + configs = BuildConfig.select(self.env) + self.assertRaises(StopIteration, configs.next) + + def test_insert(self): + config = BuildConfig(self.env, name='test', path='trunk', label='Test') + config.insert() + + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("SELECT name,path,label,active,description " + "FROM bitten_config") + self.assertEqual(('test', 'trunk', 'Test', 0, ''), cursor.fetchone()) + + def test_insert_no_name(self): + config = BuildConfig(self.env) + self.assertRaises(AssertionError, config.insert) + + def test_update(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("INSERT INTO bitten_config (name,path,label,active) " + "VALUES (%s,%s,%s,%s)", ('test', 'trunk', 'Test', 0)) + + config = BuildConfig.fetch(self.env, 'test') + config.path = 'some_branch' + config.label = 'Updated' + config.active = True + config.description = 'Bla bla bla' + config.update() + + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("SELECT name,path,label,active,description " + "FROM bitten_config") + self.assertEqual(('test', 'some_branch', 'Updated', 1, 'Bla bla bla'), + cursor.fetchone()) + self.assertEqual(None, cursor.fetchone()) + + def test_update_name(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("INSERT INTO bitten_config (name,path,label,active) " + "VALUES (%s,%s,%s,%s)", ('test', 'trunk', 'Test', 0)) + + config = BuildConfig.fetch(self.env, 'test') + config.name = 'foobar' + config.update() + + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("SELECT name,path,label,active,description " + "FROM bitten_config") + self.assertEqual(('foobar', 'trunk', 'Test', 0, ''), cursor.fetchone()) + self.assertEqual(None, cursor.fetchone()) + + def test_update_no_name(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("INSERT INTO bitten_config (name,path,label,active) " + "VALUES (%s,%s,%s,%s)", ('test', 'trunk', 'Test', 0)) + + config = BuildConfig.fetch(self.env, 'test') + config.name = None + self.assertRaises(AssertionError, config.update) + + def test_update_name_with_platform(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("INSERT INTO bitten_config (name,path,label,active) " + "VALUES (%s,%s,%s,%s)", ('test', 'trunk', 'Test', 0)) + cursor.execute("INSERT INTO bitten_platform (config,name) " + "VALUES (%s,%s)", ('test', 'NetBSD')) + + config = BuildConfig.fetch(self.env, 'test') + config.name = 'foobar' + config.update() + + cursor.execute("SELECT config,name FROM bitten_platform") + self.assertEqual(('foobar', 'NetBSD'), cursor.fetchone()) + self.assertEqual(None, cursor.fetchone()) + + def test_delete(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("INSERT INTO bitten_config (name,path,label,active) " + "VALUES (%s,%s,%s,%s)", ('test', 'trunk', 'Test', 0)) + + config = BuildConfig.fetch(self.env, 'test') + config.delete() + self.assertEqual(False, config.exists) + + cursor.execute("SELECT * FROM bitten_config WHERE name=%s", ('test',)) + self.assertEqual(None, cursor.fetchone()) + + def test_delete_non_existing(self): + config = BuildConfig(self.env, 'test') + self.assertRaises(AssertionError, config.delete) + + +class TargetPlatformTestCase(unittest.TestCase): + + def setUp(self): + self.env = EnvironmentStub() + self.env.path = '' + + db = self.env.get_db_cnx() + cursor = db.cursor() + connector, _ = DatabaseManager(self.env)._get_connector() + for table in TargetPlatform._schema: + for stmt in connector.to_sql(table): + cursor.execute(stmt) + db.commit() + + def test_new(self): + platform = TargetPlatform(self.env) + self.assertEqual(False, platform.exists) + self.assertEqual([], platform.rules) + + def test_insert(self): + platform = TargetPlatform(self.env, config='test', name='Windows XP') + platform.rules += [(Build.OS_NAME, 'Windows'), (Build.OS_VERSION, 'XP')] + platform.insert() + + assert platform.exists + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("SELECT config,name FROM bitten_platform " + "WHERE id=%s", (platform.id,)) + self.assertEqual(('test', 'Windows XP'), cursor.fetchone()) + + cursor.execute("SELECT propname,pattern,orderno FROM bitten_rule " + "WHERE id=%s", (platform.id,)) + self.assertEqual((Build.OS_NAME, 'Windows', 0), cursor.fetchone()) + self.assertEqual((Build.OS_VERSION, 'XP', 1), cursor.fetchone()) + + def test_fetch(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("INSERT INTO bitten_platform (config,name) " + "VALUES (%s,%s)", ('test', 'Windows')) + id = db.get_last_id(cursor, 'bitten_platform') + platform = TargetPlatform.fetch(self.env, id) + assert platform.exists + self.assertEqual('test', platform.config) + self.assertEqual('Windows', platform.name) + + def test_select(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.executemany("INSERT INTO bitten_platform (config,name) " + "VALUES (%s,%s)", [('test', 'Windows'), + ('test', 'Mac OS X')]) + platforms = list(TargetPlatform.select(self.env, config='test')) + self.assertEqual(2, len(platforms)) + + +class BuildTestCase(unittest.TestCase): + + def setUp(self): + self.env = EnvironmentStub() + self.env.path = '' + + db = self.env.get_db_cnx() + cursor = db.cursor() + connector, _ = DatabaseManager(self.env)._get_connector() + for table in Build._schema: + for stmt in connector.to_sql(table): + cursor.execute(stmt) + db.commit() + + def test_new(self): + build = Build(self.env) + self.assertEqual(None, build.id) + self.assertEqual(Build.PENDING, build.status) + self.assertEqual(0, build.stopped) + self.assertEqual(0, build.started) + + def test_insert(self): + build = Build(self.env, config='test', rev='42', rev_time=12039, + platform=1) + build.slave_info.update({Build.IP_ADDRESS: '127.0.0.1', + Build.MAINTAINER: 'joe@example.org'}) + build.insert() + + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("SELECT config,rev,platform,slave,started,stopped,status" + " FROM bitten_build WHERE id=%s" % build.id) + self.assertEqual(('test', '42', 1, '', 0, 0, 'P'), cursor.fetchone()) + + cursor.execute("SELECT propname,propvalue FROM bitten_slave") + expected = {Build.IP_ADDRESS: '127.0.0.1', + Build.MAINTAINER: 'joe@example.org'} + for propname, propvalue in cursor: + self.assertEqual(expected[propname], propvalue) + + def test_insert_no_config_or_rev_or_rev_time_or_platform(self): + build = Build(self.env) + self.assertRaises(AssertionError, build.insert) + + build = Build(self.env, rev='42', rev_time=12039, platform=1) + self.assertRaises(AssertionError, build.insert) # No config + + build = Build(self.env, config='test', rev_time=12039, platform=1) + self.assertRaises(AssertionError, build.insert) # No rev + + build = Build(self.env, config='test', rev='42', platform=1) + self.assertRaises(AssertionError, build.insert) # No rev time + + build = Build(self.env, config='test', rev='42', rev_time=12039) + self.assertRaises(AssertionError, build.insert) # No platform + + def test_insert_no_slave(self): + build = Build(self.env, config='test', rev='42', rev_time=12039, + platform=1) + build.status = Build.SUCCESS + self.assertRaises(AssertionError, build.insert) + build.status = Build.FAILURE + self.assertRaises(AssertionError, build.insert) + build.status = Build.IN_PROGRESS + self.assertRaises(AssertionError, build.insert) + build.status = Build.PENDING + build.insert() + + def test_insert_invalid_status(self): + build = Build(self.env, config='test', rev='42', rev_time=12039, + status='DUNNO') + self.assertRaises(AssertionError, build.insert) + + def test_fetch(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("INSERT INTO bitten_build (config,rev,rev_time,platform," + "slave,started,stopped,status) " + "VALUES (%s,%s,%s,%s,%s,%s,%s,%s)", + ('test', '42', 12039, 1, 'tehbox', 15006, 16007, + Build.SUCCESS)) + build_id = db.get_last_id(cursor, 'bitten_build') + cursor.executemany("INSERT INTO bitten_slave VALUES (%s,%s,%s)", + [(build_id, Build.IP_ADDRESS, '127.0.0.1'), + (build_id, Build.MAINTAINER, 'joe@example.org')]) + + build = Build.fetch(self.env, build_id) + self.assertEquals(build_id, build.id) + self.assertEquals('127.0.0.1', build.slave_info[Build.IP_ADDRESS]) + self.assertEquals('joe@example.org', build.slave_info[Build.MAINTAINER]) + + def test_update(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("INSERT INTO bitten_build (config,rev,rev_time,platform," + "slave,started,stopped,status) " + "VALUES (%s,%s,%s,%s,%s,%s,%s,%s)", + ('test', '42', 12039, 1, 'tehbox', 15006, 16007, + Build.SUCCESS)) + build_id = db.get_last_id(cursor, 'bitten_build') + cursor.executemany("INSERT INTO bitten_slave VALUES (%s,%s,%s)", + [(build_id, Build.IP_ADDRESS, '127.0.0.1'), + (build_id, Build.MAINTAINER, 'joe@example.org')]) + + build = Build.fetch(self.env, build_id) + build.status = Build.FAILURE + build.update() + + +class BuildStepTestCase(unittest.TestCase): + + def setUp(self): + self.env = EnvironmentStub() + self.env.path = '' + + db = self.env.get_db_cnx() + cursor = db.cursor() + connector, _ = DatabaseManager(self.env)._get_connector() + for table in BuildStep._schema: + for stmt in connector.to_sql(table): + cursor.execute(stmt) + db.commit() + + def test_new(self): + step = BuildStep(self.env) + self.assertEqual(False, step.exists) + self.assertEqual(None, step.build) + self.assertEqual(None, step.name) + + def test_insert(self): + step = BuildStep(self.env, build=1, name='test', description='Foo bar', + status=BuildStep.SUCCESS) + step.insert() + self.assertEqual(True, step.exists) + + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("SELECT build,name,description,status,started,stopped " + "FROM bitten_step") + self.assertEqual((1, 'test', 'Foo bar', BuildStep.SUCCESS, 0, 0), + cursor.fetchone()) + + def test_insert_with_errors(self): + step = BuildStep(self.env, build=1, name='test', description='Foo bar', + status=BuildStep.SUCCESS) + step.errors += ['Foo', 'Bar'] + step.insert() + self.assertEqual(True, step.exists) + + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("SELECT build,name,description,status,started,stopped " + "FROM bitten_step") + self.assertEqual((1, 'test', 'Foo bar', BuildStep.SUCCESS, 0, 0), + cursor.fetchone()) + cursor.execute("SELECT message FROM bitten_error ORDER BY orderno") + self.assertEqual(('Foo',), cursor.fetchone()) + self.assertEqual(('Bar',), cursor.fetchone()) + + def test_insert_no_build_or_name(self): + step = BuildStep(self.env, name='test') + self.assertRaises(AssertionError, step.insert) # No build + + step = BuildStep(self.env, build=1) + self.assertRaises(AssertionError, step.insert) # No name + + def test_fetch(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("INSERT INTO bitten_step VALUES (%s,%s,%s,%s,%s,%s)", + (1, 'test', 'Foo bar', BuildStep.SUCCESS, 0, 0)) + + step = BuildStep.fetch(self.env, build=1, name='test') + self.assertEqual(1, step.build) + self.assertEqual('test', step.name) + self.assertEqual('Foo bar', step.description) + self.assertEqual(BuildStep.SUCCESS, step.status) + + def test_fetch_with_errors(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("INSERT INTO bitten_step VALUES (%s,%s,%s,%s,%s,%s)", + (1, 'test', 'Foo bar', BuildStep.SUCCESS, 0, 0)) + cursor.executemany("INSERT INTO bitten_error VALUES (%s,%s,%s,%s)", + [(1, 'test', 'Foo', 0), (1, 'test', 'Bar', 1)]) + + step = BuildStep.fetch(self.env, build=1, name='test') + self.assertEqual(1, step.build) + self.assertEqual('test', step.name) + self.assertEqual('Foo bar', step.description) + self.assertEqual(BuildStep.SUCCESS, step.status) + self.assertEqual(['Foo', 'Bar'], step.errors) + + def test_select(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.executemany("INSERT INTO bitten_step VALUES (%s,%s,%s,%s,%s,%s)", + [(1, 'test', 'Foo bar', BuildStep.SUCCESS, 1, 2), + (1, 'dist', 'Foo baz', BuildStep.FAILURE, 2, 3)]) + + steps = list(BuildStep.select(self.env, build=1)) + self.assertEqual(1, steps[0].build) + self.assertEqual('test', steps[0].name) + self.assertEqual('Foo bar', steps[0].description) + self.assertEqual(BuildStep.SUCCESS, steps[0].status) + self.assertEqual(1, steps[1].build) + self.assertEqual('dist', steps[1].name) + self.assertEqual('Foo baz', steps[1].description) + self.assertEqual(BuildStep.FAILURE, steps[1].status) + + +class BuildLogTestCase(unittest.TestCase): + + def setUp(self): + self.env = EnvironmentStub() + self.env.path = '' + + db = self.env.get_db_cnx() + cursor = db.cursor() + connector, _ = DatabaseManager(self.env)._get_connector() + for table in BuildLog._schema: + for stmt in connector.to_sql(table): + cursor.execute(stmt) + db.commit() + + def test_new(self): + log = BuildLog(self.env) + self.assertEqual(False, log.exists) + self.assertEqual(None, log.id) + self.assertEqual(None, log.build) + self.assertEqual(None, log.step) + self.assertEqual('', log.generator) + self.assertEqual([], log.messages) + + def test_insert(self): + log = BuildLog(self.env, build=1, step='test', generator='distutils') + log.messages = [ + (BuildLog.INFO, 'running tests'), + (BuildLog.ERROR, 'tests failed') + ] + log.insert() + self.assertNotEqual(None, log.id) + + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("SELECT build,step,generator FROM bitten_log " + "WHERE id=%s", (log.id,)) + self.assertEqual((1, 'test', 'distutils'), cursor.fetchone()) + cursor.execute("SELECT level,message FROM bitten_log_message " + "WHERE log=%s ORDER BY line", (log.id,)) + self.assertEqual((BuildLog.INFO, 'running tests'), cursor.fetchone()) + self.assertEqual((BuildLog.ERROR, 'tests failed'), cursor.fetchone()) + + def test_insert_empty(self): + log = BuildLog(self.env, build=1, step='test', generator='distutils') + log.messages = [] + log.insert() + self.assertNotEqual(None, log.id) + + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("SELECT build,step,generator FROM bitten_log " + "WHERE id=%s", (log.id,)) + self.assertEqual((1, 'test', 'distutils'), cursor.fetchone()) + cursor.execute("SELECT COUNT(*) FROM bitten_log_message " + "WHERE log=%s", (log.id,)) + self.assertEqual(0, cursor.fetchone()[0]) + + def test_insert_no_build_or_step(self): + log = BuildLog(self.env, step='test') + self.assertRaises(AssertionError, log.insert) # No build + + step = BuildStep(self.env, build=1) + self.assertRaises(AssertionError, log.insert) # No step + + def test_delete(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("INSERT INTO bitten_log (build,step,generator) " + "VALUES (%s,%s,%s)", (1, 'test', 'distutils')) + id = db.get_last_id(cursor, 'bitten_log') + cursor.executemany("INSERT INTO bitten_log_message " + "VALUES (%s,%s,%s,%s)", + [(id, 1, BuildLog.INFO, 'running tests'), + (id, 2, BuildLog.ERROR, 'tests failed')]) + + log = BuildLog.fetch(self.env, id=id, db=db) + self.assertEqual(True, log.exists) + log.delete() + self.assertEqual(False, log.exists) + + cursor.execute("SELECT * FROM bitten_log WHERE id=%s", (id,)) + self.assertEqual(True, not cursor.fetchall()) + cursor.execute("SELECT * FROM bitten_log_message WHERE log=%s", (id,)) + self.assertEqual(True, not cursor.fetchall()) + + def test_delete_new(self): + log = BuildLog(self.env, build=1, step='test', generator='foo') + self.assertRaises(AssertionError, log.delete) + + def test_fetch(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("INSERT INTO bitten_log (build,step,generator) " + "VALUES (%s,%s,%s)", (1, 'test', 'distutils')) + id = db.get_last_id(cursor, 'bitten_log') + cursor.executemany("INSERT INTO bitten_log_message " + "VALUES (%s,%s,%s,%s)", + [(id, 1, BuildLog.INFO, 'running tests'), + (id, 2, BuildLog.ERROR, 'tests failed')]) + + log = BuildLog.fetch(self.env, id=id, db=db) + self.assertEqual(True, log.exists) + self.assertEqual(id, log.id) + self.assertEqual(1, log.build) + self.assertEqual('test', log.step) + self.assertEqual('distutils', log.generator) + self.assertEqual((BuildLog.INFO, 'running tests'), log.messages[0]) + self.assertEqual((BuildLog.ERROR, 'tests failed'), log.messages[1]) + + def test_select(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("INSERT INTO bitten_log (build,step,generator) " + "VALUES (%s,%s,%s)", (1, 'test', 'distutils')) + id = db.get_last_id(cursor, 'bitten_log') + cursor.executemany("INSERT INTO bitten_log_message " + "VALUES (%s,%s,%s,%s)", + [(id, 1, BuildLog.INFO, 'running tests'), + (id, 2, BuildLog.ERROR, 'tests failed')]) + + logs = BuildLog.select(self.env, build=1, step='test', db=db) + log = logs.next() + self.assertEqual(True, log.exists) + self.assertEqual(id, log.id) + self.assertEqual(1, log.build) + self.assertEqual('test', log.step) + self.assertEqual('distutils', log.generator) + self.assertEqual((BuildLog.INFO, 'running tests'), log.messages[0]) + self.assertEqual((BuildLog.ERROR, 'tests failed'), log.messages[1]) + self.assertRaises(StopIteration, logs.next) + + +class ReportTestCase(unittest.TestCase): + + def setUp(self): + self.env = EnvironmentStub() + self.env.path = '' + + db = self.env.get_db_cnx() + cursor = db.cursor() + connector, _ = DatabaseManager(self.env)._get_connector() + for table in Report._schema: + for stmt in connector.to_sql(table): + cursor.execute(stmt) + db.commit() + + def test_delete(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("INSERT INTO bitten_report " + "(build,step,category,generator) VALUES (%s,%s,%s,%s)", + (1, 'test', 'test', 'unittest')) + report_id = db.get_last_id(cursor, 'bitten_report') + cursor.executemany("INSERT INTO bitten_report_item " + "(report,item,name,value) VALUES (%s,%s,%s,%s)", + [(report_id, 0, 'file', 'tests/foo.c'), + (report_id, 0, 'result', 'failure'), + (report_id, 1, 'file', 'tests/bar.c'), + (report_id, 1, 'result', 'success')]) + + report = Report.fetch(self.env, report_id, db=db) + report.delete(db=db) + self.assertEqual(False, report.exists) + report = Report.fetch(self.env, report_id, db=db) + self.assertEqual(None, report) + + def test_insert(self): + report = Report(self.env, build=1, step='test', category='test', + generator='unittest') + report.items = [ + {'file': 'tests/foo.c', 'status': 'failure'}, + {'file': 'tests/bar.c', 'status': 'success'} + ] + report.insert() + + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("SELECT build,step,category,generator " + "FROM bitten_report WHERE id=%s", (report.id,)) + self.assertEqual((1, 'test', 'test', 'unittest'), + cursor.fetchone()) + cursor.execute("SELECT item,name,value FROM bitten_report_item " + "WHERE report=%s ORDER BY item", (report.id,)) + items = [] + prev_item = None + for item, name, value in cursor: + if item != prev_item: + items.append({name: value}) + prev_item = item + else: + items[-1][name] = value + self.assertEquals(2, len(items)) + seen_foo, seen_bar = False, False + for item in items: + if item['file'] == 'tests/foo.c': + self.assertEqual('failure', item['status']) + seen_foo = True + if item['file'] == 'tests/bar.c': + self.assertEqual('success', item['status']) + seen_bar = True + self.assertEquals((True, True), (seen_foo, seen_bar)) + + def test_insert_dupe(self): + report = Report(self.env, build=1, step='test', category='test', + generator='unittest') + report.insert() + + report = Report(self.env, build=1, step='test', category='test', + generator='unittest') + self.assertRaises(AssertionError, report.insert) + + def test_insert_empty_items(self): + report = Report(self.env, build=1, step='test', category='test', + generator='unittest') + report.items = [{}, {}] + report.insert() + + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("SELECT build,step,category,generator " + "FROM bitten_report WHERE id=%s", (report.id,)) + self.assertEqual((1, 'test', 'test', 'unittest'), + cursor.fetchone()) + cursor.execute("SELECT COUNT(*) FROM bitten_report_item " + "WHERE report=%s", (report.id,)) + self.assertEqual(0, cursor.fetchone()[0]) + + def test_fetch(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("INSERT INTO bitten_report " + "(build,step,category,generator) VALUES (%s,%s,%s,%s)", + (1, 'test', 'test', 'unittest')) + report_id = db.get_last_id(cursor, 'bitten_report') + cursor.executemany("INSERT INTO bitten_report_item " + "(report,item,name,value) VALUES (%s,%s,%s,%s)", + [(report_id, 0, 'file', 'tests/foo.c'), + (report_id, 0, 'result', 'failure'), + (report_id, 1, 'file', 'tests/bar.c'), + (report_id, 1, 'result', 'success')]) + + report = Report.fetch(self.env, report_id) + self.assertEquals(report_id, report.id) + self.assertEquals('test', report.step) + self.assertEquals('test', report.category) + self.assertEquals('unittest', report.generator) + self.assertEquals(2, len(report.items)) + assert {'file': 'tests/foo.c', 'result': 'failure'} in report.items + assert {'file': 'tests/bar.c', 'result': 'success'} in report.items + + def test_select(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("INSERT INTO bitten_report " + "(build,step,category,generator) VALUES (%s,%s,%s,%s)", + (1, 'test', 'test', 'unittest')) + report1_id = db.get_last_id(cursor, 'bitten_report') + cursor.execute("INSERT INTO bitten_report " + "(build,step,category,generator) VALUES (%s,%s,%s,%s)", + (1, 'test', 'coverage', 'trace')) + report2_id = db.get_last_id(cursor, 'bitten_report') + cursor.executemany("INSERT INTO bitten_report_item " + "(report,item,name,value) VALUES (%s,%s,%s,%s)", + [(report1_id, 0, 'file', 'tests/foo.c'), + (report1_id, 0, 'result', 'failure'), + (report1_id, 1, 'file', 'tests/bar.c'), + (report1_id, 1, 'result', 'success'), + (report2_id, 0, 'file', 'tests/foo.c'), + (report2_id, 0, 'loc', '12'), + (report2_id, 0, 'cov', '50'), + (report2_id, 1, 'file', 'tests/bar.c'), + (report2_id, 1, 'loc', '20'), + (report2_id, 1, 'cov', '25')]) + + reports = Report.select(self.env, build=1, step='test') + for idx, report in enumerate(reports): + if report.id == report1_id: + self.assertEquals('test', report.step) + self.assertEquals('test', report.category) + self.assertEquals('unittest', report.generator) + self.assertEquals(2, len(report.items)) + assert {'file': 'tests/foo.c', 'result': 'failure'} \ + in report.items + assert {'file': 'tests/bar.c', 'result': 'success'} \ + in report.items + elif report.id == report1_id: + self.assertEquals('test', report.step) + self.assertEquals('coverage', report.category) + self.assertEquals('trace', report.generator) + self.assertEquals(2, len(report.items)) + assert {'file': 'tests/foo.c', 'loc': '12', 'cov': '50'} \ + in report.items + assert {'file': 'tests/bar.c', 'loc': '20', 'cov': '25'} \ + in report.items + self.assertEqual(1, idx) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(BuildConfigTestCase, 'test')) + suite.addTest(unittest.makeSuite(TargetPlatformTestCase, 'test')) + suite.addTest(unittest.makeSuite(BuildTestCase, 'test')) + suite.addTest(unittest.makeSuite(BuildStepTestCase, 'test')) + suite.addTest(unittest.makeSuite(BuildLogTestCase, 'test')) + suite.addTest(unittest.makeSuite(ReportTestCase, 'test')) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/tests/queue.py @@ -0,0 +1,358 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +import os +import shutil +import tempfile +import time +import unittest + +from trac.db import DatabaseManager +from trac.test import EnvironmentStub, Mock +from bitten.model import BuildConfig, TargetPlatform, Build, schema +from bitten.queue import BuildQueue, collect_changes + + +class CollectChangesTestCase(unittest.TestCase): + """ + Unit tests for the `bitten.queue.collect_changes` function. + """ + + def setUp(self): + self.env = EnvironmentStub() + self.env.path = tempfile.mkdtemp() + + db = self.env.get_db_cnx() + cursor = db.cursor() + connector, _ = DatabaseManager(self.env)._get_connector() + for table in schema: + for stmt in connector.to_sql(table): + cursor.execute(stmt) + + self.config = BuildConfig(self.env, name='test', path='somepath') + self.config.insert(db=db) + self.platform = TargetPlatform(self.env, config='test', name='Foo') + self.platform.insert(db=db) + db.commit() + + def tearDown(self): + shutil.rmtree(self.env.path) + + def test_stop_on_copy(self): + self.env.get_repository = lambda authname=None: Mock( + get_node=lambda path, rev=None: Mock( + get_history=lambda: [('otherpath', 123, 'copy')] + ), + normalize_path=lambda path: path + ) + + retval = list(collect_changes(self.env.get_repository(), self.config)) + self.assertEqual(0, len(retval)) + + def test_stop_on_minrev(self): + self.env.get_repository = lambda authname=None: Mock( + get_node=lambda path, rev=None: Mock( + get_entries=lambda: [Mock(), Mock()], + get_history=lambda: [('somepath', 123, 'edit'), + ('somepath', 121, 'edit'), + ('somepath', 120, 'edit')] + ), + normalize_path=lambda path: path, + rev_older_than=lambda rev1, rev2: rev1 < rev2 + ) + + self.config.min_rev = 123 + self.config.update() + + retval = list(collect_changes(self.env.get_repository(), self.config)) + self.assertEqual(1, len(retval)) + self.assertEqual(123, retval[0][1]) + + def test_skip_until_maxrev(self): + self.env.get_repository = lambda authname=None: Mock( + get_node=lambda path, rev=None: Mock( + get_entries=lambda: [Mock(), Mock()], + get_history=lambda: [('somepath', 123, 'edit'), + ('somepath', 121, 'edit'), + ('somepath', 120, 'edit')] + ), + normalize_path=lambda path: path, + rev_older_than=lambda rev1, rev2: rev1 < rev2 + ) + + self.config.max_rev=121 + self.config.update() + + retval = list(collect_changes(self.env.get_repository(), self.config)) + self.assertEqual(2, len(retval)) + self.assertEqual(121, retval[0][1]) + self.assertEqual(120, retval[1][1]) + + def test_skip_empty_dir(self): + def _mock_get_node(path, rev=None): + if rev and rev == 121: + return Mock( + get_entries=lambda: [] + ) + else: + return Mock( + get_entries=lambda: [Mock(), Mock()], + get_history=lambda: [('somepath', 123, 'edit'), + ('somepath', 121, 'edit'), + ('somepath', 120, 'edit')] + ) + + self.env.get_repository = lambda authname=None: Mock( + get_node=_mock_get_node, + normalize_path=lambda path: path, + rev_older_than=lambda rev1, rev2: rev1 < rev2 + ) + + retval = list(collect_changes(self.env.get_repository(), self.config)) + self.assertEqual(2, len(retval)) + self.assertEqual(123, retval[0][1]) + self.assertEqual(120, retval[1][1]) + + +class BuildQueueTestCase(unittest.TestCase): + + def setUp(self): + self.env = EnvironmentStub() + self.env.path = tempfile.mkdtemp() + os.mkdir(os.path.join(self.env.path, 'snapshots')) + + db = self.env.get_db_cnx() + cursor = db.cursor() + connector, _ = DatabaseManager(self.env)._get_connector() + for table in schema: + for stmt in connector.to_sql(table): + cursor.execute(stmt) + db.commit() + + # Hook up a dummy repository + self.repos = Mock() + self.env.get_repository = lambda authname=None: self.repos + + def tearDown(self): + shutil.rmtree(self.env.path) + + def test_get_build_for_slave(self): + """ + Make sure that a pending build of an activated configuration is + scheduled for a slave that matches the target platform. + """ + BuildConfig(self.env, 'test', active=True).insert() + platform = TargetPlatform(self.env, config='test', name='Foo') + platform.insert() + build = Build(self.env, config='test', platform=platform.id, rev=123, + rev_time=42, status=Build.PENDING) + build.insert() + build_id = build.id + + queue = BuildQueue(self.env) + build = queue.get_build_for_slave('foobar', {}) + self.assertEqual(build_id, build.id) + + def test_next_pending_build_no_matching_slave(self): + """ + Make sure that builds for which there is no slave matching the target + platform are not scheduled. + """ + BuildConfig(self.env, 'test', active=True).insert() + build = Build(self.env, config='test', platform=1, rev=123, rev_time=42, + status=Build.PENDING) + build.insert() + build_id = build.id + + queue = BuildQueue(self.env) + build = queue.get_build_for_slave('foobar', {}) + self.assertEqual(None, build) + + def test_next_pending_build_inactive_config(self): + """ + Make sure that builds for a deactived build config are not scheduled. + """ + BuildConfig(self.env, 'test').insert() + platform = TargetPlatform(self.env, config='test', name='Foo') + platform.insert() + build = Build(self.env, config='test', platform=platform.id, rev=123, + rev_time=42, status=Build.PENDING) + build.insert() + + queue = BuildQueue(self.env) + build = queue.get_build_for_slave('foobar', {}) + self.assertEqual(None, build) + + def test_populate_not_build_all(self): + self.env.get_repository = lambda authname=None: Mock( + get_changeset=lambda rev: Mock(date=rev * 1000), + get_node=lambda path, rev=None: Mock( + get_entries=lambda: [Mock(), Mock()], + get_history=lambda: [('somepath', 123, 'edit'), + ('somepath', 121, 'edit'), + ('somepath', 120, 'edit')] + ), + normalize_path=lambda path: path, + rev_older_than=lambda rev1, rev2: rev1 < rev2 + ) + BuildConfig(self.env, 'test', path='somepath', active=True).insert() + platform1 = TargetPlatform(self.env, config='test', name='P1') + platform1.insert() + platform2 = TargetPlatform(self.env, config='test', name='P2') + platform2.insert() + + queue = BuildQueue(self.env) + queue.populate() + queue.populate() + queue.populate() + + builds = list(Build.select(self.env, config='test')) + builds.sort(lambda a, b: cmp(a.platform, b.platform)) + self.assertEqual(2, len(builds)) + self.assertEqual(platform1.id, builds[0].platform) + self.assertEqual('123', builds[0].rev) + self.assertEqual(platform2.id, builds[1].platform) + self.assertEqual('123', builds[1].rev) + + def test_populate_build_all(self): + self.env.get_repository = lambda authname=None: Mock( + get_changeset=lambda rev: Mock(date=rev * 1000), + get_node=lambda path, rev=None: Mock( + get_entries=lambda: [Mock(), Mock()], + get_history=lambda: [('somepath', 123, 'edit'), + ('somepath', 121, 'edit'), + ('somepath', 120, 'edit')] + ), + normalize_path=lambda path: path, + rev_older_than=lambda rev1, rev2: rev1 < rev2 + ) + BuildConfig(self.env, 'test', path='somepath', active=True).insert() + platform1 = TargetPlatform(self.env, config='test', name='P1') + platform1.insert() + platform2 = TargetPlatform(self.env, config='test', name='P2') + platform2.insert() + + queue = BuildQueue(self.env, build_all=True) + queue.populate() + queue.populate() + queue.populate() + + builds = list(Build.select(self.env, config='test')) + builds.sort(lambda a, b: cmp(a.platform, b.platform)) + self.assertEqual(6, len(builds)) + self.assertEqual(platform1.id, builds[0].platform) + self.assertEqual('123', builds[0].rev) + self.assertEqual(platform1.id, builds[1].platform) + self.assertEqual('121', builds[1].rev) + self.assertEqual(platform1.id, builds[2].platform) + self.assertEqual('120', builds[2].rev) + self.assertEqual(platform2.id, builds[3].platform) + self.assertEqual('123', builds[3].rev) + self.assertEqual(platform2.id, builds[4].platform) + self.assertEqual('121', builds[4].rev) + self.assertEqual(platform2.id, builds[5].platform) + self.assertEqual('120', builds[5].rev) + + def test_reset_orphaned_builds(self): + BuildConfig(self.env, 'test').insert() + platform = TargetPlatform(self.env, config='test', name='Foo') + platform.insert() + build1 = Build(self.env, config='test', platform=platform.id, rev=123, + rev_time=42, status=Build.IN_PROGRESS, slave='heinz', + started=time.time() - 600) # Started ten minutes ago + build1.insert() + + build2 = Build(self.env, config='test', platform=platform.id, rev=124, + rev_time=42, status=Build.IN_PROGRESS, slave='heinz', + started=time.time() - 60) # Started a minute ago + build2.insert() + + queue = BuildQueue(self.env, timeout=300) # 5 minutes timeout + build = queue.reset_orphaned_builds() + self.assertEqual(Build.PENDING, Build.fetch(self.env, build1.id).status) + self.assertEqual(Build.IN_PROGRESS, + Build.fetch(self.env, build2.id).status) + + def test_match_slave_match(self): + BuildConfig(self.env, 'test', active=True).insert() + platform = TargetPlatform(self.env, config='test', name="Unix") + platform.rules.append(('family', 'posix')) + platform.insert() + platform_id = platform.id + + queue = BuildQueue(self.env) + platforms = queue.match_slave('foo', {'family': 'posix'}) + self.assertEqual(1, len(platforms)) + self.assertEqual(platform_id, platforms[0].id) + + def test_register_slave_match_simple_fail(self): + BuildConfig(self.env, 'test', active=True).insert() + platform = TargetPlatform(self.env, config='test', name="Unix") + platform.rules.append(('family', 'posix')) + platform.insert() + + queue = BuildQueue(self.env) + platforms = queue.match_slave('foo', {'family': 'nt'}) + self.assertEqual([], platforms) + + def test_register_slave_match_regexp(self): + BuildConfig(self.env, 'test', active=True).insert() + platform = TargetPlatform(self.env, config='test', name="Unix") + platform.rules.append(('version', '8\.\d\.\d')) + platform.insert() + platform_id = platform.id + + queue = BuildQueue(self.env) + platforms = queue.match_slave('foo', {'version': '8.2.0'}) + self.assertEqual(1, len(platforms)) + self.assertEqual(platform_id, platforms[0].id) + + def test_register_slave_match_regexp_multi(self): + BuildConfig(self.env, 'test', active=True).insert() + platform = TargetPlatform(self.env, config='test', name="Unix") + platform.rules.append(('os', '^Linux')) + platform.rules.append(('processor', '^[xi]\d?86$')) + platform.insert() + platform_id = platform.id + + queue = BuildQueue(self.env) + platforms = queue.match_slave('foo', {'os': 'Linux', 'processor': 'i686'}) + self.assertEqual(1, len(platforms)) + self.assertEqual(platform_id, platforms[0].id) + + def test_register_slave_match_regexp_fail(self): + BuildConfig(self.env, 'test', active=True).insert() + platform = TargetPlatform(self.env, config='test', name="Unix") + platform.rules.append(('version', '8\.\d\.\d')) + platform.insert() + + queue = BuildQueue(self.env) + platforms = queue.match_slave('foo', {'version': '7.8.1'}) + self.assertEqual([], platforms) + + def test_register_slave_match_regexp_invalid(self): + BuildConfig(self.env, 'test', active=True).insert() + platform = TargetPlatform(self.env, config='test', name="Unix") + platform.rules.append(('version', '8(\.\d')) + platform.insert() + + queue = BuildQueue(self.env) + platforms = queue.match_slave('foo', {'version': '7.8.1'}) + self.assertEqual([], platforms) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(CollectChangesTestCase, 'test')) + suite.addTest(unittest.makeSuite(BuildQueueTestCase, 'test')) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/tests/recipe.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +import os +import shutil +import tempfile +import unittest + +from bitten.recipe import Recipe, InvalidRecipeError +from bitten.util import xmlio + + +class RecipeTestCase(unittest.TestCase): + + def setUp(self): + self.basedir = os.path.realpath(tempfile.mkdtemp()) + + def tearDown(self): + shutil.rmtree(self.basedir) + + def test_empty_recipe(self): + xml = xmlio.parse('<build/>') + recipe = Recipe(xml, basedir=self.basedir) + self.assertEqual(self.basedir, recipe.ctxt.basedir) + steps = list(recipe) + self.assertEqual(0, len(steps)) + + def test_empty_step(self): + xml = xmlio.parse('<build>' + ' <step id="foo" description="Bar"></step>' + '</build>') + recipe = Recipe(xml, basedir=self.basedir) + steps = list(recipe) + self.assertEqual(1, len(steps)) + self.assertEqual('foo', steps[0].id) + self.assertEqual('Bar', steps[0].description) + self.assertEqual('fail', steps[0].onerror) + + def test_validate_bad_root(self): + xml = xmlio.parse('<foo></foo>') + recipe = Recipe(xml, basedir=self.basedir) + self.assertRaises(InvalidRecipeError, recipe.validate) + + def test_validate_no_steps(self): + xml = xmlio.parse('<build></build>') + recipe = Recipe(xml, basedir=self.basedir) + self.assertRaises(InvalidRecipeError, recipe.validate) + + def test_validate_child_not_step(self): + xml = xmlio.parse('<build><foo/></build>') + recipe = Recipe(xml, basedir=self.basedir) + self.assertRaises(InvalidRecipeError, recipe.validate) + + def test_validate_child_not_step(self): + xml = xmlio.parse('<build><foo/></build>') + recipe = Recipe(xml, basedir=self.basedir) + self.assertRaises(InvalidRecipeError, recipe.validate) + + def test_validate_step_without_id(self): + xml = xmlio.parse('<build><step><cmd/></step></build>') + recipe = Recipe(xml, basedir=self.basedir) + self.assertRaises(InvalidRecipeError, recipe.validate) + + def test_validate_step_with_empty_id(self): + xml = xmlio.parse('<build><step id=""><cmd/></step></build>') + recipe = Recipe(xml, basedir=self.basedir) + self.assertRaises(InvalidRecipeError, recipe.validate) + + def test_validate_step_without_commands(self): + xml = xmlio.parse('<build><step id="test"/></build>') + recipe = Recipe(xml, basedir=self.basedir) + self.assertRaises(InvalidRecipeError, recipe.validate) + + def test_validate_step_with_command_children(self): + xml = xmlio.parse('<build><step id="test">' + '<somecmd><child1/><child2/></somecmd>' + '</step></build>') + recipe = Recipe(xml, basedir=self.basedir) + self.assertRaises(InvalidRecipeError, recipe.validate) + + def test_validate_step_with_duplicate_id(self): + xml = xmlio.parse('<build>' + '<step id="test"><somecmd></somecmd></step>' + '<step id="test"><othercmd></othercmd></step>' + '</build>') + recipe = Recipe(xml, basedir=self.basedir) + self.assertRaises(InvalidRecipeError, recipe.validate) + + def test_validate_successful(self): + xml = xmlio.parse('<build>' + '<step id="foo"><somecmd></somecmd></step>' + '<step id="bar"><othercmd></othercmd></step>' + '</build>') + recipe = Recipe(xml, basedir=self.basedir) + recipe.validate() + +def suite(): + return unittest.makeSuite(RecipeTestCase, 'test') + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/tests/slave.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +import os +import shutil +import tempfile +import unittest + +from trac.test import Mock +from bitten.slave import BuildSlave + + +class BuildSlaveTestCase(unittest.TestCase): + + def setUp(self): + self.work_dir = tempfile.mkdtemp(prefix='bitten_test') + self.slave = BuildSlave(None, work_dir=self.work_dir) + + def tearDown(self): + shutil.rmtree(self.work_dir) + + def _create_file(self, *path): + filename = os.path.join(self.work_dir, *path) + fd = file(filename, 'w') + fd.close() + return filename + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(BuildSlaveTestCase, 'test')) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/tests/web_ui.py @@ -0,0 +1,235 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +import shutil +import tempfile +import unittest + +from trac.core import TracError +from trac.db import DatabaseManager +from trac.perm import PermissionCache, PermissionSystem +from trac.test import EnvironmentStub, Mock +from trac.util.html import Markup +from trac.web.href import Href +from bitten.main import BuildSystem +from bitten.model import Build, BuildConfig, BuildStep, TargetPlatform, schema +from bitten.web_ui import BuildConfigController, SourceFileLinkFormatter + + +class BuildConfigControllerTestCase(unittest.TestCase): + + def setUp(self): + self.env = EnvironmentStub(enable=['trac.*', 'bitten.*']) + self.env.path = tempfile.mkdtemp() + + # Create tables + db = self.env.get_db_cnx() + cursor = db.cursor() + connector, _ = DatabaseManager(self.env)._get_connector() + for table in schema: + for stmt in connector.to_sql(table): + cursor.execute(stmt) + + # Set up permissions + self.env.config.set('trac', 'permission_store', + 'DefaultPermissionStore') + + # Hook up a dummy repository + self.repos = Mock( + get_node=lambda path, rev=None: Mock(get_history=lambda: [], + isdir=True), + normalize_path=lambda path: path, + sync=lambda: None + ) + self.env.get_repository = lambda authname=None: self.repos + + def tearDown(self): + shutil.rmtree(self.env.path) + + def test_overview(self): + PermissionSystem(self.env).grant_permission('joe', 'BUILD_VIEW') + req = Mock(method='GET', base_path='', cgi_location='', + path_info='/build', href=Href('/trac'), args={}, chrome={}, + perm=PermissionCache(self.env, 'joe')) + + module = BuildConfigController(self.env) + assert module.match_request(req) + _, data, _ = module.process_request(req) + + self.assertEqual('overview', data['page_mode']) + + def test_view_config(self): + config = BuildConfig(self.env, name='test', path='trunk') + config.insert() + platform = TargetPlatform(self.env, config='test', name='any') + platform.insert() + + PermissionSystem(self.env).grant_permission('joe', 'BUILD_VIEW') + req = Mock(method='GET', base_path='', cgi_location='', + path_info='/build/test', href=Href('/trac'), args={}, + chrome={}, authname='joe', + perm=PermissionCache(self.env, 'joe')) + + root = Mock(get_entries=lambda: ['foo'], + get_history=lambda: [('trunk', rev, 'edit') for rev in + range(123, 111, -1)]) + self.repos = Mock(get_node=lambda path, rev=None: root, + sync=lambda: None, normalize_path=lambda path: path) + + module = BuildConfigController(self.env) + assert module.match_request(req) + _, data, _ = module.process_request(req) + + self.assertEqual('view_config', data['page_mode']) + assert not 'next' in req.chrome['links'] + + def test_view_config_paging(self): + config = BuildConfig(self.env, name='test', path='trunk') + config.insert() + platform = TargetPlatform(self.env, config='test', name='any') + platform.insert() + + PermissionSystem(self.env).grant_permission('joe', 'BUILD_VIEW') + req = Mock(method='GET', base_path='', cgi_location='', + path_info='/build/test', href=Href('/trac'), args={}, + chrome={}, authname='joe', + perm=PermissionCache(self.env, 'joe')) + + root = Mock(get_entries=lambda: ['foo'], + get_history=lambda: [('trunk', rev, 'edit') for rev in + range(123, 110, -1)]) + self.repos = Mock(get_node=lambda path, rev=None: root, + sync=lambda: None, normalize_path=lambda path: path) + + module = BuildConfigController(self.env) + assert module.match_request(req) + _, data, _ = module.process_request(req) + + if req.chrome: + self.assertEqual('/trac/build/test?page=2', + req.chrome['links']['next'][0]['href']) + + +class SourceFileLinkFormatterTestCase(unittest.TestCase): + + def setUp(self): + self.env = EnvironmentStub(enable=['trac.*', 'bitten.*']) + + # Create tables + db = self.env.get_db_cnx() + cursor = db.cursor() + connector, _ = DatabaseManager(self.env)._get_connector() + for table in schema: + for stmt in connector.to_sql(table): + cursor.execute(stmt) + + # Hook up a dummy repository + self.repos = Mock( + get_node=lambda path, rev=None: Mock(get_history=lambda: [], + isdir=True), + normalize_path=lambda path: path, + sync=lambda: None + ) + self.env.get_repository = lambda authname=None: self.repos + + def tearDown(self): + pass + + def test_format_simple_link_in_repos(self): + BuildConfig(self.env, name='test', path='trunk').insert() + build = Build(self.env, config='test', platform=1, rev=123, rev_time=42, + status=Build.SUCCESS, slave='hal') + build.insert() + step = BuildStep(self.env, build=build.id, name='foo', + status=BuildStep.SUCCESS) + step.insert() + + self.repos.get_node = lambda path, rev: (path, rev) + + req = Mock(method='GET', href=Href('/trac'), authname='hal') + comp = SourceFileLinkFormatter(self.env) + formatter = comp.get_formatter(req, build) + + output = formatter(step, None, None, u'error in foo/bar.c: bad') + self.assertEqual(Markup, type(output)) + self.assertEqual('error in <a href="/trac/browser/trunk/foo/bar.c">' + 'foo/bar.c</a>: bad', output) + + def test_format_simple_link_not_in_repos(self): + BuildConfig(self.env, name='test', path='trunk').insert() + build = Build(self.env, config='test', platform=1, rev=123, rev_time=42, + status=Build.SUCCESS, slave='hal') + build.insert() + step = BuildStep(self.env, build=build.id, name='foo', + status=BuildStep.SUCCESS) + step.insert() + + def _raise(): + raise TracError('No such node') + self.repos.get_node = lambda path, rev: _raise() + + req = Mock(method='GET', href=Href('/trac'), authname='hal') + comp = SourceFileLinkFormatter(self.env) + formatter = comp.get_formatter(req, build) + + output = formatter(step, None, None, u'error in foo/bar.c: bad') + self.assertEqual(Markup, type(output)) + self.assertEqual('error in foo/bar.c: bad', output) + + def test_format_link_in_repos_with_line(self): + BuildConfig(self.env, name='test', path='trunk').insert() + build = Build(self.env, config='test', platform=1, rev=123, rev_time=42, + status=Build.SUCCESS, slave='hal') + build.insert() + step = BuildStep(self.env, build=build.id, name='foo', + status=BuildStep.SUCCESS) + step.insert() + + self.repos.get_node = lambda path, rev: (path, rev) + + req = Mock(method='GET', href=Href('/trac'), authname='hal') + comp = SourceFileLinkFormatter(self.env) + formatter = comp.get_formatter(req, build) + + output = formatter(step, None, None, u'error in foo/bar.c:123: bad') + self.assertEqual(Markup, type(output)) + self.assertEqual('error in <a href="/trac/browser/trunk/foo/bar.c#L123">' + 'foo/bar.c:123</a>: bad', output) + + def test_format_link_not_in_repos_with_line(self): + BuildConfig(self.env, name='test', path='trunk').insert() + build = Build(self.env, config='test', platform=1, rev=123, rev_time=42, + status=Build.SUCCESS, slave='hal') + build.insert() + step = BuildStep(self.env, build=build.id, name='foo', + status=BuildStep.SUCCESS) + step.insert() + + def _raise(): + raise TracError('No such node') + self.repos.get_node = lambda path, rev: _raise() + + req = Mock(method='GET', href=Href('/trac'), authname='hal') + comp = SourceFileLinkFormatter(self.env) + formatter = comp.get_formatter(req, build) + + output = formatter(step, None, None, u'error in foo/bar.c:123: bad') + self.assertEqual(Markup, type(output)) + self.assertEqual('error in foo/bar.c:123: bad', output) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(BuildConfigControllerTestCase, 'test')) + suite.addTest(unittest.makeSuite(SourceFileLinkFormatterTestCase, 'test')) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/upgrades.py @@ -0,0 +1,291 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +"""Automated upgrades for the Bitten database tables, and other data stored +in the Trac environment.""" + +import os +import sys + +from trac.db import DatabaseManager + +__docformat__ = 'restructuredtext en' + +def add_log_table(env, db): + """Add a table for storing the builds logs.""" + from bitten.model import BuildLog, BuildStep + cursor = db.cursor() + + connector, _ = DatabaseManager(env)._get_connector() + for table in BuildLog._schema: + for stmt in connector.to_sql(table): + cursor.execute(stmt) + + cursor.execute("SELECT build,name,log FROM bitten_step " + "WHERE log IS NOT NULL") + for build, step, log in cursor: + build_log = BuildLog(env, build, step) + build_log.messages = [(BuildLog.INFO, msg) for msg in log.splitlines()] + build_log.insert(db) + + cursor.execute("CREATE TEMP TABLE old_step AS SELECT * FROM bitten_step") + cursor.execute("DROP TABLE bitten_step") + for table in BuildStep._schema: + for stmt in connector.to_sql(table): + cursor.execute(stmt) + cursor.execute("INSERT INTO bitten_step (build,name,description,status," + "started,stopped) SELECT build,name,description,status," + "started,stopped FROM old_step") + +def add_recipe_to_config(env, db): + """Add a column for storing the build recipe to the build configuration + table.""" + from bitten.model import BuildConfig + cursor = db.cursor() + + cursor.execute("CREATE TEMP TABLE old_config AS " + "SELECT * FROM bitten_config") + cursor.execute("DROP TABLE bitten_config") + + connector, _ = DatabaseManager(env)._get_connector() + for table in BuildConfig._schema: + for stmt in connector.to_sql(table): + cursor.execute(stmt) + + cursor.execute("INSERT INTO bitten_config (name,path,active,recipe,min_rev," + "max_rev,label,description) SELECT name,path,0,'',NULL," + "NULL,label,description FROM old_config") + +def add_config_to_reports(env, db): + """Add the name of the build configuration as metadata to report documents + stored in the BDB XML database.""" + + from bitten.model import Build + try: + from bsddb3 import db as bdb + import dbxml + except ImportError: + return + + dbfile = os.path.join(env.path, 'db', 'bitten.dbxml') + if not os.path.isfile(dbfile): + return + + dbenv = bdb.DBEnv() + dbenv.open(os.path.dirname(dbfile), + bdb.DB_CREATE | bdb.DB_INIT_LOCK | bdb.DB_INIT_LOG | + bdb.DB_INIT_MPOOL | bdb.DB_INIT_TXN, 0) + + mgr = dbxml.XmlManager(dbenv, 0) + xtn = mgr.createTransaction() + container = mgr.openContainer(dbfile, dbxml.DBXML_TRANSACTIONAL) + uc = mgr.createUpdateContext() + + container.addIndex(xtn, '', 'config', 'node-metadata-equality-string', uc) + + qc = mgr.createQueryContext() + for value in mgr.query(xtn, 'collection("%s")/report' % dbfile, qc): + doc = value.asDocument() + metaval = dbxml.XmlValue() + if doc.getMetaData('', 'build', metaval): + build_id = int(metaval.asNumber()) + build = Build.fetch(env, id=build_id, db=db) + if build: + doc.setMetaData('', 'config', dbxml.XmlValue(build.config)) + container.updateDocument(xtn, doc, uc) + else: + # an orphaned report, for whatever reason... just remove it + container.deleteDocument(xtn, doc, uc) + + xtn.commit() + container.close() + dbenv.close(0) + +def add_order_to_log(env, db): + """Add order column to log table to make sure that build logs are displayed + in the order they were generated.""" + from bitten.model import BuildLog + cursor = db.cursor() + + cursor.execute("CREATE TEMP TABLE old_log AS " + "SELECT * FROM bitten_log") + cursor.execute("DROP TABLE bitten_log") + + connector, _ = DatabaseManager(env)._get_connector() + for stmt in connector.to_sql(BuildLog._schema[0]): + cursor.execute(stmt) + + cursor.execute("INSERT INTO bitten_log (id,build,step,generator,orderno) " + "SELECT id,build,step,type,0 FROM old_log") + +def add_report_tables(env, db): + """Add database tables for report storage.""" + from bitten.model import Report + cursor = db.cursor() + + connector, _ = DatabaseManager(env)._get_connector() + for table in Report._schema: + for stmt in connector.to_sql(table): + cursor.execute(stmt) + +def xmldb_to_db(env, db): + """Migrate report data from Berkeley DB XML to SQL database. + + Depending on the number of reports stored, this might take rather long. + After the upgrade is done, the bitten.dbxml file (and any BDB XML log files) + may be deleted. BDB XML is no longer used by Bitten. + """ + from bitten.model import Report + from bitten.util import xmlio + try: + from bsddb3 import db as bdb + import dbxml + except ImportError: + return + + dbfile = os.path.join(env.path, 'db', 'bitten.dbxml') + if not os.path.isfile(dbfile): + return + + dbenv = bdb.DBEnv() + dbenv.open(os.path.dirname(dbfile), + bdb.DB_CREATE | bdb.DB_INIT_LOCK | bdb.DB_INIT_LOG | + bdb.DB_INIT_MPOOL | bdb.DB_INIT_TXN, 0) + + mgr = dbxml.XmlManager(dbenv, 0) + xtn = mgr.createTransaction() + container = mgr.openContainer(dbfile, dbxml.DBXML_TRANSACTIONAL) + + def get_pylint_items(xml): + for problems_elem in xml.children('problems'): + for problem_elem in problems_elem.children('problem'): + item = {'type': 'problem'} + item.update(problem_elem.attr) + yield item + + def get_trace_items(xml): + for cov_elem in xml.children('coverage'): + item = {'type': 'coverage', 'name': cov_elem.attr['module'], + 'file': cov_elem.attr['file'], + 'percentage': cov_elem.attr['percentage']} + lines = 0 + line_hits = [] + for line_elem in cov_elem.children('line'): + lines += 1 + line_hits.append(line_elem.attr['hits']) + item['lines'] = lines + item['line_hits'] = ' '.join(line_hits) + yield item + + def get_unittest_items(xml): + for test_elem in xml.children('test'): + item = {'type': 'test'} + item.update(test_elem.attr) + for child_elem in test_elem.children(): + item[child_elem.name] = child_elem.gettext() + yield item + + qc = mgr.createQueryContext() + for value in mgr.query(xtn, 'collection("%s")/report' % dbfile, qc, 0): + doc = value.asDocument() + metaval = dbxml.XmlValue() + build, step = None, None + if doc.getMetaData('', 'build', metaval): + build = metaval.asNumber() + if doc.getMetaData('', 'step', metaval): + step = metaval.asString() + + report_types = {'pylint': ('lint', get_pylint_items), + 'trace': ('coverage', get_trace_items), + 'unittest': ('test', get_unittest_items)} + xml = xmlio.parse(value.asString()) + report_type = xml.attr['type'] + category, get_items = report_types[report_type] + sys.stderr.write('.') + sys.stderr.flush() + report = Report(env, build, step, category=category, + generator=report_type) + report.items = list(get_items(xml)) + try: + report.insert(db=db) + except AssertionError: + # Duplicate report, skip + pass + sys.stderr.write('\n') + sys.stderr.flush() + + xtn.abort() + container.close() + dbenv.close(0) + +def normalize_file_paths(env, db): + """Normalize the file separator in file names in reports.""" + cursor = db.cursor() + cursor.execute("SELECT report,item,value FROM bitten_report_item " + "WHERE name='file'") + rows = cursor.fetchall() or [] + for report, item, value in rows: + if '\\' in value: + cursor.execute("UPDATE bitten_report_item SET value=%s " + "WHERE report=%s AND item=%s AND name='file'", + (value.replace('\\', '/'), report, item)) + +def fixup_generators(env, db): + """Upgrade the identifiers for the recipe commands that generated log + messages and report data.""" + + mapping = { + 'pipe': 'http://bitten.cmlenz.net/tools/sh#pipe', + 'make': 'http://bitten.cmlenz.net/tools/c#make', + 'distutils': 'http://bitten.cmlenz.net/tools/python#distutils', + 'exec_': 'http://bitten.cmlenz.net/tools/python#exec' # Ambigious + } + cursor = db.cursor() + cursor.execute("SELECT id,generator FROM bitten_log " + "WHERE generator IN (%s)" + % ','.join([repr(key) for key in mapping.keys()])) + for log_id, generator in cursor: + cursor.execute("UPDATE bitten_log SET generator=%s " + "WHERE id=%s", (mapping[generator], log_id)) + + mapping = { + 'unittest': 'http://bitten.cmlenz.net/tools/python#unittest', + 'trace': 'http://bitten.cmlenz.net/tools/python#trace', + 'pylint': 'http://bitten.cmlenz.net/tools/python#pylint' + } + cursor.execute("SELECT id,generator FROM bitten_report " + "WHERE generator IN (%s)" + % ','.join([repr(key) for key in mapping.keys()])) + for report_id, generator in cursor: + cursor.execute("UPDATE bitten_report SET generator=%s " + "WHERE id=%s", (mapping[generator], report_id)) + +def add_error_table(env, db): + """Add the bitten_error table for recording step failure reasons.""" + from trac.db import Table, Column + + table = Table('bitten_error', key=('build', 'step', 'orderno'))[ + Column('build', type='int'), Column('step'), Column('message'), + Column('orderno', type='int') + ] + cursor = db.cursor() + + connector, _ = DatabaseManager(env)._get_connector() + for stmt in connector.to_sql(table): + cursor.execute(stmt) + +map = { + 2: [add_log_table], + 3: [add_recipe_to_config], + 4: [add_config_to_reports], + 5: [add_order_to_log, add_report_tables, xmldb_to_db], + 6: [normalize_file_paths, fixup_generators], + 7: [add_error_table] +}
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/util/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +"""Generic utility functions and classes. + +Functionality in these modules have no dependencies on modules outside of this +package, so that they could theoretically be used in other projects. +""" + +__docformat__ = 'restructuredtext en'
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/util/loc.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 1998 Dinu C. Gherman <gherman@europemail.com> +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2007 Edgewall Software +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. +# +# This module is based on the pycount.py script written by Dinu C. +# Gherman, and is used here under the following license: +# +# Permission to use, copy, modify, and distribute this software +# and its documentation without fee and for any purpose, except +# direct commerial advantage, is hereby granted, provided that +# the above copyright notice appear in all copies and that both +# that copyright notice and this permission notice appear in +# supporting documentation. + +"""Support for counting the lines of code in Python programs.""" + +import re + +__all__ = ['BLANK', 'CODE', 'COMMENT', 'DOC', 'count'] +__docformat__ = 'restructuredtext en' + +# Reg. exps. to find the end of a triple quote, given that +# we know we're in one; use the "match" method; .span()[1] +# will be the index of the character following the final +# quote. +_squote3_finder = re.compile( + r"([^\']|" + r"\.|" + r"'[^\']|" + r"'\.|" + r"''[^\']|" + r"''\.)*'''") + +_dquote3_finder = re.compile( + r'([^\"]|' + r'\.|' + r'"[^\"]|' + r'"\.|' + r'""[^\"]|' + r'""\.)*"""') + +# Reg. exps. to find the leftmost one-quoted string; use the +# "search" method; .span()[0] bounds the string found. +_dquote1_finder = re.compile(r'"([^"]|\.)*"') +_squote1_finder = re.compile(r"'([^']|\.)*'") + +# _is_comment matches pure comment line. +_is_comment = re.compile(r"^[ \t]*#").match + +# _is_blank matches empty line. +_is_blank = re.compile(r"^[ \t]*$").match + +# find leftmost splat or quote. +_has_nightmare = re.compile(r"""[\"'#]""").search + +# _is_doc_candidate matches lines that start with a triple quote. +_is_doc_candidate = re.compile(r"^[ \t]*('''|\"\"\")") + +BLANK, CODE, COMMENT, DOC = 0, 1, 2, 3 + +def count(source): + """Parse the given file-like object as Python source code. + + For every line in the code, this function yields a ``(lineno, type, line)`` + tuple, where ``lineno`` is the line number (starting at 0), ``type`` is + one of `BLANK`, `CODE`, `COMMENT` or `DOC`, and ``line`` is the actual + content of the line. + + :param source: a file-like object containing Python code + """ + + quote3_finder = {'"': _dquote3_finder, "'": _squote3_finder} + quote1_finder = {'"': _dquote1_finder, "'": _squote1_finder } + + in_doc = False + in_triple_quote = None + + for lineno, line in enumerate(source): + classified = False + + if in_triple_quote: + if in_doc: + yield lineno, DOC, line + else: + yield lineno, CODE, line + classified = True + m = in_triple_quote.match(line) + if m == None: + continue + # Get rid of everything through the end of the triple. + end = m.span()[1] + line = line[end:] + in_doc = in_triple_quote = False + + if _is_blank(line): + if not classified: + yield lineno, BLANK, line + continue + + if _is_comment(line): + if not classified: + yield lineno, COMMENT, line + continue + + # Now we have a code line, a doc start line, or crap left + # over following the close of a multi-line triple quote; in + # (& only in) the last case, classified==1. + if not classified: + if _is_doc_candidate.match(line): + yield lineno, DOC, line + in_doc = True + else: + yield lineno, CODE, line + + # The only reason to continue parsing is to make sure the + # start of a multi-line triple quote isn't missed. + while True: + m = _has_nightmare(line) + if not m: + break + else: + i = m.span()[0] + + ch = line[i] # splat or quote + if ch == '#': + # Chop off comment; and there are no quotes + # remaining because splat was leftmost. + break + # A quote is leftmost. + elif ch * 3 == line[i:i + 3]: + # at the start of a triple quote + in_triple_quote = quote3_finder[ch] + m = in_triple_quote.match(line, i + 3) + if m: + # Remove the string & continue. + end = m.span()[1] + line = line[:i] + line[end:] + in_doc = in_triple_quote = False + else: + # Triple quote doesn't end on this line. + break + else: + # At a single quote; remove the string & continue. + prev_line = line[:] + line = re.sub(quote1_finder[ch], ' ', line, 1) + # No more change detected, so be quiet or give up. + if prev_line == line: + # Let's be quiet and hope only one line is affected. + line = ''
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/util/testrunner.py @@ -0,0 +1,280 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2008 Matt Good <matt@matt-good.net> +# Copyright (C) 2008 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +from distutils import log +from distutils.errors import DistutilsOptionError +import os +import re +from StringIO import StringIO +import sys +import time +from pkg_resources import Distribution, EntryPoint, PathMetadata, \ + normalize_path, require, working_set +from setuptools.command.test import test +from unittest import _TextTestResult, TextTestRunner + +from bitten import __version__ as VERSION +from bitten.util import xmlio + +__docformat__ = 'restructuredtext en' + + +class XMLTestResult(_TextTestResult): + + def __init__(self, stream, descriptions, verbosity): + _TextTestResult.__init__(self, stream, descriptions, verbosity) + self.tests = [] + + def startTest(self, test): + _TextTestResult.startTest(self, test) + filename = sys.modules[test.__module__].__file__ + if filename.endswith('.pyc') or filename.endswith('.pyo'): + filename = filename[:-1] + self.tests.append([test, filename, time.time(), None, None]) + + def stopTest(self, test): + self.tests[-1][2] = time.time() - self.tests[-1][2] + _TextTestResult.stopTest(self, test) + + +class XMLTestRunner(TextTestRunner): + + def __init__(self, stream=sys.stdout, xml_stream=None): + TextTestRunner.__init__(self, stream, descriptions=0, verbosity=2) + self.xml_stream = xml_stream + + def _makeResult(self): + return XMLTestResult(self.stream, self.descriptions, self.verbosity) + + def run(self, test): + result = TextTestRunner.run(self, test) + if not self.xml_stream: + return result + + root = xmlio.Element('unittest-results') + for testcase, filename, timetaken, stdout, stderr in result.tests: + status = 'success' + tb = None + + if testcase in [e[0] for e in result.errors]: + status = 'error' + tb = [e[1] for e in result.errors if e[0] is testcase][0] + elif testcase in [f[0] for f in result.failures]: + status = 'failure' + tb = [f[1] for f in result.failures if f[0] is testcase][0] + + name = str(testcase) + fixture = None + description = testcase.shortDescription() or '' + if description.startswith('doctest of '): + name = 'doctest' + fixture = description[11:] + description = None + else: + match = re.match('(\w+)\s+\(([\w.]+)\)', name) + if match: + name = match.group(1) + fixture = match.group(2) + + test_elem = xmlio.Element('test', file=filename, name=name, + fixture=fixture, status=status, + duration=timetaken) + if description: + test_elem.append(xmlio.Element('description')[description]) + if stdout: + test_elem.append(xmlio.Element('stdout')[stdout]) + if stderr: + test_elem.append(xmlio.Element('stdout')[stderr]) + if tb: + test_elem.append(xmlio.Element('traceback')[tb]) + root.append(test_elem) + + root.write(self.xml_stream, newlines=True) + return result + + +class unittest(test): + description = test.description + ', and optionally record code coverage' + + user_options = test.user_options + [ + ('xml-output=', None, + "Path to the XML file where test results are written to"), + ('coverage-dir=', None, + "Directory where coverage files are to be stored"), + ('coverage-summary=', None, + "Path to the file where the coverage summary should be stored"), + ('coverage-method=', None, + "Whether to use trace.py or coverage.py to collect code coverage. " + "Valid options are 'trace' (the default) or 'coverage'.") + ] + + def initialize_options(self): + test.initialize_options(self) + self.xml_output = None + self.xml_output_file = None + self.coverage_summary = None + self.coverage_dir = None + self.coverage_method = 'trace' + + def finalize_options(self): + test.finalize_options(self) + + if self.xml_output is not None: + output_dir = os.path.dirname(self.xml_output) or '.' + if not os.path.exists(output_dir): + os.makedirs(output_dir) + self.xml_output_file = open(self.xml_output, 'w') + + if self.coverage_method not in ('trace', 'coverage', 'figleaf'): + raise DistutilsOptionError('Unknown coverage method %r' % + self.coverage_method) + + def run_tests(self): + if self.coverage_summary: + if self.coverage_method == 'coverage': + self._run_with_coverage() + elif self.coverage_method == 'figleaf': + self._run_with_figleaf() + else: + self._run_with_trace() + else: + self._run_tests() + + def _run_with_figleaf(self): + import figleaf + figleaf.start() + try: + self._run_tests() + finally: + figleaf.stop() + figleaf.write_coverage(self.coverage_summary) + + def _run_with_coverage(self): + import coverage + coverage.use_cache(False) + coverage.start() + try: + self._run_tests() + finally: + coverage.stop() + + modules = [m for _, m in sys.modules.items() + if m is not None and hasattr(m, '__file__') + and os.path.splitext(m.__file__)[-1] in ('.py', '.pyc')] + + # Generate summary file + buf = StringIO() + coverage.report(modules, file=buf) + buf.seek(0) + fileobj = open(self.coverage_summary, 'w') + try: + filter_coverage(buf, fileobj) + finally: + fileobj.close() + + if self.coverage_dir: + if not os.path.exists(self.coverage_dir): + os.makedirs(self.coverage_dir) + coverage.annotate(modules, directory=self.coverage_dir, + ignore_errors=True) + + def _run_with_trace(self): + from trace import Trace + trace = Trace(ignoredirs=[sys.prefix, sys.exec_prefix], trace=False, + count=True) + try: + trace.runfunc(self._run_tests) + finally: + results = trace.results() + real_stdout = sys.stdout + sys.stdout = open(self.coverage_summary, 'w') + try: + results.write_results(show_missing=True, summary=True, + coverdir=self.coverage_dir) + finally: + sys.stdout.close() + sys.stdout = real_stdout + + def _run_tests(self): + old_path = sys.path[:] + ei_cmd = self.get_finalized_command("egg_info") + path_item = normalize_path(ei_cmd.egg_base) + metadata = PathMetadata( + path_item, normalize_path(ei_cmd.egg_info) + ) + dist = Distribution(path_item, metadata, project_name=ei_cmd.egg_name) + working_set.add(dist) + require(str(dist.as_requirement())) + loader_ep = EntryPoint.parse("x=" + self.test_loader) + loader_class = loader_ep.load(require=False) + + try: + import unittest + unittest.main( + None, None, [unittest.__file__] + self.test_args, + testRunner=XMLTestRunner(stream=sys.stdout, + xml_stream=self.xml_output_file), + testLoader=loader_class() + ) + except SystemExit, e: + return e.code + + +def filter_coverage(infile, outfile): + for idx, line in enumerate(infile): + if idx < 2 or line.startswith('--'): + outfile.write(line) + continue + parts = line.split() + name = parts[0] + if name == 'TOTAL': + continue + if name not in sys.modules: + outfile.write(line) + continue + filename = os.path.normpath(sys.modules[name].__file__) + if filename.endswith('.pyc') or filename.endswith('.pyo'): + filename = filename[:-1] + outfile.write(line.rstrip() + ' ' + filename + '\n') + + +def main(): + from distutils.dist import Distribution + from optparse import OptionParser + + parser = OptionParser(usage='usage: %prog [options] test_suite ...', + version='%%prog %s' % VERSION) + parser.add_option('-o', '--xml-output', action='store', dest='xml_output', + metavar='FILE', help='write XML test results to FILE') + parser.add_option('-d', '--coverage-dir', action='store', + dest='coverage_dir', metavar='DIR', + help='store coverage results in DIR') + parser.add_option('-s', '--coverage-summary', action='store', + dest='coverage_summary', metavar='FILE', + help='write coverage summary to FILE') + options, args = parser.parse_args() + if len(args) < 1: + parser.error('incorrect number of arguments') + + cmd = unittest(Distribution()) + cmd.initialize_options() + cmd.test_suite = args[0] + if hasattr(options, 'xml_output'): + cmd.xml_output = options.xml_output + if hasattr(options, 'coverage_summary'): + cmd.coverage_summary = options.coverage_summary + if hasattr(options, 'coverage_dir'): + cmd.coverage_dir = options.coverage_dir + cmd.finalize_options() + cmd.run() + +if __name__ == '__main__': + main(sys.argv)
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/util/tests/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +import doctest +import unittest + +from bitten.util import xmlio + +def suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocTestSuite(xmlio)) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/util/xmlio.py @@ -0,0 +1,318 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +"""Utility code for easy input and output of XML. + +The current implementation uses `xml.dom.minidom` under the hood for parsing. +""" + +import os +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO +from UserDict import DictMixin + +import cgi +import string + +__trans = string.maketrans ("", "") +__todel = "" +for c in range (0, 256): + c1 = chr (c) + if not c1 in string.printable: + __todel += c1 +del c, c1 + +__all__ = ['Fragment', 'Element', 'ParsedElement', 'parse'] +__docformat__ = 'restructuredtext en' + +def _escape_text(text): + """Escape special characters in the provided text so that it can be safely + included in XML text nodes. + """ + return cgi.escape (str(text)).translate (__trans, __todel) + +def _escape_attr(attr): + """Escape special characters in the provided text so that it can be safely + included in XML attribute values. + """ + return _escape_text(attr).replace('"', '"') + + +class Fragment(object): + """A collection of XML elements.""" + __slots__ = ['children'] + + def __init__(self): + """Create an XML fragment.""" + self.children = [] + + def __getitem__(self, nodes): + """Add nodes to the fragment.""" + if not isinstance(nodes, (list, tuple)): + nodes = [nodes] + for node in nodes: + self.append(node) + return self + + def __str__(self): + """Return a string representation of the XML fragment.""" + buf = StringIO() + self.write(buf) + return buf.getvalue() + + def append(self, node): + """Append an element or fragment as child.""" + if isinstance(node, Element): + self.children.append(node) + elif isinstance(node, Fragment): + self.children += node.children + elif node is not None and node != '': + self.children.append(str(node)) + + def write(self, out, newlines=False): + """Serializes the element and writes the XML to the given output + stream. + """ + for child in self.children: + if isinstance(child, (Element, ParsedElement)): + child.write(out, newlines=newlines) + else: + if child.startswith('<'): + out.write('<![CDATA[' + child + ']]>') + else: + out.write(_escape_text(child)) + + +class Element(Fragment): + """Simple XML output generator based on the builder pattern. + + Construct XML elements by passing the tag name to the constructor: + + >>> print Element('foo') + <foo/> + + Attributes can be specified using keyword arguments. The values of the + arguments will be converted to strings and any special XML characters + escaped: + + >>> print Element('foo', bar=42) + <foo bar="42"/> + >>> print Element('foo', bar='1 < 2') + <foo bar="1 < 2"/> + >>> print Element('foo', bar='"baz"') + <foo bar=""baz""/> + + The order in which attributes are rendered is undefined. + + Elements can be using item access notation: + + >>> print Element('foo')[Element('bar'), Element('baz')] + <foo><bar/><baz/></foo> + + Text nodes can be nested in an element by using strings instead of elements + in item access. Any special characters in the strings are escaped + automatically: + + >>> print Element('foo')['Hello world'] + <foo>Hello world</foo> + >>> print Element('foo')[42] + <foo>42</foo> + >>> print Element('foo')['1 < 2'] + <foo>1 < 2</foo> + + This technique also allows mixed content: + + >>> print Element('foo')['Hello ', Element('b')['world']] + <foo>Hello <b>world</b></foo> + + Finally, text starting with an opening angle bracket is treated specially: + under the assumption that the text actually contains XML itself, the whole + thing is wrapped in a CDATA block instead of escaping all special characters + individually: + + >>> print Element('foo')['<bar a="3" b="4"><baz/></bar>'] + <foo><![CDATA[<bar a="3" b="4"><baz/></bar>]]></foo> + """ + __slots__ = ['name', 'attr'] + + def __init__(self, name_, **attr): + """Create an XML element using the specified tag name. + + The tag name must be supplied as the first positional argument. All + keyword arguments following it are handled as attributes of the element. + """ + Fragment.__init__(self) + self.name = name_ + self.attr = dict([(name, value) for name, value in attr.items() + if value is not None]) + + def write(self, out, newlines=False): + """Serializes the element and writes the XML to the given output + stream. + """ + out.write('<') + out.write(self.name) + for name, value in self.attr.items(): + out.write(' %s="%s"' % (name, _escape_attr(value))) + if self.children: + out.write('>') + Fragment.write(self, out, newlines) + out.write('</' + self.name + '>') + else: + out.write('/>') + if newlines: + out.write(os.linesep) + + +class ParseError(Exception): + """Exception thrown when there's an error parsing an XML document.""" + + +def parse(text_or_file): + """Parse an XML document provided as string or file-like object. + + Returns an instance of `ParsedElement` that can be used to traverse the + parsed document. + """ + from xml.dom import minidom + from xml.parsers import expat + try: + if isinstance(text_or_file, (str, unicode)): + dom = minidom.parseString(text_or_file) + else: + dom = minidom.parse(text_or_file) + return ParsedElement(dom.documentElement) + except expat.error, e: + raise ParseError(e) + + +class ParsedElement(object): + """Representation of an XML element that was parsed from a string or + file. + + This class should not be used directly. Rather, XML text parsed using + `xmlio.parse()` will return an instance of this class. + + >>> xml = parse('<root/>') + >>> print xml.name + root + + Parsed elements can be serialized to a string using the `write()` method: + + >>> import sys + >>> parse('<root></root>').write(sys.stdout) + <root/> + + For convenience, this is also done when coercing the object to a string + using the builtin ``str()`` function, which is used when printing an + object: + + >>> print parse('<root></root>') + <root/> + + (Note that serializing the element will produce a normalized representation + that may not excatly match the input string.) + + Attributes are accessed via the `attr` member: + + >>> print parse('<root foo="bar"/>').attr['foo'] + bar + + Attributes can also be updated, added or removed: + + >>> xml = parse('<root foo="bar"/>') + >>> xml.attr['foo'] = 'baz' + >>> print xml + <root foo="baz"/> + + >>> del xml.attr['foo'] + >>> print xml + <root/> + + >>> xml.attr['foo'] = 'bar' + >>> print xml + <root foo="bar"/> + + CDATA sections are included in the text content of the element returned by + `gettext()`: + + >>> xml = parse('<root>foo<![CDATA[ <bar> ]]>baz</root>') + >>> xml.gettext() + 'foo <bar> baz' + """ + __slots__ = ['_node', 'attr'] + + class _Attrs(DictMixin): + """Simple wrapper around the element attributes to provide a dictionary + interface.""" + def __init__(self, node): + self._node = node + def __getitem__(self, name): + attr = self._node.getAttributeNode(name) + if not attr: + raise KeyError(name) + return attr.value.encode('utf-8') + def __setitem__(self, name, value): + self._node.setAttribute(name, value) + def __delitem__(self, name): + self._node.removeAttribute(name) + def keys(self): + return [key.encode('utf-8') for key in self._node.attributes.keys()] + + def __init__(self, node): + self._node = node + self.attr = ParsedElement._Attrs(node) + + name = property(fget=lambda self: self._node.localName, + doc='Local name of the element') + namespace = property(fget=lambda self: self._node.namespaceURI, + doc='Namespace URI of the element') + + def children(self, name=None): + """Iterate over the child elements of this element. + + If the parameter `name` is provided, only include elements with a + matching local name. Otherwise, include all elements. + """ + for child in [c for c in self._node.childNodes if c.nodeType == 1]: + if name in (None, child.tagName): + yield ParsedElement(child) + + def __iter__(self): + return self.children() + + def gettext(self): + """Return the text content of this element. + + This concatenates the values of all text and CDATA nodes that are + immediate children of this element. + """ + return ''.join([c.nodeValue.encode('utf-8') + for c in self._node.childNodes + if c.nodeType in (3, 4)]) + + def write(self, out, newlines=False): + """Serializes the element and writes the XML to the given output + stream. + """ + self._node.writexml(out, newl=newlines and '\n' or '') + + def __str__(self): + """Return a string representation of the XML element.""" + buf = StringIO() + self.write(buf) + return buf.getvalue() + + +if __name__ == '__main__': + import doctest + doctest.testmod()
new file mode 100644 --- /dev/null +++ b/trac-0.11/bitten/web_ui.py @@ -0,0 +1,626 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +"""Implementation of the Bitten web interface.""" + +from datetime import datetime +import posixpath +import re +from StringIO import StringIO + +import pkg_resources +from genshi.builder import tag +from trac.core import * +from trac.timeline import ITimelineEventProvider +from trac.util import escape, pretty_timedelta, format_datetime, shorten_line, \ + Markup +from trac.util.html import html +from trac.web import IRequestHandler +from trac.web.chrome import INavigationContributor, ITemplateProvider, \ + add_link, add_stylesheet, add_ctxtnav, \ + prevnext_nav, add_script +from trac.wiki import wiki_to_html, wiki_to_oneliner +from bitten.api import ILogFormatter, IReportChartGenerator, IReportSummarizer +from bitten.model import BuildConfig, TargetPlatform, Build, BuildStep, \ + BuildLog, Report +from bitten.queue import collect_changes + +_status_label = {Build.PENDING: 'pending', + Build.IN_PROGRESS: 'in progress', + Build.SUCCESS: 'completed', + Build.FAILURE: 'failed'} + +def _get_build_data(env, req, build): + data = {'id': build.id, 'name': build.slave, 'rev': build.rev, + 'status': _status_label[build.status], + 'cls': _status_label[build.status].replace(' ', '-'), + 'href': req.href.build(build.config, build.id), + 'chgset_href': req.href.changeset(build.rev)} + if build.started: + data['started'] = format_datetime(build.started) + data['started_delta'] = pretty_timedelta(build.started) + data['duration'] = pretty_timedelta(build.started) + if build.stopped: + data['stopped'] = format_datetime(build.stopped) + data['stopped_delta'] = pretty_timedelta(build.stopped) + data['duration'] = pretty_timedelta(build.stopped, build.started) + data['slave'] = { + 'name': build.slave, + 'ipnr': build.slave_info.get(Build.IP_ADDRESS), + 'os_name': build.slave_info.get(Build.OS_NAME), + 'os_family': build.slave_info.get(Build.OS_FAMILY), + 'os_version': build.slave_info.get(Build.OS_VERSION), + 'machine': build.slave_info.get(Build.MACHINE), + 'processor': build.slave_info.get(Build.PROCESSOR) + } + return data + + +class BittenChrome(Component): + """Provides the Bitten templates and static resources.""" + + implements(INavigationContributor, ITemplateProvider) + + # INavigationContributor methods + + def get_active_navigation_item(self, req): + """Called by Trac to determine which navigation item should be marked + as active. + + :param req: the request object + """ + return 'build' + + def get_navigation_items(self, req): + """Return the navigation item for access the build status overview from + the Trac navigation bar.""" + if 'BUILD_VIEW' in req.perm: + yield ('mainnav', 'build', + tag.a('Builds Status', href=req.href.build(), accesskey=5)) + + # ITemplatesProvider methods + + def get_htdocs_dirs(self): + """Return the directories containing static resources.""" + return [('bitten', pkg_resources.resource_filename(__name__, 'htdocs'))] + + def get_templates_dirs(self): + """Return the directories containing templates.""" + return [pkg_resources.resource_filename(__name__, 'templates')] + + +class BuildConfigController(Component): + """Implements the web interface for build configurations.""" + + implements(IRequestHandler) + + # IRequestHandler methods + + def match_request(self, req): + match = re.match(r'/build(?:/([\w.-]+))?/?$', req.path_info) + if match: + if match.group(1): + req.args['config'] = match.group(1) + return True + + def process_request(self, req): + req.perm.require('BUILD_VIEW') + + action = req.args.get('action') + view = req.args.get('view') + config = req.args.get('config') + + if config: + data = self._render_config(req, config) + elif view == 'inprogress': + data = self._render_inprogress(req) + else: + data = self._render_overview(req) + + add_stylesheet(req, 'bitten/bitten.css') + return 'bitten_config.html', data, None + + # Internal methods + + def _render_overview(self, req): + data = {'title': 'Build Status'} + show_all = False + if req.args.get('show') == 'all': + show_all = True + data['show_all'] = show_all + + configs = [] + for config in BuildConfig.select(self.env, include_inactive=show_all): + description = config.description + if description: + description = wiki_to_html(description, self.env, req) + config_data = { + 'name': config.name, 'label': config.label or config.name, + 'active': config.active, 'path': config.path, + 'description': description, + 'href': req.href.build(config.name), + 'builds': [] + } + configs.append(config_data) + if not config.active: + continue + + repos = self.env.get_repository(req.authname) + if hasattr(repos, 'sync'): + repos.sync() + + prev_rev = None + for platform, rev, build in collect_changes(repos, config): + if rev != prev_rev: + if prev_rev is None: + chgset = repos.get_changeset(rev) + config_data['youngest_rev'] = { + 'id': rev, 'href': req.href.changeset(rev), + 'author': chgset.author or 'anonymous', + 'date': format_datetime(chgset.date), + 'message': wiki_to_oneliner( + shorten_line(chgset.message), self.env, req=req) + } + else: + break + prev_rev = rev + if build: + build_data = _get_build_data(self.env, req, build) + build_data['platform'] = platform.name + config_data['builds'].append(build_data) + else: + config_data['builds'].append({ + 'platform': platform.name, 'status': 'pending' + }) + + data['configs'] = configs + data['page_mode'] = 'overview' + add_link(req, 'views', req.href.build(view='inprogress'), + 'In Progress Builds') + add_ctxtnav(req, 'In Progress Builds', + req.href.build(view='inprogress')) + return data + + def _render_inprogress(self, req): + data = {'title': 'In Progress Builds', + 'page_mode': 'view-inprogress'} + + db = self.env.get_db_cnx() + + configs = [] + for config in BuildConfig.select(self.env, include_inactive=False): + self.log.debug(config.name) + if not config.active: + continue + + in_progress_builds = Build.select(self.env, config=config.name, + status=Build.IN_PROGRESS, db=db) + + current_builds = 0 + builds = [] + # sort correctly by revision. + for build in sorted(in_progress_builds, + cmp=lambda x, y: int(y.rev) - int(x.rev)): + rev = build.rev + build_data = _get_build_data(self.env, req, build) + build_data['rev'] = rev + build_data['rev_href'] = req.href.changeset(rev) + platform = TargetPlatform.fetch(self.env, build.platform) + build_data['platform'] = platform.name + build_data['steps'] = [] + + for step in BuildStep.select(self.env, build=build.id, db=db): + build_data['steps'].append({ + 'name': step.name, + 'description': step.description, + 'duration': datetime.fromtimestamp(step.stopped) - \ + datetime.fromtimestamp(step.started), + 'failed': not step.successful, + 'errors': step.errors, + 'href': build_data['href'] + '#step_' + step.name + }) + + builds.append(build_data) + current_builds += 1 + + if current_builds == 0: + continue + + description = config.description + if description: + description = wiki_to_html(description, self.env, req) + configs.append({ + 'name': config.name, 'label': config.label or config.name, + 'active': config.active, 'path': config.path, + 'description': description, + 'href': req.href.build(config.name), + 'builds': builds + }) + + data['configs'] = configs + return data + + def _render_config(self, req, config_name): + db = self.env.get_db_cnx() + + config = BuildConfig.fetch(self.env, config_name, db=db) + data = {'title': 'Build Configuration "%s"' \ + % config.label or config.name, + 'page_mode': 'view_config'} + add_link(req, 'up', req.href.build(), 'Build Status') + description = config.description + if description: + description = wiki_to_html(description, self.env, req) + data['config'] = { + 'name': config.name, 'label': config.label, 'path': config.path, + 'min_rev': config.min_rev, + 'min_rev_href': req.href.changeset(config.min_rev), + 'max_rev': config.max_rev, + 'max_rev_href': req.href.changeset(config.max_rev), + 'active': config.active, 'description': description, + 'browser_href': req.href.browser(config.path) + } + + platforms = list(TargetPlatform.select(self.env, config=config_name, + db=db)) + data['config']['platforms'] = [ + {'name': platform.name, 'id': platform.id} + for platform in platforms + ] + + has_reports = False + for report in Report.select(self.env, config=config.name, db=db): + has_reports = True + break + + if has_reports: + chart_generators = [] + for generator in ReportChartController(self.env).generators: + for category in generator.get_supported_categories(): + chart_generators.append({ + 'href': req.href.build(config.name, 'chart/' + category) + }) + data['config']['charts'] = chart_generators + charts_license = self.config.get('bitten', 'charts_license') + if charts_license: + data['config']['charts_license'] = charts_license + + page = max(1, int(req.args.get('page', 1))) + more = False + data['page_number'] = page + + repos = self.env.get_repository(req.authname) + if hasattr(repos, 'sync'): + repos.sync() + + builds_per_page = 12 * len(platforms) + idx = 0 + builds = {} + for platform, rev, build in collect_changes(repos, config): + if idx >= page * builds_per_page: + more = True + break + elif idx >= (page - 1) * builds_per_page: + builds.setdefault(rev, {}) + builds[rev].setdefault('href', req.href.changeset(rev)) + if build and build.status != Build.PENDING: + build_data = _get_build_data(self.env, req, build) + build_data['steps'] = [] + for step in BuildStep.select(self.env, build=build.id, + db=db): + build_data['steps'].append({ + 'name': step.name, + 'description': step.description, + 'duration': datetime.fromtimestamp(step.stopped) - \ + datetime.fromtimestamp(step.started), + 'failed': not step.successful, + 'errors': step.errors, + 'href': build_data['href'] + '#step_' + step.name + }) + builds[rev][platform.id] = build_data + idx += 1 + data['config']['builds'] = builds + + if page > 1: + if page == 2: + prev_href = req.href.build(config.name) + else: + prev_href = req.href.build(config.name, page=page - 1) + add_link(req, 'prev', prev_href, 'Previous Page') + if more: + next_href = req.href.build(config.name, page=page + 1) + add_link(req, 'next', next_href, 'Next Page') + prevnext_nav(req, 'Page') + return data + + +class BuildController(Component): + """Renders the build page.""" + implements(INavigationContributor, IRequestHandler, ITimelineEventProvider) + + log_formatters = ExtensionPoint(ILogFormatter) + report_summarizers = ExtensionPoint(IReportSummarizer) + + # INavigationContributor methods + + def get_active_navigation_item(self, req): + return 'build' + + def get_navigation_items(self, req): + return [] + + # IRequestHandler methods + + def match_request(self, req): + match = re.match(r'/build/([\w.-]+)/(\d+)', req.path_info) + if match: + if match.group(1): + req.args['config'] = match.group(1) + if match.group(2): + req.args['id'] = match.group(2) + return True + + def process_request(self, req): + req.perm.require('BUILD_VIEW') + + db = self.env.get_db_cnx() + build_id = int(req.args.get('id')) + build = Build.fetch(self.env, build_id, db=db) + assert build, 'Build %s does not exist' % build_id + + if req.method == 'POST': + if req.args.get('action') == 'invalidate': + self._do_invalidate(req, build, db) + req.redirect(req.href.build(build.config, build.id)) + + add_link(req, 'up', req.href.build(build.config), + 'Build Configuration') + status2title = {Build.SUCCESS: 'Success', Build.FAILURE: 'Failure', + Build.IN_PROGRESS: 'In Progress'} + data = {'title': 'Build %s - %s' % (build_id, + status2title[build.status]), + 'page_mode': 'view_build', + 'build': {}} + config = BuildConfig.fetch(self.env, build.config, db=db) + data['build']['config'] = { + 'name': config.label, + 'href': req.href.build(config.name) + } + + formatters = [] + for formatter in self.log_formatters: + formatters.append(formatter.get_formatter(req, build)) + + summarizers = {} # keyed by report type + for summarizer in self.report_summarizers: + categories = summarizer.get_supported_categories() + summarizers.update(dict([(cat, summarizer) for cat in categories])) + + data['build'].update(_get_build_data(self.env, req, build)) + steps = [] + for step in BuildStep.select(self.env, build=build.id, db=db): + steps.append({ + 'name': step.name, 'description': step.description, + 'duration': pretty_timedelta(step.started, step.stopped), + 'failed': step.status == BuildStep.FAILURE, + 'errors': step.errors, + 'log': self._render_log(req, build, formatters, step), + 'reports': self._render_reports(req, config, build, summarizers, + step) + }) + data['build']['steps'] = steps + data['build']['can_delete'] = ('BUILD_DELETE' in req.perm) + + repos = self.env.get_repository(req.authname) + chgset = repos.get_changeset(build.rev) + data['build']['chgset_author'] = chgset.author + + add_script(req, 'bitten/tabset.js') + add_stylesheet(req, 'bitten/bitten.css') + return 'bitten_build.html', data, None + + # ITimelineEventProvider methods + + def get_timeline_filters(self, req): + if 'BUILD_VIEW' in req.perm: + yield ('build', 'Builds') + + def get_timeline_events(self, req, start, stop, filters): + if 'build' not in filters: + return + + if isinstance(start, datetime): # Trac>=0.11 + from trac.util.datefmt import to_timestamp + start = to_timestamp(start) + stop = to_timestamp(stop) + + add_stylesheet(req, 'bitten/bitten.css') + + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("SELECT b.id,b.config,c.label,b.rev,p.name," + "b.stopped,b.status FROM bitten_build AS b" + " INNER JOIN bitten_config AS c ON (c.name=b.config) " + " INNER JOIN bitten_platform AS p ON (p.id=b.platform) " + "WHERE b.stopped>=%s AND b.stopped<=%s " + "AND b.status IN (%s, %s) ORDER BY b.stopped", + (start, stop, Build.SUCCESS, Build.FAILURE)) + + event_kinds = {Build.SUCCESS: 'successbuild', + Build.FAILURE: 'failedbuild'} + for id, config, label, rev, platform, stopped, status in cursor: + + errors = [] + if status == Build.FAILURE: + for step in BuildStep.select(self.env, build=id, + status=BuildStep.FAILURE, + db=db): + errors += [(step.name, error) for error + in step.errors] + + title = tag('Build of ', tag.em('%s [%s]' % (label, rev)), + ' on %s %s' % (platform, _status_label[status])) + message = '' + if req.args.get('format') == 'rss': + href = req.abs_href.build(config, id) + if errors: + buf = StringIO() + prev_step = None + for step, error in errors: + if step != prev_step: + if prev_step is not None: + buf.write('</ul>') + buf.write('<p>Step %s failed:</p><ul>' \ + % escape(step)) + prev_step = step + buf.write('<li>%s</li>' % escape(error)) + buf.write('</ul>') + message = Markup(buf.getvalue()) + else: + href = req.href.build(config, id) + if errors: + steps = [] + for step, error in errors: + if step not in steps: + steps.append(step) + steps = [Markup('<em>%s</em>') % step for step in steps] + if len(steps) < 2: + message = steps[0] + elif len(steps) == 2: + message = Markup(' and ').join(steps) + elif len(steps) > 2: + message = Markup(', ').join(steps[:-1]) + ', and ' + \ + steps[-1] + message = Markup('Step%s %s failed') % ( + len(steps) != 1 and 's' or '', message + ) + yield event_kinds[status], href, title, stopped, None, message + + # Internal methods + + def _do_invalidate(self, req, build, db): + self.log.info('Invalidating build %d', build.id) + + for step in BuildStep.select(self.env, build=build.id, db=db): + step.delete(db=db) + + build.slave = None + build.started = build.stopped = 0 + build.status = Build.PENDING + build.slave_info = {} + build.update() + + db.commit() + + req.redirect(req.href.build(build.config)) + + def _render_log(self, req, build, formatters, step): + items = [] + for log in BuildLog.select(self.env, build=build.id, step=step.name): + for level, message in log.messages: + for format in formatters: + message = format(step, log.generator, level, message) + items.append({'level': level, 'message': message}) + return items + + def _render_reports(self, req, config, build, summarizers, step): + reports = [] + for report in Report.select(self.env, build=build.id, step=step.name): + summarizer = summarizers.get(report.category) + if summarizer: + tmpl, data = summarizer.render_summary(req, config, build, + step, report.category) + else: + tmpl = data = None + reports.append({'category': report.category, + 'template': tmpl, 'data': data}) + return reports + + +class ReportChartController(Component): + implements(IRequestHandler) + + generators = ExtensionPoint(IReportChartGenerator) + + # IRequestHandler methods + + def match_request(self, req): + match = re.match(r'/build/([\w.-]+)/chart/(\w+)', req.path_info) + if match: + req.args['config'] = match.group(1) + req.args['category'] = match.group(2) + return True + + def process_request(self, req): + category = req.args.get('category') + config = BuildConfig.fetch(self.env, name=req.args.get('config')) + + for generator in self.generators: + if category in generator.get_supported_categories(): + tmpl, data = generator.generate_chart_data(req, config, + category) + break + else: + raise TracError('Unknown report category "%s"' % category) + + return tmpl, data, 'text/xml' + + +class SourceFileLinkFormatter(Component): + """Detects references to files in the build log and renders them as links + to the repository browser. + """ + + implements(ILogFormatter) + + _fileref_re = re.compile('(?P<path>[\w.-]+(?:/[\w.-]+)+)(?P<line>(:\d+))?') + + def get_formatter(self, req, build): + """Return the log message formatter function.""" + config = BuildConfig.fetch(self.env, name=build.config) + repos = self.env.get_repository(req.authname) + href = req.href.browser + cache = {} + + def _replace(m): + filepath = posixpath.normpath(m.group('path').replace('\\', '/')) + if not cache.get(filepath) is True: + parts = filepath.split('/') + path = '' + for part in parts: + path = posixpath.join(path, part) + if path not in cache: + try: + repos.get_node(posixpath.join(config.path, path), + build.rev) + cache[path] = True + except TracError: + cache[path] = False + if cache[path] is False: + return m.group(0) + link = href(config.path, filepath) + if m.group('line'): + link += '#L' + m.group('line')[1:] + return Markup(tag.a(m.group(0), href=link)) + + def _formatter(step, type, level, message): + buf = [] + offset = 0 + for mo in self._fileref_re.finditer(message): + start, end = mo.span() + if start > offset: + buf.append(message[offset:start]) + buf.append(_replace(mo)) + offset = end + if offset < len(message): + buf.append(message[offset:]) + return Markup("").join(buf) + + return _formatter
new file mode 100644 --- /dev/null +++ b/trac-0.11/doc/commands.txt @@ -0,0 +1,890 @@ +.. -*- mode: rst; encoding: utf-8 -*- + +===================== +Build Recipe Commands +===================== + +`Build recipes`_ are represented by XML documents. This page describes what +commands are generally available in recipes. Please note, though, that +third-party packages can add additional commands, which would then be +documented by that third party. + +.. _`build recipes`: recipes.html + +.. contents:: Contents + :depth: 2 +.. sectnum:: + + +Generic Commands +================ + +These are commands that are used without a namespace prefix. + + +------------ +``<report>`` +------------ + +Parse an XML file and send it to the master as a report with a given category. +Use this command in conjunction with the ``<sh:pipe>`` or ``<x:transform>`` +commands to send custom reports to the build master. + +Parameters +---------- + ++--------------+-------------------------------------------------------------+ +| Name | Description | ++==============+=============================================================+ +| ``category`` | Category of the report (for example "test" or "coverage"). | ++--------------+-------------------------------------------------------------+ +| ``file`` | Path to the XML file containing the report data, relative | +| | to the project directory. | ++--------------+-------------------------------------------------------------+ + +Both parameters must be specified. + + +Shell Tools +=========== + +A bundle of generic tools that are not specific to any programming language or +tool-chain. + +:Namespace: ``http://bitten.cmlenz.net/tools/sh`` +:Common prefix: ``sh`` + + +------------- +``<sh:exec>`` +------------- + +Executes a program or script. + +Parameters +---------- + ++----------------+-----------------------------------------------------------+ +| Name | Description | ++================+===========================================================+ +| ``executable`` | The name of the executable program. | ++----------------+-----------------------------------------------------------+ +| ``file`` | Path to the script to execute, relative to the project | +| | directory | ++----------------+-----------------------------------------------------------+ +| ``output`` | Path to the output file | ++----------------+-----------------------------------------------------------+ +| ``args`` | Any arguments to pass to the executable or script | ++----------------+-----------------------------------------------------------+ + +Either ``executable`` or ``file`` must be specified. + +Examples +-------- + +TODO + + +------------- +``<sh:pipe>`` +------------- + +Pipes the content of a file through a program or script. + +Parameters +---------- + ++----------------+-----------------------------------------------------------+ +| Name | Description | ++================+===========================================================+ +| ``executable`` | The name of the executable program. | ++----------------+-----------------------------------------------------------+ +| ``file`` | Path to the script to execute, relative to the project | +| | directory | ++----------------+-----------------------------------------------------------+ +| ``input`` | Path to the input file | ++----------------+-----------------------------------------------------------+ +| ``output`` | Path to the output file | ++----------------+-----------------------------------------------------------+ +| ``args`` | Any arguments to pass to the executable or script | ++----------------+-----------------------------------------------------------+ + +Either ``executable`` or ``file`` must be specified. + +Examples +-------- + +TODO + + +C/Unix Tools +============ + +These commands provide support for tools commonly used for development of C/C++ +applications on Unix platforms, such as ``make``. + +:Namespace: ``http://bitten.cmlenz.net/tools/c`` +:Common prefix: ``c`` + + +------------------ +``<c:autoreconf>`` +------------------ + +Executes ths autotool autoreconf. + +Parameters +---------- + + :param force: consider all files obsolete + :param install: copy missing auxiliary files + :param symlink: install symbolic links instead of copies + :param warnings: report the warnings falling in CATEGORY + :prepend_include: prepend directories to search path + :include: append directories to search path + + ++--------------+-------------------------------------------------------------+ +| Name | Description | ++==============+=============================================================+ +| ``force`` | Consider all files obsolete | ++--------------+-------------------------------------------------------------+ +| ``install`` | Copy missing auxiliary files | ++--------------+-------------------------------------------------------------+ +| ``symlink`` | Install symbolic links instead of copies | ++--------------+-------------------------------------------------------------+ +| ``warnings`` | Report the warnings related to category | +| | (which can actually be a comma separated list) | ++--------------+-------------------------------------------------------------+ +| ``prepend_include`` | Prepend directories to search path | ++--------------+-------------------------------------------------------------+ +| ``include`` | Append directories to search path | ++--------------+-------------------------------------------------------------+ + +Examples +-------- + +.. code-block:: xml + + <c:autoreconf force="1" install="1" warnings="cross,syntax,error"/> + +Runs the ``autoreconf`` tool in the base directory with the option: force, install +and 3 warning categories active: cross,syntax,error. This is equivalent to:: + + autoreconf --force --install --warnings=cross,syntax,error + + +----------------- +``<c:configure>`` +----------------- + +Executes a configure script as generated by Autoconf. + +Parameters +---------- + ++--------------+-------------------------------------------------------------+ +| Name | Description | ++==============+=============================================================+ +| ``file`` | Name of the configure script (defaults to "configure") | ++--------------+-------------------------------------------------------------+ +| ``enable`` | List of features to enable, separated by spaces. | ++--------------+-------------------------------------------------------------+ +| ``disable`` | List of features to disable, separated by spaces. | ++--------------+-------------------------------------------------------------+ +| ``with`` | List of packages to include, separated by spaces. | ++--------------+-------------------------------------------------------------+ +| ``without`` | List of packages to exclude, separated by spaces. | ++--------------+-------------------------------------------------------------+ +| ``cflags`` | Value of the `CFLAGS` variable to pass to the script. | ++--------------+-------------------------------------------------------------+ +| ``cxxflags`` | Value of the `CXXFLAGS` variable to pass to the script. | ++--------------+-------------------------------------------------------------+ + +Examples +-------- + +.. code-block:: xml + + <c:configure enable="threadsafe" cflags="-O"/> + +Runs the ``configure`` script in the base directory, enable the ``threadsafe`` +feature, and passing ``-O`` as ``CFLAGS``. This is equivalent to:: + + ./configure --enable-threadsafe CFLAGS="-O" + + +------------ +``<c:gcov>`` +------------ + +Run gcov_ to extract coverage data where available. + +.. _gcov: http://gcc.gnu.org/onlinedocs/gcc/Gcov-Intro.html + +Parameters +---------- + ++--------------+------------------------------------------------------------+ +| Name | Description | ++==============+============================================================+ +| ``include`` | List of glob patterns (separated by space) that specify | +| | which source files should be included in the coverage | +| | report | ++--------------+------------------------------------------------------------+ +| ``exclude`` | List of glob patterns (separated by space) that specify | +| | which source files should be excluded from the coverage | +| | report | ++--------------+------------------------------------------------------------+ +| ``prefix`` | Optional prefix name that is added to object files by the | +| | build system | ++--------------+------------------------------------------------------------+ + + +------------ +``<c:make>`` +------------ + +Executes a Makefile. + +Parameters +---------- + ++----------------+-----------------------------------------------------------+ +| Name | Description | ++================+===========================================================+ +| ``target`` | Name of the target to execute (defaults to "all") | ++----------------+-----------------------------------------------------------+ +| ``file`` | Path to the Makefile that should be used. | ++----------------+-----------------------------------------------------------+ +| ``keep-going`` | Whether `make` should try to continue even after | +| | encountering errors. | ++----------------+-----------------------------------------------------------+ +| ``jobs`` | Number of parallel jobs used by make. | ++----------------+-----------------------------------------------------------+ +| ``directory`` | Path of the directory in which make should be called. | ++----------------+-----------------------------------------------------------+ +| ``args`` | Any space separated arguments to pass to the makefile. | +| | Usually in the form: | +| | ``"parameter1=value1 parameter2=value2"``. | ++----------------+-----------------------------------------------------------+ + + +Examples +-------- + +.. code-block:: xml + + <c:make target="compile" file="build/Makefile" /> + +Runs the target "compile" of the ``Makefile`` located in the sub-directory +``build``. + +.. code-block:: xml + + <c:make target="compile" file="build/Makefile" directory="work" args="coverage=1" /> + +Same as previous but execute the command in the ``work`` directory and call +the makefile with the command line argument ``coverage=1``. + +--------------- +``<c:cppunit>`` +--------------- + +Report the test output generated by the CppUnit_ unit testing framework. The +output from CppUnit must be in XML format and in already, specified by the +``file`` argument of this recipe. + +.. _cppunit: http://cppunit.sourceforge.net + +Parameters +---------- + ++----------------+-----------------------------------------------------------+ +| Name | Description | ++================+===========================================================+ +| ``file`` | Path to the cppunit XML output file. | ++----------------+-----------------------------------------------------------+ + +Examples +-------- + +.. code-block:: xml + + <sh:exec executable="run_unit_tests" output="test_results.xml" /> + <c:cppunit file="test_results.xml" /> + +Runs the program ``run_unit_tests`` to gather the data output by CppUnit in the +``test_results.xml`` file and then reports it. + + +Java Tools +========== + +A bundle of recipe commands that support tools commonly used by Java projects. + +:Namespace: ``http://bitten.cmlenz.net/tools/java`` +:Common prefix: ``java`` + + +-------------- +``<java:ant>`` +-------------- + +Runs an Ant_ build. + +.. _ant: http://ant.apache.org/ + +Parameters +---------- + ++----------------+-----------------------------------------------------------+ +| Name | Description | ++================+===========================================================+ +| ``file`` | Path of the build file, relative to the project source | +| | directory (default is ``build.xml``). | ++----------------+-----------------------------------------------------------+ +| ``target`` | Name of the build target(s) to execute. | ++----------------+-----------------------------------------------------------+ +| ``args`` | Additional arguments to pass to Ant, separated by | +| | whitespace. | ++----------------+-----------------------------------------------------------+ +| ``keep_going`` | Tell Ant to continue even when errors are in encountered | +| | in the build. | ++----------------+-----------------------------------------------------------+ + +Examples +-------- + +.. code-block:: xml + + <java:ant target="compile" /> + +Executes the target ``compile`` of the ``build.xml`` buildfile at the top of the +project source directory. + + +-------------------- +``<java:cobertura>`` +-------------------- + +Extract code coverage data from a Cobertura_ XML file. + +.. _cobertura: http://cobertura.sourceforge.net/ + +Parameters +---------- + ++----------------+-----------------------------------------------------------+ +| Name | Description | ++================+===========================================================+ +| ``file`` | Path to the XML file generated by Cobertura | ++----------------+-----------------------------------------------------------+ + +Examples +-------- + +.. code-block:: xml + + <java:cobertura file="build/cobertura.xml" /> + +Reads the specifid XML file, extracts the coverage data, and builds a coverage +report to be sent to the build master. + + +---------------- +``<java:junit>`` +---------------- + +Extracts information about unit test results from a file in JUnit_ XML format. + +.. _junit: http://junit.org/index.htm + +Parameters +---------- + ++----------------+-----------------------------------------------------------+ +| Name | Description | ++================+===========================================================+ +| ``file`` | Path to the JUnit XML test results file. This can include | +| | wildcards, in which case all the file matching the | +| | pattern will be included. | ++----------------+-----------------------------------------------------------+ +| ``srcdir`` | Path of the directory unit test sources. Used to link the | +| | test cases to files. | ++----------------+-----------------------------------------------------------+ + +The ``file`` attribute is required. + +Examples +-------- + +.. code-block:: xml + + <java:junit file="build/tests/results/TEST-*.xml" srcdir="src/tests" /> + +Collects the test results from all files in the `build/tests/results` directory +that match the pattern `TEST-*.xml`. Also, maps the class names in the results +files to Java source files in the directory `src/tests`. + + +PHP Tools +========= + +A bundle of recipe commands for PHP_ projects. + +:Namespace: ``http://bitten.cmlenz.net/tools/php`` +:Common prefix: ``php`` + +.. _php: http://php.net/ + +--------------- +``<php:phing>`` +--------------- + +Runs a Phing_ build. + +.. _phing: http://phing.info/ + +Parameters +---------- + ++-------------------+-------------------------------------------------------+ +| Name | Description | ++===================+=======================================================+ +| ``file`` | Path of the build file, relative to the project | +| | source directory (default is ``build.xml``). | ++-------------------+-------------------------------------------------------+ +| ``target`` | Name of the build target(s) to execute. | ++-------------------+-------------------------------------------------------+ +| ``args`` | Additional arguments to pass to Phing, separated by | +| | whitespace. | ++-------------------+-------------------------------------------------------+ +| ``executable`` | Phing executable program (default is ``phing``). | ++-------------------+-------------------------------------------------------+ + + +Examples +-------- + +.. code-block:: xml + + <php:phing target="compile" /> + +Executes the target ``compile`` of the ``build.xml`` buildfile at the top of the +project source directory. + + +----------------- +``<php:phpunit>`` +----------------- + +Extracts information from PHPUnit_ test results recorded in an XML file. + +.. _phpunit: http://www.phpunit.de/ + +Parameters +---------- + ++----------------+-----------------------------------------------------------+ +| Name | Description | ++================+===========================================================+ +| ``file`` | Path to the XML results file, relative to the project | +| | source directory. | ++----------------+-----------------------------------------------------------+ + +Examples +-------- + +.. code-block:: xml + + <php:phpunit file="build/test-results.xml"/> + +Extracts the test results from the XML file located at +``build/test-results.xml``. + + +------------------ +``<php:coverage>`` +------------------ + +Extracts coverage information Phing_'s code coverage task recorded in an XML +file. + +Parameters +---------- + ++---------------+-----------------------------------------------------------+ +| Name | Description | ++===============+===========================================================+ +| ``file`` | Path to the XML coverage file, relative to the project | +| | source directory. | ++---------------+-----------------------------------------------------------+ + +Examples +-------- + +.. code-block:: xml + + <php:coverage file="build/coverage.xml" /> + + +Python Tools +============ + +A bundle of recipe commands that support tools commonly used by Python_ +projects. + +:Namespace: ``http://bitten.cmlenz.net/tools/python`` +:Common prefix: ``python`` + +.. _python: http://www.python.org/ + + +----------------- +``<python:exec>`` +----------------- + +Executes a Python script. + +Parameters +---------- + ++----------------+-----------------------------------------------------------+ +| Name | Description | ++================+===========================================================+ +| ``file`` | Path of the script to execute, relative to the project | +| | source directory. | ++----------------+-----------------------------------------------------------+ +| ``module`` | Name of the Python module to execute. | ++----------------+-----------------------------------------------------------+ +| ``function`` | Name of the function in the Python module to run. Only | +| | works when also specifying the `module` attribute. | ++----------------+-----------------------------------------------------------+ +| ``args`` | Any arguments that should be passed to the script. | ++----------------+-----------------------------------------------------------+ +| ``output`` | Path to a file where any output by the script should be | +| | recorded. | ++----------------+-----------------------------------------------------------+ + +Either `file` or `module` must be specified. + +Examples +-------- + +.. code-block:: xml + + <python:exec module="pylint.lint" output="pylint-report.txt" args="myproj" /> + +Executes Pylint_ on the module/package ``myproj`` and stores the output into a +file named ``pylint-report.txt``. + + +---------------------- +``<python:distutils>`` +---------------------- + +Executes a distutils_ script. + +.. _distutils: http://docs.python.org/lib/module-distutils.html + +Parameters +---------- + ++----------------+-----------------------------------------------------------+ +| Name | Description | ++================+===========================================================+ +| `command` | The name of the `distutils` command that should be run | ++----------------+-----------------------------------------------------------+ +| `options` | Additional options to pass to the command, separated by | +| | spaces | ++----------------+-----------------------------------------------------------+ + +Examples +-------- + +.. code-block:: xml + + <python:distutils command="sdist" /> + +Instructs `distutils` to produce a source distribution. + +.. code-block:: xml + + <python:distutils command="unittest" options=" + --xml-output build/test-results.xml + --coverage-summary build/test-coverage.txt + --coverage-dir build/coverage"/> + +Instructs `distutils` to run the ``unittest`` command (which is provided by +Bitten), and passes the options needed to determine the output paths for test +results and code coverage reports. + + +--------------------- +``<python:unittest>`` +--------------------- + +Extracts information from unittest_ results recorded in an XML file. + +.. _unittest: http://docs.python.org/lib/module-unittest.html +.. note:: This report must be used in conjunction with the ``distutils`` command + "unittest" that comes with Bitten. + +Parameters +---------- + ++----------------+-----------------------------------------------------------+ +| Name | Description | ++================+===========================================================+ +| ``file`` | Path to the XML results file, relative to the project | +| | source directory. | ++----------------+-----------------------------------------------------------+ + +Examples +-------- + +.. code-block:: xml + + <python:unittest file="build/test-results.xml"/> + +Extracts the test results from the XML file located at +``build/test-results.xml``. + + +------------------ +``<python:trace>`` +------------------ + +Extracts coverage information recorded by the built-in Python module +``trace.py``. + +Parameters +---------- + ++--------------+-------------------------------------------------------------+ +| Name | Description | ++==============+=============================================================+ +| ``summary`` | Path to the summary file written by ``trace.py``, | +| | relative to the project source directory. | ++--------------+-------------------------------------------------------------+ +| ``coverdir`` | Path to the directory containing the coverage files written | +| | by ``trace.py``, relative to the project source directory. | ++--------------+-------------------------------------------------------------+ +| ``include`` | List of glob patterns (separated by space) that specify | +| | which Python file should be included in the coverage report | ++--------------+-------------------------------------------------------------+ +| ``exclude`` | List of glob patterns (separated by space) that specify | +| | which Python file should be excluded from the coverage | +| | report | ++--------------+-------------------------------------------------------------+ + +Examples +-------- + +.. code-block:: xml + + <python:trace summary="build/trace.out" coverdir="build/coverage" /> + +------------------- +``<python:pylint>`` +------------------- + +Extracts information from Pylint_ reports. + +.. _pylint: http://www.logilab.org/projects/pylint + +Parameters +---------- + ++--------------+-------------------------------------------------------------+ +| Name | Description | ++==============+=============================================================+ +| ``file`` | Path to the file containing the Pylint output, relative to | +| | the project source directory. | ++--------------+-------------------------------------------------------------+ + +Examples +-------- + +.. code-block:: xml + + <python:pylint file="build/pylint.out" /> + + +Subversion Tools +================ + +A collection of recipe commands for working with the Subversion_ version +control system. This commands are commonly used as the first step of a build +recipe to actually pull the code that should be built from the repository. + +.. _subversion: http://subversion.tigris.org/ + +:Namespace: ``http://bitten.cmlenz.net/tools/svn`` +:Common prefix: ``svn`` + + +------------------ +``<svn:checkout>`` +------------------ + +Check out a working copy from a Subversion repository. + +Parameters +---------- + ++--------------+-------------------------------------------------------------+ +| Name | Description | ++==============+=============================================================+ +| ``url`` | URL of the repository. | ++--------------+-------------------------------------------------------------+ +| ``path`` | The path inside the repository that should be checked out. | +| | You should normally set this to ``${path}`` so that the | +| | path of the build configuration is used. | ++--------------+-------------------------------------------------------------+ +| ``revision`` | The revision that should be checked out. You should | +| | normally set this to ``${revision}`` so that the revision | +| | of the build is used. | ++--------------+-------------------------------------------------------------+ +| ``dir`` | Path specifying which directory the sources should be | +| | checked out to (defaults to '.'). | ++--------------+-------------------------------------------------------------+ +| ``verbose`` | Whether to log the list of checked out files (defaults to | +| | False). | ++--------------+-------------------------------------------------------------+ + + +Examples +-------- + +.. code-block:: xml + + <svn:checkout url="http://svn.example.org/repos/myproject/" + path="${path}" revision="${revision}"/> + +This checks out the a working copy into the current directory. + + +---------------- +``<svn:export>`` +---------------- + +Download a file or directory from a Subversion repository. This is similar to +performing a checkout, but will not include the meta-data Subversion uses to +connect the local working copy to the repository (i.e. it does not include the +``.svn`` directories.) + +Parameters +---------- + ++--------------+-------------------------------------------------------------+ +| Name | Description | ++==============+=============================================================+ +| ``url`` | URL of the repository. | ++--------------+-------------------------------------------------------------+ +| ``path`` | The path inside the repository that should be checked out. | +| | You should normally set this to ``${path}`` so that the | +| | path of the build configuration is used. | ++--------------+-------------------------------------------------------------+ +| ``revision`` | The revision that should be checked out. You should | +| | normally set this to ``${revision}`` so that the revision | +| | of the build is used. | ++--------------+-------------------------------------------------------------+ +| ``dir`` | Path specifying which directory the sources should be | +| | exported to (defaults to '.') | ++--------------+-------------------------------------------------------------+ + +Examples +-------- + +.. code-block:: xml + + <svn:export url="http://svn.example.org/repos/myproject/" + path="${path}" revision="${revision}"/> + +This downloads the file or directory at ``${path}`` from the Subversion +repository at ``http://svn.example.org/repos/myproject/``. Variables are used +for the ``path`` and ``revision`` attributes so they are populated from the +properties of the build and build configuration. + + +---------------- +``<svn:update>`` +---------------- + +Update an existing working copy from a Subversion repository to a specific +revision. + +Parameters +---------- + ++--------------+-------------------------------------------------------------+ +| Name | Description | ++==============+=============================================================+ +| ``revision`` | The revision that should be checked out. You should | +| | normally set this to ``${revision}`` so that the revision | +| | of the build is used. | ++--------------+-------------------------------------------------------------+ +| ``dir`` | Path specifying the directory containing the sources to be | +| | updated (defaults to '.') | ++--------------+-------------------------------------------------------------+ + +Examples +-------- + +.. code-block:: xml + + <svn:update revision="${revision}"/> + +This updates the working copy in the current directory. The revision is +specified as a variable so that it is populated from the properties of the +build. + + +XML Tools +========= + +A collection of recipe commands for XML processing. + +:Namespace: ``http://bitten.cmlenz.net/tools/xml`` +:Common prefix: ``x`` + + +----------------- +``<x:transform>`` +----------------- + +Apply an XSLT stylesheet . + +.. note:: that this command requires either libxslt_ (with `Python bindings`_) + or, on Windows platforms, MSXML (version 3 or later) to be installed + on the slave machine. + +.. _libxslt: http://xmlsoft.org/XSLT/ +.. _`python bindings`: http://xmlsoft.org/XSLT/python.html + +Parameters +---------- + ++----------------+-----------------------------------------------------------+ +| Name | Description | ++================+===========================================================+ +| ``src`` | Path of the source XML file. | ++----------------+-----------------------------------------------------------+ +| ``dest`` | Path of the destition XML file. | ++----------------+-----------------------------------------------------------+ +| ``stylesheet`` | Path to the XSLT stylesheet file. | ++----------------+-----------------------------------------------------------+ + +All these are interpreted relative to the project source directory. + +Examples +-------- + +.. code-block:: xml + + <x:transform src="src.xml" dest="dest.xml" stylesheet="util/convert.xsl" /> + +This applies the stylesheet in ``util/convert.xsl`` to the source file +``src.xml``, and writes the resulting XML document to ``dest.xml``.
new file mode 100644 --- /dev/null +++ b/trac-0.11/doc/index.txt @@ -0,0 +1,25 @@ +.. -*- mode: rst; encoding: utf-8 -*- + +======= +Preface +======= + +.. image:: logo.png + :width: 538 + :height: 298 + :align: center + :alt: Bitten + :class: logo + +------------------------------- +Continuous Integration for Trac +------------------------------- + +Bitten is a Python-based framework for collecting various software metrics via +continuous integration. It builds on Trac to provide an integrated web-based +user interface. + + * `Installation <install.html>`_ + * `Build Recipes <recipes.html>`_ + * `Build Recipe Commands <commands.html>`_ + * `Generated API Documentation <api/index.html>`_
new file mode 100644 --- /dev/null +++ b/trac-0.11/doc/install.txt @@ -0,0 +1,121 @@ +.. -*- mode: rst; encoding: utf-8 -*- + +============ +Installation +============ + +.. contents:: Contents + :depth: 2 +.. sectnum:: + + +Prerequisites +============= + +Bitten is written in Python, so make sure that you have Python installed. +You'll need Python 2.3 or later. Also, make sure that setuptools_, version 0.6a2 +or later, is installed. + +.. _setuptools: http://peak.telecommunity.com/DevCenter/setuptools + +If that's taken care of, you just need to download and unpack the Bitten +distribution, and execute the command:: + + $ python setup.py install + +from the top of the directory where you unpacked (or checked out) the Bitten +code. Note that you may need administrator/root privileges for this step, as +it will by default attempt to install Bitten to the Python site-packages +directory on your system. + +It's also a good idea to run the unit tests at this point, to make sure that +the code works as expected on your platform:: + + $ python setup.py test + + +What's left to do now depends on whether you want to use the build master and +web interface, or just the build slave. In the latter case, you're already +done. You might need to install software that the build of your project +requires, but the Bitten build slave itself doesn't require anything extra. + +For the build master and web interface, you'll need to install Trac_ 0.10 or +later and the TracWebAdmin_ plugin. Please refer to the Trac documentation for +information on how it is installed. + +.. _trac: http://trac.edgewall.org/ +.. _tracwebadmin: http://trac.edgewall.org/wiki/WebAdmin + + +Build Master Configuration +========================== + +Once both Bitten and Trac are installed and working, you'll have to introduce +Bitten to your Trac project environment. If you don't have a Trac project +set up yet, you'll need to do so in order to use Bitten. + +If you already have a Trac project environment, the Bitten plugin needs to be +explicitly enabled in the Trac configuration. This is done by adding it to the +``[components]`` section in ``/path/to/projenv/conf/trac.ini``: + +.. code-block:: ini + + [components] + bitten.* = enabled + +The Trac web interface should now inform you with an error message that the +environment needs to be upgraded. To do this, run:: + + $ trac-admin /path/to/projenv upgrade + +This will create the database tables and directories that Bitten requires. +You probably also want to grant permissions to someone (such as yourself) +to manage build configurations, and allow anonymous users to view the +status and results of builds:: + + $ trac-admin /path/to/projenv permission add anonymous BUILD_EXEC + $ trac-admin /path/to/projenv permission add anonymous BUILD_VIEW + $ trac-admin /path/to/projenv permission add [yourname] BUILD_ADMIN + +You should now see an additional tab labeled "Build Status" in the Trac +navigation bar. This link will take you to the list of build configurations, +which at this point is of course empty. + +To add build configurations, you need to have the TracWebAdmin_ plugin +installed. + +.. warning:: The TracWebAdmin needs to be installed even if you're using Trac + 0.11 or later, which basically provides a builtin web + administration interface. Make sure that you disable the plugin in + that case, though (``webadmin.* = disabled``). While somewhat + counterintuitive, this process allows the Bitten administration UI + to neatly integrate into the new web administration interface added + in Trac 0.11. + +If both TracWebAdmin_ and Bitten are installed, and you are logged in as a user +with the required permissions, you should see additional administration pages +inside the “Admin” area, under a group named “Builds”. These pages allow you to +set options of the build master, and manage build configurations. + +Add a new build configuration and fill out the form. Also, add at least one +target platform after saving the configuration. Last but not least, you'll have +to "activate" your new build configuration. + + +Running the Build Slave +======================= + +The build slave can be run on any machine that can connect to the machine +on which the build master is running. The installation of Bitten should have put +a `bitten-slave` executable on your path. If the script is not on your path, +look for it in the `bin` or `scripts` subdirectory of your Python installation. + +To get a list of options for the build slave, execute it with the `--help` +option:: + + $ bitten-slave --help + +To run the build slave against a Bitten-enabled Trac site installed at +http://myproject.example.org/trac, you'd run:: + + $ bitten-slave http://myproject.example.org/trac/builds
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..665402ec268b69d884c17379346cf580f2e6355c GIT binary patch literal 41793 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
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..bb4daf76efb72105cca5ea1e1b97fe70dbde9ad9 GIT binary patch literal 20260 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�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
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..aba3415df5c2aebadd03ccf631cd9549f721648a GIT binary patch literal 5889 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
new file mode 100644 --- /dev/null +++ b/trac-0.11/doc/recipes.txt @@ -0,0 +1,68 @@ +.. -*- mode: rst; encoding: utf-8 -*- + +============= +Build Recipes +============= + +A build recipe tells a build slave how a project is to be built. It consists of +multiple build steps, each defining a command to execute, and where artifacts +can be found after that command has successfully completed. + +Build recipes are intended to supplement existing project build files (such as +Makefiles), not to replace them. In general, a recipe will be much simpler than +the build file itself, because it doesn't deal with all the details of the +build. It just automates the execution of the build and lets the build slave +locate any artifacts and metrics data generated in the course of the build. + +A recipe can and should split the build into multiple separate steps so that the +build slave can provide better status reporting to the build master while the +build is still in progress. This is important for builds that might take long to +execute. In addition, build steps help organize the build results for a more +structured presentation. + +.. contents:: Contents + :depth: 2 +.. sectnum:: + + +File Format +=========== + +Build recipes are stored internally in an XML-based format. Recipe documents +have a single ``<build>`` root element with one or more ``<step>`` child +elements. The steps are executed in the order they appear in the recipe. + +A ``<step>`` element will consist of any number of commands and reports. Most of +these elements are declared in XML namespaces, where the namespace URI defines +a collection of related commands. + +Commonly, the first step of any build recipe will perform the checkout from the +repository. + +.. code-block:: xml + + <build xmlns:python="http://bitten.cmlenz.net/tools/python" + xmlns:svn="http://bitten.cmlenz.net/tools/svn"> + + <step id="checkout" description="Checkout source from repository"> + <svn:checkout url="http://svn.example.org/repos/foo" + path="${path}" revision="${revision}" /> + </step> + + <step id="build" description="Compile to byte code"> + <python:distutils command="build"/> + </step> + + <step id="test" description="Run unit tests"> + <python:distutils command="unittest"/> + <python:unittest file="build/test-results.xml"/> + <python:trace summary="build/test-coverage.txt" + coverdir="build/coverage" include="trac*" exclude="*.tests.*"/> + </step> + + </build> + +See `Build Recipe Commands`_ for a comprehensive reference of the commands +available by default. + +.. _`build recipe commands`: commands.html
new file mode 100644 --- /dev/null +++ b/trac-0.11/scripts/proxy.py @@ -0,0 +1,93 @@ +# Based on the proxy module from the Medusa project +# Used for inspecting the communication between two BEEP peers + +import asynchat +import asyncore +import socket +import sys + + +class proxy_server(asyncore.dispatcher): + + def __init__(self, host, port): + asyncore.dispatcher.__init__ (self) + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.set_reuse_addr() + self.there = (host, port) + here = ('', port + 1) + self.bind(here) + self.listen(5) + + def handle_accept(self): + proxy_receiver(self, self.accept()) + + +class proxy_sender(asynchat.async_chat): + + def __init__(self, receiver, address): + asynchat.async_chat.__init__(self) + self.receiver = receiver + self.set_terminator(None) + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.buffer = '' + self.set_terminator('\r\n') + self.connect(address) + print 'L:', '<wait for incoming connections>' + + def handle_connect(self): + print 'L:', '<open connection>' + + def collect_incoming_data(self, data): + self.buffer = self.buffer + data + + def found_terminator(self): + data = self.buffer + self.buffer = '' + for line in data.splitlines(): + print 'L:', '\x1b[35m' + line + '\x1b[0m' + self.receiver.push(data + '\r\n') + + def handle_close(self): + self.receiver.close() + self.close() + + +class proxy_receiver(asynchat.async_chat): + + channel_counter = 0 + + def __init__(self, server, (conn, addr)): + asynchat.async_chat.__init__(self, conn) + self.set_terminator('\r\n') + self.server = server + self.id = self.channel_counter + self.channel_counter = self.channel_counter + 1 + self.sender = proxy_sender (self, server.there) + self.sender.id = self.id + self.buffer = '' + + def collect_incoming_data (self, data): + self.buffer = self.buffer + data + + def found_terminator(self): + data = self.buffer + self.buffer = '' + for line in data.splitlines(): + print 'I:', '\x1b[34m' + line + '\x1b[0m' + self.sender.push (data + '\r\n') + + def handle_connect(self): + print 'I:', '<open connection>' + + def handle_close(self): + print 'I:', '<close connection>' + self.sender.close() + self.close() + + +if __name__ == '__main__': + if len(sys.argv) < 3: + print 'Usage: %s <server-host> <server-port>' % sys.argv[0] + else: + ps = proxy_server(sys.argv[1], int(sys.argv[2])) + asyncore.loop()
new file mode 100644 --- /dev/null +++ b/trac-0.11/setup.cfg @@ -0,0 +1,8 @@ +[egg_info] +tag_build = dev +tag_svn_revision = true + +[unittest] +xml_output = build/test-results.xml +coverage_summary = build/test-coverage.txt +coverage_dir = build/coverage
new file mode 100755 --- /dev/null +++ b/trac-0.11/setup.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Edgewall Software +# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.edgewall.org/wiki/License. + +import os +from setuptools import setup, find_packages +import sys + +sys.path.append(os.path.join('doc', 'common')) +try: + from doctools import build_doc, test_doc +except ImportError: + build_doc = test_doc = None + +NS = 'http://bitten.cmlenz.net/tools/' + +setup( + name = 'Bitten', + version = '0.6', + description = 'Continuous integration for Trac', + long_description = \ +"""A Trac plugin for collecting software metrics via continuous integration.""", + author = 'Edgewall Software', + author_email = 'info@edgewall.org', + license = 'BSD', + url = 'http://bitten.edgewall.org/', + download_url = 'http://bitten.edgewall.org/wiki/Download', + zip_safe = False, + + packages = find_packages(exclude=['*.tests*']), + package_data = { + 'bitten': ['htdocs/*.*', + 'htdocs/charts_library/*.swf', + 'templates/*.html'] + }, + test_suite = 'bitten.tests.suite', + entry_points = { + 'console_scripts': [ + 'bitten-slave = bitten.slave:main' + ], + 'distutils.commands': [ + 'unittest = bitten.util.testrunner:unittest' + ], + 'trac.plugins': [ + 'bitten.admin = bitten.admin', + 'bitten.main = bitten.main', + 'bitten.master = bitten.master', + 'bitten.web_ui = bitten.web_ui', + 'bitten.testing = bitten.report.testing', + 'bitten.coverage = bitten.report.coverage' + ], + 'bitten.recipe_commands': [ + NS + 'sh#exec = bitten.build.shtools:exec_', + NS + 'sh#pipe = bitten.build.shtools:pipe', + NS + 'c#configure = bitten.build.ctools:configure', + NS + 'c#autoreconf = bitten.build.ctools:autoreconf', + NS + 'c#cppunit = bitten.build.ctools:cppunit', + NS + 'c#cunit = bitten.build.ctools:cunit', + NS + 'c#gcov = bitten.build.ctools:gcov', + NS + 'c#make = bitten.build.ctools:make', + NS + 'java#ant = bitten.build.javatools:ant', + NS + 'java#junit = bitten.build.javatools:junit', + NS + 'java#cobertura = bitten.build.javatools:cobertura', + NS + 'php#phing = bitten.build.phptools:phing', + NS + 'php#phpunit = bitten.build.phptools:phpunit', + NS + 'php#coverage = bitten.build.phptools:coverage', + NS + 'python#coverage = bitten.build.pythontools:coverage', + NS + 'python#distutils = bitten.build.pythontools:distutils', + NS + 'python#exec = bitten.build.pythontools:exec_', + NS + 'python#figleaf = bitten.build.pythontools:figleaf', + NS + 'python#pylint = bitten.build.pythontools:pylint', + NS + 'python#trace = bitten.build.pythontools:trace', + NS + 'python#unittest = bitten.build.pythontools:unittest', + NS + 'svn#checkout = bitten.build.svntools:checkout', + NS + 'svn#export = bitten.build.svntools:export', + NS + 'svn#update = bitten.build.svntools:update', + NS + 'xml#transform = bitten.build.xmltools:transform' + ] + }, + + cmdclass = {'build_doc': build_doc, 'test_doc': test_doc} +)