Statistics
| Branch: | Tag: | Revision:

root / snf-tools / synnefo_tools / burnin / logger.py @ 2afd10bf

History | View | Annotate | Download (14.9 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
    if args:
103
        return (msg % args) + "\n"
104
    else:
105
        return msg + "\n"
106

    
107

    
108
def _list_to_string(lst, append=""):
109
    """Convert a list of strings to string
110

111
    Append the value given in L{append} in front of all lines
112
    (except of the first line).
113

114
    """
115
    if isinstance(lst, list):
116
        return append.join(lst).rstrip('\n')
117
    else:
118
        return lst.rstrip('\n')
119

    
120

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

    
132

    
133
def _locate_input(contents, section):
134
    """Locate position to insert text
135

136
    Given a section location the next possition to insert text inside that
137
    section.
138

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

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

    
158

    
159
def _add_testsuite_results(contents, section, testsuite):
160
    """Add the given testsuite to results
161

162
    Well we know that SECTION_FAILED is the last line and SECTION_PASSED is the
163
    line before, so we are going to cheat here and use this information.
164

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

    
180

    
181
def _write_log_file(file_location, section, message):
182
    """Write something to our log file
183

184
    For this we have to get the lock, read and parse the file add the new
185
    message and re-write the file.
186

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

    
205

    
206
# --------------------------------------------------------------------
207
# The Log class
208
class Log(object):
209
    """Burnin logger
210

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

218
        The file to be used by our file logger will be created inside
219
        the L{output_dir} with name the current timestamp.
220

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

235
        """
236
        self.verbose = verbose
237
        self.use_colors = use_colors
238
        self.in_parallel = in_parallel
239
        self.quiet = quiet
240

    
241
        assert output_dir
242

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

    
255
        if curr_time is None:
256
            curr_time = datetime.datetime.now()
257
        timestamp = datetime.datetime.strftime(
258
            curr_time, "%Y%m%d%H%M%S (%a %b %d %Y %H:%M)")
259
        file_name = timestamp + ".log"
260
        self.file_location = os.path.join(output_dir, file_name)
261

    
262
        self._write_to_stdout(None, "Starting burnin with id %s\n" % timestamp)
263

    
264
        # Create the logging file
265
        self._create_logging_file(timestamp)
266

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

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

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

    
293
    # ----------------------------------
294
    # Logging methods
295
    def debug(self, section, msg, *args):
296
        """Debug messages (verbose 2)
297

298
        We show debug messages only to stdout. The message will be formatted
299
        using the args.
300

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

    
307
    def log(self, section, msg, *args):
308
        """Normal messages (verbose 0)"""
309
        assert section, "Section can not be empty"
310

    
311
        msg = _list_to_string(msg)
312

    
313
        colored_msg = self._color_message(None, msg, *args)
314
        self._write_to_stdout(section, colored_msg)
315

    
316
        plain_msg = _format_message(msg, *args)
317
        self._write_to_file(section, plain_msg)
318

    
319
    def info(self, section, msg, *args):
320
        """Info messages (verbose 1)
321

322
        Prepare message and write it to file logger and stdout logger
323

324
        """
325
        assert section, "Section can not be empty"
326

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

    
332
        plain_msg = _format_message(msg, *args)
333
        self._write_to_file(section, plain_msg)
334

    
335
    def warning(self, section, msg, *args):
336
        """Warning messages"""
337
        assert section, "Section can not be empty"
338

    
339
        msg = "  (WW) " + _list_to_string(msg, "       ")
340

    
341
        colored_msg = self._color_message(_yellow, msg, *args)
342
        self._write_to_stderr(section, colored_msg)
343

    
344
        plain_msg = _format_message(msg, *args)
345
        self._write_to_file(section, plain_msg)
346

    
347
    def error(self, section, msg, *args):
348
        """Error messages"""
349
        assert section, "Section can not be empty"
350

    
351
        msg = "  (EE) " + _list_to_string(msg, "       ")
352

    
353
        colored_msg = self._color_message(_red, msg, *args)
354
        self._write_to_stderr(section, colored_msg)
355

    
356
        plain_msg = _format_message(msg, *args)
357
        self._write_to_file(section, plain_msg)
358

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

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

    
375
    def _write_to_file(self, section, msg):
376
        """Write to file"""
377
        _write_log_file(self.file_location, section, msg)
378

    
379
    # ----------------------------------
380
    # Handle testsuites
381
    def testsuite_start(self, testsuite):
382
        """Start a new testsuite
383

384
        Add a new section in the logging file
385

386
        """
387
        assert testsuite, "Testsuite name can not be emtpy"
388

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

    
393
        new_section_entry = \
394
            SECTION_SEPARATOR + "\n" + SECTION_PREFIX + testsuite + "\n\n\n\n"
395
        _write_log_file(self.file_location, SECTION_NEW, new_section_entry)
396

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

    
402
    def testsuite_success(self, testsuite):
403
        """A testsuite has successfully finished
404

405
        Update Results
406

407
        """
408
        assert testsuite, "Testsuite name can not be emtpy"
409

    
410
        # Add our testsuite to Results
411
        _write_log_file(self.file_location, SECTION_PASSED, testsuite)
412

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

    
418
    def testsuite_failure(self, testsuite):
419
        """A testsuite has failed
420

421
        Update Results
422

423
        """
424
        assert testsuite, "Testsuite name can not be emtpy"
425

    
426
        # Add our testsuite to Results
427
        _write_log_file(self.file_location, SECTION_FAILED, testsuite)
428

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

    
434
    # ----------------------------------
435
    # Colors
436
    def _color_message(self, color_fun, msg, *args):
437
        """Color a message before printing it
438

439
        The color_fun parameter is used when we want the whole message to be
440
        colored.
441

442
        """
443
        if self.use_colors:
444
            if callable(color_fun):
445
                if args:
446
                    return color_fun((msg % args)) + "\n"
447
                else:
448
                    return color_fun(msg) + "\n"
449
            else:
450
                args = tuple([_blue(arg) for arg in args])
451
                return _format_message(msg, *args)
452
        else:
453
            return _format_message(msg, *args)