summaryrefslogtreecommitdiff
path: root/lib/spack/spack/util/log_parse.py
blob: bf88a9635eeaf0a058efa617a493d6a8e94547ed (plain) (blame)
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
# Copyright 2013-2023 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)

import io
import sys

from ctest_log_parser import BuildError, BuildWarning, CTestLogParser

import llnl.util.tty as tty
from llnl.util.tty.color import cescape, colorize

__all__ = ["parse_log_events", "make_log_context"]


def parse_log_events(stream, context=6, jobs=None, profile=False):
    """Extract interesting events from a log file as a list of LogEvent.

    Args:
        stream (str or typing.IO): build log name or file object
        context (int): lines of context to extract around each log event
        jobs (int): number of jobs to parse with; default ncpus
        profile (bool): print out profile information for parsing

    Returns:
        (tuple): two lists containig ``BuildError`` and
            ``BuildWarning`` objects.

    This is a wrapper around ``ctest_log_parser.CTestLogParser`` that
    lazily constructs a single ``CTestLogParser`` object.  This ensures
    that all the regex compilation is only done once.
    """
    if parse_log_events.ctest_parser is None:
        parse_log_events.ctest_parser = CTestLogParser(profile=profile)

    result = parse_log_events.ctest_parser.parse(stream, context, jobs)
    if profile:
        parse_log_events.ctest_parser.print_timings()
    return result


#: lazily constructed CTest log parser
parse_log_events.ctest_parser = None  # type: ignore[attr-defined]


def _wrap(text, width):
    """Break text into lines of specific width."""
    lines = []
    pos = 0
    while pos < len(text):
        lines.append(text[pos : pos + width])
        pos += width
    return lines


def make_log_context(log_events, width=None):
    """Get error context from a log file.

    Args:
        log_events (list): list of events created by
            ``ctest_log_parser.parse()``
        width (int or None): wrap width; ``0`` for no limit; ``None`` to
            auto-size for terminal
    Returns:
        str: context from the build log with errors highlighted

    Parses the log file for lines containing errors, and prints them out
    with line numbers and context.  Errors are highlighted with '>>' and
    with red highlighting (if color is enabled).

    Events are sorted by line number before they are displayed.
    """
    error_lines = set(e.line_no for e in log_events)
    log_events = sorted(log_events, key=lambda e: e.line_no)

    num_width = len(str(max(error_lines or [0]))) + 4
    line_fmt = "%%-%dd%%s" % num_width
    indent = " " * (5 + num_width)

    if width is None:
        _, width = tty.terminal_size()
    if width <= 0:
        width = sys.maxsize
    wrap_width = width - num_width - 6

    out = io.StringIO()
    next_line = 1
    for event in log_events:
        start = event.start

        if isinstance(event, BuildError):
            color = "R"
        elif isinstance(event, BuildWarning):
            color = "Y"
        else:
            color = "W"

        if next_line != 1 and start > next_line:
            out.write("\n     ...\n\n")

        if start < next_line:
            start = next_line

        for i in range(start, event.end):
            # wrap to width
            lines = _wrap(event[i], wrap_width)
            lines[1:] = [indent + ln for ln in lines[1:]]
            wrapped_line = line_fmt % (i, "\n".join(lines))

            if i in error_lines:
                out.write(colorize("  @%s{>> %s}\n" % (color, cescape(wrapped_line))))
            else:
                out.write("     %s\n" % wrapped_line)

        next_line = event.end

    return out.getvalue()