Developer Zone

Advanced Software Development with MATLAB

The (Build) Matrix Laboratory

Ah the joys of a new release! R2017b is now out and packed with some great new features. So here I go, heading out to an excitement fueled upgrade-mania when lo and behold, my glee is rudely interrupted when I see my code is broken in the new release.

What a drain on an otherwise pleasant experience! It turns out that the error is due to some of my more "creative" and "clever" coding practices that have helped me get my job done in the past, and when I dig in I see that I've been warned about this change coming for years. The problem is I never seem to have the time to comb the release notes as much as I'd like to, and even if I did realizing that my code I wrote 5 years ago has some of the problems mentioned is a tall order.

To compound that, even though I'd like to upgrade, when I make the needed adjustments to do so I worry about breaking the users of my code who haven't upgraded yet.

What in the world to do?

Let's use some continuous integration! Last I checked we were still in charge of the machines so let's put them to good use. It is actually quite easy to put this into practice using Jenkins. Jenkins supports what is known as matrix builds so that we can ensure our code works across many different MATLAB releases. To set this up, simply create your project as a Multi-Configuration Project in Jenkins.

Once this project is created we get the opportunity to define our own configuration matrix. This means that we can add an axis of configuration where we can define a particular system property (i.e. environment variable) as well as the different values we'd like to have for each of these values. What I want to do is add the MATLAB version and point it to several releases I'd like to test against, like so:

Once this is done, Jenkins will create multiple configurations for our build and it will be parameterized across this value. So, as we set up the build step (which we would set up to run our tests against thecode changes) this value can be leveraged in the build script to ensure this happens on all the releases. For example, our build script can point to the different installation locations of the different MATLAB releases I want to test against:

Simple as that! What does this give us? It gives us a new build configuration (which really feels like a new build) for every value of MATLAB_VERSION. To see how this can help, let's look at a quick example. Here we have a bit of source code simulating a mass spring damper...

function [x, t] = simulateSystem

springMassDamperDesign; % Create design variable.

if ~isstruct(design) || ~all(isfield(design,{'c','k'}))
    error('simulateSystem:InvalidDesign:ShouldBeStruct', ...
        'The design should be a structure with fields "c" and "k"');
end

% Design variables
c = design.c;
k = design.k;

% Constant variables
z0 = [-0.1; 0];  % Initial Position and Velocity
m = 1500;        % Mass

odefun = @(t,z) [0 1; -k/m -c/m]*z;
[t, z] = ode45(odefun, [0, 3], z0);

% The first column is the position (displacement from equilibrium)
x = z(:, 1);

...as well as a corresponding script which defines the design of the system in terms of the spring stiffness and damping:

m = 1500; % Need to know the mass to determine critical damping

design.k = 5e6;                   % Spring Constant
design.c = 2*m*sqrt(design.k/m); % Damping Coefficient to be critically damped

clear m;

Along with this source we also have a file that defines a couple tests for this design to ensure it achieves the right characteristics in terms of acceptable overshoot and settling time of a step response:

function tests = designTest
tests = functiontests(localfunctions); 
end

function testSettlingTime(testCase) 
%%Test that the system settles to within 0.001 of zero under 2 seconds.

[position, time] = simulateSystem; 

positionAfterSettling = position(time > .002);

%For this example, verify the first value after the settling time.
verifyLessThan(testCase, abs(positionAfterSettling), 2);
end

function testOvershoot(testCase)
 %Test to ensure that overshoot is less than 0.01

[position, time] = simulateSystem;
overshoot = max(position);

verifyLessThan(testCase, overshoot, 0.01);
end

Finally, here is our test running script that will kick off the testing:

try
    import('matlab.unittest.TestRunner');
    import('matlab.unittest.plugins.XMLPlugin');
    import('matlab.unittest.plugins.ToFile');

    
    ws = getenv('WORKSPACE');
    
    src = fullfile(ws, 'source');
    addpath(src);
    
    tests = fullfile(ws, 'tests');
    suite = testsuite(tests);

    % Create and configure the runner
    runner = TestRunner.withTextOutput('Verbosity',3);

    % Add the plugin for test results
    resultsDir = fullfile(ws, 'testresults');
    mkdir(resultsDir);
    resultsFile = fullfile(resultsDir, 'testResults.xml');
    runner.addPlugin(XMLPlugin.producingJUnitFormat(resultsFile));
     
    results = runner.run(suite)
catch e
    disp(getReport(e,'extended'));
    exit(1);
end
quit('force');

With this set up you can see that each build actually gives us four different configurations so we test across all releases.

Here we see we are just fine to upgrade to R2017b. How's that for some peace of mind? Trusting your tests gives you the confidence needed to upgrade without worry and/or to be notified quickly when adjustments need to be made.

Now let's evolve the code. I am not the biggest fan of the design script because it offends my sensibilities to have the clear m call in there. I'd like to change this implementation:

m = 1500; % Need to know the mass to determine critical damping

design.k = 5e6;                   % Spring Constant
design.c = 2*m*sqrt(design.k/m); % Damping Coefficient to be critically damped

clear m;

...to this:

design.k = 5e6;                   % Spring Constant
design.c = 2*getMass*sqrt(design.k/getMass); % Damping Coefficient to be critically damped

function m = getMass
m = 1500; % Need to know the mass to determine critical damping
end

This seems cleaner to me. I'd rather leverage a local function in the script so that the variable we'd like to clear simply never is created in the workspace.

However, checking this change in produces the following:

It looks like local functions in scripts weren't around in R2016a, so I'll have to wait on that change until I no longer need to support my code in that release. Thanks, build matrix!




Published with MATLAB® R2017b

|
  • print

Comments

To leave a comment, please click here to sign in to your MathWorks Account or create a new one.