2
# This file is part of Checkbox.
4
# Copyright 2010 Canonical Ltd.
6
# Checkbox is free software: you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation, either version 3 of the License, or
9
# (at your option) any later version.
11
# Checkbox is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
# GNU General Public License for more details.
16
# You should have received a copy of the GNU General Public License
17
# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
27
from collections import defaultdict
28
from gettext import gettext as _
30
from checkbox.lib.resolver import Resolver
32
from checkbox.arguments import coerce_arguments
33
from checkbox.plugin import Plugin
34
from checkbox.properties import (
47
"type": String(required=False),
48
"status": String(required=False),
49
"suite": String(required=False),
50
"description": String(required=False),
51
"purpose": String(required=False),
52
"steps": String(required=False),
53
"info": String(required=False),
54
"verification": String(required=False),
55
"command": String(required=False),
56
"depends": List(String(), required=False),
57
"duration": Float(required=False),
58
"environ": List(String(), required=False),
59
"requires": List(String(), separator=r"\n", required=False),
60
"resources": List(String(), required=False),
61
"timeout": Int(required=False),
62
"user": String(required=False),
63
"data": String(required=False)})
66
class JobsInfo(Plugin):
68
# Domain for internationalization
69
domain = String(default="checkbox")
71
# Space separated list of directories where job files are stored.
72
directories = List(Path(),
73
default_factory=lambda: "%(checkbox_share)s/jobs")
75
# List of jobs to blacklist
76
blacklist = List(String(), default_factory=lambda: "")
78
# Path to blacklist file
79
blacklist_file = Path(required=False)
81
# List of jobs to whitelist
82
whitelist = List(String(), default_factory=lambda: "")
84
# Path to whitelist file
85
whitelist_file = Path(required=False)
87
def register(self, manager):
88
super(JobsInfo, self).register(manager)
90
self.whitelist_patterns = self.get_patterns(
91
self.whitelist, self.whitelist_file)
92
self.blacklist_patterns = self.get_patterns(
93
self.blacklist, self.blacklist_file)
94
self.selected_jobs = defaultdict(list)
96
self._missing_dependencies_report = ""
98
self._manager.reactor.call_on("prompt-begin", self.prompt_begin)
99
self._manager.reactor.call_on("gather", self.gather)
100
if logging.getLogger().getEffectiveLevel() <= logging.DEBUG:
101
self._manager.reactor.call_on(
102
"prompt-gather", self.post_gather, 90)
103
self._manager.reactor.call_on("report-job", self.report_job, -100)
105
def prompt_begin(self, interface):
107
Capture interface object to use it later
110
self.interface = interface
111
self.unused_patterns = (
112
self.whitelist_patterns + self.blacklist_patterns)
114
def check_ordered_messages(self, messages):
115
"""Return whether the list of messages are ordered or not.
116
Also populates a _missing_dependencies_report string variable
117
with a report of any jobs that are required but not present
120
all_names = set([message['name'] for message in messages])
121
messages_ordered = True
122
missing_dependencies = defaultdict(set)
124
for message in messages:
125
name = message["name"]
126
for dependency in message.get("depends", []):
127
if dependency not in names_so_far:
128
messages_ordered = False
129
#Two separate checks :) we *could* save a negligible
130
#bit of time by putting this inside the previous "if"
131
#but we're not in *that* big a hurry.
132
if dependency not in all_names:
133
missing_dependencies[name].add(dependency)
134
names_so_far.add(name)
136
#Now assemble the list of missing deps into a nice report
137
jobs_and_missing_deps = ["{} required by {}".format(job_name,
138
", ".join(missing_dependencies[job_name]))
139
for job_name in missing_dependencies]
140
self._missing_dependencies_report = "\n".join(jobs_and_missing_deps)
142
return messages_ordered
144
def get_patterns(self, strings, filename=None):
145
"""Return the list of strings as compiled regular expressions."""
148
file = open(filename)
150
error_message = (_("Failed to open file '%s': %s")
151
% (filename, e.strerror))
152
logging.critical(error_message)
153
sys.stderr.write("%s\n" % error_message)
154
sys.exit(os.EX_NOINPUT)
156
strings.extend([l.strip() for l in file.readlines()])
158
return [re.compile(r"^%s$" % s) for s in strings
159
if s and not s.startswith("#")]
161
def get_unique_messages(self, messages):
162
"""Return the list of messages without any duplicates, giving
163
precedence to messages that are the longest.
167
for message in messages:
168
name = message["name"]
169
index = unique_indexes.get(name)
171
unique_indexes[name] = len(unique_messages)
172
unique_messages.append(message)
173
elif len(message) > len(unique_messages[index]):
174
unique_messages[index] = message
176
return unique_messages
179
# Register temporary handler for report-message events
182
def report_message(message):
183
if self.whitelist_patterns:
184
name = message["name"]
185
names = [name for p in self.whitelist_patterns
190
messages.append(message)
192
# Set domain and message event handler
193
old_domain = gettext.textdomain()
194
gettext.textdomain(self.domain)
195
event_id = self._manager.reactor.call_on(
196
"report-message", report_message, 100)
198
for directory in self.directories:
199
self._manager.reactor.fire("message-directory", directory)
201
for message in messages:
202
self._manager.reactor.fire("report-job", message)
204
# Unset domain and event handler
205
self._manager.reactor.cancel_call(event_id)
206
gettext.textdomain(old_domain)
208
# Get unique messages from the now complete list
209
messages = self.get_unique_messages(messages)
211
# Apply whitelist ordering
212
if self.whitelist_patterns:
213
def key_function(obj):
215
for pattern in self.whitelist_patterns:
216
if pattern.match(name):
217
return self.whitelist_patterns.index(pattern)
219
messages = sorted(messages, key=key_function)
221
if not self.check_ordered_messages(messages):
222
#One of two things may have happened if we enter this code path.
223
#Either the jobs are not in topological ordering,
224
#Or they are in topological ordering but a dependency is
226
old_message_names = [
227
message["name"] + "\n" for message in messages]
228
resolver = Resolver(key_func=lambda m: m["name"])
229
for message in messages:
231
message, *message.get("depends", []))
232
messages = resolver.get_dependents()
234
if (self.whitelist_patterns and
235
logging.getLogger().getEffectiveLevel() <= logging.DEBUG):
236
new_message_names = [
237
message["name"] + "\n" for message in messages]
238
#This will contain a report of out-of-order jobs.
239
detailed_text = "".join(
240
difflib.unified_diff(
245
#First, we report missing dependencies, if any.
246
if self._missing_dependencies_report:
247
primary = _("Dependencies are missing so some jobs "
249
secondary = _("To fix this, close checkbox and add "
250
"the missing dependencies to the "
252
self._manager.reactor.fire("prompt-warning",
256
self._missing_dependencies_report)
257
#If detailed_text is empty, it means the problem
258
#was missing dependencies, which we already reported.
259
#Otherwise, we also need to report reordered jobs here.
261
primary = _("Whitelist not topologically ordered")
262
secondary = _("Jobs will be reordered to fix broken "
264
self._manager.reactor.fire("prompt-warning",
270
self._manager.reactor.fire("report-jobs", messages)
272
def post_gather(self, interface):
274
Verify that all patterns were used
276
if logging.getLogger().getEffectiveLevel() > logging.DEBUG:
279
orphan_test_cases = []
280
for name, jobs in self.selected_jobs.items():
281
is_test = any(job.get('type') == 'test' for job in jobs)
282
has_suite = any(job.get('suite') for job in jobs)
283
if is_test and not has_suite:
284
orphan_test_cases.append(name)
286
if orphan_test_cases:
288
('Test cases not included in any test suite:\n'
290
'This might cause problems '
291
'when uploading test cases results.\n'
292
'Please make sure that the patterns you used are up-to-date\n'
293
.format('\n'.join(['- {0}'.format(tc)
294
for tc in orphan_test_cases])))
295
self._manager.reactor.fire('prompt-warning', self.interface,
296
'Orphan test cases detected',
297
"Some test cases aren't included "
301
if self.unused_patterns:
303
('Unused patterns:\n'
305
"Please make sure that the patterns you used are up-to-date\n"
306
.format('\n'.join(['- {0}'.format(p.pattern[1:-1])
307
for p in self.unused_patterns])))
308
self._manager.reactor.fire('prompt-warning', self.interface,
310
'Please make sure that the patterns '
311
'you used are up-to-date',
314
@coerce_arguments(job=job_schema)
315
def report_job(self, job):
318
patterns = self.whitelist_patterns or self.blacklist_patterns
320
match = next((p for p in patterns if p.match(name)), None)
322
# Keep track of which patterns didn't match any job
323
if match in self.unused_patterns:
324
self.unused_patterns.remove(match)
325
self.selected_jobs[name].append(job)
327
# Stop if job not in whitelist or in blacklist
328
self._manager.reactor.stop()