Preserve extensions when truncating filenames (#636)
diff --git a/mobly/logger.py b/mobly/logger.py
index a39d185..46ac2bf 100644
--- a/mobly/logger.py
+++ b/mobly/logger.py
@@ -23,9 +23,13 @@
from mobly import records
from mobly import utils
+LINUX_MAX_FILENAME_LENGTH = 255
# Filename sanitization mappings for Windows.
# See https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions
-WINDOWS_MAX_FILENAME_LENGTH = 260
+# Although the documentation says that 260 (including terminating nul, so 259)
+# is the max length. From manually testing on a Windows 10 machine, the actual
+# length seems to be lower.
+WINDOWS_MAX_FILENAME_LENGTH = 237
WINDOWS_RESERVED_CHARACTERS_REPLACEMENTS = {
'<':
'-',
@@ -253,6 +257,32 @@
create_latest_log_alias(log_path, alias=alias)
+def _truncate_filename(filename, max_length):
+ """Truncates a filename while trying to preserve the extension.
+
+ Args:
+ filename: string, the filename to potentially truncate.
+
+ Returns:
+ The truncated filename that is less than or equal to the given maximum
+ length.
+ """
+ if len(filename) <= max_length:
+ return filename
+
+ if '.' in filename:
+ filename, extension = filename.rsplit('.', 1)
+ # Subtract one for the extension's period.
+ if len(extension) > max_length - 1:
+ # This is kind of a degrenerate case where the extension is extremely
+ # long, in which case, just return the truncated filename.
+ return filename[:max_length]
+ return '.'.join(
+ [filename[:max_length - len(extension) - 1], extension])
+ else:
+ return filename[:max_length]
+
+
def _sanitize_windows_filename(filename):
"""Sanitizes a filename for Windows.
@@ -272,8 +302,7 @@
if re.match(WINDOWS_RESERVED_FILENAME_REGEX, filename):
return WINDOWS_RESERVED_FILENAME_PREFIX + filename
- if len(filename) > WINDOWS_MAX_FILENAME_LENGTH:
- filename = filename[:WINDOWS_MAX_FILENAME_LENGTH]
+ filename = _truncate_filename(filename, WINDOWS_MAX_FILENAME_LENGTH)
# In order to meet max length, none of these replacements should increase
# the length of the filename.
@@ -296,16 +325,17 @@
"""Sanitizes a filename for various operating systems.
Args:
- filename: string, the filename to sanitize.
+ filename: string, the filename to sanitize.
Returns:
- A string that is safe to use as a filename on various operating systems.
+ A string that is safe to use as a filename on various operating systems.
"""
# Split `filename` into the directory and filename in case the user
# accidentally passed in the full path instead of the name.
dirname = os.path.dirname(filename)
basename = os.path.basename(filename)
basename = _sanitize_windows_filename(basename)
+ basename = _truncate_filename(basename, LINUX_MAX_FILENAME_LENGTH)
# Replace spaces with underscores for convenience reasons.
basename = basename.replace(' ', '_')
return os.path.join(dirname, basename)
diff --git a/tests/mobly/logger_test.py b/tests/mobly/logger_test.py
index 2ad064c..2112656 100755
--- a/tests/mobly/logger_test.py
+++ b/tests/mobly/logger_test.py
@@ -91,10 +91,31 @@
self.assertEqual(logger.sanitize_filename(fake_filename),
expected_filename)
- def test_sanitize_filename_when_over_260_characters(self):
+ def test_sanitize_filename_when_over_max_characters(self):
fake_filename = 'l' * 300
- expected_filename = 'l' * 260
- self.assertEqual(len(logger.sanitize_filename(fake_filename)), 260)
+ expected_filename = 'l' * 237
+ self.assertEqual(len(logger.sanitize_filename(fake_filename)), 237)
+ self.assertEqual(logger.sanitize_filename(fake_filename),
+ expected_filename)
+
+ def test_sanitize_filename_when_over_max_characters_with_extension(self):
+ fake_filename = 'l' * 300 + '.txt'
+ expected_filename = 'l' * 233 + '.txt'
+ self.assertEqual(len(logger.sanitize_filename(fake_filename)), 237)
+ self.assertEqual(logger.sanitize_filename(fake_filename),
+ expected_filename)
+
+ def test_sanitize_filename_when_extension_at_max_characters(self):
+ fake_filename = 'l' * 300 + '.' + 't' * 236
+ expected_filename = '.' + 't' * 236
+ self.assertEqual(len(logger.sanitize_filename(fake_filename)), 237)
+ self.assertEqual(logger.sanitize_filename(fake_filename),
+ expected_filename)
+
+ def test_sanitize_filename_when_extension_over_max_characters(self):
+ fake_filename = 'l' * 300 + '.' + 't' * 300
+ expected_filename = 'l' * 237
+ self.assertEqual(len(logger.sanitize_filename(fake_filename)), 237)
self.assertEqual(logger.sanitize_filename(fake_filename),
expected_filename)