This repository has been archived by the owner on Jun 1, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 12
/
lint.py
235 lines (188 loc) · 7.58 KB
/
lint.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
234
235
from twisted.python import log
from buildbot.status.builder import WARNINGS
from buildbot.steps.shell import ShellCommand
try:
import cStringIO
StringIO = cStringIO
except ImportError:
import StringIO
class LintStep(ShellCommand):
"""
A L{ShellCommand} that generates summary information of errors generated
during a build, and new errors generated vs. the most recent trunk build.
@ivar worse: a L{bool} indicating whether this build is worse with respect
to reported errors than the most recent trunk build.
"""
flunkOnWarnings = True
def createSummary(self, logObj):
logText = logObj.getText()
self.worse = self.processLogs(self.getPreviousLog(), logText)
def processLogs(self, oldText, newText):
currentErrors = self.computeErrors(newText)
previousErrors = self.computeErrors(oldText)
self.addCompleteLog('%s errors' % self.lintChecker, '\n'.join(self.formatErrors(currentErrors)))
self.formatErrors(previousErrors)
newErrors = self.computeDifference(currentErrors, previousErrors)
if newErrors:
allNewErrors = self.formatErrors(newErrors)
self.addCompleteLog('new %s errors' % self.lintChecker, '\n'.join(allNewErrors))
return bool(newErrors)
def computeErrors(self, logText):
"""
@type logText: L{str}
@param logText: output of lint command
@return: L{dict} of L{set}s containing errors generated by lint, grouped by
type
"""
raise NotImplementedError("Must implement computeErrors for a Lint step")
def formatErrors(self, newErrors):
raise NotImplementedError("Must implement formatErrors for a Lint step")
@staticmethod
def computeDifference(current, previous):
"""
Takes two dicts of sets, and computes the keywise difference.
@type current: L{dict} of L{set}s
@param current: errors from current build
@type previous: L{dict} of L{set}s
@param previous: errors from previous build
@return
@rtype L{dict}
"""
new = {}
for errorType in current:
errors = (
current[errorType] -
previous.get(errorType, set()))
log.msg("Found %d new errors of type %s" % (len(errors), errorType))
if errors:
new[errorType] = errors
return new
def getPreviousLog(self):
"""
Gets the output of lint from the last build of trunk.
@return: output of lint from last trunk build
@rtype: L{str}
"""
build = self._getLastBuild()
if build is None:
log.msg("Found no previous build, returning empty error log")
return ""
for logObj in build.getLogs():
if logObj.step.name == self.name and logObj.name == 'stdio':
text = logObj.getText()
log.msg("Found error log, returning %d bytes" % (len(text),))
return text
log.msg("Did not find error log, returning empty error log")
return ""
def _getLastBuild(self):
"""
Gets the L{BuildStatus} object of the most recent build of trunk.
@return: most recent build of trunk
@rtype: L{BuildStatus}
"""
status = self.build.build_status
number = status.getNumber()
if number == 0:
log.msg("last result is undefined because this is the first build")
return None
builder = status.getBuilder()
targetRevision = self.getProperty('lint_revision')
log.msg(format='Looking for build of %(revision)s', revision=targetRevision)
count = 0
lastTrunkBuild = None
while count < 200 and number > 0:
number -= 1
build = builder.getBuild(number)
if not build:
continue
branch = build.getProperty("branch")
revision = build.getProperty('got_revision')
if branch == "trunk":
count += 1
if revision == targetRevision:
log.msg(format="Found build %(number)d of trunk at %(revision)s",
number=number, revision=revision)
return build
else:
log.msg(format="skipping build %(number)d of trunk at %(revision)s",
number=number, revision=revision)
try:
if revision and not lastTrunkBuild:
lastTrunkBuild = (revision, build)
except TypeError:
pass
else:
log.msg(format="skipping build %(number)d of branch %(branch)r at %(revision)s",
number=number, revision=revision, branch=branch)
log.msg(format="falling off the end after searching %(count)d builds",
count=status.getNumber() - number)
if lastTrunkBuild:
revision, build = lastTrunkBuild
log.msg(format="Using build %(number)d at %(revision)s instead of %(targetRevision)s",
number=build.getNumber(), revision=revision, targetRevision=targetRevision)
return build
return None
def evaluateCommand(self, cmd):
if self.worse:
return WARNINGS
return ShellCommand.evaluateCommand(self, cmd)
class CheckDocumentation(LintStep):
"""
Run Pydoctor over the source to check for errors in API
documentation.
"""
name = 'api-documentation'
command = ('tox', '-e', 'apidocs')
description = ["checking", "api", "docs"]
descriptionDone = ["api", "docs"]
lintChecker = 'pydoctor'
@staticmethod
def computeErrors(logText):
errors = {}
for line in StringIO.StringIO(logText):
try:
# Mostly get rid of the trailing \n
line = line.strip()
if 'invalid ref to' in line:
key = 'invalid ref'
# Discard the line number since it's pretty unstable
# over time
fqpnlineno, rest = line.split(' ', 1)
fqpn, lineno = fqpnlineno.split(':')
value = '%s: %s' % (fqpn, rest)
elif 'found unknown field on' in line:
key = 'unknown fields'
value = line
else:
continue
errors.setdefault(key, set()).add(value)
except: # TODO: This should be handled better.
log.err()
return errors
def formatErrors(self, newErrors):
allNewErrors = []
for errorType in newErrors:
allNewErrors.extend(newErrors[errorType])
allNewErrors.sort()
return allNewErrors
def getText(self, cmd, results):
if results == WARNINGS:
return ["api", "docs"]
return ShellCommand.getText(self, cmd, results)
def filterTox(logText):
"""
Filter out the tox output for lint tox envs -- where there's only one
command.
"""
toxStatus = 'NOT_STARTED'
for line in StringIO.StringIO(logText):
if " runtests: commands[0] | " in line:
# Tox has started, further lines should be read
toxStatus = 'STARTED'
elif "ERROR: InvocationError:" in line:
# Tox is finished
toxStatus = 'FINISHED'
elif '___ summary ___' in line:
toxStatus = 'FINISHED'
elif toxStatus == 'STARTED':
yield line.strip("\n")