blob: 4c4679d8e5b0db11976040a4ea5faebdc33b1cde [file] [log] [blame]
maruel@chromium.org561d4b22013-09-26 21:08:08 +00001#!/usr/bin/env python
2# coding=utf-8
maruelea586f32016-04-05 11:11:33 -07003# Copyright 2013 The LUCI Authors. All rights reserved.
maruelf1f5e2a2016-05-25 17:10:39 -07004# Use of this source code is governed under the Apache License, Version 2.0
5# that can be found in the LICENSE file.
maruel@chromium.org561d4b22013-09-26 21:08:08 +00006
maruel9cdd7612015-12-02 13:40:52 -08007import getpass
maruel@chromium.org561d4b22013-09-26 21:08:08 +00008import os
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -04009import StringIO
10import subprocess
maruel@chromium.org561d4b22013-09-26 21:08:08 +000011import sys
Marc-Antoine Ruel76cfcee2019-04-01 23:16:36 +000012import tempfile
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -040013import time
maruel@chromium.org561d4b22013-09-26 21:08:08 +000014
Marc-Antoine Ruel76cfcee2019-04-01 23:16:36 +000015# Mutates sys.path.
16import test_env
maruel@chromium.org561d4b22013-09-26 21:08:08 +000017
Marc-Antoine Ruel76cfcee2019-04-01 23:16:36 +000018# third_party/
Marc-Antoine Ruelf1d827c2014-11-24 15:22:25 -050019from depot_tools import auto_stub
Marc-Antoine Ruel76cfcee2019-04-01 23:16:36 +000020
maruel@chromium.org561d4b22013-09-26 21:08:08 +000021from utils import file_path
maruel4e732992015-10-16 10:17:21 -070022from utils import fs
maruel@chromium.org561d4b22013-09-26 21:08:08 +000023
24
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -040025def write_content(filepath, content):
maruel4e732992015-10-16 10:17:21 -070026 with fs.open(filepath, 'wb') as f:
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -040027 f.write(content)
28
29
Marc-Antoine Ruelf1d827c2014-11-24 15:22:25 -050030class FilePathTest(auto_stub.TestCase):
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -040031 def setUp(self):
32 super(FilePathTest, self).setUp()
33 self._tempdir = None
34
35 def tearDown(self):
maruel4b14f042015-10-06 12:08:08 -070036 try:
37 if self._tempdir:
maruel4e732992015-10-16 10:17:21 -070038 for dirpath, dirnames, filenames in fs.walk(
maruel4b14f042015-10-06 12:08:08 -070039 self._tempdir, topdown=True):
40 for filename in filenames:
41 file_path.set_read_only(os.path.join(dirpath, filename), False)
42 for dirname in dirnames:
43 file_path.set_read_only(os.path.join(dirpath, dirname), False)
44 file_path.rmtree(self._tempdir)
45 finally:
46 super(FilePathTest, self).tearDown()
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -040047
48 @property
49 def tempdir(self):
50 if not self._tempdir:
Marc-Antoine Ruel0eb2eb22019-01-29 21:00:16 +000051 self._tempdir = tempfile.mkdtemp(prefix=u'file_path_test')
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -040052 return self._tempdir
53
vadimshe42aeba2016-06-03 12:32:21 -070054 def test_atomic_replace_new_file(self):
55 path = os.path.join(self.tempdir, 'new_file')
56 file_path.atomic_replace(path, 'blah')
57 with open(path, 'rb') as f:
58 self.assertEqual('blah', f.read())
59 self.assertEqual([u'new_file'], os.listdir(self.tempdir))
60
61 def test_atomic_replace_existing_file(self):
62 path = os.path.join(self.tempdir, 'existing_file')
63 with open(path, 'wb') as f:
64 f.write('existing body')
65 file_path.atomic_replace(path, 'new body')
66 with open(path, 'rb') as f:
67 self.assertEqual('new body', f.read())
68 self.assertEqual([u'existing_file'], os.listdir(self.tempdir))
69
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -040070 def assertFileMode(self, filepath, mode, umask=None):
Marc-Antoine Ruel76cfcee2019-04-01 23:16:36 +000071 umask = test_env.umask() if umask is None else umask
maruel4e732992015-10-16 10:17:21 -070072 actual = fs.stat(filepath).st_mode
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -040073 expected = mode & ~umask
74 self.assertEqual(
75 expected,
76 actual,
77 (filepath, oct(expected), oct(actual), oct(umask)))
78
79 def assertMaskedFileMode(self, filepath, mode):
80 """It's usually when the file was first marked read only."""
81 self.assertFileMode(filepath, mode, 0 if sys.platform == 'win32' else 077)
82
maruel@chromium.org561d4b22013-09-26 21:08:08 +000083 def test_native_case_end_with_os_path_sep(self):
84 # Make sure the trailing os.path.sep is kept.
Marc-Antoine Ruel76cfcee2019-04-01 23:16:36 +000085 path = file_path.get_native_path_case(test_env.CLIENT_DIR) + os.path.sep
maruel@chromium.org561d4b22013-09-26 21:08:08 +000086 self.assertEqual(file_path.get_native_path_case(path), path)
87
88 def test_native_case_end_with_dot_os_path_sep(self):
Marc-Antoine Ruel76cfcee2019-04-01 23:16:36 +000089 path = file_path.get_native_path_case(test_env.CLIENT_DIR + os.path.sep)
maruel@chromium.org561d4b22013-09-26 21:08:08 +000090 self.assertEqual(
91 file_path.get_native_path_case(path + '.' + os.path.sep),
92 path)
93
94 def test_native_case_non_existing(self):
95 # Make sure it doesn't throw on non-existing files.
96 non_existing = 'trace_input_test_this_file_should_not_exist'
97 path = os.path.expanduser('~/' + non_existing)
98 self.assertFalse(os.path.exists(path))
Marc-Antoine Ruel76cfcee2019-04-01 23:16:36 +000099 path = file_path.get_native_path_case(test_env.CLIENT_DIR) + os.path.sep
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000100 self.assertEqual(file_path.get_native_path_case(path), path)
101
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400102 def test_delete_wd_rf(self):
103 # Confirms that a RO file in a RW directory can be deleted on non-Windows.
104 dir_foo = os.path.join(self.tempdir, 'foo')
105 file_bar = os.path.join(dir_foo, 'bar')
maruel4e732992015-10-16 10:17:21 -0700106 fs.mkdir(dir_foo, 0777)
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400107 write_content(file_bar, 'bar')
108 file_path.set_read_only(dir_foo, False)
109 file_path.set_read_only(file_bar, True)
110 self.assertFileMode(dir_foo, 040777)
111 self.assertMaskedFileMode(file_bar, 0100444)
112 if sys.platform == 'win32':
113 # On Windows, a read-only file can't be deleted.
114 with self.assertRaises(OSError):
maruel4e732992015-10-16 10:17:21 -0700115 fs.remove(file_bar)
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400116 else:
maruel4e732992015-10-16 10:17:21 -0700117 fs.remove(file_bar)
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400118
119 def test_delete_rd_wf(self):
120 # Confirms that a Rw file in a RO directory can be deleted on Windows only.
121 dir_foo = os.path.join(self.tempdir, 'foo')
122 file_bar = os.path.join(dir_foo, 'bar')
maruel4e732992015-10-16 10:17:21 -0700123 fs.mkdir(dir_foo, 0777)
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400124 write_content(file_bar, 'bar')
125 file_path.set_read_only(dir_foo, True)
126 file_path.set_read_only(file_bar, False)
127 self.assertMaskedFileMode(dir_foo, 040555)
128 self.assertFileMode(file_bar, 0100666)
129 if sys.platform == 'win32':
130 # A read-only directory has a convoluted meaning on Windows, it means that
131 # the directory is "personalized". This is used as a signal by Windows
132 # Explorer to tell it to look into the directory for desktop.ini.
133 # See http://support.microsoft.com/kb/326549 for more details.
134 # As such, it is important to not try to set the read-only bit on
135 # directories on Windows since it has no effect other than trigger
136 # Windows Explorer to look for desktop.ini, which is unnecessary.
maruel4e732992015-10-16 10:17:21 -0700137 fs.remove(file_bar)
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400138 else:
139 with self.assertRaises(OSError):
maruel4e732992015-10-16 10:17:21 -0700140 fs.remove(file_bar)
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400141
142 def test_delete_rd_rf(self):
143 # Confirms that a RO file in a RO directory can't be deleted.
144 dir_foo = os.path.join(self.tempdir, 'foo')
145 file_bar = os.path.join(dir_foo, 'bar')
maruel4e732992015-10-16 10:17:21 -0700146 fs.mkdir(dir_foo, 0777)
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400147 write_content(file_bar, 'bar')
148 file_path.set_read_only(dir_foo, True)
149 file_path.set_read_only(file_bar, True)
150 self.assertMaskedFileMode(dir_foo, 040555)
151 self.assertMaskedFileMode(file_bar, 0100444)
152 with self.assertRaises(OSError):
153 # It fails for different reason depending on the OS. See the test cases
154 # above.
maruel4e732992015-10-16 10:17:21 -0700155 fs.remove(file_bar)
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400156
157 def test_hard_link_mode(self):
158 # Creates a hard link, see if the file mode changed on the node or the
159 # directory entry.
160 dir_foo = os.path.join(self.tempdir, 'foo')
161 file_bar = os.path.join(dir_foo, 'bar')
162 file_link = os.path.join(dir_foo, 'link')
maruel4e732992015-10-16 10:17:21 -0700163 fs.mkdir(dir_foo, 0777)
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400164 write_content(file_bar, 'bar')
165 file_path.hardlink(file_bar, file_link)
166 self.assertFileMode(file_bar, 0100666)
167 self.assertFileMode(file_link, 0100666)
168 file_path.set_read_only(file_bar, True)
169 self.assertMaskedFileMode(file_bar, 0100444)
170 self.assertMaskedFileMode(file_link, 0100444)
171 # This is bad news for Windows; on Windows, the file must be writeable to be
172 # deleted, but the file node is modified. This means that every hard links
173 # must be reset to be read-only after deleting one of the hard link
174 # directory entry.
175
Marc-Antoine Ruel0a795bd2015-01-16 20:32:10 -0500176 def test_rmtree_unicode(self):
177 subdir = os.path.join(self.tempdir, 'hi')
maruel4e732992015-10-16 10:17:21 -0700178 fs.mkdir(subdir)
Marc-Antoine Ruel0a795bd2015-01-16 20:32:10 -0500179 filepath = os.path.join(
180 subdir, u'\u0627\u0644\u0635\u064A\u0646\u064A\u0629')
maruel4e732992015-10-16 10:17:21 -0700181 with fs.open(filepath, 'wb') as f:
Marc-Antoine Ruel0a795bd2015-01-16 20:32:10 -0500182 f.write('hi')
183 # In particular, it fails when the input argument is a str.
184 file_path.rmtree(str(subdir))
185
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400186 if sys.platform == 'darwin':
187 def test_native_case_symlink_wrong_case(self):
Marc-Antoine Ruel76cfcee2019-04-01 23:16:36 +0000188 base_dir = file_path.get_native_path_case(test_env.TESTS_DIR)
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400189 trace_inputs_dir = os.path.join(base_dir, 'trace_inputs')
190 actual = file_path.get_native_path_case(trace_inputs_dir)
191 self.assertEqual(trace_inputs_dir, actual)
192
193 # Make sure the symlink is not resolved.
194 data = os.path.join(trace_inputs_dir, 'Files2')
195 actual = file_path.get_native_path_case(data)
196 self.assertEqual(
197 os.path.join(trace_inputs_dir, 'files2'), actual)
198
199 data = os.path.join(trace_inputs_dir, 'Files2', '')
200 actual = file_path.get_native_path_case(data)
201 self.assertEqual(
202 os.path.join(trace_inputs_dir, 'files2', ''), actual)
203
204 data = os.path.join(trace_inputs_dir, 'Files2', 'Child1.py')
205 actual = file_path.get_native_path_case(data)
206 # TODO(maruel): Should be child1.py.
207 self.assertEqual(
208 os.path.join(trace_inputs_dir, 'files2', 'Child1.py'), actual)
209
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000210 if sys.platform in ('darwin', 'win32'):
211 def test_native_case_not_sensitive(self):
212 # The home directory is almost guaranteed to have mixed upper/lower case
213 # letters on both Windows and OSX.
214 # This test also ensures that the output is independent on the input
215 # string case.
216 path = os.path.expanduser(u'~')
217 self.assertTrue(os.path.isdir(path))
218 path = path.replace('/', os.path.sep)
219 if sys.platform == 'win32':
220 # Make sure the drive letter is upper case for consistency.
221 path = path[0].upper() + path[1:]
222 # This test assumes the variable is in the native path case on disk, this
223 # should be the case. Verify this assumption:
224 self.assertEqual(path, file_path.get_native_path_case(path))
225 self.assertEqual(
226 file_path.get_native_path_case(path.lower()),
227 file_path.get_native_path_case(path.upper()))
228
229 def test_native_case_not_sensitive_non_existent(self):
230 # This test also ensures that the output is independent on the input
231 # string case.
232 non_existing = os.path.join(
233 'trace_input_test_this_dir_should_not_exist', 'really not', '')
234 path = os.path.expanduser(os.path.join(u'~', non_existing))
235 path = path.replace('/', os.path.sep)
maruel4e732992015-10-16 10:17:21 -0700236 self.assertFalse(fs.exists(path))
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000237 lower = file_path.get_native_path_case(path.lower())
238 upper = file_path.get_native_path_case(path.upper())
239 # Make sure non-existing element is not modified:
240 self.assertTrue(lower.endswith(non_existing.lower()))
241 self.assertTrue(upper.endswith(non_existing.upper()))
242 self.assertEqual(lower[:-len(non_existing)], upper[:-len(non_existing)])
243
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400244 if sys.platform == 'win32':
245 def test_native_case_alternate_datastream(self):
246 # Create the file manually, since tempfile doesn't support ADS.
Marc-Antoine Ruel3c979cb2015-03-11 13:43:28 -0400247 tempdir = unicode(tempfile.mkdtemp(prefix=u'trace_inputs'))
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400248 try:
249 tempdir = file_path.get_native_path_case(tempdir)
250 basename = 'foo.txt'
251 filename = basename + ':Zone.Identifier'
252 filepath = os.path.join(tempdir, filename)
253 open(filepath, 'w').close()
254 self.assertEqual(filepath, file_path.get_native_path_case(filepath))
255 data_suffix = ':$DATA'
256 self.assertEqual(
257 filepath + data_suffix,
258 file_path.get_native_path_case(filepath + data_suffix))
259
260 open(filepath + '$DATA', 'w').close()
261 self.assertEqual(
262 filepath + data_suffix,
263 file_path.get_native_path_case(filepath + data_suffix))
264 # Ensure the ADS weren't created as separate file. You love NTFS, don't
265 # you?
maruel4e732992015-10-16 10:17:21 -0700266 self.assertEqual([basename], fs.listdir(tempdir))
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400267 finally:
maruel4b14f042015-10-06 12:08:08 -0700268 file_path.rmtree(tempdir)
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400269
270 def test_rmtree_win(self):
271 # Mock our sleep for faster test case execution.
272 sleeps = []
273 self.mock(time, 'sleep', sleeps.append)
274 self.mock(sys, 'stderr', StringIO.StringIO())
275
276 # Open a child process, so the file is locked.
277 subdir = os.path.join(self.tempdir, 'to_be_deleted')
maruel4e732992015-10-16 10:17:21 -0700278 fs.mkdir(subdir)
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400279 script = 'import time; open(\'a\', \'w\'); time.sleep(60)'
280 proc = subprocess.Popen([sys.executable, '-c', script], cwd=subdir)
281 try:
282 # Wait until the file exist.
maruel4e732992015-10-16 10:17:21 -0700283 while not fs.isfile(os.path.join(subdir, 'a')):
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400284 self.assertEqual(None, proc.poll())
285 file_path.rmtree(subdir)
286 self.assertEqual([2, 4, 2], sleeps)
287 # sys.stderr.getvalue() would return a fair amount of output but it is
288 # not completely deterministic so we're not testing it here.
289 finally:
290 proc.wait()
291
Marc-Antoine Ruela275b292014-11-25 15:17:21 -0500292 def test_filter_processes_dir_win(self):
293 python_dir = os.path.dirname(sys.executable)
Marc-Antoine Ruel0eb2eb22019-01-29 21:00:16 +0000294 processes = file_path._filter_processes_dir_win(
295 file_path._enum_processes_win(), python_dir)
Marc-Antoine Ruela275b292014-11-25 15:17:21 -0500296 self.assertTrue(processes)
297 proc_names = [proc.ExecutablePath for proc in processes]
298 # Try to find at least one python process.
299 self.assertTrue(
300 any(proc == sys.executable for proc in proc_names), proc_names)
301
302 def test_filter_processes_tree_win(self):
303 # Create a grand-child.
304 script = (
305 'import subprocess,sys;'
306 'proc = subprocess.Popen('
307 '[sys.executable, \'-u\', \'-c\', \'import time; print(1); '
308 'time.sleep(60)\'], stdout=subprocess.PIPE); '
309 # Signal grand child is ready.
310 'print(proc.stdout.read(1)); '
311 # Wait for parent to have completed the test.
312 'sys.stdin.read(1); '
313 'proc.kill()'
314 )
315 proc = subprocess.Popen(
316 [sys.executable, '-u', '-c', script],
317 stdin=subprocess.PIPE,
318 stdout=subprocess.PIPE)
319 try:
320 proc.stdout.read(1)
321 processes = file_path.filter_processes_tree_win(
Marc-Antoine Ruel0eb2eb22019-01-29 21:00:16 +0000322 file_path._enum_processes_win())
Marc-Antoine Ruela275b292014-11-25 15:17:21 -0500323 self.assertEqual(3, len(processes), processes)
324 proc.stdin.write('a')
325 proc.wait()
326 except Exception:
327 proc.kill()
328 finally:
329 proc.wait()
330
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000331 if sys.platform != 'win32':
332 def test_symlink(self):
333 # This test will fail if the checkout is in a symlink.
Marc-Antoine Ruel76cfcee2019-04-01 23:16:36 +0000334 actual = file_path.split_at_symlink(None, test_env.CLIENT_DIR)
335 expected = (test_env.CLIENT_DIR, None, None)
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000336 self.assertEqual(expected, actual)
337
338 actual = file_path.split_at_symlink(
Marc-Antoine Ruel76cfcee2019-04-01 23:16:36 +0000339 None, os.path.join(test_env.TESTS_DIR, 'trace_inputs'))
340 expected = (os.path.join(test_env.TESTS_DIR, 'trace_inputs'), None, None)
341 self.assertEqual(expected, actual)
342
343 actual = file_path.split_at_symlink(
344 None, os.path.join(test_env.TESTS_DIR, 'trace_inputs', 'files2'))
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000345 expected = (
Marc-Antoine Ruel76cfcee2019-04-01 23:16:36 +0000346 os.path.join(test_env.TESTS_DIR, 'trace_inputs'), 'files2', '')
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000347 self.assertEqual(expected, actual)
348
349 actual = file_path.split_at_symlink(
Marc-Antoine Ruel76cfcee2019-04-01 23:16:36 +0000350 test_env.CLIENT_DIR, os.path.join('tests', 'trace_inputs', 'files2'))
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000351 expected = (
352 os.path.join('tests', 'trace_inputs'), 'files2', '')
353 self.assertEqual(expected, actual)
354 actual = file_path.split_at_symlink(
Marc-Antoine Ruel76cfcee2019-04-01 23:16:36 +0000355 test_env.CLIENT_DIR,
356 os.path.join('tests', 'trace_inputs', 'files2', 'bar'))
357 expected = (os.path.join('tests', 'trace_inputs'), 'files2', '/bar')
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000358 self.assertEqual(expected, actual)
359
360 def test_native_case_symlink_right_case(self):
361 actual = file_path.get_native_path_case(
Marc-Antoine Ruel76cfcee2019-04-01 23:16:36 +0000362 os.path.join(test_env.TESTS_DIR, 'trace_inputs'))
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000363 self.assertEqual('trace_inputs', os.path.basename(actual))
364
365 # Make sure the symlink is not resolved.
366 actual = file_path.get_native_path_case(
Marc-Antoine Ruel76cfcee2019-04-01 23:16:36 +0000367 os.path.join(test_env.TESTS_DIR, 'trace_inputs', 'files2'))
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000368 self.assertEqual('files2', os.path.basename(actual))
369
maruel9cdd7612015-12-02 13:40:52 -0800370 else:
371 def test_undeleteable_chmod(self):
372 # Create a file and a directory with an empty ACL. Then try to delete it.
373 dirpath = os.path.join(self.tempdir, 'd')
374 filepath = os.path.join(dirpath, 'f')
375 os.mkdir(dirpath)
376 with open(filepath, 'w') as f:
377 f.write('hi')
378 os.chmod(filepath, 0)
379 os.chmod(dirpath, 0)
380 file_path.rmtree(dirpath)
381
382 def test_undeleteable_owner(self):
383 # Create a file and a directory with an empty ACL. Then try to delete it.
384 dirpath = os.path.join(self.tempdir, 'd')
385 filepath = os.path.join(dirpath, 'f')
386 os.mkdir(dirpath)
387 with open(filepath, 'w') as f:
388 f.write('hi')
389 import win32security
390 user, _domain, _type = win32security.LookupAccountName(
391 '', getpass.getuser())
392 sd = win32security.SECURITY_DESCRIPTOR()
393 sd.Initialize()
394 sd.SetSecurityDescriptorOwner(user, False)
395 # Create an empty DACL, which removes all rights.
396 dacl = win32security.ACL()
397 dacl.Initialize()
398 sd.SetSecurityDescriptorDacl(1, dacl, 0)
399 win32security.SetFileSecurity(
400 fs.extend(filepath), win32security.DACL_SECURITY_INFORMATION, sd)
401 win32security.SetFileSecurity(
402 fs.extend(dirpath), win32security.DACL_SECURITY_INFORMATION, sd)
403 file_path.rmtree(dirpath)
404
maruel@chromium.org561d4b22013-09-26 21:08:08 +0000405
406if __name__ == '__main__':
Marc-Antoine Ruel76cfcee2019-04-01 23:16:36 +0000407 test_env.main()