Autotest: Better complete_failures.py email info.

The number of tests failed out of the total number of tests is given. Also,
the results are sorted alphabetically.

BUG=chromium:265929
TEST=Unittests.

Change-Id: I45847ec8958d390ae12cd41134cc568ac84c13e2
Reviewed-on: https://gerrit.chromium.org/gerrit/63680
Reviewed-by: Dennis Jeffrey <dennisjeffrey@chromium.org>
Reviewed-by: Dan Shi <dshi@chromium.org>
Commit-Queue: Keyar Hood <keyar@chromium.org>
Tested-by: Keyar Hood <keyar@chromium.org>
diff --git a/frontend/health/complete_failures.py b/frontend/health/complete_failures.py
index 09cb6c0..e7efcf1 100644
--- a/frontend/health/complete_failures.py
+++ b/frontend/health/complete_failures.py
@@ -102,19 +102,21 @@
     return {test.encode('utf8') for test in valid_test_names}
 
 
-def get_tests_to_analyze():
+def get_tests_to_analyze(recent_test_names, last_pass_times):
     """
     Get all the recently ran tests as well as the last time they have passed.
 
     The minimum datetime is given as last pass time for tests that have never
     passed.
 
+    @param recent_test_names: The set of the names of tests that have been
+        recently ran.
+    @param last_pass_times: The dictionary of test_name:last_pass_time pairs.
+
     @return the dict of test_name:last_finish_time pairs.
 
     """
-    recent_test_names = get_recently_ran_test_names()
-    last_passes = utils.get_last_pass_times()
-    prepared_passes = prepare_last_passes(last_passes)
+    prepared_passes = prepare_last_passes(last_pass_times)
 
     running_passes = {}
     for test, pass_time in prepared_passes.items():
@@ -149,21 +151,25 @@
                 pass
 
 
-def email_about_test_failure(storage):
+def email_about_test_failure(storage, all_tests):
     """
     Send an email about all the tests in the storage object if there are any.
 
     @param storage: The storage object.
+    @param all_tests: All the names of tests that have been recently ran.
 
     """
     if storage:
+        tests = sorted(storage.keys())
         mail.send(_MAIL_RESULTS_FROM,
                   [_MAIL_RESULTS_TO],
                   [],
                   'Long Failing Tests',
-                  'The following tests have been failing for '
-                  'at least %s days:\n\n' % (_DAYS_TO_BE_FAILING_TOO_LONG) +
-                  '\n'.join(storage.keys()))
+                  '%d/%d tests have been failing for at least %d days.\n'
+                  'They are the following:\n\n%s'
+                  % (len(storage), len(all_tests),
+                     _DAYS_TO_BE_FAILING_TOO_LONG,
+                     '\n'.join(tests)))
 
 
 def main():
@@ -175,9 +181,11 @@
 
     """
     storage = load_storage()
-    tests = get_tests_to_analyze()
+    all_test_names = get_recently_ran_test_names()
+    last_passes = utils.get_last_pass_times()
+    tests = get_tests_to_analyze(all_test_names, last_passes)
     store_results(tests, storage)
-    email_about_test_failure(storage)
+    email_about_test_failure(storage, all_test_names)
     save_storage(storage)
 
     return 0
diff --git a/frontend/health/complete_failures_functional_test.py b/frontend/health/complete_failures_functional_test.py
index 45d7277..a2ebb6f 100755
--- a/frontend/health/complete_failures_functional_test.py
+++ b/frontend/health/complete_failures_functional_test.py
@@ -104,8 +104,8 @@
                   ['chromeos-lab-infrastructure@google.com'],
                   [],
                   'Long Failing Tests',
-                  'The following tests have been failing for at '
-                  'least %i days:\n\ntest1\ntest2'
+                  '2/2 tests have been failing for at least %d days.\n'
+                  'They are the following:\n\ntest1\ntest2'
                   % complete_failures._DAYS_TO_BE_FAILING_TOO_LONG)
         complete_failures.save_storage(storage)
 
diff --git a/frontend/health/complete_failures_unittest.py b/frontend/health/complete_failures_unittest.py
index 17eb209..a5e0035 100755
--- a/frontend/health/complete_failures_unittest.py
+++ b/frontend/health/complete_failures_unittest.py
@@ -4,7 +4,7 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import datetime, unittest
+import collections, datetime, unittest
 
 import mox
 
@@ -14,7 +14,6 @@
 from autotest_lib.frontend import setup_django_readonly_environment
 from autotest_lib.frontend import setup_test_environment
 import complete_failures
-from autotest_lib.frontend.health import utils
 from autotest_lib.client.common_lib import mail
 from autotest_lib.frontend.tko import models
 from django import test
@@ -143,16 +142,64 @@
                 ['chromeos-lab-infrastructure@google.com'],
                 [],
                 'Long Failing Tests',
-                'The following tests have been failing for at '
-                'least %i days:\n\ntest'
+                '1/1 tests have been failing for at least %d days.\n'
+                'They are the following:\n\ntest'
                     % complete_failures._DAYS_TO_BE_FAILING_TOO_LONG)
 
         storage = {'test': datetime.datetime.min}
+        all_tests = set(storage.keys())
 
         # The ReplayAll is required or else a mox object sneaks its way into
         # the storage object somehow.
         self.mox.ReplayAll()
-        complete_failures.email_about_test_failure(storage)
+        complete_failures.email_about_test_failure(storage, all_tests)
+
+
+    def test_email_has_test_names_sorted_alphabetically(self):
+        """Test that the email report has entries sorted alphabetically."""
+        complete_failures._DAYS_TO_BE_FAILING_TOO_LONG = 60
+
+        mail.send(
+                'chromeos-test-health@google.com',
+                ['chromeos-lab-infrastructure@google.com'],
+                [],
+                'Long Failing Tests',
+                '2/2 tests have been failing for at least %d days.\n'
+                'They are the following:\n\ntest1\ntest2'
+                    % complete_failures._DAYS_TO_BE_FAILING_TOO_LONG)
+
+        # We use an OrderedDict to gurantee that the elements would be out of
+        # order if we did a simple traversal.
+        storage = collections.OrderedDict((('test2', datetime.datetime.min),
+                                           ('test1', datetime.datetime.min)))
+        all_tests = set(storage.keys())
+
+        # The ReplayAll is required or else a mox object sneaks its way into
+        # the storage object somehow.
+        self.mox.ReplayAll()
+        complete_failures.email_about_test_failure(storage, all_tests)
+
+
+    def test_email_count_of_total_number_of_tests(self):
+        """Test that the email report displays total number of tests."""
+        complete_failures._DAYS_TO_BE_FAILING_TOO_LONG = 60
+
+        mail.send(
+                'chromeos-test-health@google.com',
+                ['chromeos-lab-infrastructure@google.com'],
+                [],
+                'Long Failing Tests',
+                '1/2 tests have been failing for at least %d days.\n'
+                'They are the following:\n\ntest'
+                    % complete_failures._DAYS_TO_BE_FAILING_TOO_LONG)
+
+        storage = {'test': datetime.datetime.min}
+        all_tests = set(storage.keys()) | {'not_failure'}
+
+        # The ReplayAll is required or else a mox object sneaks its way into
+        # the storage object somehow.
+        self.mox.ReplayAll()
+        complete_failures.email_about_test_failure(storage, all_tests)
 
 
 class IsValidTestNameTests(test.TestCase):
@@ -271,18 +318,13 @@
 
     def test_returns_recent_test_names(self):
         """Test should return all the test names in the database."""
-        self.mox.StubOutWithMock(utils, 'get_last_pass_times')
-        self.mox.StubOutWithMock(complete_failures,
-            'get_recently_ran_test_names')
 
-        utils.get_last_pass_times().AndReturn({'passing_test':
-            datetime.datetime(2012, 1 ,1),
-            'old_passing_test': datetime.datetime(2011, 1, 1)})
-        complete_failures.get_recently_ran_test_names().AndReturn(
-            {'passing_test',
-             'failing_test'})
-        self.mox.ReplayAll()
-        results = complete_failures.get_tests_to_analyze()
+        recent_tests = {'passing_test', 'failing_test'}
+        last_passes = {'passing_test': datetime.datetime(2012, 1 ,1),
+                       'old_passing_test': datetime.datetime(2011, 1, 1)}
+
+        results = complete_failures.get_tests_to_analyze(recent_tests,
+                                                         last_passes)
 
         self.assertEqual(results,
                          {'passing_test': datetime.datetime(2012, 1, 1),
@@ -291,15 +333,13 @@
 
     def test_returns_failing_tests_with_min_datetime(self):
         """Test that never-passed tests are paired with datetime.min."""
-        self.mox.StubOutWithMock(utils, 'get_last_pass_times')
-        self.mox.StubOutWithMock(complete_failures,
-                                 'get_recently_ran_test_names')
 
-        utils.get_last_pass_times().AndReturn({})
-        complete_failures.get_recently_ran_test_names().AndReturn({'test'})
+        recent_tests = {'test'}
+        last_passes = {}
 
         self.mox.ReplayAll()
-        results = complete_failures.get_tests_to_analyze()
+        results = complete_failures.get_tests_to_analyze(recent_tests,
+                                                         last_passes)
 
         self.assertEqual(results, {'test': datetime.datetime.min})