~jocave/checkbox/hybrid-amd-gpu-mods

« back to all changes in this revision

Viewing changes to checkbox-old/plugins/jobs_info.py

  • Committer: Zygmunt Krynicki
  • Date: 2013-05-29 07:50:30 UTC
  • mto: This revision was merged to the branch mainline in revision 2153.
  • Revision ID: zygmunt.krynicki@canonical.com-20130529075030-ngwz245hs2u3y6us
checkbox: move current checkbox code into checkbox-old

This patch cleans up the top-level directory of the project into dedicated
sub-project directories. One for checkbox-old (the current checkbox and all the
associated stuff), one for plainbox and another for checkbox-ng.

There are some associated changes, such as updating the 'source' mode of
checkbox provider in plainbox, and fixing paths in various test scripts that we
have.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#
 
2
# This file is part of Checkbox.
 
3
#
 
4
# Copyright 2010 Canonical Ltd.
 
5
#
 
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.
 
10
#
 
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.
 
15
#
 
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/>.
 
18
#
 
19
import os
 
20
import re
 
21
import sys
 
22
 
 
23
import difflib
 
24
import gettext
 
25
import logging
 
26
 
 
27
from collections import defaultdict
 
28
from gettext import gettext as _
 
29
 
 
30
from checkbox.lib.resolver import Resolver
 
31
 
 
32
from checkbox.arguments import coerce_arguments
 
33
from checkbox.plugin import Plugin
 
34
from checkbox.properties import (
 
35
    Float,
 
36
    Int,
 
37
    List,
 
38
    Map,
 
39
    Path,
 
40
    String,
 
41
    )
 
42
 
 
43
 
 
44
job_schema = Map({
 
45
    "plugin": String(),
 
46
    "name": String(),
 
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)})
 
64
 
 
65
 
 
66
class JobsInfo(Plugin):
 
67
 
 
68
    # Domain for internationalization
 
69
    domain = String(default="checkbox")
 
70
 
 
71
    # Space separated list of directories where job files are stored.
 
72
    directories = List(Path(),
 
73
        default_factory=lambda: "%(checkbox_share)s/jobs")
 
74
 
 
75
    # List of jobs to blacklist
 
76
    blacklist = List(String(), default_factory=lambda: "")
 
77
 
 
78
    # Path to blacklist file
 
79
    blacklist_file = Path(required=False)
 
80
 
 
81
    # List of jobs to whitelist
 
82
    whitelist = List(String(), default_factory=lambda: "")
 
83
 
 
84
    # Path to whitelist file
 
85
    whitelist_file = Path(required=False)
 
86
 
 
87
    def register(self, manager):
 
88
        super(JobsInfo, self).register(manager)
 
89
 
 
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)
 
95
 
 
96
        self._missing_dependencies_report = ""
 
97
 
 
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)
 
104
 
 
105
    def prompt_begin(self, interface):
 
106
        """
 
107
        Capture interface object to use it later
 
108
        to display errors
 
109
        """
 
110
        self.interface = interface
 
111
        self.unused_patterns = (
 
112
            self.whitelist_patterns + self.blacklist_patterns)
 
113
 
 
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
 
118
           in the whitelist."""
 
119
        names_so_far = set()
 
120
        all_names = set([message['name'] for message in messages])
 
121
        messages_ordered = True
 
122
        missing_dependencies = defaultdict(set)
 
123
 
 
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)
 
135
 
 
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) 
 
141
        
 
142
        return messages_ordered
 
143
 
 
144
    def get_patterns(self, strings, filename=None):
 
145
        """Return the list of strings as compiled regular expressions."""
 
146
        if filename:
 
147
            try:
 
148
                file = open(filename)
 
149
            except IOError as e:
 
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)
 
155
            else:
 
156
                strings.extend([l.strip() for l in file.readlines()])
 
157
 
 
158
        return [re.compile(r"^%s$" % s) for s in strings
 
159
            if s and not s.startswith("#")]
 
160
 
 
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.
 
164
        """
 
165
        unique_messages = []
 
166
        unique_indexes = {}
 
167
        for message in messages:
 
168
            name = message["name"]
 
169
            index = unique_indexes.get(name)
 
170
            if index is None:
 
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
 
175
 
 
176
        return unique_messages
 
177
 
 
178
    def gather(self):
 
179
        # Register temporary handler for report-message events
 
180
        messages = []
 
181
 
 
182
        def report_message(message):
 
183
            if self.whitelist_patterns:
 
184
                name = message["name"]
 
185
                names = [name for p in self.whitelist_patterns
 
186
                    if p.match(name)]
 
187
                if not names:
 
188
                    return
 
189
 
 
190
            messages.append(message)
 
191
 
 
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)
 
197
 
 
198
        for directory in self.directories:
 
199
            self._manager.reactor.fire("message-directory", directory)
 
200
 
 
201
        for message in messages:
 
202
            self._manager.reactor.fire("report-job", message)
 
203
 
 
204
        # Unset domain and event handler
 
205
        self._manager.reactor.cancel_call(event_id)
 
206
        gettext.textdomain(old_domain)
 
207
 
 
208
        # Get unique messages from the now complete list
 
209
        messages = self.get_unique_messages(messages)
 
210
 
 
211
        # Apply whitelist ordering
 
212
        if self.whitelist_patterns:
 
213
            def key_function(obj):
 
214
                name = obj["name"]
 
215
                for pattern in self.whitelist_patterns:
 
216
                    if pattern.match(name):
 
217
                        return self.whitelist_patterns.index(pattern)
 
218
 
 
219
            messages = sorted(messages, key=key_function)
 
220
 
 
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
 
225
            #missing.
 
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:
 
230
                resolver.add(
 
231
                    message, *message.get("depends", []))
 
232
            messages = resolver.get_dependents()
 
233
 
 
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(
 
241
                        old_message_names,
 
242
                        new_message_names,
 
243
                        "old whitelist",
 
244
                        "new whitelist"))
 
245
                #First, we report missing dependencies, if any.
 
246
                if self._missing_dependencies_report:
 
247
                    primary = _("Dependencies are missing so some jobs "
 
248
                                "will not run.")
 
249
                    secondary = _("To fix this, close checkbox and add "
 
250
                                  "the missing dependencies to the "
 
251
                                  "whitelist.")
 
252
                    self._manager.reactor.fire("prompt-warning",
 
253
                            self.interface,
 
254
                            primary,
 
255
                            secondary,
 
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.
 
260
                if detailed_text:
 
261
                    primary = _("Whitelist not topologically ordered")
 
262
                    secondary = _("Jobs will be reordered to fix broken "
 
263
                                  "dependencies")
 
264
                    self._manager.reactor.fire("prompt-warning",
 
265
                                               self.interface,
 
266
                                               primary,
 
267
                                               secondary,
 
268
                                               detailed_text)
 
269
 
 
270
        self._manager.reactor.fire("report-jobs", messages)
 
271
 
 
272
    def post_gather(self, interface):
 
273
        """
 
274
        Verify that all patterns were used
 
275
        """
 
276
        if logging.getLogger().getEffectiveLevel() > logging.DEBUG:
 
277
            return
 
278
 
 
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)
 
285
 
 
286
        if orphan_test_cases:
 
287
            detailed_error = \
 
288
                ('Test cases not included in any test suite:\n'
 
289
                 '{0}\n\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 "
 
298
                                       'in any test suite',
 
299
                                       detailed_error)
 
300
 
 
301
        if self.unused_patterns:
 
302
            detailed_error = \
 
303
                ('Unused patterns:\n'
 
304
                 '{0}\n\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,
 
309
                                       'Unused patterns',
 
310
                                       'Please make sure that the patterns '
 
311
                                       'you used are up-to-date',
 
312
                                       detailed_error)
 
313
 
 
314
    @coerce_arguments(job=job_schema)
 
315
    def report_job(self, job):
 
316
        name = job["name"]
 
317
 
 
318
        patterns = self.whitelist_patterns or self.blacklist_patterns
 
319
        if patterns:
 
320
            match = next((p for p in patterns if p.match(name)), None)
 
321
            if match:
 
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)
 
326
            else:
 
327
                # Stop if job not in whitelist or in blacklist
 
328
                self._manager.reactor.stop()
 
329
 
 
330
 
 
331
factory = JobsInfo