-
Notifications
You must be signed in to change notification settings - Fork 1
/
HudsonTracPlugin.py
233 lines (191 loc) · 8.83 KB
/
HudsonTracPlugin.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# -*- coding: utf-8 -*-
"""
A Trac plugin which interfaces with the Hudson Continuous integration server
You can configure this component via the
[wiki:TracIni#hudson-section "[hudson]"]
section in the trac.ini file.
See also:
- http://hudson-ci.org/
- http://wiki.hudson-ci.org/display/HUDSON/Trac+Plugin
"""
import time
import urllib2
from xml.dom import minidom
from datetime import datetime
from genshi.builder import tag
from trac.core import *
from trac.config import Option, BoolOption
from trac.perm import IPermissionRequestor
from trac.util.datefmt import format_datetime, pretty_timedelta, to_timestamp
from trac.util.text import unicode_quote
from trac.web.chrome import INavigationContributor, ITemplateProvider, add_stylesheet
from trac.timeline.api import ITimelineEventProvider
class HudsonTracPlugin(Component):
"""Display Hudson results in the timeline and an entry in the main navigation bar.
"""
implements(INavigationContributor, ITimelineEventProvider,
ITemplateProvider, IPermissionRequestor)
disp_mod = BoolOption('hudson', 'display_modules', False, """
Display status of modules in the timeline too.
Note: enabling this may slow down the timeline retrieval significantly
""")
job_url = Option('hudson', 'job_url', 'http://localhost/hudson/', """
The url of the top-level hudson page if you want to display
all jobs, or a job or module url (such as
http://localhost/hudson/job/build_foo/) if you want only
display builds from a single job or module. This must be an
absolute url.""")
username = Option('hudson', 'username', '',
'The username to use to access hudson')
password = Option('hudson', 'password', '',
'The password to use to access hudson')
nav_url = Option('hudson', 'main_page', '/hudson/', """
The url of the hudson main page to which the trac nav entry
should link; if empty, no entry is created in the nav bar.
This may be a relative url.""")
disp_tab = BoolOption('hudson', 'display_in_new_tab', False,
'Open hudson page in new tab/window')
alt_succ = BoolOption('hudson', 'alternate_success_icon', False,
'Use an alternate success icon (green ball instead of blue)')
use_desc = BoolOption('hudson', 'display_build_descriptions', True, """
Whether to display the build descriptions for each build
instead of the canned "Build finished successfully etc."
messages.""")
display_building = BoolOption('hudson', 'display_building', False,
'Also show in-progress builds (pulsating green ball)')
def __init__(self):
api_url = unicode_quote(self.job_url, '/%:@')
if api_url and api_url[-1] != '/':
api_url += '/'
api_url += 'api/xml'
pwd_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
pwd_mgr.add_password(None, api_url, self.username, self.password)
b_auth = urllib2.HTTPBasicAuthHandler(pwd_mgr)
d_auth = urllib2.HTTPDigestAuthHandler(pwd_mgr)
self.url_opener = urllib2.build_opener(b_auth, d_auth)
self.env.log.debug("registered auth-handler for '%s', username='%s'",
api_url, self.username)
if '/job/' in api_url:
path = '/*/build[timestamp>=%(start)s][timestamp<=%(stop)s]'
depth = 1
if self.disp_mod:
path += ('|/*/module/build'
'[timestamp>=%(start)s][timestamp<=%(stop)s]')
depth += 1
else:
path = '/*/job/build[timestamp>=%(start)s][timestamp<=%(stop)s]'
depth = 2
if self.disp_mod:
path += ('|/*/job/module/build'
'[timestamp>=%(start)s][timestamp<=%(stop)s]')
depth += 1
self.info_url = ('%s?xpath=%s&depth=%s'
'&exclude=//action|//artifact|//changeSet'
'&wrapper=builds' %
(api_url.replace('%', '%%'), path, depth))
self.env.log.debug("Build-info url: '%s'", self.info_url)
# IPermissionRequestor methods
def get_permission_actions(self):
return ['BUILD_VIEW']
# INavigationContributor methods
def get_active_navigation_item(self, req):
return 'builds'
def get_navigation_items(self, req):
if self.nav_url and req.perm.has_permission('BUILD_VIEW'):
yield 'mainnav', 'builds', tag.a("Build", href=self.nav_url,
target=self.disp_tab and "hudson" or None)
# ITemplateProvider methods
def get_templates_dirs(self):
return []
return [self.env.get_templates_dir(),
self.config.get('trac', 'templates_dir')]
def get_htdocs_dirs(self):
from pkg_resources import resource_filename
return [('HudsonTrac', resource_filename(__name__, 'htdocs'))]
# ITimelineEventProvider methods
def get_timeline_filters(self, req):
if 'BUILD_VIEW' in req.perm:
yield ('build', 'Hudson Builds')
def get_timeline_events(self, req, start, stop, filters):
if 'build' not in filters or 'BUILD_VIEW' not in req.perm:
return
# xml parsing helpers
def get_text(node):
rc = ""
for node in node.childNodes:
if node.nodeType == node.TEXT_NODE:
rc += node.data
return rc
def get_string(parent, child):
nodes = parent.getElementsByTagName(child)
return nodes and get_text(nodes[0]).strip() or ''
def get_number(parent, child):
num = get_string(parent, child)
return num and int(num) or 0
start = to_timestamp(start)
stop = to_timestamp(stop)
# get and parse the build-info
url = self.info_url % {'start': start*1000, 'stop': stop*1000}
try:
try:
info = minidom.parse(self.url_opener.open(url))
except Exception:
import sys
self.env.log.exception("Error getting build info from '%s'",
url)
raise IOError(
"Error getting build info from '%s': %s: %s. This most " \
"likely means you configured a wrong job_url." % \
(url, sys.exc_info()[0].__name__, str(sys.exc_info()[1])))
finally:
self.url_opener.close()
if info.documentElement.nodeName != 'builds':
raise IOError(
"Error getting build info from '%s': returned document has "
"unexpected node '%s'. This most likely means you configured "
"a wrong job_url" % (info_url, info.documentElement.nodeName))
add_stylesheet(req, 'HudsonTrac/hudsontrac.css')
# extract all build entries
for entry in info.documentElement.getElementsByTagName("build"):
# ignore builds that are still running
if get_string(entry, 'building') == 'true':
if self.display_building:
result = 'INPROGRESS'
else:
continue
else:
result = get_string(entry, 'result')
# create timeline entry
started = get_number(entry, 'timestamp')
if result == 'INPROGRESS':
# we hope the clocks are close...
completed = time.time()
else:
duration = get_number(entry, 'duration')
completed = started + duration
completed /= 1000
started /= 1000
message, kind = {
'SUCCESS': ('Build finished successfully',
('build-successful',
'build-successful-alt')[self.alt_succ]),
'UNSTABLE': ('Build unstable', 'build-unstable'),
'ABORTED': ('Build aborted', 'build-aborted'),
'INPROGRESS': ('Build in progress', 'build-inprogress'),
}.get(result, ('Build failed', 'build-failed'))
if self.use_desc:
message = get_string(entry, 'description') or message
author = get_string(entry, 'fullName')
data = (get_string(entry, 'fullDisplayName'),
get_string(entry, 'url'),
result, message, started, completed)
yield kind, completed, author, data
def render_timeline_event(self, context, field, event):
name, url, result, message, started, completed = event[3]
if field == 'title':
return tag('Build "', tag.em(name), '" (%s)' % result.lower())
elif field == 'description':
return "%s duration %s" % \
(message, pretty_timedelta(started, completed))
elif field == 'url':
return url