{"id":334,"date":"2015-11-17T22:06:55","date_gmt":"2015-11-17T22:06:55","guid":{"rendered":"https:\/\/blogs.mathworks.com\/developer\/?p=334"},"modified":"2018-11-08T10:38:17","modified_gmt":"2018-11-08T15:38:17","slug":"youve-got-mail","status":"publish","type":"post","link":"https:\/\/blogs.mathworks.com\/developer\/2015\/11\/17\/youve-got-mail\/","title":{"rendered":"You&#8217;ve Got Mail!"},"content":{"rendered":"<div class=\"content\"><!--introduction-->Today I wanted to share some fun I've been having with customizing the emails sent from my Jenkins builds. This was inspired by Narendra's <a href=\"https:\/\/blogs.mathworks.com\/developer\/2015\/01\/29\/tap-plugin\/#comment-2373\">comment<\/a> on a previous post.\r\n\r\nSo, getting to it, I took a quick look after reading the comment and found the <a href=\"https:\/\/wiki.jenkins-ci.org\/display\/JENKINS\/Email-ext+plugin\">Email-ext<\/a> plugin that enables customization of the emails that are sent to administrators and code committers (offenders?) for failing or unstable builds.\r\n\r\nOut of the box, this plugin sends an email that lists the failing tests that were encountered. While this is nice, it doesn't pass muster in my book. Why?\r\n<div>\r\n<ol>\r\n\t<li>It is very much catered to JUnit test results by default. These aren't JUnit results #forgoodnesssakes! We are talking MATLAB code here! I want the CI system configuration to be production grade just like the code it is integrating, which means we don't accept calling MATLAB test results JUnit test results!<\/li>\r\n\t<li>Showing the failed tests is nice but I also want to see the diagnostics. If we have a chance to know immediately what the problem is then I don't want to have to click through to have to figure this out. Having the diagnostics in the email might mean I know exactly why the build fails when I get the email on my phone while <a href=\"https:\/\/en.wikipedia.org\/wiki\/Massachusetts_Bay_Transportation_Authority\">riding the T<\/a> on my way home. The remaining ride then has the benefit of valuable brain cycles already figuring out the solution. If I have to click through? No dice.<\/li>\r\n<\/ol>\r\n<\/div>\r\n<!--\/introduction-->\r\n\r\n<b>Make it MATLAB<\/b>\r\n\r\nOK, my strategy here is to pull the basic format from the default html email template because the customization API involves writing <a href=\"http:\/\/commons.apache.org\/proper\/commons-jelly\/overview.html\">jelly<\/a> scripts and I bet not many people have extensive experience writing in jelly (I certainly haven't). You can see the default jelly script for an html email is listed <a href=\"https:\/\/github.com\/jenkinsci\/email-ext-plugin\/tree\/master\/src\/main\/resources\/hudson\/plugins\/emailext\/templates\">here<\/a>, so for a first cut lets take a simplified version of that and make it our own.\r\n\r\nIf you analyze the default html template you can see the jelly script has this basic format:\r\n<pre class=\"language-matlab\">&lt;j:jelly xmlns:j=\"jelly:core\" xmlns:st=\"jelly:stapler\" xmlns:d=\"jelly:define\"&gt;\r\n\r\n&lt;STYLE&gt;\r\n&lt;!-- Define CSS Styling --&gt;\r\n&lt;\/STYLE&gt;\r\n\r\n&lt;BODY&gt;\r\n&lt;!-- Produce html content for email --&gt;\r\n&lt;\/BODY&gt;\r\n\r\n&lt;\/j:jelly&gt;\r\n\r\n<\/pre>\r\nWithin the main script body there is a node for styling and a node for the main body. For now lets just keep the styling that matches the default template, with one notable difference in that we define another class to account for filtered tests.\r\n<pre class=\"language-matlab\">  &lt;STYLE&gt;\r\n    BODY, TABLE, TD, TH, P {\r\n    font-family:Verdana,Helvetica,sans serif;\r\n    font-size:11px;\r\n    color:black;\r\n    }\r\n    h1 { color:black; }\r\n    h2 { color:black; }\r\n    h3 { color:black; }\r\n    TD.bg1 { color:white; background-color:#0000C0; font-size:120% }\r\n    TD.bg2 { color:white; background-color:#4040FF; font-size:110% }\r\n    TD.bg3 { color:white; background-color:#8080FF; }\r\n    TD.test_passed { color:blue; }\r\n    TD.test_failed { color:red; }\r\n    TD.test_filtered { color:orange; }\r\n    TD.output { font-family:Courier New; }\r\n  &lt;\/STYLE&gt;\r\n\r\n<\/pre>\r\nFor the body content why don't we prune out what we aren't working with and just keep what we are interested in at the moment. So I'll keep the general job information, the changeset information from your SCM system, and the artifacts:\r\n<pre class=\"language-matlab\">&lt;BODY&gt;\r\n&lt;j:set var=\"spc\" value=\"&amp;amp;nbsp;&amp;amp;nbsp;\" \/&gt;\r\n\r\n\r\n&lt;!-- GENERAL INFO --&gt;\r\n\r\n&lt;TABLE&gt;\r\n  &lt;TR&gt;&lt;TD align=\"right\"&gt;\r\n    &lt;j:choose&gt;\r\n      &lt;j:when test=\"${build.result=='SUCCESS'}\"&gt;\r\n        &lt;IMG SRC=\"${rooturl}static\/e59dfe28\/images\/32x32\/blue.gif\" \/&gt;\r\n      &lt;\/j:when&gt;\r\n\t  &lt;j:when test=\"${build.result=='FAILURE'}\"&gt;\r\n        &lt;IMG SRC=\"${rooturl}static\/e59dfe28\/images\/32x32\/red.gif\" \/&gt;\r\n      &lt;\/j:when&gt;\r\n      &lt;j:otherwise&gt;\r\n        &lt;IMG SRC=\"${rooturl}static\/e59dfe28\/images\/32x32\/yellow.gif\" \/&gt;\r\n      &lt;\/j:otherwise&gt;\r\n    &lt;\/j:choose&gt;\r\n  &lt;\/TD&gt;&lt;TD valign=\"center\"&gt;&lt;B style=\"font-size: 200%;\"&gt;BUILD ${build.result}&lt;\/B&gt;&lt;\/TD&gt;&lt;\/TR&gt;\r\n  &lt;TR&gt;&lt;TD&gt;Build URL&lt;\/TD&gt;&lt;TD&gt;&lt;A href=\"${rooturl}${build.url}\"&gt;${rooturl}${build.url}&lt;\/A&gt;&lt;\/TD&gt;&lt;\/TR&gt;\r\n  &lt;TR&gt;&lt;TD&gt;Project:&lt;\/TD&gt;&lt;TD&gt;${project.name}&lt;\/TD&gt;&lt;\/TR&gt;\r\n  &lt;TR&gt;&lt;TD&gt;Date of build:&lt;\/TD&gt;&lt;TD&gt;${it.timestampString}&lt;\/TD&gt;&lt;\/TR&gt;\r\n  &lt;TR&gt;&lt;TD&gt;Build duration:&lt;\/TD&gt;&lt;TD&gt;${build.durationString}&lt;\/TD&gt;&lt;\/TR&gt;\r\n&lt;\/TABLE&gt;\r\n&lt;BR\/&gt;\r\n\r\n\r\n&lt;!-- CHANGE SET --&gt;\r\n\r\n&lt;j:set var=\"changeSet\" value=\"${build.changeSet}\" \/&gt;\r\n&lt;j:if test=\"${changeSet!=null}\"&gt;\r\n  &lt;j:set var=\"hadChanges\" value=\"false\" \/&gt;\r\n  &lt;TABLE width=\"100%\"&gt;\r\n    &lt;TR&gt;&lt;TD class=\"bg1\" colspan=\"2\"&gt;&lt;B&gt;CHANGES&lt;\/B&gt;&lt;\/TD&gt;&lt;\/TR&gt;\r\n    &lt;j:forEach var=\"cs\" items=\"${changeSet}\" varStatus=\"loop\"&gt;\r\n      &lt;j:set var=\"hadChanges\" value=\"true\" \/&gt;\r\n      &lt;j:set var=\"aUser\" value=\"${cs.hudsonUser}\"\/&gt;\r\n      &lt;TR&gt;\r\n        &lt;TD colspan=\"2\" class=\"bg2\"&gt;${spc}Revision &lt;B&gt;${cs.commitId?:cs.revision?:cs.changeNumber}&lt;\/B&gt; by\r\n          &lt;B&gt;${aUser!=null?aUser.displayName:cs.author.displayName}: &lt;\/B&gt;\r\n          &lt;B&gt;(${cs.msgAnnotated})&lt;\/B&gt;\r\n         &lt;\/TD&gt;\r\n      &lt;\/TR&gt;\r\n      &lt;j:forEach var=\"p\" items=\"${cs.affectedFiles}\"&gt;\r\n        &lt;TR&gt;\r\n          &lt;TD width=\"10%\"&gt;${spc}${p.editType.name}&lt;\/TD&gt;\r\n          &lt;TD&gt;${p.path}&lt;\/TD&gt;\r\n        &lt;\/TR&gt;\r\n      &lt;\/j:forEach&gt;\r\n    &lt;\/j:forEach&gt;\r\n    &lt;j:if test=\"${!hadChanges}\"&gt;\r\n      &lt;TR&gt;&lt;TD colspan=\"2\"&gt;No Changes&lt;\/TD&gt;&lt;\/TR&gt;\r\n    &lt;\/j:if&gt;\r\n  &lt;\/TABLE&gt;\r\n&lt;BR\/&gt;\r\n&lt;\/j:if&gt;\r\n\r\n\r\n&lt;!-- ARTIFACTS --&gt;\r\n\r\n&lt;j:set var=\"artifacts\" value=\"${build.artifacts}\" \/&gt;\r\n&lt;j:if test=\"${artifacts!=null and artifacts.size()&amp;gt;0}\"&gt;\r\n  &lt;TABLE width=\"100%\"&gt;\r\n    &lt;TR&gt;&lt;TD class=\"bg1\"&gt;&lt;B&gt;BUILD ARTIFACTS&lt;\/B&gt;&lt;\/TD&gt;&lt;\/TR&gt;\r\n    &lt;TR&gt;\r\n      &lt;TD&gt;\r\n        &lt;j:forEach var=\"f\" items=\"${artifacts}\"&gt;\r\n      \t  &lt;li&gt;\r\n      \t    &lt;a href=\"${rooturl}${build.url}artifact\/${f}\"&gt;${f}&lt;\/a&gt;\r\n      \t  &lt;\/li&gt;\r\n      \t&lt;\/j:forEach&gt;\r\n      &lt;\/TD&gt;\r\n    &lt;\/TR&gt;\r\n  &lt;\/TABLE&gt;\r\n&lt;BR\/&gt;  \r\n&lt;\/j:if&gt;\r\n\r\n<\/pre>\r\nStill with me? OK good this next part gets interesting because I want to see the test results in a similar way to how MATLAB presents them to me as an array of TestResults. So rather than looping over all of the packages, we are going to print the total number of passed\/failed\/filtered tests and then create a section for failed tests and a section for filtered tests.\r\n<pre class=\"language-matlab\">&lt;!-- Test reporting TEMPLATE (references to JUnit refer to the JUnit style XML Output produced by the MATLAB TestRunner) --&gt; \r\n&lt;j:set var=\"resultList\" value=\"${it.JUnitTestResult}\" \/&gt;\r\n&lt;j:if test=\"${resultList.isEmpty()!=true}\"&gt;\r\n  &lt;TABLE width=\"100%\"&gt;\r\n    &lt;j:forEach var=\"testResult\" items=\"${resultList}\"&gt;\r\n      &lt;j:set var=\"failCount\" value=\"${failCount + testResult.getFailCount()}\"\/&gt;\r\n      &lt;j:set var=\"skipCount\" value=\"${skipCount + testResult.getSkipCount()}\"\/&gt;\r\n      &lt;j:set var=\"passCount\" value=\"${passCount + testResult.getPassCount()}\"\/&gt;\r\n    &lt;\/j:forEach&gt;\r\n    &lt;TR&gt;&lt;TD class=\"bg1\" colspan=\"2\"&gt;&lt;B&gt;MATLAB Test Suite Summary:&lt;\/B&gt; ${passCount+failCount+skipCount} Ran, ${failCount} Failed, ${passCount} Passed, ${skipCount} Filtered&lt;\/TD&gt;&lt;\/TR&gt;\r\n\r\n    &lt;TR&gt;&lt;TD class=\"bg1\" colspan=\"2\"&gt;&lt;B&gt;Failed Tests&lt;\/B&gt;&lt;\/TD&gt;&lt;\/TR&gt;\r\n    &lt;j:forEach var=\"testResult\" items=\"${resultList}\"&gt;\r\n      &lt;j:forEach var=\"packageResult\" items=\"${testResult.getChildren()}\"&gt;\r\n        &lt;j:forEach var=\"failed_test\" items=\"${packageResult.getFailedTests()}\"&gt;\r\n          &lt;TR bgcolor=\"white\"&gt;&lt;TD class=\"test_failed\" colspan=\"2\"&gt;&lt;B&gt;&lt;li&gt;Failed: ${failed_test.getFullName()} &lt;\/li&gt;&lt;\/B&gt;&lt;\/TD&gt;&lt;\/TR&gt;\r\n        &lt;\/j:forEach&gt;\r\n      &lt;\/j:forEach&gt; \r\n    &lt;\/j:forEach&gt; \r\n\r\n    &lt;TR&gt;&lt;TD class=\"bg1\" colspan=\"2\"&gt;&lt;B&gt;Filtered Tests&lt;\/B&gt;&lt;\/TD&gt;&lt;\/TR&gt;\r\n    &lt;j:forEach var=\"testResult\" items=\"${resultList}\"&gt;\r\n      &lt;j:forEach var=\"packageResult\" items=\"${testResult.getChildren()}\"&gt;\r\n        &lt;j:forEach var=\"filtered_test\" items=\"${packageResult.getSkippedTests()}\"&gt;\r\n          &lt;TR bgcolor=\"white\"&gt;&lt;TD class=\"test_filtered\" colspan=\"2\"&gt;&lt;B&gt;&lt;li&gt;Filtered: ${filtered_test.getFullName()} &lt;\/li&gt;&lt;\/B&gt;&lt;\/TD&gt;&lt;\/TR&gt;\r\n        &lt;\/j:forEach&gt;  \r\n      &lt;\/j:forEach&gt;\r\n    &lt;\/j:forEach&gt;\r\n  &lt;\/TABLE&gt;\t\r\n  &lt;BR\/&gt;\r\n&lt;\/j:if&gt;\r\n\r\n<\/pre>\r\nNote that what we have done here is analyzed information in the jenkins <tt>TestResult<\/tt>  instances that contain the information included in the JUnit style xml produced by MATLAB's <a href=\"https:\/\/www.mathworks.com\/help\/matlab\/ref\/matlab.unittest.plugins.xmlplugin-class.html\">XMLPLugin<\/a> (<a href=\"https:\/\/blogs.mathworks.com\/developer\/2015\/11\/03\/tap-diagnostics-and-junit-xml\/\">remember<\/a>?) With this information we are looping over these results three times, once to count the totals, once to print the failed tests, and once to print the filtered tests.\r\n\r\nNow we can show the log tail for posterity and close out the <tt>BODY<\/tt> and <tt>jelly<\/tt> tags.\r\n<pre class=\"language-matlab\">\r\n&lt;!-- LOG OUTPUT --&gt;\r\n\r\n&lt;TABLE width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"&gt;\r\n&lt;TR&gt;&lt;TD class=\"bg1\"&gt;&lt;B&gt;Log Tail (Full Log Attached)&lt;\/B&gt;&lt;\/TD&gt;&lt;\/TR&gt;\r\n&lt;j:forEach var=\"line\" items=\"${build.getLog(100)}\"&gt;&lt;TR&gt;&lt;TD class=\"console\"&gt;${line}&lt;\/TD&gt;&lt;\/TR&gt;&lt;\/j:forEach&gt;\r\n&lt;\/TABLE&gt;\r\n&lt;BR\/&gt;\r\n\r\n&lt;\/BODY&gt;\r\n&lt;\/j:jelly&gt;\r\n\r\n<\/pre>\r\nDoes it work? Well not yet. First we need to save this jelly script in a location that allows us to configure Jenkins to use this script when creating our emails. To do this, you need to save the script into the <b><tt>$JENKINS_HOME\/email-templates\/<\/tt><\/b> folder in order to be picked up by the plugin. If you are not the jenkins administrator this may take some cajoling of said administrator. Once that cajoling is successful however, you can configure the plugin to use this script on a project by project basis. For me this looks like this:\r\n\r\n<img decoding=\"async\" src=\"https:\/\/blogs.mathworks.com\/developer\/files\/2015ConfigCustomScript.png\" alt=\"\" hspace=\"5\" vspace=\"5\" \/>\r\n\r\nWith this script applied, I now get this email from Jenkins:\r\n\r\n<img decoding=\"async\" src=\"https:\/\/blogs.mathworks.com\/developer\/files\/2015FirstEmailTop.png\" alt=\"\" hspace=\"5\" vspace=\"5\" \/>\r\n\r\n<img decoding=\"async\" src=\"https:\/\/blogs.mathworks.com\/developer\/files\/2015FirstEmailBottom.png\" alt=\"\" hspace=\"5\" vspace=\"5\" \/>\r\n\r\nWoot woot! That email speaks MATLAB! Next step - include some diagnostics.\r\n\r\n<b>What's the diagnosis, doc?<\/b>\r\n\r\nThis is actually quite easy with a simple modification to the failures portion of the jelly script. We just need to add another table row as preformatted text along with a call to getErrorStackTrace, which is where MATLAB's diagnostics are placed.\r\n<pre class=\"language-matlab\">&lt;TR&gt;&lt;TD class=\"bg1\" colspan=\"2\"&gt;&lt;B&gt;Failed Tests&lt;\/B&gt;&lt;\/TD&gt;&lt;\/TR&gt;\r\n&lt;j:forEach var=\"testResult\" items=\"${resultList}\"&gt;\r\n  &lt;j:forEach var=\"packageResult\" items=\"${testResult.getChildren()}\"&gt;\r\n    &lt;j:forEach var=\"failed_test\" items=\"${packageResult.getFailedTests()}\"&gt;\r\n      &lt;TR bgcolor=\"white\"&gt;&lt;TD class=\"test_failed\" colspan=\"2\"&gt;&lt;B&gt;&lt;li&gt;Failed: ${failed_test.getFullName()} &lt;\/li&gt;&lt;\/B&gt;&lt;\/TD&gt;&lt;\/TR&gt;\r\n      &lt;TR&gt;&lt;TD class=\"console\"&gt;&lt;PRE&gt;${failed_test.getErrorStackTrace()}&lt;\/PRE&gt;&lt;\/TD&gt;&lt;\/TR&gt;\r\n    &lt;\/j:forEach&gt;\r\n  &lt;\/j:forEach&gt; \r\n&lt;\/j:forEach&gt; \r\n\r\n<\/pre>\r\nWith this simple change Jenkins now includes the diagnostic information in the email as well.\r\n\r\n<img decoding=\"async\" src=\"https:\/\/blogs.mathworks.com\/developer\/files\/2015EmailWithDiags.png\" alt=\"\" hspace=\"5\" vspace=\"5\" \/>\r\n\r\n<b>The Best of Both Worlds<\/b>\r\n\r\nOK, I need to do one more final tweak (I promise). While I am fan of including the diagnostics in the email, it almost seems like it is too much information at once. What I would really like is the first output we produced that could show the diagnostics one at a time for each test failure I look at. Can it be done? Well kinda sorta. Remember we are dealing with email content within email clients, not full fledged browsers rendering dynamic html and JavaScript. This would typically be easy with JavaScript, but unfortunately that's unsupported for email content. We need some form of dynamic content that reacts to mouse clicks in static html content. Mouse clicks, or mouse <i><b>hovers<\/b><\/i> .\r\n\r\nAlright, the cat is out of the bag. This can be done for the subset of email clients that support CSS hover selectors! For others, you can still send the full email content you just won't be able to see the dynamic behavior. My client happens to support the hover CSS selector so it works great for me. Here's what we need to do. First update the style section of the document with a few more style classes and the hover behavior which sets the font size of the diagnostic to zero percent when we are not hovering over the test name and sets the size to 100% when we are. This looks like the following\r\n<pre class=\"language-matlab\">&lt;STYLE&gt;\r\n  BODY, TABLE, TD, TH, P {\r\n  font-family:Verdana,Helvetica,sans serif;\r\n  font-size:11px;\r\n  color:black;\r\n  }\r\n  h1 { color:black; }\r\n  h2 { color:black; }\r\n  h3 { color:black; }\r\n  TD.bg1 { color:white; background-color:#0000C0; font-size:120% }\r\n  TD.bg2 { color:white; background-color:#4040FF; font-size:110% }\r\n  TD.bg3 { color:white; background-color:#8080FF; }\r\n  TD.test_passed { color:blue; }\r\n  TD.test_failed { color:red; }\r\n  TD.test_filtered { color:orange; }\r\n  TD.output { font-family:Courier New;font-size:0%;}\r\n  TD.build_log { font-family:Courier New;}\r\n  TR.failed_row:hover {background-color:lightgray;} \r\n  TR.failed_row:hover + TR.output_row TD.output{font-size:100%;} \r\n&lt;\/STYLE&gt;\r\n\r\n<\/pre>\r\nHere we add a few classes for table rows, and mark the test result row as light gray when we hover over it. At the same time, we also change the styling of the output row immediately following the row being hovered over to be 100%. Trickery!\r\n\r\nThis also requires a few changes to the test results portion of the jelly script as well, mainly just to opt into these different style classes.\r\n<pre class=\"language-matlab\">&lt;TR&gt;&lt;TD class=\"bg1\" colspan=\"2\"&gt;&lt;B&gt;Failed Tests&lt;\/B&gt;&lt;\/TD&gt;&lt;\/TR&gt;\r\n&lt;j:forEach var=\"testResult\" items=\"${resultList}\"&gt;\r\n  &lt;j:forEach var=\"packageResult\" items=\"${testResult.getChildren()}\"&gt;\r\n    &lt;j:forEach var=\"failed_test\" items=\"${packageResult.getFailedTests()}\"&gt;\r\n      &lt;TR class=\"failed_row\" bgcolor=\"white\"&gt;&lt;TD class=\"test_failed\" colspan=\"2\"&gt;&lt;B&gt;&lt;li&gt;Failed: ${failed_test.getFullName()} &lt;\/li&gt;&lt;\/B&gt;&lt;\/TD&gt;&lt;\/TR&gt;\r\n      &lt;TR class=\"output_row\"&gt;&lt;TD class=\"output\"&gt;&lt;PRE&gt;${failed_test.getErrorStackTrace()}&lt;\/PRE&gt;&lt;\/TD&gt;&lt;\/TR&gt;\r\n    &lt;\/j:forEach&gt;\r\n  &lt;\/j:forEach&gt; \r\n&lt;\/j:forEach&gt;\r\n\r\n<\/pre>\r\nHow'd we do? Well at least in my email client, we have a nice concise email that gives me my full diagnostic information on a per test basis when I hover over a test of interest.\r\n\r\n<img decoding=\"async\" src=\"https:\/\/blogs.mathworks.com\/developer\/files\/2015AnimatedEmail.gif\" alt=\"\" hspace=\"5\" vspace=\"5\" \/>\r\n\r\nHere is the final jelly script. Please take this and make it better and let me know how you fare in the <a href=\"https:\/\/blogs.mathworks.com\/developer\/2015\/11\/17\/youve-got-mail\/#respond\">comments<\/a>:\r\n<pre class=\"language-matlab\">&lt;j:jelly xmlns:j=\"jelly:core\" xmlns:st=\"jelly:stapler\" xmlns:d=\"jelly:define\"&gt;\r\n  \r\n  &lt;STYLE&gt;\r\n    BODY, TABLE, TD, TH, P {\r\n    font-family:Verdana,Helvetica,sans serif;\r\n    font-size:11px;\r\n    color:black;\r\n    }\r\n    h1 { color:black; }\r\n    h2 { color:black; }\r\n    h3 { color:black; }\r\n    TD.bg1 { color:white; background-color:#0000C0; font-size:120% }\r\n    TD.bg2 { color:white; background-color:#4040FF; font-size:110% }\r\n    TD.bg3 { color:white; background-color:#8080FF; }\r\n    TD.test_passed { color:blue; }\r\n    TD.test_failed { color:red; }\r\n    TD.test_filtered { color:orange; }\r\n    TD.output { font-family:Courier New;font-size:0%;}        \r\n    TD.build_log { font-family:Courier New;}        \r\n    TR.failed_row:hover {background-color:lightgray;}           \r\n    TR.failed_row:hover + TR.output_row TD.output{font-size:100%;}                \r\n  &lt;\/STYLE&gt;\r\n  &lt;BODY&gt;\r\n    &lt;j:set var=\"spc\" value=\"&amp;amp;nbsp;&amp;amp;nbsp;\" \/&gt;\r\n    \r\n    \r\n    &lt;!-- GENERAL INFO --&gt;\r\n    \r\n    &lt;TABLE&gt;\r\n      &lt;TR&gt;&lt;TD align=\"right\"&gt;\r\n          &lt;j:choose&gt;\r\n            &lt;j:when test=\"${build.result=='SUCCESS'}\"&gt;\r\n              &lt;IMG SRC=\"${rooturl}static\/e59dfe28\/images\/32x32\/blue.gif\" \/&gt;\r\n            &lt;\/j:when&gt;\r\n            &lt;j:when test=\"${build.result=='FAILURE'}\"&gt;\r\n              &lt;IMG SRC=\"${rooturl}static\/e59dfe28\/images\/32x32\/red.gif\" \/&gt;\r\n            &lt;\/j:when&gt;\r\n            &lt;j:otherwise&gt;\r\n              &lt;IMG SRC=\"${rooturl}static\/e59dfe28\/images\/32x32\/yellow.gif\" \/&gt;\r\n            &lt;\/j:otherwise&gt;\r\n          &lt;\/j:choose&gt;\r\n      &lt;\/TD&gt;&lt;TD valign=\"center\"&gt;&lt;B style=\"font-size: 200%;\"&gt;BUILD ${build.result}&lt;\/B&gt;&lt;\/TD&gt;&lt;\/TR&gt;\r\n      &lt;TR&gt;&lt;TD&gt;Build URL&lt;\/TD&gt;&lt;TD&gt;&lt;A href=\"${rooturl}${build.url}\"&gt;${rooturl}${build.url}&lt;\/A&gt;&lt;\/TD&gt;&lt;\/TR&gt;\r\n      &lt;TR&gt;&lt;TD&gt;Project:&lt;\/TD&gt;&lt;TD&gt;${project.name}&lt;\/TD&gt;&lt;\/TR&gt;\r\n      &lt;TR&gt;&lt;TD&gt;Date of build:&lt;\/TD&gt;&lt;TD&gt;${it.timestampString}&lt;\/TD&gt;&lt;\/TR&gt;\r\n      &lt;TR&gt;&lt;TD&gt;Build duration:&lt;\/TD&gt;&lt;TD&gt;${build.durationString}&lt;\/TD&gt;&lt;\/TR&gt;\r\n    &lt;\/TABLE&gt;\r\n    &lt;BR\/&gt;\r\n    \r\n    \r\n    &lt;!-- CHANGE SET --&gt;\r\n    \r\n    &lt;j:set var=\"changeSet\" value=\"${build.changeSet}\" \/&gt;\r\n    &lt;j:if test=\"${changeSet!=null}\"&gt;\r\n      &lt;j:set var=\"hadChanges\" value=\"false\" \/&gt;\r\n      &lt;TABLE width=\"100%\"&gt;\r\n        &lt;TR&gt;&lt;TD class=\"bg1\" colspan=\"2\"&gt;&lt;B&gt;Changes&lt;\/B&gt;&lt;\/TD&gt;&lt;\/TR&gt;\r\n        &lt;j:forEach var=\"cs\" items=\"${changeSet}\" varStatus=\"loop\"&gt;\r\n          &lt;j:set var=\"hadChanges\" value=\"true\" \/&gt;\r\n          &lt;j:set var=\"aUser\" value=\"${cs.hudsonUser}\"\/&gt;\r\n          &lt;TR&gt;\r\n            &lt;TD colspan=\"2\" class=\"bg2\"&gt;${spc}Revision &lt;B&gt;${cs.commitId?:cs.revision?:cs.changeNumber}&lt;\/B&gt; by\r\n              &lt;B&gt;${aUser!=null?aUser.displayName:cs.author.displayName}: &lt;\/B&gt;\r\n              &lt;B&gt;(${cs.msgAnnotated})&lt;\/B&gt;\r\n            &lt;\/TD&gt;\r\n          &lt;\/TR&gt;\r\n          &lt;j:forEach var=\"p\" items=\"${cs.affectedFiles}\"&gt;\r\n            &lt;TR&gt;\r\n              &lt;TD width=\"10%\"&gt;${spc}${p.editType.name}&lt;\/TD&gt;\r\n              &lt;TD&gt;${p.path}&lt;\/TD&gt;\r\n            &lt;\/TR&gt;\r\n          &lt;\/j:forEach&gt;\r\n        &lt;\/j:forEach&gt;\r\n        &lt;j:if test=\"${!hadChanges}\"&gt;\r\n          &lt;TR&gt;&lt;TD colspan=\"2\"&gt;No Changes&lt;\/TD&gt;&lt;\/TR&gt;\r\n        &lt;\/j:if&gt;\r\n      &lt;\/TABLE&gt;\r\n      &lt;BR\/&gt;\r\n    &lt;\/j:if&gt;\r\n    \r\n    \r\n    &lt;!-- ARTIFACTS --&gt;\r\n    \r\n    &lt;j:set var=\"artifacts\" value=\"${build.artifacts}\" \/&gt;\r\n    &lt;j:if test=\"${artifacts!=null and artifacts.size()&amp;gt;0}\"&gt;\r\n      &lt;TABLE width=\"100%\"&gt;\r\n        &lt;TR&gt;&lt;TD class=\"bg1\"&gt;&lt;B&gt;Build Artifacts&lt;\/B&gt;&lt;\/TD&gt;&lt;\/TR&gt;\r\n        &lt;TR&gt;\r\n          &lt;TD&gt;\r\n            &lt;j:forEach var=\"f\" items=\"${artifacts}\"&gt;\r\n              &lt;li&gt;\r\n                &lt;a href=\"${rooturl}${build.url}artifact\/${f}\"&gt;${f}&lt;\/a&gt;\r\n              &lt;\/li&gt;\r\n            &lt;\/j:forEach&gt;\r\n          &lt;\/TD&gt;\r\n        &lt;\/TR&gt;\r\n      &lt;\/TABLE&gt;\r\n      &lt;BR\/&gt;  \r\n    &lt;\/j:if&gt;\r\n    \r\n    &lt;!-- Test reporting TEMPLATE (references to JUnit refer to the JUnit style XML Output produced by the MATLAB TestRunner) --&gt;     \r\n    &lt;j:set var=\"resultList\" value=\"${it.JUnitTestResult}\" \/&gt;\r\n    &lt;j:if test=\"${resultList.isEmpty()!=true}\"&gt;\r\n      &lt;TABLE width=\"100%\"&gt;\r\n        &lt;j:forEach var=\"testResult\" items=\"${resultList}\"&gt;\r\n          &lt;j:set var=\"failCount\" value=\"${failCount + testResult.getFailCount()}\"\/&gt;\r\n          &lt;j:set var=\"skipCount\" value=\"${skipCount + testResult.getSkipCount()}\"\/&gt;\r\n          &lt;j:set var=\"passCount\" value=\"${passCount + testResult.getPassCount()}\"\/&gt;\r\n        &lt;\/j:forEach&gt;\r\n        &lt;TR&gt;&lt;TD class=\"bg1\" colspan=\"2\"&gt;&lt;B&gt;MATLAB Test Suite Summary:&lt;\/B&gt; ${passCount+failCount+skipCount} Ran, ${failCount} Failed, ${passCount} Passed, ${skipCount} Filtered&lt;\/TD&gt;&lt;\/TR&gt;\r\n        \r\n        &lt;TR&gt;&lt;TD class=\"bg1\" colspan=\"2\"&gt;&lt;B&gt;Failed Tests&lt;\/B&gt;&lt;\/TD&gt;&lt;\/TR&gt;\r\n        &lt;j:forEach var=\"testResult\" items=\"${resultList}\"&gt;\r\n          &lt;j:forEach var=\"packageResult\" items=\"${testResult.getChildren()}\"&gt;\r\n            &lt;j:forEach var=\"failed_test\" items=\"${packageResult.getFailedTests()}\"&gt;\r\n              &lt;TR class=\"failed_row\" bgcolor=\"white\"&gt;&lt;TD class=\"test_failed\" colspan=\"2\"&gt;&lt;B&gt;&lt;li&gt;Failed: ${failed_test.getFullName()} &lt;\/li&gt;&lt;\/B&gt;&lt;\/TD&gt;&lt;\/TR&gt;\r\n              &lt;TR class=\"output_row\"&gt;&lt;TD class=\"output\"&gt;&lt;PRE&gt;${failed_test.getErrorStackTrace()}&lt;\/PRE&gt;&lt;\/TD&gt;&lt;\/TR&gt;\r\n            &lt;\/j:forEach&gt;\r\n          &lt;\/j:forEach&gt; \r\n        &lt;\/j:forEach&gt;\r\n        \r\n        &lt;TR&gt;&lt;TD class=\"bg1\" colspan=\"2\"&gt;&lt;B&gt;Filtered Tests&lt;\/B&gt;&lt;\/TD&gt;&lt;\/TR&gt;\r\n        &lt;j:forEach var=\"testResult\" items=\"${resultList}\"&gt;\r\n          &lt;j:forEach var=\"packageResult\" items=\"${testResult.getChildren()}\"&gt;\r\n            &lt;j:forEach var=\"filtered_test\" items=\"${packageResult.getSkippedTests()}\"&gt;\r\n              &lt;TR bgcolor=\"white\"&gt;&lt;TD class=\"test_filtered\" colspan=\"2\"&gt;&lt;B&gt;&lt;li&gt;Filtered: ${filtered_test.getFullName()} &lt;\/li&gt;&lt;\/B&gt;&lt;\/TD&gt;&lt;\/TR&gt;\r\n            &lt;\/j:forEach&gt;  \r\n          &lt;\/j:forEach&gt;\r\n        &lt;\/j:forEach&gt;\r\n      &lt;\/TABLE&gt;\t\r\n      &lt;BR\/&gt;\r\n    &lt;\/j:if&gt;\r\n\r\n    &lt;!-- CONSOLE OUTPUT --&gt;\r\n      &lt;TABLE width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"&gt;\r\n        &lt;TR&gt;&lt;TD class=\"bg1\"&gt;&lt;B&gt;Build Log Tail (Full Log Attached)&lt;\/B&gt;&lt;\/TD&gt;&lt;\/TR&gt;\r\n        &lt;j:forEach var=\"line\" items=\"${build.getLog(100)}\"&gt;&lt;TR&gt;&lt;TD class=\"build_log\"&gt;${line}&lt;\/TD&gt;&lt;\/TR&gt;&lt;\/j:forEach&gt;\r\n      &lt;\/TABLE&gt;\r\n      &lt;BR\/&gt;\r\n    \r\n  &lt;\/BODY&gt;\r\n&lt;\/j:jelly&gt;\r\n\r\n           \r\n\r\n<\/pre>\r\n\r\n<script>\/\/ <![CDATA[\r\nfunction grabCode_75ef2c8f3d5343b4a978b5e7d6d452d1() {\r\n        \/\/ Remember the title so we can use it in the new page\r\n        title = document.title;\r\n\r\n        \/\/ Break up these strings so that their presence\r\n        \/\/ in the Javascript doesn't mess up the search for\r\n        \/\/ the MATLAB code.\r\n        t1='75ef2c8f3d5343b4a978b5e7d6d452d1 ' + '##### ' + 'SOURCE BEGIN' + ' #####';\r\n        t2='##### ' + 'SOURCE END' + ' #####' + ' 75ef2c8f3d5343b4a978b5e7d6d452d1';\r\n    \r\n        b=document.getElementsByTagName('body')[0];\r\n        i1=b.innerHTML.indexOf(t1)+t1.length;\r\n        i2=b.innerHTML.indexOf(t2);\r\n \r\n        code_string = b.innerHTML.substring(i1, i2);\r\n        code_string = code_string.replace(\/REPLACE_WITH_DASH_DASH\/g,'--');\r\n\r\n        \/\/ Use \/x3C\/g instead of the less-than character to avoid errors \r\n        \/\/ in the XML parser.\r\n        \/\/ Use '\\x26#60;' instead of '<' so that the XML parser\r\n        \/\/ doesn't go ahead and substitute the less-than character. \r\n        code_string = code_string.replace(\/\\x3C\/g, '\\x26#60;');\r\n\r\n        copyright = 'Copyright 2015 The MathWorks, Inc.';\r\n\r\n        w = window.open();\r\n        d = w.document;\r\n        d.write('\r\n\r\n<pre>\\n');\r\n        d.write(code_string);\r\n\r\n        \/\/ Add copyright line at the bottom if specified.\r\n        if (copyright.length > 0) {\r\n            d.writeln('');\r\n            d.writeln('%%');\r\n            if (copyright.length > 0) {\r\n                d.writeln('% _' + copyright + '_');\r\n            }\r\n        }\r\n\r\n        d.write('<\/pre>\r\n\r\n\r\n\\n');\r\n\r\n        d.title = title + ' (MATLAB code)';\r\n        d.close();\r\n    }\r\n\/\/ ]]><\/script>\r\n<p style=\"text-align: right; font-size: xx-small; font-weight: lighter; font-style: italic; color: gray;\">\r\n<a><span style=\"font-size: x-small; font-style: italic;\">Get\r\nthe MATLAB code<noscript>(requires JavaScript)<\/noscript><\/span><\/a>\r\n\r\nPublished with MATLAB\u00ae R2015b<\/p>\r\n\r\n<\/div>\r\n<!--\r\n75ef2c8f3d5343b4a978b5e7d6d452d1 ##### SOURCE BEGIN #####\r\n%%\r\n%\r\n% Today I wanted to share some fun I've been having with customizing the\r\n% emails sent from my Jenkins builds. This was inspired by Narendra's\r\n% <https:\/\/blogs.mathworks.com\/developer\/2015\/01\/29\/tap-plugin\/#comment-2373 % comment> on a previous post.\r\n%\r\n% So, getting to it, I took a quick look after reading the comment and\r\n% found the <https:\/\/wiki.jenkins-ci.org\/display\/JENKINS\/Email-ext+plugin % Email-ext> plugin that enables customization of the emails that are sent\r\n% to administrators and code committers (offenders?) for failing or unstable\r\n% builds.\r\n%\r\n% Out of the box, this plugin sends an email that lists the failing tests\r\n% that were encountered. While this is nice, it doesn't pass muster in my\r\n% book. Why?\r\n%\r\n% # It is very much catered to JUnit test results by default. These aren't\r\n% JUnit results #forgoodnesssakes! We are talking MATLAB code here! I want\r\n% the CI system configuration to be production grade just like the code it\r\n% is integrating, which means we don't accept calling MATLAB test results\r\n% JUnit test results!\r\n% # Showing the failed tests is nice but I also want to see the\r\n% diagnostics. If we have a chance to know immediately what the problem is\r\n% then I don't want to have to click through to have to figure this out.\r\n% Having the diagnostics in the email might mean I know exactly why the\r\n% build fails when I get the email on my phone while\r\n% <https:\/\/en.wikipedia.org\/wiki\/Massachusetts_Bay_Transportation_Authority riding the T> on my way home. The\r\n% remaining ride then has the benefit of valuable brain cycles already\r\n% figuring out the solution. If I have to click through? No dice.\r\n%\r\n%%\r\n% *Make it MATLAB*\r\n%\r\n% OK, my strategy here is to pull the basic format from the default html\r\n% email template because the customization API involves writing\r\n% <http:\/\/commons.apache.org\/proper\/commons-jelly\/overview.html jelly>\r\n% scripts and I bet not many people have extensive experience writing in\r\n% jelly (I certainly haven't). You can see the default jelly script for an\r\n% html email is listed\r\n% <https:\/\/github.com\/jenkinsci\/email-ext-plugin\/tree\/master\/src\/main\/resources\/hudson\/plugins\/emailext\/templates % here>, so for a first cut lets take a simplified version of that and make\r\n% it our own.\r\n%\r\n% If you analyze the default html template you can see the jelly script has this basic format:\r\n%\r\n% <include>basicStructure.jelly<\/include>\r\n%\r\n% Within the main script body there is a node for styling and a node for the\r\n% main body. For now lets just keep the styling that matches the default\r\n% template, with one notable difference in that we define another class to\r\n% account for filtered tests.\r\n%\r\n% <include>stylingV1.jelly<\/include>\r\n%\r\n% For the body content why don't we prune out what we aren't working with\r\n% and just keep what we are interested in at the moment. So I'll keep the\r\n% general job information,  the changeset information from your SCM system,\r\n% and the artifacts:\r\n%\r\n% <include>generalInfo.jelly<\/include>\r\n%\r\n% Still with me? OK good this next part gets interesting because I want to\r\n% see the test results in a similar way to how MATLAB presents them to me\r\n% as an array of TestResults. So rather than looping over all of the\r\n% packages, we are going to print the total number of\r\n% passed\/failed\/filtered tests and then create a section for failed tests\r\n% and a section for filtered tests.\r\n%\r\n% <include>testResultsV1.jelly<\/include>\r\n%\r\n% Note that what we have done here is analyzed information in the jenkins\r\n% |TestResult| instances that contain the information included in the\r\n% JUnit style xml produced by MATLAB's\r\n% <https:\/\/www.mathworks.com\/help\/matlab\/ref\/matlab.unittest.plugins.xmlplugin-class.html % XMLPLugin>\r\n% (<https:\/\/blogs.mathworks.com\/developer\/2015\/11\/03\/tap-diagnostics-and-junit-xml\/ % remember>?) With this information we are looping over these results three\r\n% times, once to count the totals, once to print the failed tests, and once\r\n% to print the filtered tests.\r\n%\r\n% Now we can show the log tail for posterity and close out the |BODY| and\r\n% |jelly| tags.\r\n%\r\n% <include>logTail.jelly<\/include>\r\n%\r\n% Does it work? Well not yet. First we need to save this jelly script in a\r\n% location that allows us to configure Jenkins to use this script when\r\n% creating our emails. To do this, you need to save the script into the\r\n% *|$JENKINS_HOME\/email-templates\/|* folder in order to be picked up by the\r\n% plugin. If you are not the jenkins administrator this may take some\r\n% cajoling of said administrator. Once that cajoling is successful however,\r\n% you can configure the plugin to use this script on a project by project\r\n% basis. For me this looks like this:\r\n%\r\n% <<2015ConfigCustomScript.png>>\r\n%\r\n% With this script applied, I now get this email from Jenkins:\r\n%\r\n% <<2015FirstEmailTop.png>>\r\n%\r\n% <<2015FirstEmailBottom.png>>\r\n%\r\n% Woot woot! That email speaks MATLAB! Next step - include some\r\n% diagnostics.\r\n%\r\n%%\r\n% *What's the diagnosis, doc?*\r\n%\r\n% This is actually quite easy with a simple modification to\r\n% the failures portion of the jelly script. We just need to add another\r\n% table row as preformatted text along with a call to getErrorStackTrace,\r\n% which is where MATLAB's diagnostics are placed.\r\n%\r\n% <include>testResultsV2.jelly<\/include>\r\n%\r\n% With this simple change Jenkins now includes the diagnostic information in the email\r\n% as well.\r\n%\r\n% <<2015EmailWithDiags.png>>\r\n%\r\n%%\r\n% *The Best of Both Worlds*\r\n%\r\n% OK, I need to do one more final tweak (I promise). While I am fan of\r\n% including the diagnostics in the email, it almost seems like it is too\r\n% much information at once. What I would really like is the first output we\r\n% produced that could show the diagnostics one at a time for each test\r\n% failure I look at. Can it be done? Well kinda sorta. Remember we are\r\n% dealing with email content within email clients, not full fledged\r\n% browsers rendering dynamic html and JavaScript. This would typically be\r\n% easy with JavaScript, but unfortunately that's unsupported for email\r\n% content. We need some form of dynamic content that reacts to mouse clicks\r\n% in static html content. Mouse clicks, or mouse _*hovers*_ .\r\n%\r\n% Alright, the cat is out of the bag. This can be done for the subset of\r\n% email clients that support CSS hover selectors! For others, you can still\r\n% send the full email content you just won't be able to see the dynamic\r\n% behavior. My client happens to support the hover CSS selector so it works\r\n% great for me. Here's what we need to do. First update the style section\r\n% of the document with a few more style classes and the hover behavior\r\n% which sets the font size of the diagnostic to zero percent when we are\r\n% not hovering over the test name and sets the size to 100% when we are.\r\n% This looks like the following\r\n%\r\n% <include>stylingV3.jelly<\/include>\r\n%\r\n% Here we add a few classes for table rows, and mark the test result row as\r\n% light gray when we hover over it. At the same time, we also change the\r\n% styling of the output row immediately following the row being hovered\r\n% over to be 100%. Trickery!\r\n%\r\n% This also requires a few changes to the test results portion of the jelly\r\n% script as well, mainly just to opt into these different style classes.\r\n%\r\n% <include>testResultsV3.jelly<\/include>\r\n%\r\n% How'd we do? Well at least in my email client, we have a nice concise\r\n% email that gives me my full diagnostic information on a per test basis\r\n% when I hover over a test of interest.\r\n%\r\n% <<2015AnimatedEmail.gif>>\r\n%\r\n% Here is the final jelly script. Please take this and make it better and\r\n% let me know how you fare in the\r\n% <https:\/\/blogs.mathworks.com\/developer\/2015\/11\/17\/youve-got-mail\/#respond % comments>:\r\n%\r\n% <include>reportMATLABTestResults.jelly<\/include>\r\n\r\n##### SOURCE END ##### 75ef2c8f3d5343b4a978b5e7d6d452d1\r\n-->","protected":false},"excerpt":{"rendered":"<div class=\"overview-image\"><img src=\"https:\/\/blogs.mathworks.com\/developer\/files\/2015AnimatedEmail.gif\" class=\"img-responsive attachment-post-thumbnail size-post-thumbnail wp-post-image\" alt=\"\" decoding=\"async\" loading=\"lazy\" \/><\/div><!--introduction-->Today I wanted to share some fun I've been having with customizing the emails sent from my Jenkins builds. This was inspired by Narendra's <a href=\"https:\/\/blogs.mathworks.com\/developer\/2015\/01\/29\/tap-plugin\/#comment-2373\">comment<\/a> on a previous post.\r\n\r\nSo, getting to it, I took a quick look after reading the comment and found the <a href=\"https:\/\/wiki.jenkins-ci.org\/display\/JENKINS\/Email-ext+plugin\">Email-ext<\/a> plugin that enables customization of the emails that are sent to administrators and code committers (offenders?) for failing or unstable builds.\r\n\r\nOut of the box, this plugin sends an email that lists the failing tests that were encountered. While this is nice, it doesn't pass muster in my book. Why?\r\n<div>\r\n<ol>\r\n\t<li>It is very much catered to JUnit test results by default. These aren't JUnit results #forgoodnesssakes! We are talking MATLAB code here! I want the CI system configuration to be production grade just like the code it is integrating, which means we don't accept calling MATLAB test results JUnit test results!<\/li>\r\n\t<li>Showing the failed tests is nice but I also want to see the diagnostics. If we have a chance to know immediately what the problem is then I don't want to have to click through to have to figure this out. Having the diagnostics in the email might mean I know exactly why the build fails when I get the email on my phone while <a href=\"https:\/\/en.wikipedia.org\/wiki\/Massachusetts_Bay_Transportation_Authority\">riding the T<\/a> on my way home. The remaining ride then has the benefit of valuable brain cycles already figuring out the solution. If I have to click through? No dice.<\/li>\r\n<\/ol>\r\n<\/div>\r\n<!--\/introduction-->... <a class=\"read-more\" href=\"https:\/\/blogs.mathworks.com\/developer\/2015\/11\/17\/youve-got-mail\/\">read more >><\/a><\/p>","protected":false},"author":90,"featured_media":340,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[4],"tags":[],"_links":{"self":[{"href":"https:\/\/blogs.mathworks.com\/developer\/wp-json\/wp\/v2\/posts\/334"}],"collection":[{"href":"https:\/\/blogs.mathworks.com\/developer\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blogs.mathworks.com\/developer\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blogs.mathworks.com\/developer\/wp-json\/wp\/v2\/users\/90"}],"replies":[{"embeddable":true,"href":"https:\/\/blogs.mathworks.com\/developer\/wp-json\/wp\/v2\/comments?post=334"}],"version-history":[{"count":23,"href":"https:\/\/blogs.mathworks.com\/developer\/wp-json\/wp\/v2\/posts\/334\/revisions"}],"predecessor-version":[{"id":1936,"href":"https:\/\/blogs.mathworks.com\/developer\/wp-json\/wp\/v2\/posts\/334\/revisions\/1936"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blogs.mathworks.com\/developer\/wp-json\/wp\/v2\/media\/340"}],"wp:attachment":[{"href":"https:\/\/blogs.mathworks.com\/developer\/wp-json\/wp\/v2\/media?parent=334"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blogs.mathworks.com\/developer\/wp-json\/wp\/v2\/categories?post=334"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blogs.mathworks.com\/developer\/wp-json\/wp\/v2\/tags?post=334"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}