Showing posts with label API. Show all posts
Showing posts with label API. Show all posts

Sunday, 1 September 2019

Parallel Apex Unit Tests and Salesforce CLI Plugins

Parallel Apex Unit Tests and Salesforce CLI Plugins

 

Introduction

In the Salesforce Winter 20 release notes was something I’ve been looking forward to for a few years - the ability to turn off parallel Apex unit test execution. By default parallel unit test are enabled (somewhat confusingly, by the fact that the Disable Parallel Apex Testing option is not checked):

 

and this typically results in a number of failures in my automated test runner package, as the tests can’t get exclusive access to write to the account others object tables.

This setting is one of the last items that I have to manually turn on when creating a scratch org, and that has been annoying me for a while. I could automate it via Selenium, but that feels like overkill, so I was very pleased to see the new Apex Settings metadata type. Among other features, this has the enableDisableParallelApexTesting field which allows me to check or uncheck the Disable Parallel Apex Testing checkbox, albeit via a metadata API deployment. The release notes also mentioned the to-be-deprecated OrgPreferenceSettings metadata object, and it turns out that this also has a mechanism for turning off parallel testing via the DisableParallelApexTesting setting, so there was no need for me to wait until Winter 20 as long as I could switch between the two mechanisms based on the API version the org is at.

Salesforce CLI Plugin

Regular readers of this blog will know that I’m a huge fan of the Salesforce Cli - I use it all the time and when I’m looking to do anything around developer tooling I always try to create a CLI plugin to host it. This was no different, although it presented a couple of challenges that I hadn’t taken on before:

  • Determining the API version that the org is at. If this is 46 or less (not sure how it would be possible to be on an earlier version of the API than Summer 19, but if Salesforce ever allow it I wanted to be covered) I need to deploy an OrgPreferences metadata type, 47 or higher I need to deploy an ApexSettings.
  • Metadata deployment from inside a plugin. 

After I scaffolded a new bbsfdx plugin and copied the commands/hello/org.ts example command to bb/test/parallel.ts, I set about solving them.

Determine the API Version

Whenever I’m doing anything new with a CLI plugin, my first port of call is the reference documentation for the Salesforce DX Core Library, and I wasn’t disappointed. I can find the API version for the org via the Connection.retrieveMaxAPIVersion function. Getting a Connection object is simple in a CLI plugin - just specify the requiresUsername property as true and a connection comes up with the rations via the org property supplied by the plugin. Getting the target API version is as simple as:

const conn = this.org.getConnection();
const apiVersion = await conn.retrieveMaxApiVersion();

So far so good.

Metadata Deployment

The simplest way to do this is to execute an existing Salesforce CLI deployment command, either force:source:deploy or force:mdapi:deploy, but I’m not a fan of this approach. Spawning a process to execute a Salesforce CLI command from within a CLI plugin seems clunky and inelegant, and it binds me to a command that I don’t control and which may be retired. I should be able replicate anything the standard commands do as I have access to the same underlying libraries.

This time the core library docs weren’t much help - there was a metadata property on the Connection object, but it didn’t have any detail, so time to look elsewhere. The Core library is built on Shinichi Tomita’s JSforce library, so I headed over to the docs for that. The API reference for the Metadata class was exactly what I was looking for, specifically the deploy method.

To deploy metadata, I need a zip file containing a manifest (package.xml) and the metadata files themselves in the directory structure mandated by the metadata API. In order to achieve this, I create a temporary directory and write the appropriate information depending on the API version (stored as a float value in the fltVersion variable):

let packageFile=join(targetDir, 'package.xml');
let packageContents='<?xml version="1.0" encoding="UTF-8"?>\n' + 
    '<Package xmlns="http://soap.sforce.com/2006/04/metadata">\n' +
    ' <types>\n' + 
    '   <name>Settings</name>\n';

let fltVersion=parseFloat(apiVersion);

if (fltVersion>46) {
  packageContents+='    <members>Apex</members>\n' + 
                   '  </types>\n' +
                   '  <version>47.0</version>\n' + 
                   '</Package>';
  let apex=join(settingsDir, 'Apex.settings');
  writeFileSync(apex, 
        '<?xml version="1.0" encoding="UTF-8"?>\n' + 
        '<ApexSettings xmlns="http://soap.sforce.com/2006/04/metadata">\n' +
        '  <enableDisableParallelApexTesting>' + attr + '</enableDisableParallelApexTesting>\n' +
        '</ApexSettings>\n'
          );
}
else {
  packageContents+='    <members>OrgPreference</members>\n' + 
                   '  </types>\n' +
                   '  <version>46.0</version>\n' + 
                   '</Package>';
  let orgPref=join(settingsDir, 'OrgPreference.settings');
  writeFileSync(orgPref, 
    '<?xml version="1.0" encoding="UTF-8"?>\n' + 
    '<OrgPreferenceSettings xmlns="http://soap.sforce.com/2006/04/metadata">\n' + 
    '  <preferences>\n' + 
    '     <settingName>DisableParallelApexTesting</settingName>\n' + 
    '     <settingValue>' + attr + '</settingValue>\n' + 
    '  </preferences>\n' + 
    '</OrgPreferenceSettings>\n'
  );
}

writeFileSync(packageFile, packageContents);

Now I have my directory structure, I need to zip it. Searching on npm for zip packages returns a lot of results, so how to choose? I bounced around sites like stack exchange to see what others were using and eventually settled on compressing for a couple of reasons. First, it supports other compression files than zip, and I might need that flexibility in the future, and second it has a really simple API and is already promisified. After installing it into my plugin node modules and importing it into the parallel.ts file, generating a zip file is a couple of lines:

const zipFile=join(tmpDir, 'pkg.zip');
await compressing.zip.compressDir(targetDir, zipFile);

Getting there. Back to the docs for the metadata deploy function, it wants a zip stream rather than a filename, so I create one of those and add the code to deploy the metadata;

let zipStream=createReadStream(zipFile);
let result=await conn.metadata.deploy(zipStream, {});

The deploy function returns information about the deployment job, so I then enter a loop to poll for the status until it is finished:

let done=false;

let deployResult:DeployResult;
while (!done) {
  deployResult=await conn.metadata.checkDeployStatus(result.id);
  done=deployResult.done;
  if (!done) {
    this.ux.log(deployResult.status + messages.getMessage('sleeping'));
    await new Promise(sleep => setTimeout(sleep, 5000));
  }
}

and there it is - a plugin to enable or disable parallel Apex unit testing in under a couple of hundred lines of Typescript (and obviously a ton of existing node modules that allow me to stand on the shoulders of giants). 

Where’s the Code?

The full code for the plugin can be found in the Github repository at : https://github.com/keirbowden/bbsfdx 

The plugin itself is published on npm at : https://www.npmjs.com/package/bbsfdx - it has been tested on MacOS.

To install the plugin into your version of sfdx, execute:

sfdx plugins:install bbsfdx

Related Posts

Saturday, 5 July 2014

Automatic Dashboard Refresh Revisited

Just over three (!) years ago I published a blog post showing a mechanism for automatically refreshing a dashboard after a timer expired.  This had a number of caveats around it, most importantly that the method I was calling to execute the refresh was undocumented and thus likely to be unsupported by Salesforce.  

With the advent of the Summer 14 release, there is now a supported way to refresh a dashboard programmatically, through the newly GA Dashboards API.  This REST based API not only allows dashboard metadata and data to be retrieved, but also contains a method to execute a dashboard refresh.

To refresh a dashboard, simply finds its ID and execute the following JavaScript (requires JQuery) from a Visualforce page:

$.ajax(
            {
              url: '/services/data/v31.0/analytics/dashboards/01ZB00000008oyd',
              type: 'PUT',
              beforeSend: function(xhr) {
                // Set the OAuth header from the session ID
                xhr.setRequestHeader('Authorization', 'Bearer {!$Api.Session_ID}');
              },
              success: function(response) {
                // do success stuff 
              error: function(jqXHR, textStatus, errorThrown) {
                // do failure stuff
              }
          }
        );

where ‘01ZB00000008oyd’ is the id of the dashboard. 

As I can now refresh directly from the page, my DashboardRefresher page no longer needs a controller and all the logic is written in JavaScript: 

<apex:page sidebar="false" showheader="false" standardstylesheets="false">
  <apex:includeScript value="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.11.1.min.js"/>
  
  <div id="countDown"></div>

  <script>
    $(document).ready(function($) {
       startCountDown(59, 1000, doRefresh);
    });

    function startCountDown(i, p, f)
    {
        var pause = p;
        var fn = f;
                
        var countDownObj = document.getElementById("countDown");
        if (countDownObj == null)
        {
            alert("div not found, check your id");
            return;
        }
                
        countDownObj.count = function(i)
        {
            countDownObj.innerHTML = 'Refreshing in ' + i + ' seconds';
            if (i == 0)
            {
                fn();
                return;
            }
            setTimeout(function() {
                          countDownObj.count(i - 1);
                         },
                       pause
            );
        }
        
        countDownObj.count(i);
    }
                
    function doRefresh()
    {
        $.ajax(
            {
              url: '/services/data/v31.0/analytics/dashboards/01ZB00000008oyd',
              type: 'PUT',
              beforeSend: function(xhr) {
                // Set the OAuth header from the session ID
                xhr.setRequestHeader('Authorization', 'Bearer {!$Api.Session_ID}');
              },
              success: function(response) {
                window.top.location='/01ZB00000008oyd';
              },
              error: function(jqXHR, textStatus, errorThrown) {
                // Oops - what went wrong?
                alert(jqXHR.status + ': ' + errorThrown);
              }
          }
        );
    }
  </script>
  
</apex:page>

Dropping this page into a dashboard counts down the timer - note the ‘As of’ time:

Screen Shot 2014 07 05 at 15 49 58

once the timer expires, the dashboard is refreshed and the page is reloaded, showing the dashboard has been refreshed.  the ‘As of’ time updated and the counter restarted:

Screen Shot 2014 07 05 at 15 50 53

You can request up to 200 dashboard refreshes per hour, but bear in mind this is an organisation-level limit rather than per-user or per-dashboard.

Friday, 28 March 2014

London SFDG March Meetup

On 26th March the London Salesforce Developer Group once again congregated at Make Positive’s offices just south of the River Thames for our monthly meetup. It was great to see a few new faces who had tried Salesforce at the Elevate London Workshop and had come back for more.

Over the last couple of months we’ve moved to themed events, where all talks relate to a specific topic.  February’s theme was Salesforce1, and March built on the knowledge gained there and covered Mobile Access to APIs.

Laurent Delcambre of Tquila was on first, talking about extending Salesforce1 functionality through the analytics API to bring in reporting functionality.

Ld

 

Next Alessio Valentini, also of Tquila, gave a talk on developing a mobile application where the functionality lives on-device and all Salesforce data is accessed via APIs.

Av

We had a great turnout again this month - the meetup really seems to have taken off this year.

Ldsize

Beer, pizza and networking filled the time before the talks, while beer and networking continued on later into the night, moving to a local hostelry once we’d emptied the Make Positive fridge.

If you are based in or around London, work with or are interested in Salesforce, and aren’t a member of this group you should join - just signup at http://www.meetup.com/LondonSalesforceDevelopers/

The next meetup takes place towards the end of April - I hope to see you there.

One more thing - we also have a new hashtag - #LonDevSFDC - search us on twitter for news and updates about the group.