← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~abentley/launchpad/detailed-build-emails into lp:launchpad/devel

 

Aaron Bentley has proposed merging lp:~abentley/launchpad/detailed-build-emails into lp:launchpad/devel.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  #603606 Recipe Build failure emails aren't as binary package build failures
  https://bugs.launchpad.net/bugs/603606


= Summary =
Fix bug #603606: Recipe Build failure emails aren't as binary package build failures

== Proposed fix ==
Update recipe build emails to use similar formatting to bianary build emails.

== Pre-implementation notes ==
Mid-implementation with Tim Penhey

== Implementation details ==
The formatting now matches.  The fields supplied are:
State, Recipe, Archive, Distroseries, Duration, Build Log and Builder.

Similarly, the subject line contains recipe build id, recipe, distroseries
and status.

Component and pocket are not provided as these do not vary.

== Tests ==
bin/test test_sourcepackagerecipebuild -t notif -t generateEmail.

== Demo and Q/A ==
Request a build.  When is processed, the resulting mail should be as described.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/code/model/tests/test_sourcepackagerecipebuild.py
  lib/lp/code/mail/sourcepackagerecipebuild.py
  lib/lp/testing/factory.py
  lib/lp/code/model/sourcepackagerecipebuild.py
  lib/canonical/launchpad/emailtemplates/build-request.txt
  lib/lp/code/mail/tests/test_sourcepackagerecipebuild.py

./lib/lp/code/mail/sourcepackagerecipebuild.py
       5: E303 too many blank lines (3)
      71: Line exceeds 78 characters.
      46: E231 missing whitespace after ','
./lib/lp/code/mail/tests/test_sourcepackagerecipebuild.py
      15: 'IStore' imported but unused
       5: E303 too many blank lines (3)
      37: W291 trailing whitespace
      38: W291 trailing whitespace
      39: W291 trailing whitespace
      42: E302 expected 2 blank lines, found 1
      37: Line has trailing whitespace.
      38: Line has trailing whitespace.
      39: Line has trailing whitespace.
-- 
https://code.launchpad.net/~abentley/launchpad/detailed-build-emails/+merge/31413
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~abentley/launchpad/detailed-build-emails into lp:launchpad/devel.
=== modified file 'lib/canonical/launchpad/emailtemplates/build-request.txt'
--- lib/canonical/launchpad/emailtemplates/build-request.txt	2010-06-08 18:31:56 +0000
+++ lib/canonical/launchpad/emailtemplates/build-request.txt	2010-07-30 19:24:53 +0000
@@ -1,1 +1,7 @@
-Build %(recipe_owner)s/%(recipe)s into %(archive)s for %(distroseries)s: %(status)s.
+ * State: %(status)s
+ * Recipe: %(recipe_owner)s/%(recipe)s
+ * Archive: %(archive_owner)s/%(archive)s
+ * Distroseries: %(distroseries)s
+ * Duration: %(duration)s
+ * Build Log: %(log_url)s
+ * Builder: %(builder_url)s

=== modified file 'lib/lp/code/mail/sourcepackagerecipebuild.py'
--- lib/lp/code/mail/sourcepackagerecipebuild.py	2010-06-09 20:24:11 +0000
+++ lib/lp/code/mail/sourcepackagerecipebuild.py	2010-07-30 19:24:53 +0000
@@ -11,6 +11,7 @@
 
 from canonical.config import config
 from canonical.launchpad.webapp import canonical_url
+from canonical.launchpad.webapp.tales import DurationFormatterAPI
 from lp.services.mail.basemailer import BaseMailer, RecipientReason
 
 
@@ -25,7 +26,8 @@
         requester = build.requester
         recipients = {requester: RecipientReason.forBuildRequester(requester)}
         return cls(
-            '%(status)s: %(recipe)s for %(distroseries)s',
+            '[recipe build #%(build_id)d] of ~%(recipe_owner)s %(recipe)s in'
+            ' %(distroseries)s: %(status)s',
             'build-request.txt', recipients,
             config.canonical.noreply_from_address, build)
 
@@ -50,13 +52,26 @@
         params = super(
             SourcePackageRecipeBuildMailer, self)._getTemplateParams(email)
         params.update({
+            'build_id': self.build.id,
             'status': self.build.buildstate.title,
             'distroseries': self.build.distroseries.name,
             'recipe': self.build.recipe.name,
             'recipe_owner': self.build.recipe.owner.name,
             'archive': self.build.archive.name,
+            'archive_owner': self.build.archive.owner.name,
+            'log_url': '',
+            'component': self.build.current_component.name,
+            'duration': '',
+            'builder_url': '',
             'build_url': canonical_url(self.build),
         })
+        if self.build.builder is not None:
+            params['builder_url'] = canonical_url(self.build.builder)
+        if self.build.buildduration is not None:
+            duration_formatter = DurationFormatterAPI(self.build.buildduration)
+            params['duration'] = duration_formatter.approximateduration()
+        if self.build.build_log_url is not None:
+            params['log_url'] = self.build.build_log_url
         return params
 
     def _getFooter(self, params):

=== modified file 'lib/lp/code/mail/tests/test_sourcepackagerecipebuild.py'
--- lib/lp/code/mail/tests/test_sourcepackagerecipebuild.py	2010-06-09 20:24:11 +0000
+++ lib/lp/code/mail/tests/test_sourcepackagerecipebuild.py	2010-07-30 19:24:53 +0000
@@ -5,42 +5,68 @@
 __metaclass__ = type
 
 
+from datetime import timedelta
 from unittest import TestLoader
 
+from storm.locals import Store
+from zope.security.proxy import removeSecurityProxy
+
 from canonical.config import config
 from canonical.launchpad.interfaces.lpstorm import IStore
-from canonical.testing import DatabaseFunctionalLayer
+from canonical.testing import LaunchpadFunctionalLayer
 from lp.buildmaster.interfaces.buildbase import BuildStatus
 from lp.code.mail.sourcepackagerecipebuild import (
     SourcePackageRecipeBuildMailer)
 from lp.testing import TestCaseWithFactory
 
+expected_body = u"""\
+ * State: Successfully built
+ * Recipe: person/recipe
+ * Archive: archiveowner/ppa
+ * Distroseries: distroseries
+ * Duration: five minutes
+ * Build Log: %s
+ * Builder: http://launchpad.dev/builders/bob
+"""
+
+superseded_body = u"""\
+ * State: Build for superseded Source
+ * Recipe: person/recipe
+ * Archive: archiveowner/ppa
+ * Distroseries: distroseries
+ * Duration: 
+ * Build Log: 
+ * Builder: 
+"""
 
 class TestSourcePackageRecipeBuildMailer(TestCaseWithFactory):
 
-    layer = DatabaseFunctionalLayer
+    layer = LaunchpadFunctionalLayer
 
     def test_generateEmail(self):
         """GenerateEmail produces the right headers and body."""
         person = self.factory.makePerson(name='person')
         cake = self.factory.makeSourcePackageRecipe(
             name=u'recipe', owner=person)
-        pantry = self.factory.makeArchive(name='ppa')
+        pantry_owner = self.factory.makePerson(name='archiveowner')
+        pantry = self.factory.makeArchive(name='ppa', owner=pantry_owner)
         secret = self.factory.makeDistroSeries(name=u'distroseries')
         build = self.factory.makeSourcePackageRecipeBuild(
             recipe=cake, distroseries=secret, archive=pantry,
-            status=BuildStatus.FULLYBUILT)
-        IStore(build).flush()
+            status=BuildStatus.FULLYBUILT, duration=timedelta(minutes=5))
+        naked_build = removeSecurityProxy(build)
+        naked_build.builder = self.factory.makeBuilder(name='bob')
+        naked_build.buildlog = self.factory.makeLibraryFileAlias()
+        Store.of(build).flush()
         mailer = SourcePackageRecipeBuildMailer.forStatus(build)
         email = build.requester.preferredemail.email
         ctrl = mailer.generateEmail(email, build.requester)
-        self.assertEqual('Successfully built: recipe for distroseries',
-            ctrl.subject)
+        self.assertEqual(
+            u'[recipe build #%d] of ~person recipe in distroseries: '
+            'Successfully built' % (build.id), ctrl.subject)
         body, footer = ctrl.body.split('\n-- \n')
         self.assertEqual(
-            'Build person/recipe into ppa for distroseries: Successfully'
-            ' built.\n', body
-            )
+            expected_body % build.build_log_url, body)
         self.assertEqual(
             'http://code.launchpad.dev/~person/+recipe/recipe/+build/1\n'
             'You are the requester of the build.\n', footer)
@@ -54,6 +80,39 @@
         self.assertEqual(
             'FULLYBUILT', ctrl.headers['X-Launchpad-Build-State'])
 
+    def test_generateEmail_with_null_fields(self):
+        """GenerateEmail works when many fields are NULL."""
+        person = self.factory.makePerson(name='person')
+        cake = self.factory.makeSourcePackageRecipe(
+            name=u'recipe', owner=person)
+        pantry_owner = self.factory.makePerson(name='archiveowner')
+        pantry = self.factory.makeArchive(name='ppa', owner=pantry_owner)
+        secret = self.factory.makeDistroSeries(name=u'distroseries')
+        build = self.factory.makeSourcePackageRecipeBuild(
+            recipe=cake, distroseries=secret, archive=pantry,
+            status=BuildStatus.SUPERSEDED)
+        Store.of(build).flush()
+        mailer = SourcePackageRecipeBuildMailer.forStatus(build)
+        email = build.requester.preferredemail.email
+        ctrl = mailer.generateEmail(email, build.requester)
+        self.assertEqual(
+            u'[recipe build #%d] of ~person recipe in distroseries: '
+            'Build for superseded Source' % (build.id), ctrl.subject)
+        body, footer = ctrl.body.split('\n-- \n')
+        self.assertEqual(superseded_body, body)
+        self.assertEqual(
+            'http://code.launchpad.dev/~person/+recipe/recipe/+build/1\n'
+            'You are the requester of the build.\n', footer)
+        self.assertEqual(
+            config.canonical.noreply_from_address, ctrl.from_addr)
+        self.assertEqual(
+            'Requester', ctrl.headers['X-Launchpad-Message-Rationale'])
+        self.assertEqual(
+            'recipe-build-status',
+            ctrl.headers['X-Launchpad-Notification-Type'])
+        self.assertEqual(
+            'SUPERSEDED', ctrl.headers['X-Launchpad-Build-State'])
+
 
 def test_suite():
     return TestLoader().loadTestsFromName(__name__)

=== modified file 'lib/lp/code/model/sourcepackagerecipebuild.py'
--- lib/lp/code/model/sourcepackagerecipebuild.py	2010-07-28 16:44:00 +0000
+++ lib/lp/code/model/sourcepackagerecipebuild.py	2010-07-30 19:24:53 +0000
@@ -202,7 +202,7 @@
 
     @classmethod
     def new(cls, distroseries, recipe, requester, archive, pocket=None,
-            date_created=None):
+            date_created=None, duration=None):
         """See `ISourcePackageRecipeBuildSource`."""
         store = IMasterStore(SourcePackageRecipeBuild)
         if pocket is None:
@@ -215,7 +215,8 @@
             requester,
             archive,
             pocket,
-            date_created=date_created)
+            date_created=date_created,
+            build_duration=duration)
         store.add(spbuild)
         return spbuild
 

=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipebuild.py'
--- lib/lp/code/model/tests/test_sourcepackagerecipebuild.py	2010-07-22 08:15:29 +0000
+++ lib/lp/code/model/tests/test_sourcepackagerecipebuild.py	2010-07-30 19:24:53 +0000
@@ -30,6 +30,8 @@
 from lp.code.interfaces.sourcepackagerecipebuild import (
     ISourcePackageRecipeBuildJob, ISourcePackageRecipeBuild,
     ISourcePackageRecipeBuildSource)
+from lp.code.mail.sourcepackagerecipebuild import (
+    SourcePackageRecipeBuildMailer)
 from lp.code.model.sourcepackagerecipebuild import SourcePackageRecipeBuild
 from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.services.mail.sendmail import format_address
@@ -319,7 +321,8 @@
         pantry = self.factory.makeArchive(name='ppa')
         secret = self.factory.makeDistroSeries(name=u'distroseries')
         build = self.factory.makeSourcePackageRecipeBuild(
-            recipe=cake, distroseries=secret, archive=pantry)
+            recipe=cake, distroseries=secret, archive=pantry,
+            duration=datetime.timedelta(minutes=5))
         removeSecurityProxy(build).buildstate = BuildStatus.FULLYBUILT
         IStore(build).flush()
         build.notify()
@@ -327,21 +330,24 @@
         requester = build.requester
         requester_address = format_address(
             requester.displayname, requester.preferredemail.email)
+        mailer = SourcePackageRecipeBuildMailer.forStatus(build)
+        expected = mailer.generateEmail(
+            requester.preferredemail.email, requester)
         self.assertEqual(
             requester_address, re.sub(r'\n\t+', ' ', message['To']))
-        self.assertEqual('Successfully built: recipe for distroseries',
-            message['Subject'])
-        body, footer = message.get_payload(decode=True).split('\n-- \n')
+        self.assertEqual(expected.subject, message['Subject'].replace(
+            '\n\t', ' '))
         self.assertEqual(
-            'Build person/recipe into ppa for distroseries: Successfully'
-            ' built.\n', body)
+            expected.body, message.get_payload(decode=True))
 
     def test_handleStatusNotifies(self):
         """"handleStatus causes notification, even if OK."""
 
         def prepare_build():
-            queue_record = self.factory.makeSourcePackageRecipeBuildJob()
-            build = queue_record.specific_job.build
+            build = self.factory.makeSourcePackageRecipeBuild(
+                duration=datetime.timedelta(minutes=5))
+            queue_record = self.factory.makeSourcePackageRecipeBuildJob(
+                recipe_build=build)
             removeSecurityProxy(build).buildstate = BuildStatus.FULLYBUILT
             queue_record.builder = self.factory.makeBuilder()
             slave = WaitingSlave('BuildStatus.OK')
@@ -372,7 +378,8 @@
         distroseries.nominatedarchindep = distroseries_i386
         build = self.factory.makeSourcePackageRecipeBuild(
             distroseries=distroseries,
-            status=BuildStatus.FULLYBUILT)
+            status=BuildStatus.FULLYBUILT,
+            duration=datetime.timedelta(minutes=5))
         build.queueBuild(build)
         return build
 

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2010-07-30 16:30:12 +0000
+++ lib/lp/testing/factory.py	2010-07-30 19:24:53 +0000
@@ -166,8 +166,6 @@
     time_counter,
     )
 from lp.translations.interfaces.potemplate import IPOTemplateSet
-from lp.translations.interfaces.translationimportqueue import (
-    RosettaImportStatus)
 from lp.translations.interfaces.translationgroup import (
     ITranslationGroupSet)
 from lp.translations.interfaces.translationimportqueue import (
@@ -1870,7 +1868,8 @@
                                      requester=None, archive=None,
                                      sourcename=None, distroseries=None,
                                      pocket=None, date_created=None,
-                                     status=BuildStatus.NEEDSBUILD):
+                                     status=BuildStatus.NEEDSBUILD,
+                                     duration=None):
         """Make a new SourcePackageRecipeBuild."""
         if recipe is None:
             recipe = self.makeSourcePackageRecipe(name=sourcename)
@@ -1887,7 +1886,8 @@
             archive=archive,
             requester=requester,
             pocket=pocket,
-            date_created=date_created)
+            date_created=date_created,
+            duration=duration)
         removeSecurityProxy(spr_build).buildstate = status
         return spr_build
 
@@ -2767,12 +2767,18 @@
 # security wrappers for them, as well as for objects created by
 # other Python libraries.
 unwrapped_types = (
-    DSCFile, InstanceType, MergeDirective2, Message, datetime, int, str, unicode)
+    DSCFile, InstanceType, MergeDirective2, Message, datetime, int, str,
+    unicode)
+
 
 
 def is_security_proxied_or_harmless(obj):
+<<<<<<< TREE
     """Check that the given object is security wrapped or a harmless object.
     """
+=======
+    """Check that the object is security wrapped or a harmless object."""
+>>>>>>> MERGE-SOURCE
     if obj is None:
         return True
     if builtin_isinstance(obj, Proxy):