Statistics
| Branch: | Tag: | Revision:

root / snf-tools / synnefo_tools / burnin / logger.py @ 12ef696f

History | View | Annotate | Download (14.3 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
    def __init__(self, output_dir, verbose=1, use_colors=True,
211
                 in_parallel=False):
212
        """Initialize our loggers
213

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

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

229
        """
230
        self.verbose = verbose
231
        self.use_colors = use_colors
232
        self.in_parallel = in_parallel
233

    
234
        assert output_dir
235

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

    
248
        timestamp = datetime.datetime.strftime(
249
            datetime.datetime.now(), "%Y%m%d%H%M%S (%a %b %d %Y %H:%M)")
250
        file_name = timestamp + ".log"
251
        self.file_location = os.path.join(output_dir, file_name)
252

    
253
        timestamp = datetime.datetime.strftime(
254
            datetime.datetime.now(), "%a %b %d %Y %H:%M")
255
        sys.stdout.write("Starting burnin (%s)\n" % timestamp)
256

    
257
        # Create the logging file
258
        self._create_logging_file(timestamp)
259

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

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

    
281
    # ----------------------------------
282
    # Logging methods
283
    def debug(self, section, msg, *args):
284
        """Debug messages (verbose 2)
285

286
        We show debug messages only to stdout. The message will be formatted
287
        using the args.
288

289
        """
290
        msg = "  (DD) " + _list_to_string(msg, append="       ")
291
        if self.verbose >= 2:
292
            colored_msg = self._color_message(None, msg, *args)
293
            self._write_to_stdout(section, colored_msg)
294

    
295
    def log(self, section, msg, *args):
296
        """Normal messages (verbose 0)"""
297
        assert section, "Section can not be empty"
298

    
299
        msg = _list_to_string(msg)
300

    
301
        colored_msg = self._color_message(None, msg, *args)
302
        self._write_to_stdout(section, colored_msg)
303

    
304
        plain_msg = _format_message(msg, *args)
305
        self._write_to_file(section, plain_msg)
306

    
307
    def info(self, section, msg, *args):
308
        """Info messages (verbose 1)
309

310
        Prepare message and write it to file logger and stdout logger
311

312
        """
313
        assert section, "Section can not be empty"
314

    
315
        msg = "  " + _list_to_string(msg, "  ")
316
        if self.verbose >= 1:
317
            colored_msg = self._color_message(None, msg, *args)
318
            self._write_to_stdout(section, colored_msg)
319

    
320
        plain_msg = _format_message(msg, *args)
321
        self._write_to_file(section, plain_msg)
322

    
323
    def warning(self, section, msg, *args):
324
        """Warning messages"""
325
        assert section, "Section can not be empty"
326

    
327
        msg = "  (WW) " + _list_to_string(msg, "       ")
328

    
329
        colored_msg = self._color_message(_yellow, msg, *args)
330
        self._write_to_stderr(section, colored_msg)
331

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

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

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

    
341
        colored_msg = self._color_message(_red, 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 _write_to_stdout(self, section, msg):
348
        """Write to stdout"""
349
        if section is not None and self.in_parallel:
350
            sys.stdout.write(section + ": " + msg)
351
        else:
352
            sys.stdout.write(msg)
353

    
354
    def _write_to_stderr(self, section, msg):
355
        """Write to stderr"""
356
        if section is not None and self.in_parallel:
357
            sys.stderr.write(section + ": " + msg)
358
        else:
359
            sys.stderr.write(msg)
360

    
361
    def _write_to_file(self, section, msg):
362
        """Write to file"""
363
        _write_log_file(self.file_location, section, msg)
364

    
365
    # ----------------------------------
366
    # Handle testsuites
367
    def testsuite_start(self, testsuite):
368
        """Start a new testsuite
369

370
        Add a new section in the logging file
371

372
        """
373
        assert testsuite, "Testsuite name can not be emtpy"
374

    
375
        # Add a new section in the logging file
376
        test_runned = "  * " + testsuite + "\n"
377
        _write_log_file(self.file_location, SECTION_RUNNED, test_runned)
378

    
379
        new_section_entry = \
380
            SECTION_SEPARATOR + "\n" + SECTION_PREFIX + testsuite + "\n\n\n\n"
381
        _write_log_file(self.file_location, SECTION_NEW, new_section_entry)
382

    
383
        # Add new section to the stdout
384
        msg = "Starting testsuite %s" % testsuite
385
        colored_msg = self._color_message(_magenta, msg)
386
        self._write_to_stdout(None, colored_msg)
387

    
388
    def testsuite_success(self, testsuite):
389
        """A testsuite has successfully finished
390

391
        Update Results
392

393
        """
394
        assert testsuite, "Testsuite name can not be emtpy"
395

    
396
        # Add our testsuite to Results
397
        _write_log_file(self.file_location, SECTION_PASSED, testsuite)
398

    
399
        # Add success to stdout
400
        msg = "Testsuite %s passed" % testsuite
401
        colored_msg = self._color_message(_green, msg)
402
        self._write_to_stdout(None, colored_msg)
403

    
404
    def testsuite_failure(self, testsuite):
405
        """A testsuite has failed
406

407
        Update Results
408

409
        """
410
        assert testsuite, "Testsuite name can not be emtpy"
411

    
412
        # Add our testsuite to Results
413
        _write_log_file(self.file_location, SECTION_FAILED, testsuite)
414

    
415
        # Add success to stdout
416
        msg = "Testsuite %s failed" % testsuite
417
        colored_msg = self._color_message(_red, msg)
418
        self._write_to_stdout(None, colored_msg)
419

    
420
    # ----------------------------------
421
    # Colors
422
    def _color_message(self, color_fun, msg, *args):
423
        """Color a message before printing it
424

425
        The color_fun parameter is used when we want the whole message to be
426
        colored.
427

428
        """
429
        if self.use_colors:
430
            if callable(color_fun):
431
                return color_fun((msg % args)) + "\n"
432
            else:
433
                args = tuple([_blue(arg) for arg in args])
434
                return _format_message(msg, *args)
435
        else:
436
            return _format_message(msg, *args)