Make all changes reportable

We want to report on "changes" triggered by a timer.  Since those
aren't really changes, they are represented by a class called
NullChange which carries as much of the interface for a Change
object as possible (not much) so that they can be enqueued in
pipelines.

If a NullChange is reportable, then pretty much any kind of
change should be considered reportable.  So remove the flag that
indicates that NullChanges and Refs are not reportable.

Only attempt to format a change report if there is an action
defined for that pipeline (in case the default formatting process
attempts to access a change attribute that is inappropriate).
Remove checks that try to avoid formatting or sending a report
based on attributes of the change (which are no longer relevant).

Add a test for using a timer trigger with an smtp reporter.

Add validation of the attributes that an smtp reporter can use in
the layout file.

Allow the operator to configure a Subject for smtp reports.

Change-Id: Icd067a7600c2922a318b9ade11b3946df4e53065
diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py
index 48f2281..697cef5 100755
--- a/tests/test_scheduler.py
+++ b/tests/test_scheduler.py
@@ -2992,6 +2992,45 @@
         self.assertEqual(A.messages[0],
                          FakeSMTP.messages[1]['body'])
 
+    def test_timer_smtp(self):
+        "Test that a periodic job is triggered"
+        self.config.set('zuul', 'layout_config',
+                        'tests/fixtures/layout-timer-smtp.yaml')
+        self.sched.reconfigure(self.config)
+        self.registerJobs()
+
+        start = time.time()
+        failed = True
+        while ((time.time() - start) < 30):
+            if len(self.history) == 2:
+                failed = False
+                break
+            else:
+                time.sleep(1)
+
+        if failed:
+            raise Exception("Expected jobs never ran")
+
+        self.waitUntilSettled()
+
+        self.assertEqual(self.getJobFromHistory(
+            'project-bitrot-stable-old').result, 'SUCCESS')
+        self.assertEqual(self.getJobFromHistory(
+            'project-bitrot-stable-older').result, 'SUCCESS')
+
+        self.assertEqual(len(FakeSMTP.messages), 1)
+
+        # A.messages only holds what FakeGerrit places in it. Thus we
+        # work on the knowledge of what the first message should be as
+        # it is only configured to go to SMTP.
+
+        self.assertEqual('zuul_from@example.com',
+                         FakeSMTP.messages[0]['from_email'])
+        self.assertEqual(['alternative_me@example.com'],
+                         FakeSMTP.messages[0]['to_email'])
+        self.assertIn('Subject: Periodic check for org/project succeeded',
+                      FakeSMTP.messages[0]['headers'])
+
     def test_client_enqueue(self):
         "Test that the RPC client can enqueue a change"
         A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')