Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for multiple logging outputs (console, file, xml) #18

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.asv
45 changes: 45 additions & 0 deletions src/MetaTestRunLogger.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
classdef MetaTestRunLogger < TestRunMonitor
%METATESTRUNLOGGER Combine several test run loggers into one
% Implement the 4 methods of a matlab-xunit test logger on each of
% a cell array of instantiated input loggers.

properties
loggers = {};
end

methods
function self = MetaTestRunLogger(loggers)
self.loggers = loggers;
end

function testComponentStarted(self, component)
for ii=1:length(self.loggers)
logger = self.loggers{ii}{1};
logger.testComponentStarted(component);
end
end

function testComponentFinished(self, component, did_pass)
for ii=1:length(self.loggers)
logger = self.loggers{ii}{1};
logger.testComponentFinished(component, did_pass);
end
end

function testCaseFailure(self, test_case, failure_exception)
for ii=1:length(self.loggers)
logger = self.loggers{ii}{1};
logger.testCaseFailure(test_case, failure_exception);
end
end

function testCaseError(self, test_case, error_exception)
for ii=1:length(self.loggers)
logger = self.loggers{ii}{1};
logger.testCaseError(test_case, error_exception);
end
end
end

end

36 changes: 32 additions & 4 deletions src/XMLTestRunLogger.m
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ function testComponentStarted(self, component)
self.TCNum = self.TCNum + 1;
self.Results.testcase{self.TCNum}.ATTRIBUTE.classname = self.CurrentClass;
self.Results.testcase{end}.ATTRIBUTE.name = component.Name;
else
elseif isa(component, 'TestSuite')
self.testSuiteFinished()
self.CurrentClass = component.Name;
end
end
Expand Down Expand Up @@ -97,7 +98,7 @@ function testCaseError(self, test_case, error_exception)
end

methods (Access = protected)
function testRunFinished(self)
function writeResults(self, filename)
self.Results.ATTRIBUTE.tests = self.TCNum;
self.Results.ATTRIBUTE.skip = 0;
self.Results.ATTRIBUTE.failures = self.FailureNum;
Expand All @@ -106,18 +107,45 @@ function testRunFinished(self)
wPref.StructItem = false;
wPref.CellItem = false;

xml_write(self.ReportFile, self.Results, 'testsuite', wPref);
xml_write(filename, self.Results, 'testsuite', wPref);

if self.ReportFileIdentifier > 0
self.synchronizeReportFiles();
end
end

% ONLY IF the ReportFile is a directory, write an xml file for each
% suite.
function testSuiteFinished(self)
[~, filename] = fileparts(self.ReportFile);
if isempty(filename) && ~isempty(self.CurrentClass)
self.Results.ATTRIBUTE.name = self.CurrentClass;
self.writeResults(self.getResultFileName());

self.Results = struct;
self.TCNum = 0;
end
end

function testRunFinished(self)
self.writeResults(self.getResultFileName());
end
end

methods (Access = private)
function value = isValidFileIdentifier(~, identifier)
value = isnumeric(identifier) && any(identifier == fopen('all'));
end


function filename = getResultFileName(self)
[pathname, filename] = fileparts(self.ReportFile);
if isempty(filename)
filename = fullfile(pathname, ['TEST-' self.CurrentClass '.xml']);
else
filename = self.ReportFile;
end
end

function pushTic(self)
self.TicStack(end+1) = tic;
end
Expand Down
23 changes: 23 additions & 0 deletions src/narginchk.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
function narginchk(minargs, maxargs)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been in MATLAB since R2011b

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, we have a production project stuck on R2010b however, YMMV.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that this will shadow the "proper" builtin version

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah true, suggest just block this file from merge to your branch - and just take the other parts.

  • The file/function signature is compatible


if (nargin ~= 2)
error('%s: Usage: narginchk(minargs, maxargs)',upper(mfilename));
elseif (~isnumeric (minargs) || ~isscalar (minargs))
error ('minargs must be a numeric scalar');
elseif (~isnumeric (maxargs) || ~isscalar (maxargs))
error ('maxargs must be a numeric scalar');
elseif (minargs > maxargs)
error ('minargs cannot be larger than maxargs')
end


args = evalin ('caller', 'nargin;');


if (args < minargs)
error ('MATLAB:narginchk:notEnoughInputs', 'Not enough input arguments.');
elseif (args > maxargs)
error ('MATLAB:narginchk:tooManyInputs', 'Too many input arguments.');
end

end
91 changes: 57 additions & 34 deletions src/runxunit.m
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
function out = runxunit(varargin)
%runxunit Run unit tests
% runxunit runs all the test cases that can be found in the current directory
% and summarizes the results in the Command Window.
% runxunit runs all the test cases that can be found in the current
% directory and summarizes the results in the Command Window.
%
% Test cases can be found in the following places in the current directory:
%
Expand All @@ -19,27 +19,33 @@
% runxunit(packagename) runs all the test cases found in the specified
% package. (This option requires R2009a or later).
%
% runxunit(mfilename) runs test cases found in the specified function or class
% name. The function or class needs to be in the current directory or on the
% MATLAB path.
% runxunit(mfilename) runs test cases found in the specified function or
% class name. The function or class needs to be in the current directory
% or on the MATLAB path.
%
% runxunit('mfilename:testname') runs the specific test case named 'testname'
% found in the function or class 'name'.
% runxunit('mfilename:testname') runs the specific test case named
% 'testname' found in the function or class 'name'.
%
% Multiple directories or file names can be specified by passing multiple
% names to runxunit, as in runxunit(name1, name2, ...) or
% runxunit({name1, name2, ...}, ...)
%
% runxunit(..., '-verbose') displays the name and result, result, and time
% taken for each test case to the Command Window.
% runxunit(..., '-verbose') displays the name, result, and time taken for
% each test case to the Command Window.
%
% runxunit(..., '-logfile', filename) directs the output of runxunit to
% the specified log file instead of to the Command Window.
% the specified log.
%
% runxunit(..., '-xmlfile', filename) directs the output of runxunit to
% the specified xUnit-formatted XML log file instead of to the Command
% Window. This format is compatible with JUnit, and can be read by many
% tools.
% the specified xUnit-formatted XML log file. This format is compatible
% with JUnit, and can be read by many tools.
%
% You can also pass an absolute directory path instead of a file path to
% '-xmlfile'. If you do this, an xml file will be created for each suite,
% which Jenkins will be able to present more helpfully.
%
% runxunit(..., '-suppress') suppresses output of runxunit to the Command
% Window.
%
% out = runxunit(...) returns a logical value that is true if all the
% tests passed.
Expand Down Expand Up @@ -85,11 +91,12 @@

verbose = false;
logfile = '';
isxml = false;
xmlfile = '';
suppress = false;
if nargin < 1
suite = TestSuite.fromPwd();
else
[name_list, verbose, logfile, isxml] = getInputNames(varargin{:});
[name_list, verbose, logfile, xmlfile, suppress] = getInputNames(varargin{:});
if numel(name_list) == 0
suite = TestSuite.fromPwd();
elseif numel(name_list) == 1
Expand All @@ -106,17 +113,33 @@
error('xunit:runxunit:noTestCasesFound', 'No test cases found.');
end

if ~ isxml
if isempty(logfile)
logfile_handle = 1; % File handle corresponding to Command Window
if(suppress && isempty(logfile) && isempty(xmlfile))
error('xunit:runtests:noOutputFound', 'You should specify at least one way to get your test results.');
end

loggers = {};

if ~suppress % Display output to command line
if verbose
loggers{end+1} = {VerboseTestRunDisplay(1)};
else
logfile_handle = fopen(logfile, 'w');
if logfile_handle < 0
loggers{end+1} = {TestRunDisplay(1)};
end
end

if ~isempty(logfile) % Log output to a log file.
logfile_handle = fopen(logfile, 'w');
if logfile_handle < 0
error('xunit:runxunit:FileOpenFailed', ...
'Could not open "%s" for writing.', logfile);
else
cleanup = onCleanup(@() fclose(logfile_handle));
end
'Could not open "%s" for writing.', logfile);
else
cleanup = onCleanup(@() fclose(logfile_handle));
end

if verbose
loggers{end+1} = {VerboseTestRunDisplay(logfile_handle)};
else
loggers{end+1} = {TestRunDisplay(logfile_handle)};
end

fprintf(logfile_handle, 'Test suite: %s\n', suite.Name);
Expand All @@ -126,24 +149,23 @@
fprintf(logfile_handle, '%s\n\n', datestr(now));
end

if isxml
monitor = XMLTestRunLogger(logfile);
elseif verbose
monitor = VerboseTestRunDisplay(logfile_handle);
else
monitor = TestRunDisplay(logfile_handle);
if ~isempty(xmlfile) % Create an xml file.
loggers{end+1} = {XMLTestRunLogger(xmlfile)};
end

monitor = MetaTestRunLogger(loggers);
did_pass = suite.run(monitor);

if nargout > 0
out = did_pass;
end

function [name_list, verbose, logfile, isxml] = getInputNames(varargin)
function [name_list, verbose, logfile, xmlfile, suppress] = getInputNames(varargin)
name_list = {};
verbose = false;
logfile = '';
isxml = false;
xmlfile = '';
suppress = false;
k = 1;
while k <= numel(varargin)
arg = varargin{k};
Expand All @@ -152,6 +174,8 @@
elseif ~isempty(arg) && (arg(1) == '-')
if strcmp(arg, '-verbose')
verbose = true;
elseif strcmp(arg, '-suppress')
suppress = true;
elseif strcmp(arg, '-logfile')
if k == numel(varargin)
error('xunit:runxunit:MissingLogfile', ...
Expand All @@ -165,8 +189,7 @@
error('xunit:runxunit:MissingXMLfile', ...
'The option -xmlfile must be followed by a filename.');
else
isxml = true;
logfile = varargin{k+1};
xmlfile = varargin{k+1};
k = k + 1;
end
else
Expand Down