• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2016 Google Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import datetime
16import logging
17import os
18import re
19import sys
20
21from mobly import records
22from mobly import utils
23
24LINUX_MAX_FILENAME_LENGTH = 255
25# Filename sanitization mappings for Windows.
26# See https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions
27# Although the documentation says that 260 (including terminating nul, so 259)
28# is the max length. From manually testing on a Windows 10 machine, the actual
29# length seems to be lower.
30WINDOWS_MAX_FILENAME_LENGTH = 237
31WINDOWS_RESERVED_CHARACTERS_REPLACEMENTS = {
32    '<':
33        '-',
34    '>':
35        '-',
36    ':':
37        '-',
38    '"':
39        '_',
40    '/':
41        '_',
42    '\\':
43        '_',
44    '|':
45        ',',
46    '?':
47        ',',
48    '*':
49        ',',
50    # Integer zero (i.e. NUL) is not a valid character.
51    # While integers 1-31 are also usually valid, they aren't sanitized because
52    # they are situationally valid.
53    chr(0):
54        '0',
55}
56# Note, although the documentation does not specify as such, COM0 and LPT0 are
57# also invalid/reserved filenames.
58WINDOWS_RESERVED_FILENAME_REGEX = re.compile(
59    r'^(CON|PRN|AUX|NUL|(COM|LPT)[0-9])(\.[^.]*)?$', re.IGNORECASE)
60WINDOWS_RESERVED_FILENAME_PREFIX = 'mobly_'
61
62log_line_format = '%(asctime)s.%(msecs).03d %(levelname)s %(message)s'
63# The micro seconds are added by the format string above,
64# so the time format does not include ms.
65log_line_time_format = '%m-%d %H:%M:%S'
66log_line_timestamp_len = 18
67
68logline_timestamp_re = re.compile(r'\d\d-\d\d \d\d:\d\d:\d\d.\d\d\d')
69
70
71def _parse_logline_timestamp(t):
72  """Parses a logline timestamp into a tuple.
73
74  Args:
75    t: Timestamp in logline format.
76
77  Returns:
78    An iterable of date and time elements in the order of month, day, hour,
79    minute, second, microsecond.
80  """
81  date, time = t.split(' ')
82  month, day = date.split('-')
83  h, m, s = time.split(':')
84  s, ms = s.split('.')
85  return (month, day, h, m, s, ms)
86
87
88def is_valid_logline_timestamp(timestamp):
89  if len(timestamp) == log_line_timestamp_len:
90    if logline_timestamp_re.match(timestamp):
91      return True
92  return False
93
94
95def logline_timestamp_comparator(t1, t2):
96  """Comparator for timestamps in logline format.
97
98  Args:
99    t1: Timestamp in logline format.
100    t2: Timestamp in logline format.
101
102  Returns:
103    -1 if t1 < t2; 1 if t1 > t2; 0 if t1 == t2.
104  """
105  dt1 = _parse_logline_timestamp(t1)
106  dt2 = _parse_logline_timestamp(t2)
107  for u1, u2 in zip(dt1, dt2):
108    if u1 < u2:
109      return -1
110    elif u1 > u2:
111      return 1
112  return 0
113
114
115def _get_timestamp(time_format, delta=None):
116  t = datetime.datetime.now()
117  if delta:
118    t = t + datetime.timedelta(seconds=delta)
119  return t.strftime(time_format)[:-3]
120
121
122def epoch_to_log_line_timestamp(epoch_time, time_zone=None):
123  """Converts an epoch timestamp in ms to log line timestamp format, which
124  is readible for humans.
125
126  Args:
127    epoch_time: integer, an epoch timestamp in ms.
128    time_zone: instance of tzinfo, time zone information.
129      Using pytz rather than python 3.2 time_zone implementation for
130      python 2 compatibility reasons.
131
132  Returns:
133    A string that is the corresponding timestamp in log line timestamp
134    format.
135  """
136  s, ms = divmod(epoch_time, 1000)
137  d = datetime.datetime.fromtimestamp(s, tz=time_zone)
138  return d.strftime('%m-%d %H:%M:%S.') + str(ms)
139
140
141def get_log_line_timestamp(delta=None):
142  """Returns a timestamp in the format used by log lines.
143
144  Default is current time. If a delta is set, the return value will be
145  the current time offset by delta seconds.
146
147  Args:
148    delta: Number of seconds to offset from current time; can be negative.
149
150  Returns:
151    A timestamp in log line format with an offset.
152  """
153  return _get_timestamp('%m-%d %H:%M:%S.%f', delta)
154
155
156def get_log_file_timestamp(delta=None):
157  """Returns a timestamp in the format used for log file names.
158
159  Default is current time. If a delta is set, the return value will be
160  the current time offset by delta seconds.
161
162  Args:
163    delta: Number of seconds to offset from current time; can be negative.
164
165  Returns:
166    A timestamp in log filen name format with an offset.
167  """
168  return _get_timestamp('%m-%d-%Y_%H-%M-%S-%f', delta)
169
170
171def _setup_test_logger(log_path, prefix=None):
172  """Customizes the root logger for a test run.
173
174  The logger object has a stream handler and a file handler. The stream
175  handler logs INFO level to the terminal, the file handler logs DEBUG
176  level to files.
177
178  Args:
179    log_path: Location of the log file.
180    prefix: A prefix for each log line in terminal.
181    filename: Name of the log file. The default is the time the logger
182      is requested.
183  """
184  log = logging.getLogger()
185  kill_test_logger(log)
186  log.propagate = False
187  log.setLevel(logging.DEBUG)
188  # Log info to stream
189  terminal_format = log_line_format
190  if prefix:
191    terminal_format = '[%s] %s' % (prefix, log_line_format)
192  c_formatter = logging.Formatter(terminal_format, log_line_time_format)
193  ch = logging.StreamHandler(sys.stdout)
194  ch.setFormatter(c_formatter)
195  ch.setLevel(logging.INFO)
196  # Log everything to file
197  f_formatter = logging.Formatter(log_line_format, log_line_time_format)
198  # Write logger output to files
199  fh_info = logging.FileHandler(
200      os.path.join(log_path, records.OUTPUT_FILE_INFO_LOG))
201  fh_info.setFormatter(f_formatter)
202  fh_info.setLevel(logging.INFO)
203  fh_debug = logging.FileHandler(
204      os.path.join(log_path, records.OUTPUT_FILE_DEBUG_LOG))
205  fh_debug.setFormatter(f_formatter)
206  fh_debug.setLevel(logging.DEBUG)
207  log.addHandler(ch)
208  log.addHandler(fh_info)
209  log.addHandler(fh_debug)
210  log.log_path = log_path
211  logging.log_path = log_path
212  logging.root_output_path = log_path
213
214
215def kill_test_logger(logger):
216  """Cleans up a test logger object by removing all of its handlers.
217
218  Args:
219    logger: The logging object to clean up.
220  """
221  for h in list(logger.handlers):
222    logger.removeHandler(h)
223    if isinstance(h, logging.FileHandler):
224      h.close()
225
226
227def create_latest_log_alias(actual_path, alias):
228  """Creates a symlink to the latest test run logs.
229
230  Args:
231    actual_path: string, the source directory where the latest test run's
232      logs are.
233    alias: string, the name of the directory to contain the latest log
234      files.
235  """
236  alias_path = os.path.join(os.path.dirname(actual_path), alias)
237  utils.create_alias(actual_path, alias_path)
238
239
240def setup_test_logger(log_path, prefix=None, alias='latest'):
241  """Customizes the root logger for a test run.
242
243  In addition to configuring the Mobly logging handlers, this also sets two
244  attributes on the `logging` module for the output directories:
245
246  root_output_path: path to the directory for the entire test run.
247  log_path: same as `root_output_path` outside of a test class run. In the
248    context of a test class run, this is the output directory for files
249    specific to a test class.
250
251  Args:
252    log_path: string, the location of the report file.
253    prefix: optional string, a prefix for each log line in terminal.
254    alias: optional string, The name of the alias to use for the latest log
255      directory. If a falsy value is provided, then the alias directory
256      will not be created, which is useful to save storage space when the
257      storage system (e.g. ZIP files) does not properly support
258      shortcut/symlinks.
259  """
260  utils.create_dir(log_path)
261  _setup_test_logger(log_path, prefix)
262  logging.debug('Test output folder: "%s"', log_path)
263  if alias:
264    create_latest_log_alias(log_path, alias=alias)
265
266
267def _truncate_filename(filename, max_length):
268  """Truncates a filename while trying to preserve the extension.
269
270  Args:
271    filename: string, the filename to potentially truncate.
272
273  Returns:
274  The truncated filename that is less than or equal to the given maximum
275  length.
276  """
277  if len(filename) <= max_length:
278    return filename
279
280  if '.' in filename:
281    filename, extension = filename.rsplit('.', 1)
282    # Subtract one for the extension's period.
283    if len(extension) > max_length - 1:
284      # This is kind of a degrenerate case where the extension is
285      # extremely long, in which case, just return the truncated filename.
286      return filename[:max_length]
287    return '.'.join([filename[:max_length - len(extension) - 1], extension])
288  else:
289    return filename[:max_length]
290
291
292def _sanitize_windows_filename(filename):
293  """Sanitizes a filename for Windows.
294
295  Refer to the following Windows documentation page for the rules:
296  https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions
297
298  If the filename matches one of Window's reserved file namespaces, then the
299  `WINDOWS_RESERVED_FILENAME_PREFIX` (i.e. "mobly_") prefix will be appended
300  to the filename to convert it into a valid Windows filename.
301
302  Args:
303    filename: string, the filename to sanitize for the Windows file system.
304
305  Returns:
306    A filename that should be safe to use on Windows.
307  """
308  if re.match(WINDOWS_RESERVED_FILENAME_REGEX, filename):
309    return WINDOWS_RESERVED_FILENAME_PREFIX + filename
310
311  filename = _truncate_filename(filename, WINDOWS_MAX_FILENAME_LENGTH)
312
313  # In order to meet max length, none of these replacements should increase
314  # the length of the filename.
315  new_filename_chars = []
316  for char in filename:
317    if char in WINDOWS_RESERVED_CHARACTERS_REPLACEMENTS:
318      new_filename_chars.append(WINDOWS_RESERVED_CHARACTERS_REPLACEMENTS[char])
319    else:
320      new_filename_chars.append(char)
321  filename = ''.join(new_filename_chars)
322  if filename.endswith('.') or filename.endswith(' '):
323    # Filenames cannot end with a period or space on Windows.
324    filename = filename[:-1] + '_'
325
326  return filename
327
328
329def sanitize_filename(filename):
330  """Sanitizes a filename for various operating systems.
331
332  Args:
333    filename: string, the filename to sanitize.
334
335  Returns:
336    A string that is safe to use as a filename on various operating systems.
337  """
338  # Split `filename` into the directory and filename in case the user
339  # accidentally passed in the full path instead of the name.
340  dirname = os.path.dirname(filename)
341  basename = os.path.basename(filename)
342  basename = _sanitize_windows_filename(basename)
343  basename = _truncate_filename(basename, LINUX_MAX_FILENAME_LENGTH)
344  # Replace spaces with underscores for convenience reasons.
345  basename = basename.replace(' ', '_')
346  return os.path.join(dirname, basename)
347
348
349def normalize_log_line_timestamp(log_line_timestamp):
350  """Replace special characters in log line timestamp with normal characters.
351
352  .. deprecated:: 1.10
353
354    This method is obsolete with the more general `sanitize_filename` method
355    and is only kept for backwards compatibility. In a future update, this
356    method may be removed.
357
358  Args:
359    log_line_timestamp: A string in the log line timestamp format. Obtained
360      with get_log_line_timestamp.
361
362  Returns:
363    A string representing the same time as input timestamp, but without
364    special characters.
365  """
366  return sanitize_filename(log_line_timestamp)
367