Statistics
| Branch: | Tag: | Revision:

root / snf-tools / synnefo_tools / burnin / logger.py @ 0c1833c8

History | View | Annotate | Download (14.8 kB)

1
# Copyright 2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
"""
35
This is the logging class for burnin
36

37
It supports logging both for the stdout/stderr as well as file logging at the
38
same time.
39

40
The stdout/stderr logger supports verbose levels and colors but the file
41
logging doesn't (we use the info verbose level for our file logger).
42

43
Our loggers have primitive support for handling parallel execution (even though
44
burnin doesn't support it yet). To do so the stdout/stderr logger prepends the
45
name of the test under execution to every line it prints. On the other hand the
46
file logger waits to lock the file, then reads it, prints the message to the
47
corresponding line and closes the file.
48

49

50
"""
51

    
52
import os
53
import sys
54
import os.path
55
import datetime
56

    
57
from synnefo_tools.burnin import filelocker
58

    
59

    
60
# --------------------------------------------------------------------
61
# Constant variables
62
LOCK_EXT = ".lock"
63
SECTION_SEPARATOR = \
64
    "-- -------------------------------------------------------------------"
65
SECTION_PREFIX = "-- "
66
SECTION_RUNNED = "Tests Runned"
67
SECTION_RESULTS = "Results"
68
SECTION_NEW = "__ADD_NEW_SECTION__"
69
SECTION_PASSED = "  * Passed:"
70
SECTION_FAILED = "  * Failed:"
71

    
72

    
73
# --------------------------------------------------------------------
74
# Helper functions
75
def _blue(msg):
76
    """Blue color"""
77
    return "\x1b[1;34m" + str(msg) + "\x1b[0m"
78

    
79

    
80
def _yellow(msg):
81
    """Yellow color"""
82
    return "\x1b[33m" + str(msg) + "\x1b[0m"
83

    
84

    
85
def _red(msg):
86
    """Yellow color"""
87
    return "\x1b[31m" + str(msg) + "\x1b[0m"
88

    
89

    
90
def _magenta(msg):
91
    """Magenta color"""
92
    return "\x1b[35m" + str(msg) + "\x1b[0m"
93

    
94

    
95
def _green(msg):
96
    """Green color"""
97
    return "\x1b[32m" + str(msg) + "\x1b[0m"
98

    
99

    
100
def _format_message(msg, *args):
101
    """Format the message using the args"""
102
    return (msg % args) + "\n"
103

    
104

    
105
def _list_to_string(lst, append=""):
106
    """Convert a list of strings to string
107

108
    Append the value given in L{append} in front of all lines
109
    (except of the first line).
110

111
    """
112
    if isinstance(lst, list):
113
        return append.join(lst).rstrip('\n')
114
    else:
115
        return lst.rstrip('\n')
116

    
117

    
118
# --------------------------------------
119
def _locate_sections(contents):
120
    """Locate the sections inside the logging file"""
121
    i = 0
122
    res = []
123
    for cnt in contents:
124
        if SECTION_SEPARATOR in cnt:
125
            res.append(i+1)
126
        i += 1
127
    return res
128

    
129

    
130
def _locate_input(contents, section):
131
    """Locate position to insert text
132

133
    Given a section location the next possition to insert text inside that
134
    section.
135

136
    """
137
    sect_locs = _locate_sections(contents)
138
    if section == SECTION_NEW:
139
        # We want to add a new section
140
        # Just return the position of SECTION_RESULTS
141
        for obj in sect_locs:
142
            if SECTION_RESULTS in contents[obj]:
143
                return obj - 1
144
    else:
145
        # We will add our message in this location
146
        for (index, obj) in enumerate(sect_locs):
147
            if section in contents[obj]:
148
                return sect_locs[index+1] - 3
149

    
150
    # We didn't find our section??
151
    sys.stderr.write("Section %s could not be found in logging file\n"
152
                     % section)
153
    sys.exit("Error in logger._locate_input")
154

    
155

    
156
def _add_testsuite_results(contents, section, testsuite):
157
    """Add the given testsuite to results
158

159
    Well we know that SECTION_FAILED is the last line and SECTION_PASSED is the
160
    line before, so we are going to cheat here and use this information.
161

162
    """
163
    if section == SECTION_PASSED:
164
        line = contents[-2]
165
        new_line = line.rstrip() + " " + testsuite + ",\n"
166
        contents[-2] = new_line
167
    elif section == SECTION_FAILED:
168
        line = contents[-1]
169
        new_line = line.rstrip() + " " + testsuite + ",\n"
170
        contents[-1] = new_line
171
    else:
172
        sys.stderr.write("Unknown section %s in _add_testsuite_results\n"
173
                         % section)
174
        sys.exit("Error in logger._add_testsuite_results")
175
    return contents
176

    
177

    
178
def _write_log_file(file_location, section, message):
179
    """Write something to our log file
180

181
    For this we have to get the lock, read and parse the file add the new
182
    message and re-write the file.
183

184
    """
185
    # Get the lock
186
    file_lock = os.path.splitext(file_location)[0] + LOCK_EXT
187
    with filelocker.lock(file_lock, filelocker.LOCK_EX):
188
        with open(file_location, "r+") as log_file:
189
            contents = log_file.readlines()
190
            if section == SECTION_PASSED or section == SECTION_FAILED:
191
                # Add testsuite to results
192
                new_contents = \
193
                    _add_testsuite_results(contents, section, message)
194
            else:
195
                # Add message to its line
196
                input_loc = _locate_input(contents, section)
197
                new_contents = \
198
                    contents[:input_loc] + [message] + contents[input_loc:]
199
            log_file.seek(0)
200
            log_file.write("".join(new_contents))
201

    
202

    
203
# --------------------------------------------------------------------
204
# The Log class
205
class Log(object):
206
    """Burnin logger
207

208
    """
209
    # ----------------------------------
210
    # Too many arguments. pylint: disable-msg=R0913
211
    def __init__(self, output_dir, verbose=1, use_colors=True,
212
                 in_parallel=False, quiet=False):
213
        """Initialize our loggers
214

215
        The file to be used by our file logger will be created inside
216
        the L{output_dir} with name the current timestamp.
217

218
        @type output_dir: string
219
        @param output_dir: the directory to save the output file
220
        @type verbose: int
221
        @param verbose: the verbose level to use for stdout/stderr logger
222
            0: verbose at minimum level (only which test we are running now)
223
            1: verbose at info level (information about our running test)
224
            2: verbose at debug level
225
        @type use_colors: boolean
226
        @param use_colors: use colors for out stdout/stderr logger
227
        @type in_parallel: boolean
228
        @param in_parallel: this signifies that burnin is running in parallel
229
        @type quiet: boolean
230
        @type quiet: do not print logs to stdout/stderr
231

232
        """
233
        self.verbose = verbose
234
        self.use_colors = use_colors
235
        self.in_parallel = in_parallel
236
        self.quiet = quiet
237

    
238
        assert output_dir
239

    
240
        # Create file for logging
241
        output_dir = os.path.expanduser(output_dir)
242
        if not os.path.exists(output_dir):
243
            self.debug(None, "Creating directory %s", output_dir)
244
            try:
245
                os.makedirs(output_dir)
246
            except OSError as err:
247
                msg = ("Failed to create folder \"%s\" with error: %s\n"
248
                       % (output_dir, err))
249
                sys.stderr.write(msg)
250
                sys.exit("Failed to create log folder")
251

    
252
        timestamp = datetime.datetime.strftime(
253
            datetime.datetime.now(), "%Y%m%d%H%M%S (%a %b %d %Y %H:%M)")
254
        file_name = timestamp + ".log"
255
        self.file_location = os.path.join(output_dir, file_name)
256

    
257
        timestamp = datetime.datetime.strftime(
258
            datetime.datetime.now(), "%a %b %d %Y %H:%M")
259
        self._write_to_stdout(None, "Starting burnin (%s)\n" % timestamp)
260

    
261
        # Create the logging file
262
        self._create_logging_file(timestamp)
263

    
264
    def _create_logging_file(self, timestamp):
265
        """Create the logging file"""
266
        self.debug(None, "Using \"%s\" file for logging", self.file_location)
267
        with open(self.file_location, 'w') as out_file:
268
            out_file.write(SECTION_SEPARATOR + "\n")
269
            out_file.write("%s%s (%s):\n\n\n\n" %
270
                           (SECTION_PREFIX, SECTION_RUNNED, timestamp))
271
            out_file.write(SECTION_SEPARATOR + "\n")
272
            out_file.write("%s%s:\n\n" % (SECTION_PREFIX, SECTION_RESULTS))
273
            out_file.write(SECTION_PASSED + "\n" + SECTION_FAILED + "\n")
274

    
275
    def __del__(self):
276
        """Delete the Log object"""
277
        # Remove the lock file
278
        if hasattr(self, "file_location"):
279
            file_lock = os.path.splitext(self.file_location)[0] + LOCK_EXT
280
            try:
281
                os.remove(file_lock)
282
            except OSError:
283
                self.debug(None, "Couldn't delete lock file")
284

    
285
    def print_logfile_to_stdout(self):
286
        """Print the contents of our log file to stdout"""
287
        with open(self.file_location, 'r') as fin:
288
            sys.stdout.write(fin.read())
289

    
290
    # ----------------------------------
291
    # Logging methods
292
    def debug(self, section, msg, *args):
293
        """Debug messages (verbose 2)
294

295
        We show debug messages only to stdout. The message will be formatted
296
        using the args.
297

298
        """
299
        msg = "  (DD) " + _list_to_string(msg, append="       ")
300
        if self.verbose >= 2:
301
            colored_msg = self._color_message(None, msg, *args)
302
            self._write_to_stdout(section, colored_msg)
303

    
304
    def log(self, section, msg, *args):
305
        """Normal messages (verbose 0)"""
306
        assert section, "Section can not be empty"
307

    
308
        msg = _list_to_string(msg)
309

    
310
        colored_msg = self._color_message(None, msg, *args)
311
        self._write_to_stdout(section, colored_msg)
312

    
313
        plain_msg = _format_message(msg, *args)
314
        self._write_to_file(section, plain_msg)
315

    
316
    def info(self, section, msg, *args):
317
        """Info messages (verbose 1)
318

319
        Prepare message and write it to file logger and stdout logger
320

321
        """
322
        assert section, "Section can not be empty"
323

    
324
        msg = "  " + _list_to_string(msg, "  ")
325
        if self.verbose >= 1:
326
            colored_msg = self._color_message(None, msg, *args)
327
            self._write_to_stdout(section, colored_msg)
328

    
329
        plain_msg = _format_message(msg, *args)
330
        self._write_to_file(section, plain_msg)
331

    
332
    def warning(self, section, msg, *args):
333
        """Warning messages"""
334
        assert section, "Section can not be empty"
335

    
336
        msg = "  (WW) " + _list_to_string(msg, "       ")
337

    
338
        colored_msg = self._color_message(_yellow, msg, *args)
339
        self._write_to_stderr(section, colored_msg)
340

    
341
        plain_msg = _format_message(msg, *args)
342
        self._write_to_file(section, plain_msg)
343

    
344
    def error(self, section, msg, *args):
345
        """Error messages"""
346
        assert section, "Section can not be empty"
347

    
348
        msg = "  (EE) " + _list_to_string(msg, "       ")
349

    
350
        colored_msg = self._color_message(_red, msg, *args)
351
        self._write_to_stderr(section, colored_msg)
352

    
353
        plain_msg = _format_message(msg, *args)
354
        self._write_to_file(section, plain_msg)
355

    
356
    def _write_to_stdout(self, section, msg):
357
        """Write to stdout"""
358
        if not self.quiet:
359
            if section is not None and self.in_parallel:
360
                sys.stdout.write(section + ": " + msg)
361
            else:
362
                sys.stdout.write(msg)
363

    
364
    def _write_to_stderr(self, section, msg):
365
        """Write to stderr"""
366
        if not self.quiet:
367
            if section is not None and self.in_parallel:
368
                sys.stderr.write(section + ": " + msg)
369
            else:
370
                sys.stderr.write(msg)
371

    
372
    def _write_to_file(self, section, msg):
373
        """Write to file"""
374
        _write_log_file(self.file_location, section, msg)
375

    
376
    # ----------------------------------
377
    # Handle testsuites
378
    def testsuite_start(self, testsuite):
379
        """Start a new testsuite
380

381
        Add a new section in the logging file
382

383
        """
384
        assert testsuite, "Testsuite name can not be emtpy"
385

    
386
        # Add a new section in the logging file
387
        test_runned = "  * " + testsuite + "\n"
388
        _write_log_file(self.file_location, SECTION_RUNNED, test_runned)
389

    
390
        new_section_entry = \
391
            SECTION_SEPARATOR + "\n" + SECTION_PREFIX + testsuite + "\n\n\n\n"
392
        _write_log_file(self.file_location, SECTION_NEW, new_section_entry)
393

    
394
        # Add new section to the stdout
395
        msg = "Starting testsuite %s" % testsuite
396
        colored_msg = self._color_message(_magenta, msg)
397
        self._write_to_stdout(None, colored_msg)
398

    
399
    def testsuite_success(self, testsuite):
400
        """A testsuite has successfully finished
401

402
        Update Results
403

404
        """
405
        assert testsuite, "Testsuite name can not be emtpy"
406

    
407
        # Add our testsuite to Results
408
        _write_log_file(self.file_location, SECTION_PASSED, testsuite)
409

    
410
        # Add success to stdout
411
        msg = "Testsuite %s passed" % testsuite
412
        colored_msg = self._color_message(_green, msg)
413
        self._write_to_stdout(None, colored_msg)
414

    
415
    def testsuite_failure(self, testsuite):
416
        """A testsuite has failed
417

418
        Update Results
419

420
        """
421
        assert testsuite, "Testsuite name can not be emtpy"
422

    
423
        # Add our testsuite to Results
424
        _write_log_file(self.file_location, SECTION_FAILED, testsuite)
425

    
426
        # Add success to stdout
427
        msg = "Testsuite %s failed" % testsuite
428
        colored_msg = self._color_message(_red, msg)
429
        self._write_to_stdout(None, colored_msg)
430

    
431
    # ----------------------------------
432
    # Colors
433
    def _color_message(self, color_fun, msg, *args):
434
        """Color a message before printing it
435

436
        The color_fun parameter is used when we want the whole message to be
437
        colored.
438

439
        """
440
        if self.use_colors:
441
            if callable(color_fun):
442
                return color_fun((msg % args)) + "\n"
443
            else:
444
                args = tuple([_blue(arg) for arg in args])
445
                return _format_message(msg, *args)
446
        else:
447
            return _format_message(msg, *args)