Statistics
| Branch: | Tag: | Revision:

root / snf-tools / synnefo_tools / burnin / logger.py @ 4c52d5bf

History | View | Annotate | Download (14 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
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 _ts_start(msg):
91
    """New testsuite color"""
92
    return "\x1b[35m" + str(msg) + "\x1b[0m"
93

    
94

    
95
def _ts_success(msg):
96
    """Testsuite passed color"""
97
    return "\x1b[42m" + str(msg) + "\x1b[0m"
98

    
99

    
100
def _ts_failure(msg):
101
    """Testsuite failed color"""
102
    return "\x1b[41m" + str(msg) + "\x1b[0m"
103

    
104

    
105
def _format_message(msg, *args):
106
    """Format the message using the args"""
107
    return (msg % args) + "\n"
108

    
109

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

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

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

    
122

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

    
134

    
135
def _locate_input(contents, section):
136
    """Locate position to insert text
137

138
    Given a section location the next possition to insert text inside that
139
    section.
140

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

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

    
160

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

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

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

    
182

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

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

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

    
207

    
208
# --------------------------------------------------------------------
209
# The Log class
210
class Log(object):
211
    """Burnin logger
212

213
    """
214
    # ----------------------------------
215
    def __init__(self, output_dir, verbose=1, use_colors=True,
216
                 in_parallel=False):
217
        """Initialize our loggers
218

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

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

234
        """
235
        self.verbose = verbose
236
        self.use_colors = use_colors
237
        self.in_parallel = in_parallel
238

    
239
        # Create file for logging
240
        output_dir = os.path.expanduser(output_dir)
241
        if not os.path.exists(output_dir):
242
            self.debug(None, "Creating directory %s", output_dir)
243
            os.makedirs(output_dir)
244
        timestamp = datetime.datetime.strftime(
245
            datetime.datetime.now(), "%Y%m%d%H%M%S (%a %b %d %Y %H:%M)")
246
        file_name = timestamp + ".log"
247
        self.file_location = os.path.join(output_dir, file_name)
248

    
249
        timestamp = datetime.datetime.strftime(
250
            datetime.datetime.now(), "%a %b %d %Y %H:%M")
251
        sys.stdout.write("Starting burnin (%s)\n" % timestamp)
252

    
253
        # Create the logging file
254
        self._create_logging_file(timestamp)
255

    
256
    def _create_logging_file(self, timestamp):
257
        """Create the logging file"""
258
        self.debug(None, "Using \"%s\" file for logging", self.file_location)
259
        with open(self.file_location, 'w') as out_file:
260
            out_file.write(SECTION_SEPARATOR + "\n")
261
            out_file.write("%s%s (%s):\n\n\n\n" %
262
                           (SECTION_PREFIX, SECTION_RUNNED, timestamp))
263
            out_file.write(SECTION_SEPARATOR + "\n")
264
            out_file.write("%s%s:\n\n" % (SECTION_PREFIX, SECTION_RESULTS))
265
            out_file.write(SECTION_PASSED + "\n" + SECTION_FAILED + "\n")
266

    
267
    def __del__(self):
268
        """Delete the Log object"""
269
        # Remove the lock file
270
        file_lock = os.path.splitext(self.file_location)[0] + LOCK_EXT
271
        try:
272
            os.remove(file_lock)
273
        except OSError:
274
            self.debug(None, "Couldn't delete lock file")
275

    
276
    # ----------------------------------
277
    # Logging methods
278
    def debug(self, section, msg, *args):
279
        """Debug messages (verbose 2)
280

281
        We show debug messages only to stdout. The message will be formatted
282
        using the args.
283

284
        """
285
        msg = "  (DD) " + _list_to_string(msg, append="       ")
286
        if self.verbose >= 2:
287
            colored_msg = self._color_message(None, msg, *args)
288
            self._write_to_stdout(section, colored_msg)
289

    
290
    def log(self, section, msg, *args):
291
        """Normal messages (verbose 0)"""
292
        assert section, "Section can not be empty"
293

    
294
        msg = _list_to_string(msg)
295

    
296
        colored_msg = self._color_message(None, msg, *args)
297
        self._write_to_stdout(section, colored_msg)
298

    
299
        plain_msg = _format_message(msg, *args)
300
        self._write_to_file(section, plain_msg)
301

    
302
    def info(self, section, msg, *args):
303
        """Info messages (verbose 1)
304

305
        Prepare message and write it to file logger and stdout logger
306

307
        """
308
        assert section, "Section can not be empty"
309

    
310
        msg = "  " + _list_to_string(msg, "  ")
311
        if self.verbose >= 1:
312
            colored_msg = self._color_message(None, msg, *args)
313
            self._write_to_stdout(section, colored_msg)
314

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

    
318
    def warning(self, section, msg, *args):
319
        """Warning messages"""
320
        assert section, "Section can not be empty"
321

    
322
        msg = "  (WW) " + _list_to_string(msg, "       ")
323

    
324
        colored_msg = self._color_message(_yellow, msg, *args)
325
        self._write_to_stderr(section, colored_msg)
326

    
327
        plain_msg = _format_message(msg, *args)
328
        self._write_to_file(section, plain_msg)
329

    
330
    def error(self, section, msg, *args):
331
        """Error messages"""
332
        assert section, "Section can not be empty"
333

    
334
        msg = "  (EE) " + _list_to_string(msg, "       ")
335

    
336
        colored_msg = self._color_message(_red, msg, *args)
337
        self._write_to_stderr(section, colored_msg)
338

    
339
        plain_msg = _format_message(msg, *args)
340
        self._write_to_file(section, plain_msg)
341

    
342
    def _write_to_stdout(self, section, msg):
343
        """Write to stdout"""
344
        if section is not None and self.in_parallel:
345
            sys.stdout.write(section + ": " + msg)
346
        else:
347
            sys.stdout.write(msg)
348

    
349
    def _write_to_stderr(self, section, msg):
350
        """Write to stderr"""
351
        if section is not None and self.in_parallel:
352
            sys.stderr.write(section + ": " + msg)
353
        else:
354
            sys.stderr.write(msg)
355

    
356
    def _write_to_file(self, section, msg):
357
        """Write to file"""
358
        _write_log_file(self.file_location, section, msg)
359

    
360
    # ----------------------------------
361
    # Handle testsuites
362
    def testsuite_start(self, testsuite):
363
        """Start a new testsuite
364

365
        Add a new section in the logging file
366

367
        """
368
        assert testsuite, "Testsuite name can not be emtpy"
369

    
370
        # Add a new section in the logging file
371
        test_runned = "  * " + testsuite + "\n"
372
        _write_log_file(self.file_location, SECTION_RUNNED, test_runned)
373

    
374
        new_section_entry = \
375
            SECTION_SEPARATOR + "\n" + SECTION_PREFIX + testsuite + "\n\n\n\n"
376
        _write_log_file(self.file_location, SECTION_NEW, new_section_entry)
377

    
378
        # Add new section to the stdout
379
        msg = "Starting testsuite %s" % testsuite
380
        colored_msg = self._color_message(_ts_start, msg)
381
        self._write_to_stdout(None, colored_msg)
382

    
383
    def testsuite_success(self, testsuite):
384
        """A testsuite has successfully finished
385

386
        Update Results
387

388
        """
389
        assert testsuite, "Testsuite name can not be emtpy"
390

    
391
        # Add our testsuite to Results
392
        _write_log_file(self.file_location, SECTION_PASSED, testsuite)
393

    
394
        # Add success to stdout
395
        msg = "Testsuite %s passed" % testsuite
396
        colored_msg = self._color_message(_ts_success, msg)
397
        self._write_to_stdout(None, colored_msg)
398

    
399
    def testsuite_failure(self, testsuite):
400
        """A testsuite has failed
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_FAILED, testsuite)
409

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

    
415
    # ----------------------------------
416
    # Colors
417
    def _color_message(self, color_fun, msg, *args):
418
        """Color a message before printing it
419

420
        The color_fun parameter is used when we want the whole message to be
421
        colored.
422

423
        """
424
        if self.use_colors:
425
            if callable(color_fun):
426
                return color_fun((msg % args)) + "\n"
427
            else:
428
                args = tuple([_blue(arg) for arg in args])
429
                return _format_message(msg, *args)
430
        else:
431
            return _format_message(msg, *args)