1 # -*- coding: utf-8 -*-
2 # Copyright 2011 Google Inc. All Rights Reserved.
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
8 # http://www.apache.org/licenses/LICENSE-2.0
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 """Implementation of gsutil test command."""
17 from __future__
import absolute_import
19 from collections
import namedtuple
29 from gslib
.command
import Command
30 from gslib
.command
import ResetFailureCount
31 from gslib
.exception
import CommandException
32 from gslib
.project_id
import PopulateProjectId
33 import gslib
.tests
as tests
34 from gslib
.util
import IS_WINDOWS
35 from gslib
.util
import NO_MAX
38 # For Python 2.6, unittest2 is required to run the tests. If it's not available,
39 # display an error if the test command is run instead of breaking the whole
41 # pylint: disable=g-import-not-at-top
43 from gslib
.tests
.util
import GetTestNames
44 from gslib
.tests
.util
import unittest
45 except ImportError as e
:
46 if 'unittest2' in str(e
):
48 GetTestNames
= None # pylint: disable=invalid-name
59 _DEFAULT_TEST_PARALLEL_PROCESSES
= 5
60 _DEFAULT_S3_TEST_PARALLEL_PROCESSES
= 50
61 _SEQUENTIAL_ISOLATION_FLAG
= 'sequential_only'
65 gsutil test [-l] [-u] [-f] [command command...]
68 _DETAILED_HELP_TEXT
= ("""
74 The gsutil test command runs the gsutil unit tests and integration tests.
75 The unit tests use an in-memory mock storage service implementation, while
76 the integration tests send requests to the production service using the
77 preferred API set in the boto configuration file (see "gsutil help apis" for
80 To run both the unit tests and integration tests, run the command with no
85 To run the unit tests only (which run quickly):
89 Tests run in parallel regardless of whether the top-level -m flag is
90 present. To limit the number of tests run in parallel to 10 at a time:
94 To force tests to run sequentially:
98 To have sequentially-run tests stop running immediately when an error occurs:
102 To run tests for one or more individual commands add those commands as
103 arguments. For example, the following command will run the cp and mv command
108 To list available tests, run the test command with the -l argument:
112 The tests are defined in the code under the gslib/tests module. Each test
113 file is of the format test_[name].py where [name] is the test name you can
114 pass to this command. For example, running "gsutil test ls" would run the
115 tests in "gslib/tests/test_ls.py".
117 You can also run an individual test class or function name by passing the
118 test module followed by the class name and optionally a test name. For
119 example, to run the an entire test class by name:
121 gsutil test naming.GsutilNamingTests
123 or an individual test function:
125 gsutil test cp.TestCp.test_streaming
127 You can list the available tests under a module or class by passing arguments
128 with the -l option. For example, to list all available test functions in the
133 To output test coverage:
135 gsutil test -c -p 500
138 This will output an HTML report to a directory named 'htmlcov'.
142 -c Output coverage information.
144 -f Exit on first sequential test failure.
146 -l List available tests.
148 -p N Run at most N tests in parallel. The default value is %d.
150 -s Run tests against S3 instead of GS.
152 -u Only run unit tests.
153 """ % _DEFAULT_TEST_PARALLEL_PROCESSES
)
156 TestProcessData
= namedtuple('TestProcessData',
157 'name return_code stdout stderr')
160 def MakeCustomTestResultClass(total_tests
):
161 """Creates a closure of CustomTestResult.
164 total_tests: The total number of tests being run.
167 An instance of CustomTestResult.
170 class CustomTestResult(unittest
.TextTestResult
):
171 """A subclass of unittest.TextTestResult that prints a progress report."""
173 def startTest(self
, test
):
174 super(CustomTestResult
, self
).startTest(test
)
176 test_id
= '.'.join(test
.id().split('.')[-2:])
177 message
= ('\r%d/%d finished - E[%d] F[%d] s[%d] - %s' % (
178 self
.testsRun
, total_tests
, len(self
.errors
),
179 len(self
.failures
), len(self
.skipped
), test_id
))
180 message
= message
[:73]
181 message
= message
.ljust(73)
182 self
.stream
.write('%s - ' % message
)
184 return CustomTestResult
187 def GetTestNamesFromSuites(test_suite
):
188 """Takes a list of test suites and returns a list of contained test names."""
189 suites
= [test_suite
]
194 if isinstance(test
, unittest
.TestSuite
):
197 test_names
.append(test
.id()[len('gslib.tests.test_'):])
201 # pylint: disable=protected-access
202 # Need to get into the guts of unittest to evaluate test cases for parallelism.
203 def TestCaseToName(test_case
):
204 """Converts a python.unittest to its gsutil test-callable name."""
205 return (str(test_case
.__class
__).split('\'')[1] + '.' +
206 test_case
._testMethodName
)
209 # pylint: disable=protected-access
210 # Need to get into the guts of unittest to evaluate test cases for parallelism.
211 def SplitParallelizableTestSuite(test_suite
):
212 """Splits a test suite into groups with different running properties.
215 test_suite: A python unittest test suite.
218 4-part tuple of lists of test names:
219 (tests that must be run sequentially,
220 tests that must be isolated in a separate process but can be run either
221 sequentially or in parallel,
222 unit tests that can be run in parallel,
223 integration tests that can run in parallel)
225 # pylint: disable=import-not-at-top
226 # Need to import this after test globals are set so that skip functions work.
227 from gslib
.tests
.testcase
.unit_testcase
import GsUtilUnitTestCase
229 sequential_tests
= []
230 parallelizable_integration_tests
= []
231 parallelizable_unit_tests
= []
233 items_to_evaluate
= [test_suite
]
234 cases_to_evaluate
= []
235 # Expand the test suites into individual test cases:
236 while items_to_evaluate
:
237 suite_or_case
= items_to_evaluate
.pop()
238 if isinstance(suite_or_case
, unittest
.suite
.TestSuite
):
239 for item
in suite_or_case
._tests
:
240 items_to_evaluate
.append(item
)
241 elif isinstance(suite_or_case
, unittest
.TestCase
):
242 cases_to_evaluate
.append(suite_or_case
)
244 for test_case
in cases_to_evaluate
:
245 test_method
= getattr(test_case
, test_case
._testMethodName
, None)
246 if getattr(test_method
, 'requires_isolation', False):
247 # Test must be isolated to a separate process, even it if is being
249 isolated_tests
.append(TestCaseToName(test_case
))
250 elif not getattr(test_method
, 'is_parallelizable', True):
251 sequential_tests
.append(TestCaseToName(test_case
))
252 elif isinstance(test_case
, GsUtilUnitTestCase
):
253 parallelizable_unit_tests
.append(TestCaseToName(test_case
))
255 parallelizable_integration_tests
.append(TestCaseToName(test_case
))
257 return (sorted(sequential_tests
),
258 sorted(isolated_tests
),
259 sorted(parallelizable_unit_tests
),
260 sorted(parallelizable_integration_tests
))
263 def CountFalseInList(input_list
):
264 """Counts number of falses in the input list."""
266 for item
in input_list
:
272 def CreateTestProcesses(parallel_tests
, test_index
, process_list
, process_done
,
273 max_parallel_tests
, root_coverage_file
=None):
274 """Creates test processes to run tests in parallel.
277 parallel_tests: List of all parallel tests.
278 test_index: List index of last created test before this function call.
279 process_list: List of running subprocesses. Created processes are appended
281 process_done: List of booleans indicating process completion. One 'False'
282 will be added per process created.
283 max_parallel_tests: Maximum number of tests to run in parallel.
284 root_coverage_file: The root .coverage filename if coverage is requested.
287 Index of last created test.
289 orig_test_index
= test_index
290 executable_prefix
= [sys
.executable
] if sys
.executable
and IS_WINDOWS
else []
291 s3_argument
= ['-s'] if tests
.util
.RUN_S3_TESTS
else []
293 process_create_start_time
= time
.time()
294 last_log_time
= process_create_start_time
295 while (CountFalseInList(process_done
) < max_parallel_tests
and
296 test_index
< len(parallel_tests
)):
297 env
= os
.environ
.copy()
298 if root_coverage_file
:
299 env
['GSUTIL_COVERAGE_OUTPUT_FILE'] = root_coverage_file
300 process_list
.append(subprocess
.Popen(
301 executable_prefix
+ [gslib
.GSUTIL_PATH
] +
302 ['-o', 'GSUtil:default_project_id=' + PopulateProjectId()] +
303 ['test'] + s3_argument
+
304 ['--' + _SEQUENTIAL_ISOLATION_FLAG
] +
305 [parallel_tests
[test_index
][len('gslib.tests.test_'):]],
306 stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
, env
=env
))
308 process_done
.append(False)
309 if time
.time() - last_log_time
> 5:
310 print ('Created %d new processes (total %d/%d created)' %
311 (test_index
- orig_test_index
, len(process_list
),
312 len(parallel_tests
)))
313 last_log_time
= time
.time()
314 if test_index
== len(parallel_tests
):
315 print ('Test process creation finished (%d/%d created)' %
316 (len(process_list
), len(parallel_tests
)))
320 class TestCommand(Command
):
321 """Implementation of gsutil test command."""
323 # Command specification. See base class for documentation.
324 command_spec
= Command
.CreateCommandSpec(
326 command_name_aliases
=[],
327 usage_synopsis
=_SYNOPSIS
,
330 supported_sub_args
='uflp:sc',
332 provider_url_ok
=False,
334 supported_private_args
=[_SEQUENTIAL_ISOLATION_FLAG
]
336 # Help specification. See help_provider.py for documentation.
337 help_spec
= Command
.HelpSpec(
339 help_name_aliases
=[],
340 help_type
='command_help',
341 help_one_line_summary
='Run gsutil tests',
342 help_text
=_DETAILED_HELP_TEXT
,
343 subcommand_help_text
={},
346 def RunParallelTests(self
, parallel_integration_tests
,
347 max_parallel_tests
, coverage_filename
):
348 """Executes the parallel/isolated portion of the test suite.
351 parallel_integration_tests: List of tests to execute.
352 max_parallel_tests: Maximum number of parallel tests to run at once.
353 coverage_filename: If not None, filename for coverage output.
356 (int number of test failures, float elapsed time)
360 process_results
= [] # Tuples of (name, return code, stdout, stderr)
361 num_parallel_failures
= 0
362 # Number of logging cycles we ran with no progress.
363 progress_less_logging_cycles
= 0
364 completed_as_of_last_log
= 0
365 num_parallel_tests
= len(parallel_integration_tests
)
366 parallel_start_time
= last_log_time
= time
.time()
367 test_index
= CreateTestProcesses(
368 parallel_integration_tests
, 0, process_list
, process_done
,
369 max_parallel_tests
, root_coverage_file
=coverage_filename
)
370 while len(process_results
) < num_parallel_tests
:
371 for proc_num
in xrange(len(process_list
)):
372 if process_done
[proc_num
] or process_list
[proc_num
].poll() is None:
374 process_done
[proc_num
] = True
375 stdout
, stderr
= process_list
[proc_num
].communicate()
376 process_list
[proc_num
].stdout
.close()
377 process_list
[proc_num
].stderr
.close()
378 # TODO: Differentiate test failures from errors.
379 if process_list
[proc_num
].returncode
!= 0:
380 num_parallel_failures
+= 1
381 process_results
.append(TestProcessData(
382 name
=parallel_integration_tests
[proc_num
],
383 return_code
=process_list
[proc_num
].returncode
,
384 stdout
=stdout
, stderr
=stderr
))
385 if len(process_list
) < num_parallel_tests
:
386 test_index
= CreateTestProcesses(
387 parallel_integration_tests
, test_index
, process_list
,
388 process_done
, max_parallel_tests
,
389 root_coverage_file
=coverage_filename
)
390 if len(process_results
) < num_parallel_tests
:
391 if time
.time() - last_log_time
> 5:
392 print '%d/%d finished - %d failures' % (
393 len(process_results
), num_parallel_tests
, num_parallel_failures
)
394 if len(process_results
) == completed_as_of_last_log
:
395 progress_less_logging_cycles
+= 1
397 completed_as_of_last_log
= len(process_results
)
398 # A process completed, so we made progress.
399 progress_less_logging_cycles
= 0
400 if progress_less_logging_cycles
> 4:
401 # Ran 5 or more logging cycles with no progress, let the user
402 # know which tests are running slowly or hanging.
404 for proc_num
in xrange(len(process_list
)):
405 if not process_done
[proc_num
]:
406 still_running
.append(parallel_integration_tests
[proc_num
])
407 print 'Still running: %s' % still_running
408 # TODO: Terminate still-running processes if they
409 # hang for a long time.
410 last_log_time
= time
.time()
412 process_run_finish_time
= time
.time()
413 if num_parallel_failures
:
414 for result
in process_results
:
415 if result
.return_code
!= 0:
416 new_stderr
= result
.stderr
.split('\n')
417 print 'Results for failed test %s:' % result
.name
418 for line
in new_stderr
:
421 return (num_parallel_failures
,
422 (process_run_finish_time
- parallel_start_time
))
424 def PrintTestResults(self
, num_sequential_tests
, sequential_success
,
425 sequential_time_elapsed
,
426 num_parallel_tests
, num_parallel_failures
,
427 parallel_time_elapsed
):
428 """Prints test results for parallel and sequential tests."""
429 # TODO: Properly track test skips.
430 print 'Parallel tests complete. Success: %s Fail: %s' % (
431 num_parallel_tests
- num_parallel_failures
, num_parallel_failures
)
433 'Ran %d tests in %.3fs (%d sequential in %.3fs, %d parallel in %.3fs)'
434 % (num_parallel_tests
+ num_sequential_tests
,
435 float(sequential_time_elapsed
+ parallel_time_elapsed
),
436 num_sequential_tests
,
437 float(sequential_time_elapsed
),
439 float(parallel_time_elapsed
)))
442 if not num_parallel_failures
and sequential_success
:
445 if num_parallel_failures
:
446 print 'FAILED (parallel tests)'
447 if not sequential_success
:
448 print 'FAILED (sequential tests)'
450 def RunCommand(self
):
451 """Command entry point for the test command."""
453 raise CommandException('On Python 2.6, the unittest2 module is required '
454 'to run the gsutil tests.')
458 max_parallel_tests
= _DEFAULT_TEST_PARALLEL_PROCESSES
459 perform_coverage
= False
460 sequential_only
= False
462 for o
, a
in self
.sub_opts
:
464 perform_coverage
= True
469 elif o
== ('--' + _SEQUENTIAL_ISOLATION_FLAG
):
470 # Called to isolate a single test in a separate process.
471 # Don't try to isolate it again (would lead to an infinite loop).
472 sequential_only
= True
474 max_parallel_tests
= long(a
)
476 if not tests
.util
.HAS_S3_CREDS
:
477 raise CommandException('S3 tests require S3 credentials. Please '
478 'add appropriate credentials to your .boto '
480 tests
.util
.RUN_S3_TESTS
= True
482 tests
.util
.RUN_INTEGRATION_TESTS
= False
484 if perform_coverage
and not coverage
:
485 raise CommandException(
486 'Coverage has been requested but the coverage module was not found. '
487 'You can install it with "pip install coverage".')
489 if (tests
.util
.RUN_S3_TESTS
and
490 max_parallel_tests
> _DEFAULT_S3_TEST_PARALLEL_PROCESSES
):
492 'Reducing parallel tests to %d due to S3 maximum bucket '
493 'limitations.', _DEFAULT_S3_TEST_PARALLEL_PROCESSES
)
494 max_parallel_tests
= _DEFAULT_S3_TEST_PARALLEL_PROCESSES
496 test_names
= sorted(GetTestNames())
497 if list_tests
and not self
.args
:
498 print 'Found %d test names:' % len(test_names
)
499 print ' ', '\n '.join(sorted(test_names
))
502 # Set list of commands to test if supplied.
504 commands_to_test
= []
505 for name
in self
.args
:
506 if name
in test_names
or name
.split('.')[0] in test_names
:
507 commands_to_test
.append('gslib.tests.test_%s' % name
)
509 commands_to_test
.append(name
)
511 commands_to_test
= ['gslib.tests.test_%s' % name
for name
in test_names
]
513 # Installs a ctrl-c handler that tries to cleanly tear down tests.
514 unittest
.installHandler()
516 loader
= unittest
.TestLoader()
520 suite
= loader
.loadTestsFromNames(commands_to_test
)
521 except (ImportError, AttributeError) as e
:
522 raise CommandException('Invalid test argument name: %s' % e
)
525 test_names
= GetTestNamesFromSuites(suite
)
526 print 'Found %d test names:' % len(test_names
)
527 print ' ', '\n '.join(sorted(test_names
))
530 if logging
.getLogger().getEffectiveLevel() <= logging
.INFO
:
534 logging
.disable(logging
.ERROR
)
537 # We want to run coverage over the gslib module, but filter out the test
538 # modules and any third-party code. We also filter out anything under the
539 # temporary directory. Otherwise, the gsutil update test (which copies
540 # code to the temporary directory) gets included in the output.
541 coverage_controller
= coverage
.coverage(
542 source
=['gslib'], omit
=['gslib/third_party/*', 'gslib/tests/*',
543 tempfile
.gettempdir() + '*'])
544 coverage_controller
.erase()
545 coverage_controller
.start()
547 num_parallel_failures
= 0
548 sequential_success
= False
550 (sequential_tests
, isolated_tests
,
551 parallel_unit_tests
, parallel_integration_tests
) = (
552 SplitParallelizableTestSuite(suite
))
554 # Since parallel integration tests are run in a separate process, they
555 # won't get the override to tests.util, so skip them here.
556 if not tests
.util
.RUN_INTEGRATION_TESTS
:
557 parallel_integration_tests
= []
559 logging
.debug('Sequential tests to run: %s', sequential_tests
)
560 logging
.debug('Isolated tests to run: %s', isolated_tests
)
561 logging
.debug('Parallel unit tests to run: %s', parallel_unit_tests
)
562 logging
.debug('Parallel integration tests to run: %s',
563 parallel_integration_tests
)
565 # If we're running an already-isolated test (spawned in isolation by a
566 # previous test process), or we have no parallel tests to run,
567 # just run sequentially. For now, unit tests are always run sequentially.
568 run_tests_sequentially
= (sequential_only
or
569 (len(parallel_integration_tests
) <= 1
570 and not isolated_tests
))
572 if run_tests_sequentially
:
573 total_tests
= suite
.countTestCases()
574 resultclass
= MakeCustomTestResultClass(total_tests
)
576 runner
= unittest
.TextTestRunner(verbosity
=verbosity
,
577 resultclass
=resultclass
,
579 ret
= runner
.run(suite
)
580 sequential_success
= ret
.wasSuccessful()
582 if max_parallel_tests
== 1:
583 # We can't take advantage of parallelism, though we may have tests that
585 sequential_tests
+= parallel_integration_tests
586 parallel_integration_tests
= []
588 sequential_start_time
= time
.time()
589 # TODO: For now, run unit tests sequentially because they are fast.
590 # We could potentially shave off several seconds of execution time
591 # by executing them in parallel with the integration tests.
592 if len(sequential_tests
) + len(parallel_unit_tests
):
593 print 'Running %d tests sequentially.' % (len(sequential_tests
) +
594 len(parallel_unit_tests
))
595 sequential_tests_to_run
= sequential_tests
+ parallel_unit_tests
596 suite
= loader
.loadTestsFromNames(
597 sorted([test_name
for test_name
in sequential_tests_to_run
]))
598 num_sequential_tests
= suite
.countTestCases()
599 resultclass
= MakeCustomTestResultClass(num_sequential_tests
)
600 runner
= unittest
.TextTestRunner(verbosity
=verbosity
,
601 resultclass
=resultclass
,
604 ret
= runner
.run(suite
)
605 sequential_success
= ret
.wasSuccessful()
607 num_sequential_tests
= 0
608 sequential_success
= True
609 sequential_time_elapsed
= time
.time() - sequential_start_time
611 # At this point, all tests get their own process so just treat the
612 # isolated tests as parallel tests.
613 parallel_integration_tests
+= isolated_tests
614 num_parallel_tests
= len(parallel_integration_tests
)
616 if not num_parallel_tests
:
619 num_processes
= min(max_parallel_tests
, num_parallel_tests
)
620 if num_parallel_tests
> 1 and max_parallel_tests
> 1:
621 message
= 'Running %d tests in parallel mode (%d processes).'
622 if num_processes
> _DEFAULT_TEST_PARALLEL_PROCESSES
:
624 ' Please be patient while your CPU is incinerated. '
625 'If your machine becomes unresponsive, consider reducing '
626 'the amount of parallel test processes by running '
627 '\'gsutil test -p <num_processes>\'.')
628 print ('\n'.join(textwrap
.wrap(
629 message
% (num_parallel_tests
, num_processes
))))
631 print ('Running %d tests sequentially in isolated processes.' %
633 (num_parallel_failures
, parallel_time_elapsed
) = self
.RunParallelTests(
634 parallel_integration_tests
, max_parallel_tests
,
635 coverage_controller
.data
.filename
if perform_coverage
else None)
636 self
.PrintTestResults(
637 num_sequential_tests
, sequential_success
,
638 sequential_time_elapsed
,
639 num_parallel_tests
, num_parallel_failures
,
640 parallel_time_elapsed
)
643 coverage_controller
.stop()
644 coverage_controller
.combine()
645 coverage_controller
.save()
646 print ('Coverage information was saved to: %s' %
647 coverage_controller
.data
.filename
)
649 if sequential_success
and not num_parallel_failures
: