Code covered by the BSD License  

Highlights from
XMLTestRunDisplay

XMLTestRunDisplay

by

 

28 Nov 2011 (Updated )

JUnit-style XML logger for MATLAB xunit framework

edu.stanford.covert.test.XMLTestRunDisplay
classdef XMLTestRunDisplay < CommandWindowTestRunDisplay
    properties (Access = protected)
        TicStack = uint64([])
        
        ReportName
        ReportLabel
        XMLFileName
        
        TimeStamp
        iTestCase
        TestCases
        
        NumFailures = 0
        NumErrors = 0
        NumSkipped = 0;
    end
    
    methods
        function this = XMLTestRunDisplay(ReportName, ReportLabel, XMLFileName)
            this = this@CommandWindowTestRunDisplay();
            this.ReportName = ReportName;
            this.ReportLabel = ReportLabel;
            this.XMLFileName = XMLFileName;
        end
    end
    
    methods (Access = protected)        
        function testRunStarted(this, component)
            this.TimeStamp = datestr(now, 31);
            this.iTestCase = 0;
            this.TestCases = repmat(...
                struct('classname', [], 'name', [], 'location', [], 'time', [], 'skipped', [], 'failure', [], 'error', [], 'stack', [], 'exception', []), ...
                component.numTestCases(), 1);           
        end
        
        function testRunFinished(this, totalRunTime)
            import edu.stanford.covert.util.escapeXML;
            
            this.displayFaults();
            
            [~, hostName] = edu.stanford.covert.util.computerInfo();
            try
                revision = edu.stanford.covert.util.revision();
            catch %#ok<CTCH>
                revision = [];
            end
            
            fid = fopen(this.XMLFileName, 'w');
            fprintf(fid, '<?xml version="1.0" encoding="UTF-8"?>\n');
            fprintf(fid, '<testsuite name="%s" label="%s" time="%f" tests="%d" failures="%d" errors="%d" skipped="%d" hostname="%s" timestamp="%s">\n', ...
                this.ReportName, this.ReportLabel, ...
                totalRunTime, ...
                numel(this.TestCases), this.NumFailures, this.NumErrors, this.NumSkipped, ...
                hostName, this.TimeStamp);
            fprintf(fid, '\t<properties>\n');
            fprintf(fid, '\t\t<property name="%s" value="%d"/>\n', 'revision', revision);
            fprintf(fid, '\t\t<property name="%s" value="%s"/>\n', 'matlab.version', version);
            fprintf(fid, '\t\t<property name="%s" value="%s"/>\n', 'java.version', version('-java'));
            fprintf(fid, '\t</properties>\n');
            for i = 1:numel(this.TestCases)
                testCase = this.TestCases(i);
                if isempty(testCase.skipped) && isempty(testCase.error) && isempty(testCase.failure)
                    fprintf(fid, '\t<testcase classname="%s" name="%s" time="%f"/>\n', ...
                        testCase.classname, testCase.name, testCase.time);
                elseif ~isempty(testCase.skipped)
                    fprintf(fid, '\t<testcase classname="%s" name="%s" time="%f" skipped="1">\n', ...
                        testCase.classname, testCase.name, testCase.time);
                    fprintf(fid, '\t\t<skipped message="%s"/>\n', escapeXML(testCase.skipped));
                    fprintf(fid, '\t</testcase>\n');
                elseif ~isempty(testCase.failure)
                    fprintf(fid, '\t<testcase classname="%s" name="%s" time="%f">\n', ...
                        testCase.classname, testCase.name, testCase.time);
                    fprintf(fid, '\t\t<failure type="%s" message="%s"><![CDATA[%s\n%s]]></failure>\n', ...
                        escapeXML(testCase.exception), escapeXML(testCase.failure), ...
                        this.formatStack(testCase.stack), testCase.failure);
                    fprintf(fid, '\t</testcase>\n');
                else
                    fprintf(fid, '\t<testcase classname="%s" name="%s" time="%f">\n', ...
                        testCase.classname, testCase.name, testCase.time);
                    fprintf(fid, '\t\t<error type="%s" message="%s"><![CDATA[%s\n%s]]></error>\n', ...
                        escapeXML(testCase.exception), escapeXML(testCase.error), ...
                        this.formatStack(testCase.stack), testCase.error);
                    fprintf(fid, '\t</testcase>\n');
                end
            end
            fprintf(fid, '\t<system-out><![CDATA[%s]]></system-out>\n', '');
            fprintf(fid, '\t<system-err><![CDATA[%s]]></system-err>\n', '');
            fprintf(fid, '</testsuite>\n');
            fclose(fid);
        end            
    end
    
    methods
        function testComponentStarted(this, component)
            this.testComponentStarted@CommandWindowTestRunDisplay(component);            
            
            %testComponentStarted Update Command Window display
            
            this.pushTic();
            
            if ~isa(component, 'TestCase')
                fprintf('\n');
            end
            
            fprintf('%s%s', this.indentationSpaces(), component.Name);
            
            if ~isa(component, 'TestCase')
                fprintf('\n');
            else
                fprintf(' %s ', this.leaderDots(component.Name));
            end
                        
            %disabled tests
            if ~isa(component, 'TestCase')
                metaClass = meta.class.fromName(component.Name);
                if ~isempty(metaClass)
                    for i = 1:numel(metaClass.Methods)
                        if numel(metaClass.Methods{i}.Name) >= 13 && strcmp(metaClass.Methods{i}.Name(1:13), 'disabled_test')                            
                            this.TestCases = [this.TestCases; struct(...
                                'classname', metaClass.Name, ...
                                'name', metaClass.Methods{i}.Name(10:end), ...
                                'location', component.Location, ...
                                'time', 0, ...
                                'skipped', 'disabled', ...
                                'failure', [], ...
                                'error', [], ...
                                'stack', [], ...
                                'exception', [])];
                            
                            this.NumSkipped = this.NumSkipped + 1;
                            fprintf('%s%s', repmat(' ', 1, 3*numel(this.TicStack)), metaClass.Methods{i}.Name(10:end));
                            fprintf(' %s ', repmat('.', 1, max(0, 60 - 3*numel(this.TicStack) - numel(metaClass.Methods{i}.Name(10:end)))));
                            fprintf('skiped in %12.6f seconds\n', 0);
                        end
                    end
                end
            end
        end
        
        function testComponentFinished(this, component, did_pass)
            %testComponentFinished Update Command Window display
            
            if ~isa(component, 'TestCase')
                fprintf('%s%s %s ', this.indentationSpaces(), component.Name, ...
                    this.leaderDots(component.Name));
            end
            
            component_run_time = toc(this.popTic());
            
            if did_pass
                fprintf('passed in %12.6f seconds\n', component_run_time);
            else
                fprintf('FAILED in %12.6f seconds\n', component_run_time);
            end
            
            if ~isa(component, 'TestCase')
                fprintf('\n');
            end
            
            if isa(component, 'TestCase')
                this.iTestCase = this.iTestCase + 1;
                
                if strcmp(component.MethodName, 'runTestCase')
                    [~, filename, ~] = fileparts(component.Location);
                    if isempty(filename)
                        classname = component.Name;
                    else
                        classname = filename;
                    end
                    this.TestCases(this.iTestCase).classname = classname;
                    this.TestCases(this.iTestCase).name = component.Name;
                else
                    this.TestCases(this.iTestCase).classname = class(component);
                    this.TestCases(this.iTestCase).name = component.MethodName;
                end
                
                this.TestCases(this.iTestCase).location = component.Location;
                this.TestCases(this.iTestCase).time = component_run_time;
            end
            
            if isempty(this.TicStack)
                this.testRunFinished(component_run_time);
            end
        end
        
        function testCaseFailure(this, test_case, failure_exception)
            this.NumFailures = this.NumFailures + 1;
                        
            this.TestCases(this.iTestCase+1).failure = failure_exception.message;
            this.TestCases(this.iTestCase+1).exception = failure_exception.identifier;
            this.TestCases(this.iTestCase+1).stack = failure_exception.stack;
            
            this.testCaseFailure@CommandWindowTestRunDisplay(test_case, failure_exception);
        end
        
        function testCaseError(this, test_case, error_exception)
            this.NumErrors = this.NumErrors + 1;
            
            this.TestCases(this.iTestCase+1).error = error_exception.message;
            this.TestCases(this.iTestCase+1).exception = error_exception.identifier;
            this.TestCases(this.iTestCase+1).stack = error_exception.stack;
            
            this.testCaseError@CommandWindowTestRunDisplay(test_case, error_exception);
        end
    end
    
    methods (Access = protected)
        function pushTic(self)
            self.TicStack(end+1) = tic;
        end
        
        function t1 = popTic(self)
            t1 = self.TicStack(end);
            self.TicStack(end) = [];
        end
        
        function str = indentationSpaces(self)
            str = repmat(' ', 1, self.numIndentationSpaces());
        end
        
        function n = numIndentationSpaces(self)
            indent_level = numel(self.TicStack) - 1;
            n = 3 * indent_level;
        end
        
        function str = leaderDots(self, name)
            num_dots = max(0, 60 - self.numIndentationSpaces() - numel(name));
            str = repmat('.', 1, num_dots);
        end
        
        function output = formatStack(this, stack)
            output = [];
            stack = this.filterStack(stack);
            for k = 1:numel(stack)
                filename = stack(k).file;
                linenumber = stack(k).line;
                output = [output sprintf('%s at line %d\n', filename, linenumber)]; %#ok<AGROW>
            end
        end
        
        function new_stack = filterStack(~, stack)
            %filterStack Remove unmeaningful stack trace calls
            %    new_stack = filterStack(stack) removes from the input stack trace calls
            %    that are framework functions and methods that are not likely to be
            %    meaningful to the user.
            
            % Testing stack traces follow this common pattern:
            %
            % 1. The first function call in the trace is often one of the assert functions
            % in the framework directory.  This is useful to see.
            %
            % 2. The next function calls are in the user-written test functions/methods and
            % the user-written code under test.  These calls are useful to see.
            %
            % 3. The final set of function calls are methods in the various framework
            % classes.  There are usually several of these calls, which clutter up the
            % stack display without being that useful.
            %
            % The pattern above suggests the following stack filtering strategy: Once the
            % stack trace has left the framework directory, do not follow the stack trace back
            % into the framework directory.
            
            mtest_directory = fileparts(which('runtests'));
            last_keeper = numel(stack);
            have_left_mtest_directory = false;
            for k = 1:numel(stack)
                directory = fileparts(stack(k).file);
                if have_left_mtest_directory
                    if strcmp(directory, mtest_directory)
                        % Stack trace has reentered mtest directory.
                        last_keeper = k - 1;
                        break;
                    end
                else
                    if ~strcmp(directory, mtest_directory)
                        have_left_mtest_directory = true;
                    end
                end
            end
            
            new_stack = stack(1:last_keeper);
        end        
    end
end

Contact us