xref: /aosp_15_r20/external/autotest/tko/parsers/version_0.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1# Lint as: python2, python3
2# pylint: disable=missing-docstring
3import os
4import re
5
6import common
7from autotest_lib.tko import models
8from autotest_lib.tko import status_lib
9from autotest_lib.tko import utils as tko_utils
10from autotest_lib.tko.parsers import base
11
12class NoHostnameError(Exception):
13    pass
14
15
16class BoardLabelError(Exception):
17    pass
18
19
20class job(models.job):
21    def __init__(self, dir):
22        job_dict = job.load_from_dir(dir)
23        super(job, self).__init__(dir, **job_dict)
24
25
26    @classmethod
27    def load_from_dir(cls, dir):
28        keyval = cls.read_keyval(dir)
29        tko_utils.dprint(str(keyval))
30
31        user = keyval.get("user", None)
32        label = keyval.get("label", None)
33        queued_time = tko_utils.get_timestamp(keyval, "job_queued")
34        started_time = tko_utils.get_timestamp(keyval, "job_started")
35        finished_time = tko_utils.get_timestamp(keyval, "job_finished")
36        machine = cls.determine_hostname(keyval, dir)
37        machine_group = cls.determine_machine_group(machine, dir)
38        machine_owner = keyval.get("owner", None)
39
40        aborted_by = keyval.get("aborted_by", None)
41        aborted_at = tko_utils.get_timestamp(keyval, "aborted_on")
42
43        return {"user": user, "label": label, "machine": machine,
44                "queued_time": queued_time, "started_time": started_time,
45                "finished_time": finished_time, "machine_owner": machine_owner,
46                "machine_group": machine_group, "aborted_by": aborted_by,
47                "aborted_on": aborted_at, "keyval_dict": keyval}
48
49
50    @classmethod
51    def determine_hostname(cls, keyval, job_dir):
52        host_group_name = keyval.get("host_group_name", None)
53        machine = keyval.get("hostname", "")
54        is_multimachine = "," in machine
55
56        # determine what hostname to use
57        if host_group_name:
58            if is_multimachine or not machine:
59                tko_utils.dprint("Using host_group_name %r instead of "
60                                 "machine name." % host_group_name)
61                machine = host_group_name
62        elif is_multimachine:
63            try:
64                machine = job.find_hostname(job_dir) # find a unique hostname
65            except NoHostnameError:
66                pass  # just use the comma-separated name
67
68        tko_utils.dprint("MACHINE NAME: %s" % machine)
69        return machine
70
71
72    @classmethod
73    def determine_machine_group(cls, hostname, job_dir):
74        machine_groups = set()
75        for individual_hostname in hostname.split(","):
76            host_keyval = models.test.parse_host_keyval(job_dir,
77                                                        individual_hostname)
78            if not host_keyval:
79                tko_utils.dprint('Unable to parse host keyval for %s'
80                                 % individual_hostname)
81            elif 'labels' in host_keyval:
82                # Use `model` label as machine group. This is to avoid the
83                # confusion of multiple boards mapping to the same platform in
84                # wmatrix. With this change, wmatrix will group tests with the
85                # same model, rather than the same platform.
86                labels = host_keyval['labels'].split(',')
87                board_labels = [l[8:] for l in labels
88                               if l.startswith('model%3A')]
89                # If the host doesn't have `model:` label, fall back to `board:`
90                # label.
91                if not board_labels:
92                    board_labels = [l[8:] for l in labels
93                               if l.startswith('board%3A')]
94                if board_labels:
95                    # Multiple board/model labels aren't supposed to
96                    # happen, but let's report something valid rather
97                    # than just failing.
98                    machine_groups.add(','.join(board_labels))
99                else:
100                    error = ('Failed to retrieve board label from host labels: '
101                             '%s' % host_keyval['labels'])
102                    tko_utils.dprint(error)
103                    raise BoardLabelError(error)
104            elif "platform" in host_keyval:
105                machine_groups.add(host_keyval["platform"])
106        machine_group = ",".join(sorted(machine_groups))
107        tko_utils.dprint("MACHINE GROUP: %s" % machine_group)
108        return machine_group
109
110
111    @staticmethod
112    def find_hostname(path):
113        hostname = os.path.join(path, "sysinfo", "hostname")
114        try:
115            with open(hostname) as rf:
116                machine = rf.readline().rstrip()
117            return machine
118        except Exception:
119            tko_utils.dprint("Could not read a hostname from "
120                             "sysinfo/hostname")
121
122        uname = os.path.join(path, "sysinfo", "uname_-a")
123        try:
124            machine = open(uname).readline().split()[1]
125            return machine
126        except Exception:
127            tko_utils.dprint("Could not read a hostname from "
128                             "sysinfo/uname_-a")
129
130        raise NoHostnameError("Unable to find a machine name")
131
132
133class kernel(models.kernel):
134    def __init__(self, job, verify_ident=None):
135        kernel_dict = kernel.load_from_dir(job.dir, verify_ident)
136        super(kernel, self).__init__(**kernel_dict)
137
138
139    @staticmethod
140    def load_from_dir(dir, verify_ident=None):
141        # try and load the booted kernel version
142        attributes = False
143        i = 1
144        build_dir = os.path.join(dir, "build")
145        while True:
146            if not os.path.exists(build_dir):
147                break
148            build_log = os.path.join(build_dir, "debug", "build_log")
149            attributes = kernel.load_from_build_log(build_log)
150            if attributes:
151                break
152            i += 1
153            build_dir = os.path.join(dir, "build.%d" % (i))
154
155        if not attributes:
156            if verify_ident:
157                base = verify_ident
158            else:
159                base = kernel.load_from_sysinfo(dir)
160            patches = []
161            hashes = []
162        else:
163            base, patches, hashes = attributes
164        tko_utils.dprint("kernel.__init__() found kernel version %s"
165                         % base)
166
167        # compute the kernel hash
168        if base == "UNKNOWN":
169            kernel_hash = "UNKNOWN"
170        else:
171            kernel_hash = kernel.compute_hash(base, hashes)
172
173        return {"base": base, "patches": patches,
174                "kernel_hash": kernel_hash}
175
176
177    @staticmethod
178    def load_from_sysinfo(path):
179        for subdir in ("reboot1", ""):
180            uname_path = os.path.join(path, "sysinfo", subdir,
181                                      "uname_-a")
182            if not os.path.exists(uname_path):
183                continue
184            uname = open(uname_path).readline().split()
185            return re.sub("-autotest$", "", uname[2])
186        return "UNKNOWN"
187
188
189    @staticmethod
190    def load_from_build_log(path):
191        if not os.path.exists(path):
192            return None
193
194        base, patches, hashes = "UNKNOWN", [], []
195        with open(path) as rf:
196            lines = rf.readlines()
197        for line in lines:
198            head, rest = line.split(": ", 1)
199            rest = rest.split()
200            if head == "BASE":
201                base = rest[0]
202            elif head == "PATCH":
203                patches.append(patch(*rest))
204                hashes.append(rest[2])
205        return base, patches, hashes
206
207
208class test(models.test):
209    def __init__(self, subdir, testname, status, reason, test_kernel,
210                 machine, started_time, finished_time, iterations,
211                 attributes, labels):
212        # for backwards compatibility with the original parser
213        # implementation, if there is no test version we need a NULL
214        # value to be used; also, if there is a version it should
215        # be terminated by a newline
216        if "version" in attributes:
217            attributes["version"] = str(attributes["version"])
218        else:
219            attributes["version"] = None
220
221        super(test, self).__init__(subdir, testname, status, reason,
222                                   test_kernel, machine, started_time,
223                                   finished_time, iterations,
224                                   attributes, labels)
225
226
227    @staticmethod
228    def load_iterations(keyval_path):
229        return iteration.load_from_keyval(keyval_path)
230
231
232class patch(models.patch):
233    def __init__(self, spec, reference, hash):
234        tko_utils.dprint("PATCH::%s %s %s" % (spec, reference, hash))
235        super(patch, self).__init__(spec, reference, hash)
236        self.spec = spec
237        self.reference = reference
238        self.hash = hash
239
240
241class iteration(models.iteration):
242    @staticmethod
243    def parse_line_into_dicts(line, attr_dict, perf_dict):
244        key, value = line.split("=", 1)
245        perf_dict[key] = value
246
247
248class status_line(object):
249    def __init__(self, indent, status, subdir, testname, reason,
250                 optional_fields):
251        # pull out the type & status of the line
252        if status == "START":
253            self.type = "START"
254            self.status = None
255        elif status.startswith("END "):
256            self.type = "END"
257            self.status = status[4:]
258        else:
259            self.type = "STATUS"
260            self.status = status
261        assert (self.status is None or
262                self.status in status_lib.statuses)
263
264        # save all the other parameters
265        self.indent = indent
266        self.subdir = self.parse_name(subdir)
267        self.testname = self.parse_name(testname)
268        self.reason = reason
269        self.optional_fields = optional_fields
270
271
272    @staticmethod
273    def parse_name(name):
274        if name == "----":
275            return None
276        return name
277
278
279    @staticmethod
280    def is_status_line(line):
281        return re.search(r"^\t*(\S[^\t]*\t){3}", line) is not None
282
283
284    @classmethod
285    def parse_line(cls, line):
286        if not status_line.is_status_line(line):
287            return None
288        match = re.search(r"^(\t*)(.*)$", line, flags=re.DOTALL)
289        if not match:
290            # A more useful error message than:
291            #  AttributeError: 'NoneType' object has no attribute 'groups'
292            # to help us debug what happens on occasion here.
293            raise RuntimeError("line %r could not be parsed." % line)
294        indent, line = match.groups()
295        indent = len(indent)
296
297        # split the line into the fixed and optional fields
298        parts = line.rstrip("\n").split("\t")
299
300        part_index = 3
301        status, subdir, testname = parts[0:part_index]
302
303        # all optional parts should be of the form "key=value". once we've found
304        # a non-matching part, treat it and the rest of the parts as the reason.
305        optional_fields = {}
306        while part_index < len(parts):
307            kv = re.search(r"^(\w+)=(.+)", parts[part_index])
308            if not kv:
309                break
310
311            optional_fields[kv.group(1)] = kv.group(2)
312            part_index += 1
313
314        reason = "\t".join(parts[part_index:])
315
316        # build up a new status_line and return it
317        return cls(indent, status, subdir, testname, reason,
318                   optional_fields)
319
320
321class parser(base.parser):
322    @staticmethod
323    def make_job(dir):
324        return job(dir)
325
326
327    def state_iterator(self, buffer):
328        new_tests = []
329        boot_count = 0
330        group_subdir = None
331        sought_level = 0
332        stack = status_lib.status_stack()
333        current_kernel = kernel(self.job)
334        boot_in_progress = False
335        alert_pending = None
336        started_time = None
337
338        while not self.finished or buffer.size():
339            # stop processing once the buffer is empty
340            if buffer.size() == 0:
341                yield new_tests
342                new_tests = []
343                continue
344
345            # parse the next line
346            line = buffer.get()
347            tko_utils.dprint('\nSTATUS: ' + line.strip())
348            line = status_line.parse_line(line)
349            if line is None:
350                tko_utils.dprint('non-status line, ignoring')
351                continue # ignore non-status lines
352
353            # have we hit the job start line?
354            if (line.type == "START" and not line.subdir and
355                not line.testname):
356                sought_level = 1
357                tko_utils.dprint("found job level start "
358                                 "marker, looking for level "
359                                 "1 groups now")
360                continue
361
362            # have we hit the job end line?
363            if (line.type == "END" and not line.subdir and
364                not line.testname):
365                tko_utils.dprint("found job level end "
366                                 "marker, looking for level "
367                                 "0 lines now")
368                sought_level = 0
369
370            # START line, just push another layer on to the stack
371            # and grab the start time if this is at the job level
372            # we're currently seeking
373            if line.type == "START":
374                group_subdir = None
375                stack.start()
376                if line.indent == sought_level:
377                    started_time = \
378                                 tko_utils.get_timestamp(
379                        line.optional_fields, "timestamp")
380                tko_utils.dprint("start line, ignoring")
381                continue
382            # otherwise, update the status on the stack
383            else:
384                tko_utils.dprint("GROPE_STATUS: %s" %
385                                 [stack.current_status(),
386                                  line.status, line.subdir,
387                                  line.testname, line.reason])
388                stack.update(line.status)
389
390            if line.status == "ALERT":
391                tko_utils.dprint("job level alert, recording")
392                alert_pending = line.reason
393                continue
394
395            # ignore Autotest.install => GOOD lines
396            if (line.testname == "Autotest.install" and
397                line.status == "GOOD"):
398                tko_utils.dprint("Successful Autotest "
399                                 "install, ignoring")
400                continue
401
402            # ignore END lines for a reboot group
403            if (line.testname == "reboot" and line.type == "END"):
404                tko_utils.dprint("reboot group, ignoring")
405                continue
406
407            # convert job-level ABORTs into a 'CLIENT_JOB' test, and
408            # ignore other job-level events
409            if line.testname is None:
410                if (line.status == "ABORT" and
411                    line.type != "END"):
412                    line.testname = "CLIENT_JOB"
413                else:
414                    tko_utils.dprint("job level event, "
415                                    "ignoring")
416                    continue
417
418            # use the group subdir for END lines
419            if line.type == "END":
420                line.subdir = group_subdir
421
422            # are we inside a block group?
423            if (line.indent != sought_level and
424                line.status != "ABORT" and
425                not line.testname.startswith('reboot.')):
426                if line.subdir:
427                    tko_utils.dprint("set group_subdir: "
428                                     + line.subdir)
429                    group_subdir = line.subdir
430                tko_utils.dprint("ignoring incorrect indent "
431                                 "level %d != %d," %
432                                 (line.indent, sought_level))
433                continue
434
435            # use the subdir as the testname, except for
436            # boot.* and kernel.* tests
437            if (line.testname is None or
438                not re.search(r"^(boot(\.\d+)?$|kernel\.)",
439                              line.testname)):
440                if line.subdir and '.' in line.subdir:
441                    line.testname = line.subdir
442
443            # has a reboot started?
444            if line.testname == "reboot.start":
445                started_time = tko_utils.get_timestamp(
446                    line.optional_fields, "timestamp")
447                tko_utils.dprint("reboot start event, "
448                                 "ignoring")
449                boot_in_progress = True
450                continue
451
452            # has a reboot finished?
453            if line.testname == "reboot.verify":
454                line.testname = "boot.%d" % boot_count
455                tko_utils.dprint("reboot verified")
456                boot_in_progress = False
457                verify_ident = line.reason.strip()
458                current_kernel = kernel(self.job, verify_ident)
459                boot_count += 1
460
461            if alert_pending:
462                line.status = "ALERT"
463                line.reason = alert_pending
464                alert_pending = None
465
466            # create the actual test object
467            finished_time = tko_utils.get_timestamp(
468                line.optional_fields, "timestamp")
469            final_status = stack.end()
470            tko_utils.dprint("Adding: "
471                             "%s\nSubdir:%s\nTestname:%s\n%s" %
472                             (final_status, line.subdir,
473                              line.testname, line.reason))
474            new_test = test.parse_test(self.job, line.subdir,
475                                       line.testname,
476                                       final_status, line.reason,
477                                       current_kernel,
478                                       started_time,
479                                       finished_time)
480            started_time = None
481            new_tests.append(new_test)
482
483        # the job is finished, but we never came back from reboot
484        if boot_in_progress:
485            testname = "boot.%d" % boot_count
486            reason = "machine did not return from reboot"
487            tko_utils.dprint(("Adding: ABORT\nSubdir:----\n"
488                              "Testname:%s\n%s")
489                             % (testname, reason))
490            new_test = test.parse_test(self.job, None, testname,
491                                       "ABORT", reason,
492                                       current_kernel, None, None)
493            new_tests.append(new_test)
494        yield new_tests
495