From bac65b26a1f0fc52053bb4824c937b192914a61b Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Tue, 13 Mar 2012 17:32:10 +0000 Subject: [PATCH] Added machinery for unit testing of the Matlab toolbox Unit tests are written using the Matlab xUnit Test Framework --- .gitignore | 3 + SConstruct | 4 + ext/matlab_xunit/+xunit/+utils/Contents.m | 24 ++ .../+xunit/+utils/arrayToString.m | 96 +++++ .../+xunit/+utils/compareFloats.m | 128 +++++++ .../+xunit/+utils/comparisonMessage.m | 33 ++ .../+xunit/+utils/containsRegexp.m | 17 + ext/matlab_xunit/+xunit/+utils/generateDoc.m | 14 + .../+xunit/+utils/isAlmostEqual.m | 46 +++ .../+xunit/+utils/isSetUpString.m | 12 + .../+xunit/+utils/isTearDownString.m | 12 + .../+xunit/+utils/isTestCaseSubclass.m | 39 ++ ext/matlab_xunit/+xunit/+utils/isTestString.m | 15 + .../+xunit/+utils/parseFloatAssertInputs.m | 56 +++ .../+xunit/+utils/stringToCellArray.m | 14 + .../CommandWindowTestRunDisplay.m | 30 ++ ext/matlab_xunit/Contents.m | 33 ++ ext/matlab_xunit/FunctionHandleTestCase.m | 154 ++++++++ ext/matlab_xunit/README.txt | 3 + ext/matlab_xunit/TestCase.m | 103 ++++++ ext/matlab_xunit/TestCaseInDir.m | 30 ++ ext/matlab_xunit/TestCaseWithAddPath.m | 53 +++ ext/matlab_xunit/TestComponent.m | 59 +++ ext/matlab_xunit/TestComponentInDir.m | 58 +++ ext/matlab_xunit/TestRunDisplay.m | 258 +++++++++++++ ext/matlab_xunit/TestRunLogger.m | 104 ++++++ ext/matlab_xunit/TestRunMonitor.m | 35 ++ ext/matlab_xunit/TestSuite.m | 342 ++++++++++++++++++ ext/matlab_xunit/TestSuiteInDir.m | 50 +++ ext/matlab_xunit/VerboseTestRunDisplay.m | 112 ++++++ ext/matlab_xunit/assertElementsAlmostEqual.m | 61 ++++ ext/matlab_xunit/assertEqual.m | 43 +++ ext/matlab_xunit/assertExceptionThrown.m | 53 +++ ext/matlab_xunit/assertFalse.m | 31 ++ ext/matlab_xunit/assertFilesEqual.m | 68 ++++ ext/matlab_xunit/assertTrue.m | 33 ++ ext/matlab_xunit/assertVectorsAlmostEqual.m | 59 +++ ext/matlab_xunit/info.xml | 28 ++ ext/matlab_xunit/initTestSuite.m | 58 +++ ext/matlab_xunit/license.txt | 27 ++ ext/matlab_xunit/runtests.m | 160 ++++++++ interfaces/matlab/testpath.m | 12 +- src/matlab/SConscript | 2 + test/SConscript | 81 ++++- test/matlab/runCanteraTests.m | 16 + test/matlab/testImport.m | 12 + 46 files changed, 2661 insertions(+), 20 deletions(-) create mode 100644 ext/matlab_xunit/+xunit/+utils/Contents.m create mode 100644 ext/matlab_xunit/+xunit/+utils/arrayToString.m create mode 100644 ext/matlab_xunit/+xunit/+utils/compareFloats.m create mode 100644 ext/matlab_xunit/+xunit/+utils/comparisonMessage.m create mode 100644 ext/matlab_xunit/+xunit/+utils/containsRegexp.m create mode 100644 ext/matlab_xunit/+xunit/+utils/generateDoc.m create mode 100644 ext/matlab_xunit/+xunit/+utils/isAlmostEqual.m create mode 100644 ext/matlab_xunit/+xunit/+utils/isSetUpString.m create mode 100644 ext/matlab_xunit/+xunit/+utils/isTearDownString.m create mode 100644 ext/matlab_xunit/+xunit/+utils/isTestCaseSubclass.m create mode 100644 ext/matlab_xunit/+xunit/+utils/isTestString.m create mode 100644 ext/matlab_xunit/+xunit/+utils/parseFloatAssertInputs.m create mode 100644 ext/matlab_xunit/+xunit/+utils/stringToCellArray.m create mode 100644 ext/matlab_xunit/CommandWindowTestRunDisplay.m create mode 100644 ext/matlab_xunit/Contents.m create mode 100644 ext/matlab_xunit/FunctionHandleTestCase.m create mode 100644 ext/matlab_xunit/README.txt create mode 100644 ext/matlab_xunit/TestCase.m create mode 100644 ext/matlab_xunit/TestCaseInDir.m create mode 100644 ext/matlab_xunit/TestCaseWithAddPath.m create mode 100644 ext/matlab_xunit/TestComponent.m create mode 100644 ext/matlab_xunit/TestComponentInDir.m create mode 100644 ext/matlab_xunit/TestRunDisplay.m create mode 100644 ext/matlab_xunit/TestRunLogger.m create mode 100644 ext/matlab_xunit/TestRunMonitor.m create mode 100644 ext/matlab_xunit/TestSuite.m create mode 100644 ext/matlab_xunit/TestSuiteInDir.m create mode 100644 ext/matlab_xunit/VerboseTestRunDisplay.m create mode 100644 ext/matlab_xunit/assertElementsAlmostEqual.m create mode 100644 ext/matlab_xunit/assertEqual.m create mode 100644 ext/matlab_xunit/assertExceptionThrown.m create mode 100644 ext/matlab_xunit/assertFalse.m create mode 100644 ext/matlab_xunit/assertFilesEqual.m create mode 100644 ext/matlab_xunit/assertTrue.m create mode 100644 ext/matlab_xunit/assertVectorsAlmostEqual.m create mode 100644 ext/matlab_xunit/info.xml create mode 100644 ext/matlab_xunit/initTestSuite.m create mode 100644 ext/matlab_xunit/license.txt create mode 100644 ext/matlab_xunit/runtests.m create mode 100644 test/matlab/runCanteraTests.m create mode 100644 test/matlab/testImport.m diff --git a/.gitignore b/.gitignore index 6f57cc9d8..99159a6ee 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,9 @@ *.obj *.exe.manifest build/ +test/work/ include/cantera/base/config.h +interfaces/matlab/ctpath.m stage/ .sconsign.dblite .sconf_temp @@ -23,6 +25,7 @@ config.log *.def *.msi *.mex* +*.asv .cproject .project .pydevproject diff --git a/SConstruct b/SConstruct index 8bd7dbeaa..03ad19efb 100644 --- a/SConstruct +++ b/SConstruct @@ -123,6 +123,10 @@ if os.name == 'nt' and 'TMP' in os.environ: if os.name == 'nt': env['ENV']['SystemRoot'] = os.environ['SystemRoot'] +# Needed for Matlab to source ~/.matlab7rc.sh +if 'HOME' in os.environ: + env['ENV']['HOME'] = os.environ['HOME'] + # Fix an issue with Unicode sneaking into the environment on Windows if os.name == 'nt': for key,val in env['ENV'].iteritems(): diff --git a/ext/matlab_xunit/+xunit/+utils/Contents.m b/ext/matlab_xunit/+xunit/+utils/Contents.m new file mode 100644 index 000000000..7b0f01703 --- /dev/null +++ b/ext/matlab_xunit/+xunit/+utils/Contents.m @@ -0,0 +1,24 @@ +% UTILS Utility package for MATLAB xUnit Test Framework +% +% Array Comparison +% compareFloats - Compare floating-point arrays using tolerance +% +% Test Case Discovery Functions +% isTestCaseSubclass - True for name of TestCase subclass +% +% String Functions +% arrayToString - Convert array to string for display +% comparisonMessage - Assertion message string for comparing two arrays +% containsRegexp - True if string contains regular expression +% isSetUpString - True for string that looks like a setup function +% isTearDownString - True for string that looks like teardown function +% isTestString - True for string that looks like a test function +% stringToCellArray - Convert string to cell array of strings +% +% Miscellaneous Functions +% generateDoc - Publish test scripts in mtest/doc +% parseFloatAssertInputs - Common input-parsing logic for several functions + +% Steven L. Eddins +% Copyright 2008-2009 The MathWorks, Inc. + diff --git a/ext/matlab_xunit/+xunit/+utils/arrayToString.m b/ext/matlab_xunit/+xunit/+utils/arrayToString.m new file mode 100644 index 000000000..87bad514c --- /dev/null +++ b/ext/matlab_xunit/+xunit/+utils/arrayToString.m @@ -0,0 +1,96 @@ +function s = arrayToString(A) +%arrayToString Convert array to string for display. +% S = arrayToString(A) converts the array A into a string suitable for +% including in assertion messages. Small arrays are converted using disp(A). +% Large arrays are displayed similar to the way structure field values display +% using disp. + +% Steven L. Eddins +% Copyright 2009 The MathWorks, Inc. + +if isTooBigToDisp(A) + s = dispAsStructField(A); +else + s = dispAsArray(A); +end + +%=============================================================================== +function tf = isTooBigToDisp(A) +% Use a heuristic to determine if the array is to convert to a string using +% disp. The heuristic is based on the size of the array in bytes, as reported +% by the whos function. + +whos_output = whos('A'); +byte_threshold = 1000; +tf = whos_output.bytes > byte_threshold; + +%=============================================================================== +function s = dispAsArray(A) +% Convert A to a string using disp. Remove leading and trailing blank lines. + +s = evalc('disp(A)'); +if isempty(s) + % disp displays nothing for some kinds of empty arrays. + s = dispAsStructField(A); +else + s = postprocessDisp(s); +end + +%=============================================================================== +function s = dispAsStructField(A) +% Convert A to a string using structure field display. + +b.A = A; +s = evalc('disp(b)'); +s = postprocessStructDisp(s); + +%=============================================================================== +function out = postprocessDisp(in) +% Remove leading and trailing blank lines from input string. Don't include a +% newline at the end. + +lines = xunit.utils.stringToCellArray(in); + +% Remove leading blank lines. +lines = removeLeadingBlankLines(lines); + +% Remove trailing blank lines. +while ~isempty(lines) && isBlankLine(lines{end}) + lines(end) = []; +end + +% Convert cell of strings to single string with newlines. Don't put a newline +% at the end. +out = sprintf('%s\n', lines{1:end-1}); +out = [out, lines{end}]; + +%=============================================================================== +function out = postprocessStructDisp(in) +% Return the portion of the display string to the right of the colon in the +% output of the first structure field. Input is a string. + +lines = xunit.utils.stringToCellArray(in); + +% Remove leading blank lines +lines = removeLeadingBlankLines(lines); + +line = lines{1}; +idx = find(line == ':'); +out = line((idx+2):end); % struct fields display with blank space following colon + +%=============================================================================== +function out = removeLeadingBlankLines(in) +% Input and output are cell arrays of strings. + +out = in; +while ~isempty(out) && isBlankLine(out{1}) + out(1) = []; +end + +%=============================================================================== +function tf = isBlankLine(line) +% Input is a string. + +tf = all(isspace(line)); + + diff --git a/ext/matlab_xunit/+xunit/+utils/compareFloats.m b/ext/matlab_xunit/+xunit/+utils/compareFloats.m new file mode 100644 index 000000000..f4ec111e2 --- /dev/null +++ b/ext/matlab_xunit/+xunit/+utils/compareFloats.m @@ -0,0 +1,128 @@ +function result = compareFloats(varargin) +%compareFloats Compare floating-point arrays using tolerance. +% result = compareFloats(A, B, compare_type, tol_type, tol, floor_tol) +% compares the floating-point arrays A and B using a tolerance. compare_type +% is either 'elementwise' or 'vector'. tol_type is either 'relative' or +% 'absolute'. tol and floor_tol are the scalar tolerance values. +% +% There are four different tolerance tests used, depending on the comparison +% type and the tolerance type: +% +% 1. Comparison type: 'elementwise' Tolerance type: 'relative' +% +% all( abs(A(:) - B(:)) <= tol * max(abs(A(:)), abs(B(:))) + floor_tol ) +% +% 2. Comparison type: 'elementwise' Tolerance type: 'absolute' +% +% all( abs(A(:) - B(:) <= tol ) +% +% 3. Comparison type: 'vector' Tolerance type: 'relative' +% +% norm(A(:) - B(:) <= tol * max(norm(A(:)), norm(B(:))) + floor_tol +% +% 4. Comparison type: 'vector' Tolerance type: 'absolute' +% +% norm(A(:) - B(:)) <= tol +% +% Note that floor_tol is not used when the tolerance type is 'absolute'. +% +% compare_type, tol_type, tol, and floor_tol are all optional inputs. The +% default value for compare_type is 'elementwise'. The default value for +% tol_type is 'relative'. If both A and B are double, then the default value +% for tol is sqrt(eps), and the default value for floor_tol is eps. If either +% A or B is single, then the default value for tol is sqrt(eps('single')), and +% the default value for floor_tol is eps('single'). +% +% If A or B is complex, then the tolerance test is applied independently to +% the real and imaginary parts. +% +% For elementwise comparisons, compareFloats returns true for two elements +% that are both NaN, or for two infinite elements that have the same sign. +% For vector comparisons, compareFloats returns false if any input elements +% are infinite or NaN. + +% Steven L. Eddins +% Copyright 2008-2009 The MathWorks, Inc. + +if nargin >= 3 + % compare_type specified. Grab it and then use parseFloatAssertInputs to + % process the remaining input arguments. + compare_type = varargin{3}; + varargin(3) = []; + if isempty(strcmp(compare_type, {'elementwise', 'vector'})) + error('compareFloats:unrecognizedCompareType', ... + 'COMPARE_TYPE must be ''elementwise'' or ''vector''.'); + end +else + compare_type = 'elementwise'; +end + +params = xunit.utils.parseFloatAssertInputs(varargin{:}); + +A = params.A(:); +B = params.B(:); + +switch compare_type + case 'elementwise' + magFcn = @abs; + + case 'vector' + magFcn = @norm; + + otherwise + error('compareFloats:unrecognizedCompareType', ... + 'COMPARE_TYPE must be ''elementwise'' or ''vector''.'); +end + +switch params.ToleranceType + case 'relative' + coreCompareFcn = @(A, B) magFcn(A - B) <= ... + params.Tolerance * max(magFcn(A), magFcn(B)) + ... + params.FloorTolerance; + + case 'absolute' + coreCompareFcn = @(A, B) magFcn(A - B) <= params.Tolerance; + + otherwise + error('compareFloats:unrecognizedToleranceType', ... + 'TOL_TYPE must be ''relative'' or ''absolute''.'); +end + +if strcmp(compare_type, 'elementwise') + compareFcn = @(A, B) ( coreCompareFcn(A, B) | bothNaN(A, B) | sameSignInfs(A, B) ) & ... + ~oppositeSignInfs(A, B) & ... + ~finiteAndInfinite(A, B); +else + compareFcn = @(A, B) coreCompareFcn(A, B) & ... + isfinite(magFcn(A)) & ... + isfinite(magFcn(B)); +end + +if isreal(A) && isreal(B) + result = compareFcn(A, B); +else + result = compareFcn(real(A), real(B)) & compareFcn(imag(A), imag(B)); +end + +result = all(result); + +%=============================================================================== +function out = bothNaN(A, B) + +out = isnan(A) & isnan(B); + +%=============================================================================== +function out = oppositeSignInfs(A, B) + +out = isinf(A) & isinf(B) & (sign(A) ~= sign(B)); + +%=============================================================================== +function out = sameSignInfs(A, B) + +out = isinf(A) & isinf(B) & (sign(A) == sign(B)); + +%=============================================================================== +function out = finiteAndInfinite(A, B) + +out = xor(isinf(A), isinf(B)); + diff --git a/ext/matlab_xunit/+xunit/+utils/comparisonMessage.m b/ext/matlab_xunit/+xunit/+utils/comparisonMessage.m new file mode 100644 index 000000000..68e9a5213 --- /dev/null +++ b/ext/matlab_xunit/+xunit/+utils/comparisonMessage.m @@ -0,0 +1,33 @@ +function msg = comparisonMessage(user_message, assertion_message, A, B) +%comparisonMessage Generate assertion message when comparing two arrays. +% msg = comparisonMessage(user_message, assertion_message, A, B) returns a +% string appropriate to use in a call to throw inside an assertion function +% that compares two arrays A and B. +% +% The string returned has the following form: +% +% +% +% +% First input: +% +% +% Second input: +% +% +% user_message can be the empty string, '', in which case user_message is +% skipped. + +% Steven L. Eddins +% Copyright 2009 The MathWorks, Inc. + +msg = sprintf('%s\n\n%s\n%s\n\n%s\n%s', ... + assertion_message, ... + 'First input:', ... + xunit.utils.arrayToString(A), ... + 'Second input:', ... + xunit.utils.arrayToString(B)); + +if ~isempty(user_message) + msg = sprintf('%s\n%s', user_message, msg); +end diff --git a/ext/matlab_xunit/+xunit/+utils/containsRegexp.m b/ext/matlab_xunit/+xunit/+utils/containsRegexp.m new file mode 100644 index 000000000..efbc51bcb --- /dev/null +++ b/ext/matlab_xunit/+xunit/+utils/containsRegexp.m @@ -0,0 +1,17 @@ +function tf = containsRegexp(str, exp) +%containsRegexp True if string contains regular expression +% TF = containsRegexp(str, exp) returns true if the string str contains the +% regular expression exp. If str is a cell array of strings, then +% containsRegexp tests each string in the cell array, returning the results in +% a logical array with the same size as str. + +% Steven L. Eddins +% Copyright 2008-2009 The MathWorks, Inc. + +% Convert to canonical input form: A cell array of strings. +if ~iscell(str) + str = {str}; +end + +matches = regexp(str, exp); +tf = ~cellfun('isempty', matches); diff --git a/ext/matlab_xunit/+xunit/+utils/generateDoc.m b/ext/matlab_xunit/+xunit/+utils/generateDoc.m new file mode 100644 index 000000000..6a9959aef --- /dev/null +++ b/ext/matlab_xunit/+xunit/+utils/generateDoc.m @@ -0,0 +1,14 @@ +function generateDoc +%generateDoc Publish the example scripts in the doc directory + +% Steven L. Eddins +% Copyright 2008-2009 The MathWorks, Inc. + +doc_dir = fullfile(fileparts(which('runtests')), '..', 'doc'); +addpath(doc_dir); +cd(doc_dir) +mfiles = dir('*.m'); +for k = 1:numel(mfiles) + publish(mfiles(k).name); + cd(doc_dir) +end diff --git a/ext/matlab_xunit/+xunit/+utils/isAlmostEqual.m b/ext/matlab_xunit/+xunit/+utils/isAlmostEqual.m new file mode 100644 index 000000000..7ad43933d --- /dev/null +++ b/ext/matlab_xunit/+xunit/+utils/isAlmostEqual.m @@ -0,0 +1,46 @@ +function same = isAlmostEqual(A, B, reltol) +%isAlmostEqual Equality test using relative tolerance +% same = isAlmostEqual(A, B, reltol), for two floating-point arrays A and B, +% tests A and B for equality using the specified relative tolerance. +% isAlmostEqual returns true if the following relationship is satisfied for +% all values in A and B: +% +% abs(A - B) ./ max(abs(A), abs(B)) <= reltol +% +% same = isAlmostEqual(A, B) uses the following value for the relative +% tolerance: +% +% 100 * max(eps(class(A)), eps(class(B))) +% +% If either A or B is not a floating-point array, then isAlmostEqual returns +% the result of isequal(A, B). + +% Steven L. Eddins +% Copyright 2008-2009 The MathWorks, Inc. + +if ~isfloat(A) || ~isfloat(B) + same = isequal(A, B); + return +end + +if nargin < 3 + reltol = 100 * max(eps(class(A)), eps(class(B))); +end + +if ~isequal(size(A), size(B)) + same = false; + return +end + +A = A(:); +B = B(:); + +delta = abs(A - B) ./ max(max(abs(A), abs(B)), 1); + +% Some floating-point values require special handling. +delta((A == 0) & (B == 0)) = 0; +delta(isnan(A) & isnan(B)) = 0; +delta((A == Inf) & (B == Inf)) = 0; +delta((A == -Inf) & (B == -Inf)) = 0; + +same = all(delta <= reltol); diff --git a/ext/matlab_xunit/+xunit/+utils/isSetUpString.m b/ext/matlab_xunit/+xunit/+utils/isSetUpString.m new file mode 100644 index 000000000..e3204fbd9 --- /dev/null +++ b/ext/matlab_xunit/+xunit/+utils/isSetUpString.m @@ -0,0 +1,12 @@ +function tf = isSetUpString(str) +%isSetUpString True if string looks like the name of a setup function +% tf = isSetUpString(str) returns true if the string str looks like the name +% of a setup function. If str is a cell array of strings, then isSetUpString +% tests each string in the cell array, returning the results in a logical +% array with the same size as str. + +% Steven L. Eddins +% Copyright 2008-2009 The MathWorks, Inc. + +setup_exp = '^[sS]et[uU]p'; +tf = xunit.utils.containsRegexp(str, setup_exp); diff --git a/ext/matlab_xunit/+xunit/+utils/isTearDownString.m b/ext/matlab_xunit/+xunit/+utils/isTearDownString.m new file mode 100644 index 000000000..6077a81f9 --- /dev/null +++ b/ext/matlab_xunit/+xunit/+utils/isTearDownString.m @@ -0,0 +1,12 @@ +function tf = isTearDownString(str) +%isTearDownString True if string looks like the name of a teardown function +% tf = isTearDownString(str) returns true if the string str looks like the +% name of a teardown function. If str is a cell array of strings, then +% isTearDownString tests each string in the cell array, returning the results +% in a logical array with the same size as str. + +% Steven L. Eddins +% Copyright 2008-2009 The MathWorks, Inc. + +setup_exp = '^[tT]ear[dD]own'; +tf = xunit.utils.containsRegexp(str, setup_exp); diff --git a/ext/matlab_xunit/+xunit/+utils/isTestCaseSubclass.m b/ext/matlab_xunit/+xunit/+utils/isTestCaseSubclass.m new file mode 100644 index 000000000..4df0c7fd4 --- /dev/null +++ b/ext/matlab_xunit/+xunit/+utils/isTestCaseSubclass.m @@ -0,0 +1,39 @@ +function tf = isTestCaseSubclass(name) +%isTestCaseSubclass True for name of a TestCase subclass +% tf = isTestCaseSubclass(name) returns true if the string name is the name of +% a TestCase subclass on the MATLAB path. + +% Steven L. Eddins +% Copyright 2008-2009 The MathWorks, Inc. + +tf = false; + +class_meta = meta.class.fromName(name); +if isempty(class_meta) + % Not the name of a class + return; +end + +if strcmp(class_meta.Name, 'TestCase') + tf = true; +else + tf = isMetaTestCaseSubclass(class_meta); +end + +function tf = isMetaTestCaseSubclass(class_meta) + +tf = false; + +if strcmp(class_meta.Name, 'TestCase') + tf = true; +else + % Invoke function recursively on parent classes. + super_classes = class_meta.SuperClasses; + for k = 1:numel(super_classes) + if isMetaTestCaseSubclass(super_classes{k}) + tf = true; + break; + end + end +end + diff --git a/ext/matlab_xunit/+xunit/+utils/isTestString.m b/ext/matlab_xunit/+xunit/+utils/isTestString.m new file mode 100644 index 000000000..e7053d4af --- /dev/null +++ b/ext/matlab_xunit/+xunit/+utils/isTestString.m @@ -0,0 +1,15 @@ +function tf = isTestString(str) +%isTestString True if string looks like the name of a test +% tf = isTestString(str) returns true if the string str looks like the name of +% a test. If str is a cell array of strings, then isTestString tests each +% string in the cell array, returning the results in a logical array with the +% same size as str. + +% Steven L. Eddins +% Copyright 2008-2009 The MathWorks, Inc. + +test_at_beginning = '^[tT]est'; +test_at_end = '[tT]est$'; + +tf = xunit.utils.containsRegexp(str, test_at_beginning) | ... + xunit.utils.containsRegexp(str, test_at_end); diff --git a/ext/matlab_xunit/+xunit/+utils/parseFloatAssertInputs.m b/ext/matlab_xunit/+xunit/+utils/parseFloatAssertInputs.m new file mode 100644 index 000000000..b36366e90 --- /dev/null +++ b/ext/matlab_xunit/+xunit/+utils/parseFloatAssertInputs.m @@ -0,0 +1,56 @@ +function params = parseFloatAssertInputs(varargin) +%parseFloatAssertInputs Parse inputs for floating-point assertion functions. +% params = parseFloatAssertInputs(varargin) parses the input arguments for +% assertElementsAlmostEqual, assertVectorsAlmostEqual, and compareFcn. It +% returns a parameter struct containing the fields: +% +% A B Message ToleranceType Tolerance FloorTolerance + +% Steven L. Eddins +% Copyright 2008-2009 The MathWorks, Inc. + +error(nargchk(2, 6, nargin, 'struct')); + +params = struct('A', {[]}, 'B', {[]}, 'ToleranceType', {[]}, ... + 'Tolerance', {[]}, 'FloorTolerance', {[]}, 'Message', {''}); + +% The first two input arguments are always A and B. +params.A = varargin{1}; +params.B = varargin{2}; +varargin(1:2) = []; + +% If the last argument is a message string, process it and remove it from the list. +if (numel(varargin) >= 1) && ischar(varargin{end}) && ... + ~any(strcmp(varargin{end}, {'relative', 'absolute'})) + params.Message = varargin{end}; + varargin(end) = []; +else + params.Message = ''; +end + +try + epsilon = max(eps(class(params.A)), eps(class(params.B))); +catch + epsilon = eps; +end + +if numel(varargin) < 3 + % floor_tol not specified; set default. + params.FloorTolerance = sqrt(epsilon); +else + params.FloorTolerance = varargin{3}; +end + +if numel(varargin) < 2 + % tol not specified; set default. + params.Tolerance = sqrt(epsilon); +else + params.Tolerance = varargin{2}; +end + +if numel(varargin) < 1 + % tol_type not specified; set default. + params.ToleranceType = 'relative'; +else + params.ToleranceType = varargin{1}; +end diff --git a/ext/matlab_xunit/+xunit/+utils/stringToCellArray.m b/ext/matlab_xunit/+xunit/+utils/stringToCellArray.m new file mode 100644 index 000000000..e2025aa4d --- /dev/null +++ b/ext/matlab_xunit/+xunit/+utils/stringToCellArray.m @@ -0,0 +1,14 @@ +function c = stringToCellArray(s) +%stringToCellArray Convert string with newlines to cell array of strings. +% C = stringToCellArray(S) converts the input string S to a cell array of +% strings, breaking up S at new lines. + +% Steven L. Eddins +% Copyright 2009 The MathWorks, Inc. + +if isempty(s) + c = cell(0, 1); +else + c = textscan(s, '%s', 'Delimiter', '\n', 'Whitespace', ''); + c = c{1}; +end diff --git a/ext/matlab_xunit/CommandWindowTestRunDisplay.m b/ext/matlab_xunit/CommandWindowTestRunDisplay.m new file mode 100644 index 000000000..01676317e --- /dev/null +++ b/ext/matlab_xunit/CommandWindowTestRunDisplay.m @@ -0,0 +1,30 @@ +classdef CommandWindowTestRunDisplay < TestRunDisplay + %CommandWindowTestRunDisplay Print test suite execution results to Command Window. + % CommandWindowTestRunDisplay is a subclass of TestRunMonitor. If a + % CommandWindowTestRunDisplay object is passed to the run method of a + % TestComponent, such as a TestSuite or a TestCase, it will print information + % to the Command Window as the test run proceeds. + % + % CommandWindowTestRunDisplay methods: + % testComponentStarted - Update Command Window display + % testComponentFinished - Update Command Window display + % testCaseFailure - Log test failure information + % testCaseError - Log test error information + % + % CommandWindowTestRunDisplay properties: + % TestCaseCount - Number of test cases executed + % Faults - Struct array of test fault info + % + % See also TestRunLogger, TestRunMonitor, TestSuite + + % Steven L. Eddins + % Copyright 2008-2010 The MathWorks, Inc. + + methods + function self = CommandWindowTestRunDisplay + self = self@TestRunDisplay(1); + end + end + +end + diff --git a/ext/matlab_xunit/Contents.m b/ext/matlab_xunit/Contents.m new file mode 100644 index 000000000..79b2579eb --- /dev/null +++ b/ext/matlab_xunit/Contents.m @@ -0,0 +1,33 @@ +% MATLAB xUnit Test Framework +% Version 3.1 (R2010b) 19-Nov-2010 +% +% Running Unit Tests +% runtests - Run unit tests +% +% Writing Unit Tests +% assertElementsAlmostEqual - Assert floating-point array elements almost equal +% assertEqual - Assert that inputs are equal +% assertFilesEqual - Assert that two files have the same content +% assertExceptionThrown - Assert that specified exception is thrown +% assertFalse - Assert that input condition is false +% assertTrue - Assert that input condition is true +% assertVectorsAlmostEqual - Assert floating-point vectors almost equal in norm sense +% initTestSuite - Utility script used for subfunction-based tests +% +% Framework Classes +% CommandWindowTestRunDisplay - Print test suite results to command window +% FunctionHandleTestCase - Test case based on a function handle +% TestCase - Class defining interface for test cases +% TestCaseInDir - Test case requiring temporary directory change +% TestCaseWithAddPath - Test case requiring temporary path modification +% TestComponent - Abstract base class for TestCase and TestSuite +% TestComponentInDir - Test component requiring temporary directory change +% TestLogger - Collect data (silently) from running test suite +% TestRunDisplay - Print test suite execution results +% TestRunMonitor - Abstract base class for monitoring test suite +% TestSuite - Collection of TestComponent objects +% TestSuiteInDir - Test suite requiring temporary directory change +% %VerboseTestRunDisplay - Print test suite execution results + +% Steven L. Eddins +% Copyright 2008-2010 The MathWorks, Inc. \ No newline at end of file diff --git a/ext/matlab_xunit/FunctionHandleTestCase.m b/ext/matlab_xunit/FunctionHandleTestCase.m new file mode 100644 index 000000000..7f8028c21 --- /dev/null +++ b/ext/matlab_xunit/FunctionHandleTestCase.m @@ -0,0 +1,154 @@ +classdef FunctionHandleTestCase < TestCase +%FunctionHandleTestCase Test case based on a function handle +% FunctionHandleTestCase is a TestCase subclass. It defines a test case object +% that executes by running a function handle instead of by running a method of +% the TestCase subclass. +% +% FunctionHandleTestCase methods: +% FunctionHandleTestCase - Constructor +% runTestCase - Run function handle test +% setUp - Run test-fixture setup function +% tearDown - Run test-fixture teardown function +% +% FunctionHandleTestCase properties: +% TestFcn - Function handle of test function +% SetupFcn - Function handle of setup function +% TeardownFcn - Function handle of teardown function +% TestData - Data needed by test function or teardown function +% +% See also TestCase, TestSuite + +% Steven L. Eddins +% Copyright 2008-2009 The MathWorks, Inc. + + properties (SetAccess = protected, GetAccess = protected, Hidden = true) + %TestFcn - Function handle of test function + % If SetupFcn has one or more output arguments, then TestFcn is + % called with this syntax: + % + % TestFcn(data) + % + % where data is the return value from SetupFcn. Otherwise, TestFcn is + % called with no input and no output arguments. + TestFcn; + + %SetupFcn - Function handle of setup function + % If SetupFcn has one or more output arguments, then SetupFcn is + % called with this syntax: + % + % data = SetupFcn() + % + % and data will be saved in the TestData property. Otherwise, SetupFcn + % is called with no input and no output arguments. + SetupFcn; + + %TeardownFcn - Function handle of teardown function + % If SetupFcn has one or more output arguments, then TeardownFcn is + % called with this syntax: + % + % TeardownFcn(data) + % + % were data is the return value from SetupFcn. Otherwise, TeardownFcn + % is called with no input and no output arguments. + TeardownFcn; + + %TestData - Data needed by test function or teardown function. + TestData; + end + + methods + function self = FunctionHandleTestCase(testFcn, setupFcn, teardownFcn) + %FunctionHandleTestCase Constructor + % FunctionHandleTestCase(testFcn, setupFcn, teardownFcn) creates a + % TestCase object that executes by running the function handle + % TestFcn. setupFcn is a function handle that will be executed + % before testFcn, and teardownFcn is a function handle that will + % be executed after TestFcn. Either setupFcn or teardownFcn can + % be empty. + % + % If setupFcn is function handle that has one output argument, + % then the three test functions will be called using these + % syntaxes: + % + % testData = setupFcn(); + % testFcn(testData); + % teardownFcn(testData); + % + % Otherwise, the three test functions are all called with no input + % arguments: + % + % setupFcn(); + % TestFcn(); + % teardownFcn(); + + % Call the base class constructor. Give it the name of the + % FunctionHandleTestCase method that executes TestFcn. + self = self@TestCase('runTestCase'); + + self.TestFcn = testFcn; + self.SetupFcn = setupFcn; + self.TeardownFcn = teardownFcn; + + % Determine the name and M-file location of the function handle. + functionHandleInfo = functions(testFcn); + self.Name = functionHandleInfo.function; + if strcmp(functionHandleInfo.type, 'anonymous') + % Anonymous function handles don't have an M-file location. + self.Location = ''; + else + self.Location = functionHandleInfo.file; + end + end + + function runTestCase(self) + %runTestCase Run function handle test + % test_case.run() calls the test function handle. If a nonempty + % SetupFcn was provided and it has at least one output argument, + % pass self.TestData to the test function. Otherwise, call the + % test function with no input arguments. + if ~isempty(self.SetupFcn) && nargout(self.SetupFcn) > 0 + self.TestFcn(self.TestData); + else + self.TestFcn(); + end + end + + function setUp(self) + %setUp Run test-fixture setup function + % If a nonempty SetupFcn was provided, run it. If the SetupFcn + % has at least one output argument, capture the first output + % argument in instance data (TestData). + if ~isempty(self.SetupFcn) + if nargout(self.SetupFcn) > 0 + if nargout(self.SetupFcn) > 1 + message = sprintf(['A test fixture setup function returns more than one output argument. ', ... + 'The test harness only calls the setup function with one output argument. ', ... + 'Return a struct or a cell array from your setup function if you need to bundle several parts together.', ... + '\nTest name: %s\nTest location: %s'], ... + self.Name, self.Location); + warning('xunit:FunctionHandleTestCase:TooManySetupOutputs', ... + '%s', message); + end + self.TestData = self.SetupFcn(); + else + self.SetupFcn(); + end + end + end + + function tearDown(self) + %tearDown Run test-fixture teardown function + % If a nonempty TeardownFcn was provided, run it. If there is + % TestData (the output of the SetupFcn), then pass it to + % TeardownFcn. Otherwise, call TeardownFcn with no input + % arguments. + if ~isempty(self.TeardownFcn) + if ~isempty(self.SetupFcn) && (nargout(self.SetupFcn) > 0) + self.TeardownFcn(self.TestData); + else + self.TeardownFcn(); + end + end + end + end +end \ No newline at end of file diff --git a/ext/matlab_xunit/README.txt b/ext/matlab_xunit/README.txt new file mode 100644 index 000000000..7e9fcfb81 --- /dev/null +++ b/ext/matlab_xunit/README.txt @@ -0,0 +1,3 @@ +The full Matlab xUnit package (including documentation) is available from: + +http://www.mathworks.com/matlabcentral/fileexchange/22846-matlab-xunit-test-framework diff --git a/ext/matlab_xunit/TestCase.m b/ext/matlab_xunit/TestCase.m new file mode 100644 index 000000000..8b09ff875 --- /dev/null +++ b/ext/matlab_xunit/TestCase.m @@ -0,0 +1,103 @@ +%TestCase Class defining interface for test cases +% The TestCase class defines an individual test case. +% +% Normally a test writer will create their own test class that is a subclass +% of TestCase. Each instance of the TestCase subclass that gets created will +% be associated with a single test method. +% +% If a test fixture is needed, override the setUp() and tearDown() methods. +% +% TestSuite(subclass_name), where subclass_name is the name of a TestCase +% subclass, creates a test suite containing one TestCase instance per test +% method contained in the subclass. +% +% A simpler test-writing alternative to use subfunction-based M-file tests. +% See the MATLAB xUnit documentation. +% +% TestCase methods: +% TestCase - Constructor +% run - Execute the test case +% +% TestCase properties: +% Location - Location of M-file containing the test case +% Name - Name of test case +% +% See also TestComponent, TestSuite + +% Steven L. Eddins +% Copyright 2008-2010 The MathWorks, Inc. + +classdef TestCase < TestComponent + + properties + MethodName + end + + methods + function self = TestCase(testMethod) + %TestCase Constructor + % TestCase(methodName) constructs a TestCase object using the + % specified testMethod (a string). + + self.MethodName = testMethod; + self.Name = testMethod; + self.Location = which(class(self)); + end + + function did_pass = run(self, monitor) + %run Execute the test case + % test_case.run(monitor) calls the TestCase object's setUp() + % method, then the test method, then the tearDown() method. + % observer is a TestRunObserver object. The testStarted(), + % testFailure(), testError(), and testFinished() methods of + % observer are called at the appropriate times. monitor is a + % TestRunMonitor object. Typically it is either a TestRunLogger + % subclass or a CommandWindowTestRunDisplay subclass. + % + % test_case.run() automatically uses a + % CommandWindowTestRunDisplay object in order to print test + % suite execution information to the Command Window. + + if nargin < 2 + monitor = CommandWindowTestRunDisplay(); + end + + did_pass = true; + monitor.testComponentStarted(self); + + try + self.setUp(); + f = str2func(self.MethodName); + + try + % Call the test method. + f(self); + catch failureException + monitor.testCaseFailure(self, failureException); + did_pass = false; + end + + self.tearDown(); + + catch errorException + monitor.testCaseError(self, errorException); + did_pass = false; + end + + monitor.testComponentFinished(self, did_pass); + end + + function num = numTestCases(self) + num = 1; + end + + function print(self, numLeadingBlanks) + if nargin < 2 + numLeadingBlanks = 0; + end + fprintf('%s%s\n', blanks(numLeadingBlanks), self.Name); + end + + end + +end diff --git a/ext/matlab_xunit/TestCaseInDir.m b/ext/matlab_xunit/TestCaseInDir.m new file mode 100644 index 000000000..8c80f2ae6 --- /dev/null +++ b/ext/matlab_xunit/TestCaseInDir.m @@ -0,0 +1,30 @@ +%TestCaseInDir Test case requiring temporary directory change +% The TestCaseInDir class defines a test case that has to be run by first +% changing to a specified directory. +% +% The setUp method adds the starting directory to the path and then uses cd to +% change into the specified directory. The tearDown method restores the +% original path and directory. +% +% TestCaseInDir is used by MATLAB xUnit's own test suite in order to test itself. +% +% TestCaseInDir methods: +% TestCaseInDir - Constructor +% +% See also TestCase, TestCaseWithAddPath, TestComponent + +% Steven L. Eddins +% Copyright 2008-2009 The MathWorks, Inc. + +classdef TestCaseInDir < TestCase & TestComponentInDir + + methods + function self = TestCaseInDir(methodName, testDirectory) + %TestCaseInDir Constructor + % TestCaseInDir(testName, testDirectory) constructs a test case + % using the specified name and located in the specified directory. + self = self@TestCase(methodName); + self = self@TestComponentInDir(testDirectory); + end + end +end diff --git a/ext/matlab_xunit/TestCaseWithAddPath.m b/ext/matlab_xunit/TestCaseWithAddPath.m new file mode 100644 index 000000000..0f0e9f661 --- /dev/null +++ b/ext/matlab_xunit/TestCaseWithAddPath.m @@ -0,0 +1,53 @@ +%TestCaseInDir Test case requiring temporary path modification +% The TestCaseInDir class defines a test case that has to be run by first +% adding a specific directory to the path. +% +% The setUp method adds the directory to the path, and the tearDown method +% restores the original path. +% +% TestCaseWithAddPath is used by MATLAB xUnit's own test suite in order to test +% itself. +% +% TestCaseWithAddPath methods: +% TestCaseWithAddPath - Constructor +% setUp - Add test directory to MATLAB path +% tearDown - Restore original MATLAB path +% +% See also TestCase, TestCaseInDir + +% Steven L. Eddins +% Copyright 2008-2009 The MathWorks, Inc. + +classdef TestCaseWithAddPath < TestCase + properties (SetAccess = private, GetAccess = private) + %TestDirectory - Directory to be added to the path + TestDirectory + + %OriginalPath - Path prior to adding the test directory + OriginalPath + end + + methods + function self = TestCaseWithAddPath(methodName, testDirectory) + %TestCaseInDir Constructor + % TestCaseInDir(testName, testDirectory) constructs a test case + % using the specified name and located in the specified directory. + self = self@TestCase(methodName); + self.TestDirectory = testDirectory; + end + + function setUp(self) + %setUp Add test directory to MATLAB path. + % test_case.setUp() saves the current path in the OriginalPath + % property and then adds the TestDirectory to the MATLAB path. + self.OriginalPath = path; + addpath(self.TestDirectory); + end + + function tearDown(self) + %tearDown Restore original MATLAB path + % test_case.tearDown() restores the saved MATLAB path. + path(self.OriginalPath); + end + end +end diff --git a/ext/matlab_xunit/TestComponent.m b/ext/matlab_xunit/TestComponent.m new file mode 100644 index 000000000..e68b5b54c --- /dev/null +++ b/ext/matlab_xunit/TestComponent.m @@ -0,0 +1,59 @@ +classdef TestComponent < handle +%TestComponent Abstract base class for TestCase and TestSuite +% +% TestComponent methods: +% run - Run all test cases in test component +% print - Display summary of test component to Command Window +% numTestCases - Number of test cases in test component +% setUp - Initialize test fixture +% tearDown - Clean up text fixture +% +% TestComponent properties: +% Name - Name of test component +% Location - Directory where test component is defined +% +% See TestCase, TestSuite + +% Steven L. Eddins +% Copyright 2008-2009 The MathWorks, Inc. + + properties + Name = ''; + Location = ''; + end + + properties (Access = 'protected') + PrintIndentationSize = 4 + end + + methods (Abstract) + print() + %print Display summary of test component to Command Window + % obj.print() displays information about the test component to the + % Command Window. + + run() + %run Execute test cases + % obj.run() executes all the test cases in the test component + + numTestCases() + %numTestCases Number of test cases in test component + end + + methods + function setUp(self) + %setUp Set up test fixture + % test_component.setUp() is called at the beginning of the run() + % method. Test writers can override setUp if necessary to + % initialize a test fixture. + end + + function tearDown(self) + %tearDown Tear down test fixture + % test_component.tearDown() is at the end of the method. Test + % writers can override tearDown if necessary to clean up a test + % fixture. + end + + end +end \ No newline at end of file diff --git a/ext/matlab_xunit/TestComponentInDir.m b/ext/matlab_xunit/TestComponentInDir.m new file mode 100644 index 000000000..d1193c6c0 --- /dev/null +++ b/ext/matlab_xunit/TestComponentInDir.m @@ -0,0 +1,58 @@ +%TestComponentInDir Test component requiring temporary directory change +% The TestComponentInDir class defines a test component that has to be run by +% first changing to a specified directory. +% +% The setUp method adds the starting directory to the path and then uses cd to +% change into the specified directory. The tearDown method restores the +% original path and directory. +% +% TestComponentInDir methods: +% TestComponentInDir - Constructor +% setUp - Add test directory to MATLAB path +% tearDown - Restore original MATLAB path +% +% See also TestComponent + +% Steven L. Eddins +% Copyright 2008-2009 The MathWorks, Inc. + +classdef TestComponentInDir < TestComponent + properties (SetAccess = private, GetAccess = protected) + %TestDirectory - Directory to change to in the test fixture + TestDirectory + + %OriginalPath - Path prior to adding the starting directory + OriginalPath + + %OriginalDirectory - Starting directory + OriginalDirectory + end + + methods + function self = TestComponentInDir(testDirectory) + %TestCaseInDir Constructor + % TestCaseInDir(testName, testDirectory) constructs a test case + % using the specified name and located in the specified directory. + self.TestDirectory = testDirectory; + end + + function setUp(self) + %setUp Add test directory to MATLAB path + % test_case.setUp() saves the current directory in the + % OriginalDirectory property, saves the current path in the + % OriginalPath property, and then uses cd to change into the test + % directory. + self.OriginalDirectory = pwd; + self.OriginalPath = path; + addpath(pwd); + cd(self.TestDirectory); + end + + function tearDown(self) + %tearDown Restore original MATLAB path and directory + % test_case.tearDown() restores the original path and directory. + cd(self.OriginalDirectory); + path(self.OriginalPath); + end + end +end diff --git a/ext/matlab_xunit/TestRunDisplay.m b/ext/matlab_xunit/TestRunDisplay.m new file mode 100644 index 000000000..57ae200c7 --- /dev/null +++ b/ext/matlab_xunit/TestRunDisplay.m @@ -0,0 +1,258 @@ +classdef TestRunDisplay < TestRunMonitor +%TestRunDisplay Print test suite execution results. +% TestRunDisplay is a subclass of TestRunMonitor. If a TestRunDisplay +% object is passed to the run method of a TestComponent, such as a +% TestSuite or a TestCase, it will print information to the Command +% Window (or specified file handle) as the test run proceeds. +% +% TestRunDisplay methods: +% testComponentStarted - Update Command Window display +% testComponentFinished - Update Command Window display +% testCaseFailure - Log test failure information +% testCaseError - Log test error information +% +% TestRunDisplay properties: +% TestCaseCount - Number of test cases executed +% Faults - Struct array of test fault info +% +% See also TestRunLogger, TestRunMonitor, TestSuite + +% Steven L. Eddins +% Copyright 2008-2010 The MathWorks, Inc. + + properties (SetAccess = private) + %TestCaseCount - Number of test cases executed + TestCaseCount + + %Faults - Struct array of test fault info + % Faults is a struct array with these fields: + % Type - either 'failure' or 'error' + % TestCase - the TestCase object that suffered the fault + % Exception - the MException thrown when the fault occurred + Faults = struct('Type', {}, 'TestCase', {}, 'Exception', {}); + + end + + properties (SetAccess = private, GetAccess = private) + %InitialTic - Out of tic at beginning of test run + InitialTic + + %InitialComponent First test component executed + % InitialComponent is set to the first test component executed in the + % test run. This component is saved so that the end of the test run + % can be identified. + InitialComponent = [] + + end + + properties (Access = protected) + %FileHandle - Handle used by fprintf for displaying results. + % Default value of 1 displays to Command Window. + FileHandle = 1 + end + + + methods + function self = TestRunDisplay(output) + if nargin > 0 + if ischar(output) + self.FileHandle = fopen(output, 'w'); + if self.FileHandle < 0 + error('xunit:TestRunDisplay:FileOpenError', ... + 'Could not open file "%s" for writing.', ... + filename); + end + else + self.FileHandle = output; + end + end + end + + function testComponentStarted(self, component) + %testComponentStarted Update Command Window display + % If the InitialComponent property is not yet set, + % obj.testComponentStarted(component) sets the property and calls + % obj.testRunStarted(component). + + if isempty(self.InitialComponent) + self.InitialComponent = component; + self.testRunStarted(component); + end + end + + function testComponentFinished(self, component, did_pass) + %testComponentFinished Update Command Window display + % If component is a TestCase object, then + % obj.testComponentFinished(component, did_pass) prints pass/fail + % information to the Command Window. + % + % If component is the InitialComponent, then + % obj.testRunFinished(did_pass) is called. + + if isa(component, 'TestCase') + self.TestCaseCount = self.TestCaseCount + 1; + if did_pass + fprintf(self.FileHandle, '.'); + else + fprintf(self.FileHandle, 'F'); + end + line_length = 20; + if mod(self.TestCaseCount, line_length) == 0 + fprintf(self.FileHandle, '\n'); + end + end + + if isequal(component, self.InitialComponent) + self.testRunFinished(did_pass); + end + end + + function testCaseFailure(self, test_case, failure_exception) + %testCaseFailure Log test failure information + % obj.testCaseFailure(test_case, failure_exception) logs the test + % case failure information. + + self.logFault('failure', test_case, ... + failure_exception); + end + + function testCaseError(self, test_case, error_exception) + %testCaseError Log test error information + % obj.testCaseError(test_case, error_exception) logs the test + % case error information. + + self.logFault('error', test_case, ... + error_exception); + end + + end + + methods (Access = protected) + function testRunStarted(self, component) + %testRunStarted Update Command Window display + % obj.testRunStarted(component) displays information about the test + % run to the Command Window. + + self.InitialTic = tic; + self.TestCaseCount = 0; + num_cases = component.numTestCases(); + if num_cases == 1 + str = 'case'; + else + str = 'cases'; + end + fprintf(self.FileHandle, 'Starting test run with %d test %s.\n', ... + num_cases, str); + end + + function testRunFinished(self, did_pass) + %testRunFinished Update Command Window display + % obj.testRunFinished(component) displays information about the test + % run results, including any test failures, to the Command Window. + + if did_pass + result = 'PASSED'; + else + result = 'FAILED'; + end + + fprintf(self.FileHandle, '\n%s in %.3f seconds.\n', result, toc(self.InitialTic)); + + self.displayFaults(); + end + + + + function logFault(self, type, test_case, exception) + %logFault Log test fault information + % obj.logFault(type, test_case, exception) logs test fault + % information. type is either 'failure' or 'error'. test_case is a + % TestCase object. exception is an MException object. + + self.Faults(end + 1).Type = type; + self.Faults(end).TestCase = test_case; + self.Faults(end).Exception = exception; + end + + function displayFaults(self) + %displayFaults Display test fault info to Command Window + % obj.displayFaults() displays a summary of each test failure and + % test error to the command window. + for k = 1:numel(self.Faults) + faultData = self.Faults(k); + if strcmp(faultData.Type, 'failure') + str = 'Failure'; + else + str = 'Error'; + end + fprintf(self.FileHandle, '\n===== Test Case %s =====\nLocation: %s\nName: %s\n\n', str, ... + faultData.TestCase.Location, faultData.TestCase.Name); + displayStack(filterStack(faultData.Exception.stack), ... + self.FileHandle); + fprintf(self.FileHandle, '\n%s\n', faultData.Exception.message); + + fprintf(self.FileHandle, '\n'); + end + end + + end + +end + +function displayStack(stack, file_handle) +%displayStack Display stack trace from MException instance +% displayStack(stack) prints information about an exception stack to the +% command window. + +for k = 1:numel(stack) + filename = stack(k).file; + linenumber = stack(k).line; + href = sprintf('matlab: opentoline(''%s'',%d)', filename, linenumber); + fprintf(file_handle, '%s at line %d\n', filename, href, linenumber); +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 + diff --git a/ext/matlab_xunit/TestRunLogger.m b/ext/matlab_xunit/TestRunLogger.m new file mode 100644 index 000000000..2a82978ad --- /dev/null +++ b/ext/matlab_xunit/TestRunLogger.m @@ -0,0 +1,104 @@ +%TestRunLogger Collect data (silently) from running test suite +% TestRunLogger is a subclass of TestRunMonitor uses to collect information +% from an executing test component (either a test case or a test suite). +% It maintains a record of event notifications received, as well as any test +% failures or test errors. +% +% TestRunLogger methods: +% testComponentStarted - Log test component started +% testComponentFinished - Log test component finished +% testCaseFailure - Log test case failure +% testCaseError - Log test case error +% +% TestRunLogger properties: +% Log - Cell array of test notification strings +% NumFailures - Number of test failures during execution +% NumErrors - Number of test errors during execution +% NumTestCases - Total number of test cases executed +% Faults - Struct array of test fault information +% +% See also CommandWindowTestRunDisplay, TestRunMonitor, TestSuite + +% Steven L. Eddins +% Copyright 2008-2009 The MathWorks, Inc. + +classdef TestRunLogger < TestRunMonitor + + properties (SetAccess = protected) + %Log Cell array of test notification strings + % Test notification strings include 'TestRunStarted', + % 'TestRunFinished', 'TestComponentStarted', 'TestComponentFinished', + % 'TestCaseFailure', and 'TestCaseError'. + Log + + %NumFailures Number of test failures during execution + NumFailures = 0 + + %NumErrors Number of test errors during execution + NumErrors = 0 + + %NumTestCases Total number of test cases executed + NumTestCases = 0 + + %Faults Struct array of test fault information + % Faults is a struct array with the fields Type, TestCase, and + % Exception. Type is either 'failure' or 'error'. TestCase is the + % test case object that triggered the fault. Exception is the + % MException object thrown during the fault. + Faults = struct('Type', {}, 'TestCase', {}, 'Exception', {}); + end + + properties (SetAccess = private, GetAccess = private) + InitialTestComponent = [] + end + + methods + + function testComponentStarted(self, component) + if isempty(self.InitialTestComponent) + self.InitialTestComponent = component; + self.appendToLog('TestRunStarted'); + end + + self.appendToLog('TestComponentStarted'); + + if isa(component, 'TestCase') + self.NumTestCases = self.NumTestCases + 1; + end + end + + function testComponentFinished(self, component, did_pass) + self.appendToLog('TestComponentFinished'); + + if isequal(component, self.InitialTestComponent) + self.appendToLog('TestRunFinished'); + end + end + + function testCaseFailure(self, test_case, failure_exception) + self.appendToLog('TestCaseFailure'); + self.NumFailures = self.NumFailures + 1; + self.logFault('failure', test_case, ... + failure_exception); + end + + function testCaseError(self, test_case, error_exception) + self.appendToLog('TestCaseError'); + self.NumErrors = self.NumErrors + 1; + self.logFault('error', test_case, ... + error_exception); + end + end + + methods (Access = private) + function appendToLog(self, item) + self.Log{end+1} = item; + end + + function logFault(self, type, test_case, exception) + self.Faults(end + 1).Type = type; + self.Faults(end).TestCase = test_case; + self.Faults(end).Exception = exception; + end + end +end diff --git a/ext/matlab_xunit/TestRunMonitor.m b/ext/matlab_xunit/TestRunMonitor.m new file mode 100644 index 000000000..4af749039 --- /dev/null +++ b/ext/matlab_xunit/TestRunMonitor.m @@ -0,0 +1,35 @@ +%TestRunMonitor Abstract base class for monitoring a running test suite +% The abstract TestRunMonitor class defines an object that can observe and +% record the results of running a test suite. The run() method of a +% TestComponent object takes a TestRunMonitor object as an input argument. +% +% Different test suite logging or reporting functionality can be achieved by +% subclassing TestRunMonitor. For example, see the TestRunLogger and the +% CommandWindowTestRunDisplay classes. +% +% TestRunMonitor methods: +% TestRunMonitor - Constructor +% testComponentStarted - Called at beginning of test component run +% testComponentFinished - Called when test component run finished +% testCaseFailure - Called when a test case fails +% testCaseError - Called when a test case causes an error +% +% See also CommandWindowTestRunDisplay, TestRunLogger, TestCase, TestSuite + +% Steven L. Eddins +% Copyright 2008-2009 The MathWorks, Inc. + +classdef TestRunMonitor < handle + + methods (Abstract) + + testComponentStarted(self, component) + + testComponentFinished(self, component, did_pass) + + testCaseFailure(self, test_case, failure_exception) + + testCaseError(self, test_case, error_exception) + + end +end diff --git a/ext/matlab_xunit/TestSuite.m b/ext/matlab_xunit/TestSuite.m new file mode 100644 index 000000000..a55d91963 --- /dev/null +++ b/ext/matlab_xunit/TestSuite.m @@ -0,0 +1,342 @@ +%TestSuite Collection of TestComponent objects +% The TestSuite class defines a collection of TestComponent objects. +% +% TestSuite methods: +% TestSuite - Constructor +% add - Add test component to test suite +% print - Display test suite summary to Command Window +% run - Run the test suite +% keepMatchingTestCase - Keep only the named test component +% fromName - Construct test suite from directory or MATLAB function file name +% fromTestCaseClassName - Construct test suite from TestCase class name +% fromPackageName - Construct test suite from package name +% fromPwd - Construct test suite from present directory +% +% TestSuite properties: +% TestComponents - Cell array of TestComponent objects +% +% Examples +% -------- +% Run all the test cases in the SampleTests1 class. Display test suite +% progress and a summary of results in the Command Window. +% +% TestSuite('SampleTests1').run() +% +% Construct a test suite from all test components found in the current +% directory. +% +% suite = TestSuite.fromPwd(); +% +% Construct a test suite from all test components found in the package +% 'mytool.tests'. (Note that the "+" character at the beginning of the package +% folder name on disk is not part of the package name.) +% +% suite = TestSuite.fromPackageName('mytool.tests'); +% +% Run all the test cases in the SampleTests class. Display no output to the +% Command Window. Upon completion, query the number of test failures and test +% errors. +% +% logger = TestRunLogger(); +% TestSuite('SampleTests1').run(logger); +% numFailures = logger.NumFailures +% numErrors = logger.NumErrors +% +% See also CommandWindowTestRunDisplay, TestCase, TestComponent, TestRunLogger + +% Steven L. Eddins +% Copyright 2008-2010 The MathWorks, Inc. + +classdef TestSuite < TestComponent + + properties (SetAccess = protected) + TestComponents = {}; + end + + methods + + function self = TestSuite(name) + %TestSuite Constructor + % suite = TestSuite constructs an empty test suite. suite = + % TestSuite(name) constructs a test suite by searching for test + % cases defined in an M-file with the specified name. + + if nargin >= 1 + self = TestSuite.fromName(name); + end + end + + function did_pass_out = run(self, monitor) + %run Execute test cases in test suite + % did_pass = suite.run() executes all test cases in the test + % suite, returning a logical value indicating whether or not all + % test cases passed. + + if nargin < 2 + monitor = CommandWindowTestRunDisplay(); + end + + monitor.testComponentStarted(self); + did_pass = true; + + self.setUp(); + + for k = 1:numel(self.TestComponents) + this_component_passed = self.TestComponents{k}.run(monitor); + did_pass = did_pass && this_component_passed; + end + + self.tearDown(); + + monitor.testComponentFinished(self, did_pass); + + if nargout > 0 + did_pass_out = did_pass; + end + end + + function num = numTestCases(self) + %numTestCases Number of test cases in test suite + + num = 0; + for k = 1:numel(self.TestComponents) + component_k = self.TestComponents{k}; + num = num + component_k.numTestCases(); + end + end + + function print(self, numLeadingBlanks) + %print Display test suite summary to Command Window + % test_suite.print() displays a summary of the test suite to the + % Command Window. + + if nargin < 2 + numLeadingBlanks = 0; + end + fprintf('%s%s\n', blanks(numLeadingBlanks), self.Name); + for k = 1:numel(self.TestComponents) + self.TestComponents{k}.print(numLeadingBlanks + ... + self.PrintIndentationSize); + end + end + + function add(self, component) + %add Add test component to test suite + % test_suite.add(component) adds the TestComponent object to the + % test suite. + + if iscell(component) + self.TestComponents((1:numel(component)) + end) = component; + else + self.TestComponents{end + 1} = component; + end + end + + function keepMatchingTestCase(self, name) + %keepMatchingTestCase Keep only the named test component + % test_suite.keepMatchingTestCase(name) keeps only the test + % component with a matching name and discards the rest. + + idx = []; + for k = 1:numel(self.TestComponents) + if strcmp(self.TestComponents{k}.Name, name) + idx = k; + break; + end + end + if isempty(idx) + self.TestComponents = {}; + else + self.TestComponents = self.TestComponents(idx); + end + end + + end + + methods (Static) + function suite = fromTestCaseClassName(class_name) + %fromTestCaseClassName Construct test suite from TestCase class name + % suite = TestSuite.fromTestCaseClassName(name) constructs a + % TestSuite object from the name of a TestCase subclass. + + if ~xunit.utils.isTestCaseSubclass(class_name) + error('xunit:fromTestCaseClassName', ... + 'Input string "%s" is not the name of a TestCase class.', ... + class_name); + end + + suite = TestSuite; + suite.Name = class_name; + suite.Location = which(class_name); + + methods = getClassMethods(class_name); + for k = 1:numel(methods) + if methodIsConstructor(methods{k}) + continue + end + + method_name = methods{k}.Name; + if xunit.utils.isTestString(method_name) + suite.add(feval(class_name, method_name)); + end + end + + end + + function suite = fromName(name) + %fromName Construct test suite from M-file name + % test_suite = TestSuite.fromName(name) constructs a TestSuite + % object from an M-file with the given name. The name can be of a + % directory, a TestCase subclass, or an M-file containing a simple + % test or containing subfunction-based tests. + % + % Optionally, name can contain a colon (':') followed by filter + % string. The filter string is used to select a particular named + % test case. For example, TestSuite.fromName('MyTests:testA') + % constructs a TestSuite object containing only the test case + % named 'testA' found in the TestCase subclass MyTests. + + if isdir(name) + suite = TestSuiteInDir(name); + suite.gatherTestCases(); + return; + end + + [name, filter_string] = strtok(name, ':'); + if ~isempty(filter_string) + filter_string = filter_string(2:end); + end + + if xunit.utils.isTestCaseSubclass(name) + suite = TestSuite.fromTestCaseClassName(name); + + elseif ~isempty(meta.class.fromName(name)) + % Input is the name of a class that is not a TestCase subclass. + % Return an empty test suite. + suite = TestSuite(); + suite.Name = name; + + elseif isPackage(name) + suite = TestSuite.fromPackageName(name); + + else + + try + if nargout(name) == 0 + suite = TestSuite(); + suite.Name = name; + suite.add(FunctionHandleTestCase(str2func(name), [], [])); + suite.Location = which(name); + + else + suite = feval(name); + if ~isa(suite, 'TestSuite') + error('Function did not return a TestSuite object.'); + end + end + + catch + % Ordinary function does not appear to contain tests. + % Return an empty test suite. + suite = TestSuite(); + suite.Name = name; + end + end + + if ~isempty(filter_string) + suite.keepMatchingTestCase(filter_string); + end + end + + function test_suite = fromPwd() + %fromPwd Construct test suite from present directory + % test_suite = TestSuite.fromPwd() constructs a TestSuite object + % from all the test components in the present working directory. + % all TestCase subclasses will be found, as well as simple and + % subfunction-based M-file tests beginning with the string 'test' + % or 'Test'. + + test_suite = TestSuite(); + test_suite.Name = pwd; + test_suite.Location = pwd; + + mfiles = dir(fullfile('.', '*.m')); + for k = 1:numel(mfiles) + [path, name] = fileparts(mfiles(k).name); + if xunit.utils.isTestCaseSubclass(name) + test_suite.add(TestSuite.fromTestCaseClassName(name)); + elseif xunit.utils.isTestString(name) + suite_k = TestSuite.fromName(name); + if ~isempty(suite_k.TestComponents) + test_suite.add(suite_k); + end + end + end + end + + function test_suite = fromPackageName(name) + %fromPackageName Construct test suite from package name + % test_suite = TestSuite.fromPackageName(name) constructs a + % TestSuite object from all the test components found in the + % specified package. + + package_info = meta.package.fromName(name); + if isempty(package_info) + error('xunit:fromPackageName:invalidName', ... + 'Input string "%s" is not the name of a package.', ... + name); + end + test_suite = TestSuite(); + test_suite.Name = name; + test_suite.Location = 'Package'; + + for k = 1:numel(package_info.Packages) + pkg_name = package_info.Packages{k}.Name; + pkg_suite = TestSuite.fromPackageName(pkg_name); + if ~isempty(pkg_suite.TestComponents) + test_suite.add(TestSuite.fromPackageName(pkg_name)); + end + end + + class_names = cell(1, numel(package_info.Classes)); + for k = 1:numel(package_info.Classes) + class_name = package_info.Classes{k}.Name; + class_names{k} = class_name; + if xunit.utils.isTestCaseSubclass(class_name) + test_suite.add(TestSuite.fromTestCaseClassName(class_name)); + end + end + + for k = 1:numel(package_info.Functions) + function_name = package_info.Functions{k}.Name; + if xunit.utils.isTestString(function_name) + full_function_name = [package_info.Name '.' package_info.Functions{k}.Name]; + if ~ismember(full_function_name, class_names) + suite_k = TestSuite.fromName(full_function_name); + if ~isempty(suite_k.TestComponents) + test_suite.add(suite_k); + end + end + end + end + end + end +end + +function tf = isPackage(name) +tf = ~isempty(meta.package.fromName(name)); +end + +function methods = getClassMethods(class_name) +class_meta = meta.class.fromName(class_name); +methods = class_meta.Methods; +end + +function result = methodIsConstructor(method) +method_name = method.Name; +if ~isempty(method.DefiningClass.ContainingPackage) + method_name = [method.DefiningClass.ContainingPackage.Name, '.', ... + method_name]; +end +result = strcmp(method_name, method.DefiningClass.Name); +end diff --git a/ext/matlab_xunit/TestSuiteInDir.m b/ext/matlab_xunit/TestSuiteInDir.m new file mode 100644 index 000000000..e83ef5e89 --- /dev/null +++ b/ext/matlab_xunit/TestSuiteInDir.m @@ -0,0 +1,50 @@ +%TestSuiteInDir Test suite requiring temporary directory change +% The TestSuiteInDir class defines a test suite that has to be run by first +% changing to a specified directory. +% +% The setUp method adds the starting directory to the path and then uses cd to +% change into the specified directory. The tearDown method restores the +% original path and directory. +% +% TestSuiteInDir methods: +% TestSuiteInDir - Constructor +% gatherTestCases - Add test cases found in the target directory +% +% See also TestSuite + +% Steven L. Eddins +% Copyright 2009 The MathWorks, Inc. + +classdef TestSuiteInDir < TestSuite & TestComponentInDir + + methods + function self = TestSuiteInDir(testDirectory) + %TestCaseInDir Constructor + % TestCaseInDir(testName, testDirectory) constructs a test case + % using the specified name and located in the specified directory. + self = self@TestComponentInDir(testDirectory); + + if strcmp(testDirectory, '.') + self.Name = pwd; + self.Location = pwd; + else + [pathstr, name] = fileparts(testDirectory); + self.Name = name; + self.Location = testDirectory; + end + end + + function gatherTestCases(self) + %gatherTestCases Add test cases found in the target directory + % suite.gatherTestCases() automaticall finds all the test cases in + % the directory specified in the constructor call and adds them to + % the suite. + current_dir = pwd; + c = onCleanup(@() cd(current_dir)); + + cd(self.TestDirectory); + tmp = TestSuite.fromPwd(); + self.TestComponents = tmp.TestComponents; + end + end +end diff --git a/ext/matlab_xunit/VerboseTestRunDisplay.m b/ext/matlab_xunit/VerboseTestRunDisplay.m new file mode 100644 index 000000000..18e85b1d3 --- /dev/null +++ b/ext/matlab_xunit/VerboseTestRunDisplay.m @@ -0,0 +1,112 @@ +classdef VerboseTestRunDisplay < TestRunDisplay +%VerboseTestRunDisplay Print test suite execution results. +% VerboseTestRunDisplay is a subclass of +% TestRunDisplay. It supports the -verbose option of runtests. +% +% Overriddent methods: +% testComponentStarted - Update Command Window display +% testComponentFinished - Update Command Window display +% testRunFinished - Update Command Window display at end of run +% +% See also TestRunDisplay, TestRunLogger, TestRunMonitor, TestSuite + +% Steven L. Eddins +% Copyright 2010 The MathWorks, Inc. + + properties (SetAccess = private, GetAccess = private) + TicStack = uint64([]) + end + + methods + function self = VerboseTestRunDisplay(output) + if nargin < 1 + output = 1; + end + + self = self@TestRunDisplay(output); + end + + function testComponentStarted(self, component) + %testComponentStarted Update Command Window display + + self.pushTic(); + + if ~isa(component, 'TestCase') + fprintf(self.FileHandle, '\n'); + end + + fprintf(self.FileHandle, '%s%s', self.indentationSpaces(), component.Name); + + if ~isa(component, 'TestCase') + fprintf(self.FileHandle, '\n'); + else + fprintf(self.FileHandle, ' %s ', self.leaderDots(component.Name)); + end + end + + function testComponentFinished(self, component, did_pass) + %testComponentFinished Update Command Window display + + if ~isa(component, 'TestCase') + fprintf(self.FileHandle, '%s%s %s ', self.indentationSpaces(), component.Name, ... + self.leaderDots(component.Name)); + end + + component_run_time = toc(self.popTic()); + + if did_pass + fprintf(self.FileHandle, 'passed in %12.6f seconds\n', component_run_time); + else + fprintf(self.FileHandle, 'FAILED in %12.6f seconds\n', component_run_time); + end + + if ~isa(component, 'TestCase') + fprintf(self.FileHandle, '\n'); + end + + if isempty(self.TicStack) + self.testRunFinished(); + end + + end + + end + + methods (Access = protected) + function testRunFinished(self) + %testRunFinished Update Command Window display + % obj.testRunFinished(component) displays information about the test + % run results, including any test failures, to the Command + % Window. + + self.displayFaults(); + end + end + + methods (Access = private) + 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 + + end + +end diff --git a/ext/matlab_xunit/assertElementsAlmostEqual.m b/ext/matlab_xunit/assertElementsAlmostEqual.m new file mode 100644 index 000000000..3ac2ba6b3 --- /dev/null +++ b/ext/matlab_xunit/assertElementsAlmostEqual.m @@ -0,0 +1,61 @@ +function assertElementsAlmostEqual(varargin) +%assertElementsAlmostEqual Assert floating-point array elements almost equal. +% assertElementsAlmostEqual(A, B, tol_type, tol, floor_tol) asserts that all +% elements of floating-point arrays A and B are equal within some tolerance. +% tol_type can be 'relative' or 'absolute'. tol and floor_tol are scalar +% tolerance values. +% +% If the tolerance type is 'relative', then the tolerance test used is: +% +% all( abs(A(:) - B(:)) <= tol * max(abs(A(:)), abs(B(:))) + floor_tol ) +% +% If the tolerance type is 'absolute', then the tolerance test used is: +% +% all( abs(A(:) - B(:)) <= tol ) +% +% tol_type, tol, and floor_tol are all optional. The default value for +% tol_type is 'relative'. If both A and B are double, then the default value +% for tol and floor_tol is sqrt(eps). If either A or B is single, then the +% default value for tol and floor_tol is sqrt(eps('single')). +% +% If A or B is complex, then the tolerance test is applied independently to +% the real and imaginary parts. +% +% Corresponding elements in A and B that are both NaN, or are both infinite +% with the same sign, are considered to pass the tolerance test. +% +% assertElementsAlmostEqual(A, B, ..., msg) prepends the string msg to the +% output message if A and B fail the tolerance test. + +% Steven L. Eddins +% Copyright 2008-2010 The MathWorks, Inc. + +params = xunit.utils.parseFloatAssertInputs(varargin{:}); + +if ~isequal(size(params.A), size(params.B)) + message = xunit.utils.comparisonMessage(params.Message, ... + 'Inputs are not the same size.', ... + params.A, params.B); + throwAsCaller(MException('assertElementsAlmostEqual:sizeMismatch', ... + '%s', message)); +end + +if ~(isfloat(params.A) && isfloat(params.B)) + message = xunit.utils.comparisonMessage(params.Message, ... + 'Inputs are not both floating-point.', ... + params.A, params.B); + throwAsCaller(MException('assertElementsAlmostEqual:notFloat', ... + '%s', message)); +end + +if ~xunit.utils.compareFloats(params.A, params.B, 'elementwise', ... + params.ToleranceType, params.Tolerance, params.FloorTolerance) + + tolerance_message = sprintf('Input elements are not all equal within %s tolerance: %g', ... + params.ToleranceType, params.Tolerance); + message = xunit.utils.comparisonMessage(params.Message, tolerance_message, ... + params.A, params.B); + + throwAsCaller(MException('assertElementsAlmostEqual:tolExceeded', ... + '%s', message)); +end diff --git a/ext/matlab_xunit/assertEqual.m b/ext/matlab_xunit/assertEqual.m new file mode 100644 index 000000000..72349cd0d --- /dev/null +++ b/ext/matlab_xunit/assertEqual.m @@ -0,0 +1,43 @@ +function assertEqual(A, B, custom_message) +%assertEqual Assert that inputs are equal +% assertEqual(A, B) throws an exception if A and B are not equal. A and B +% must have the same class and sparsity to be considered equal. +% +% assertEqual(A, B, MESSAGE) prepends the string MESSAGE to the assertion +% message if A and B are not equal. +% +% Examples +% -------- +% % This call returns silently. +% assertEqual([1 NaN 2], [1 NaN 2]); +% +% % This call throws an error. +% assertEqual({'A', 'B', 'C'}, {'A', 'foo', 'C'}); +% +% See also assertElementsAlmostEqual, assertVectorsAlmostEqual + +% Steven L. Eddins +% Copyright 2008-2010 The MathWorks, Inc. + +if nargin < 3 + custom_message = ''; +end + +if ~ (issparse(A) == issparse(B)) + message = xunit.utils.comparisonMessage(custom_message, ... + 'One input is sparse and the other is not.', A, B); + throwAsCaller(MException('assertEqual:sparsityNotEqual', '%s', message)); +end + +if ~strcmp(class(A), class(B)) + message = xunit.utils.comparisonMessage(custom_message, ... + 'The inputs differ in class.', A, B); + throwAsCaller(MException('assertEqual:classNotEqual', '%s', message)); +end + +if ~isequalwithequalnans(A, B) + message = xunit.utils.comparisonMessage(custom_message, ... + 'Inputs are not equal.', A, B); + throwAsCaller(MException('assertEqual:nonEqual', '%s', message)); +end + diff --git a/ext/matlab_xunit/assertExceptionThrown.m b/ext/matlab_xunit/assertExceptionThrown.m new file mode 100644 index 000000000..bbe6b722e --- /dev/null +++ b/ext/matlab_xunit/assertExceptionThrown.m @@ -0,0 +1,53 @@ +function assertExceptionThrown(f, expectedId, custom_message) +%assertExceptionThrown Assert that specified exception is thrown +% assertExceptionThrown(F, expectedId) calls the function handle F with no +% input arguments. If the result is a thrown exception whose identifier is +% expectedId, then assertExceptionThrown returns silently. If no exception is +% thrown, then assertExceptionThrown throws an exception with identifier equal +% to 'assertExceptionThrown:noException'. If a different exception is thrown, +% then assertExceptionThrown throws an exception identifier equal to +% 'assertExceptionThrown:wrongException'. +% +% assertExceptionThrown(F, expectedId, msg) prepends the string msg to the +% assertion message. +% +% Example +% ------- +% % This call returns silently. +% f = @() error('a:b:c', 'error message'); +% assertExceptionThrown(f, 'a:b:c'); +% +% % This call returns silently. +% assertExceptionThrown(@() sin, 'MATLAB:minrhs'); +% +% % This call throws an error because calling sin(pi) does not error. +% assertExceptionThrown(@() sin(pi), 'MATLAB:foo'); + +% Steven L. Eddins +% Copyright 2008-2010 The MathWorks, Inc. + +noException = false; +try + f(); + noException = true; + +catch exception + if ~strcmp(exception.identifier, expectedId) + message = sprintf('Expected exception %s but got exception %s.', ... + expectedId, exception.identifier); + if nargin >= 3 + message = sprintf('%s\n%s', custom_message, message); + end + throwAsCaller(MException('assertExceptionThrown:wrongException', ... + '%s', message)); + end +end + +if noException + message = sprintf('Expected exception "%s", but none thrown.', ... + expectedId); + if nargin >= 3 + message = sprintf('%s\n%s', custom_message, message); + end + throwAsCaller(MException('assertExceptionThrown:noException', '%s', message)); +end diff --git a/ext/matlab_xunit/assertFalse.m b/ext/matlab_xunit/assertFalse.m new file mode 100644 index 000000000..7bc3e8420 --- /dev/null +++ b/ext/matlab_xunit/assertFalse.m @@ -0,0 +1,31 @@ +function assertFalse(condition, message) +%assertFalse Assert that input condition is false +% assertFalse(CONDITION, MESSAGE) throws an exception containing the string +% MESSAGE if CONDITION is not false. +% +% MESSAGE is optional. +% +% Examples +% -------- +% assertFalse(isreal(sqrt(-1))) +% +% assertFalse(isreal(sqrt(-1)), ... +% 'Expected isreal(sqrt(-1)) to be false.') +% +% See also assertTrue + +% Steven L. Eddins +% Copyright 2008-2010 The MathWorks, Inc. + +if nargin < 2 + message = 'Asserted condition is not false.'; +end + +if ~isscalar(condition) || ~islogical(condition) + throwAsCaller(MException('assertFalse:invalidCondition', ... + 'CONDITION must be a scalar logical value.')); +end + +if condition + throwAsCaller(MException('assertFalse:trueCondition', '%s', message)); +end diff --git a/ext/matlab_xunit/assertFilesEqual.m b/ext/matlab_xunit/assertFilesEqual.m new file mode 100644 index 000000000..d1cfc5174 --- /dev/null +++ b/ext/matlab_xunit/assertFilesEqual.m @@ -0,0 +1,68 @@ +function assertFilesEqual(filename1, filename2, user_message) +%assertFilesEqual Assert that files contain the same contents. +% assertFilesEqual(filename1, filename2) throws an exception if the two +% specified files do not contain the same contents. +% +% assertFilesEqual(filename1, filename2, message) prepends the specified +% message string to the assertion message. + +% Steven L. Eddins +% Copyright 2009-2010 The MathWorks, Inc. + +if nargin < 3 + user_message = ''; +end + +fid1 = fopen(filename1, 'r'); +if (fid1 < 0) + message = sprintf('%s\nCould not open file for reading: %s', ... + user_message, filename1); + throwAsCaller(MException('assertFilesEqual:readFailure', ... + '%s', message)); +else + c1 = onCleanup(@() fclose(fid1)); +end + +fid2 = fopen(filename2, 'r'); +if (fid2 < 0) + message = sprintf('%s\nCould not open file for reading: %s', ... + user_message, filename2); + throwAsCaller(MException('assertFilesEqual:readFailure', '%s', message)); +else + c2 = onCleanup(@() fclose(fid2)); +end + +block_size = 100000; +num_blocks = 0; +done = false; +while ~done + block_from_file1 = fread(fid1, block_size, '*uint8'); + block_from_file2 = fread(fid2, block_size, '*uint8'); + + if numel(block_from_file1) ~= numel(block_from_file2) + fseek(fid1, 0, 'eof'); + fseek(fid2, 0, 'eof'); + message = sprintf('The two files are not the same size. File "%s" has %d bytes and file "%s" has %d bytes', ... + filename1, ftell(fid1), filename2, ftell(fid2)); + if ~isempty(user_message) + message = sprintf('%s\n%s', user_message, message); + end + throwAsCaller(MException('assertFilesEqual:sizeMismatch', '%s', message)); + end + + if ~isequal(block_from_file1, block_from_file2) + first_difference_in_block = find(block_from_file1 ~= block_from_file2); + first_difference = num_blocks * block_size + first_difference_in_block; + + message = sprintf('Files are not equal. First difference is at byte %d, where file "%s" contains 0x%X and file "%s" contains 0x%X', ... + first_difference, filename1, block_from_file1(first_difference_in_block), ... + filename2, block_from_file2(first_difference_in_block)); + if ~isempty(user_message) + message = sprintf('%s\n%s', user_message, message); + end + throwAsCaller(MException('assertFilesEqual:valuesDiffer', '%s', message)); + end + + done = numel(block_from_file1) < block_size; + num_blocks = num_blocks + 1; +end \ No newline at end of file diff --git a/ext/matlab_xunit/assertTrue.m b/ext/matlab_xunit/assertTrue.m new file mode 100644 index 000000000..52787a675 --- /dev/null +++ b/ext/matlab_xunit/assertTrue.m @@ -0,0 +1,33 @@ +function assertTrue(condition, message) +%assertTrue Assert that input condition is true +% assertTrue(CONDITION, MESSAGE) throws an exception containing the string +% MESSAGE if CONDITION is not true. +% +% MESSAGE is optional. +% +% Examples +% -------- +% % This call returns silently. +% assertTrue(rand < 1, 'Expected output of rand to be less than 1') +% +% % This call throws an error. +% assertTrue(sum(sum(magic(3))) == 0, ... +% 'Expected sum of elements of magic(3) to be 0') +% +% See also assertEqual, assertFalse + +% Steven L. Eddins +% Copyright 2008-2010 The MathWorks, Inc. + +if nargin < 2 + message = 'Asserted condition is not true.'; +end + +if ~isscalar(condition) || ~islogical(condition) + throwAsCaller(MException('assertTrue:invalidCondition', ... + 'CONDITION must be a scalar logical value.')); +end + +if ~condition + throwAsCaller(MException('assertTrue:falseCondition', '%s', message)); +end diff --git a/ext/matlab_xunit/assertVectorsAlmostEqual.m b/ext/matlab_xunit/assertVectorsAlmostEqual.m new file mode 100644 index 000000000..e4f10343a --- /dev/null +++ b/ext/matlab_xunit/assertVectorsAlmostEqual.m @@ -0,0 +1,59 @@ +function assertVectorsAlmostEqual(varargin) +%assertVectorsAlmostEqual Assert floating-point vectors almost equal in norm sense. +% assertVectorsAlmostEqual(A, B, tol_type, tol, floor_tol) asserts that the +% vectors A and B are equal, in the L2-norm sense and within some tolerance. +% tol_type can be 'relative' or 'absolute'. tol and floor_tol are scalar +% tolerance values. +% +% If the tolerance type is 'relative', then the tolerance test used is: +% +% all( norm(A - B) <= tol * max(norm(A), norm(B)) + floor_tol ) +% +% If the tolerance type is 'absolute', then the tolerance test used is: +% +% all( norm(A - B) <= tol ) +% +% tol_type, tol, and floor_tol are all optional. The default value for +% tol_type is 'relative'. If both A and B are double, then the default value +% for tol and floor_tol is sqrt(eps). If either A or B is single, then the +% default value for tol and floor_tol is sqrt(eps('single')). +% +% If A or B is complex, then the tolerance test is applied independently to +% the real and imaginary parts. +% +% Any infinite or NaN element of A or B will cause an assertion failure. +% +% assertVectorsAlmostEqual(A, B, ..., msg) prepends the string msg to the +% assertion message if A and B fail the tolerance test. + +% Steven L. Eddins +% Copyright 2008-2010 The MathWorks, Inc. + +params = xunit.utils.parseFloatAssertInputs(varargin{:}); + +if ~isequal(size(params.A), size(params.B)) + message = xunit.utils.comparisonMessage(params.Message, ... + 'Inputs are not the same size.', ... + params.A, params.B); + throwAsCaller(MException('assertVectorsAlmostEqual:sizeMismatch', ... + '%s', message)); +end + +if ~(isfloat(params.A) && isfloat(params.B)) + message = xunit.utils.comparisonMessage(params.Message, ... + 'Inputs are not both floating-point.', ... + params.A, params.B); + throwAsCaller(MException('assertVectorsAlmostEqual:notFloat', ... + '%s', message)); +end + +if ~xunit.utils.compareFloats(params.A, params.B, 'vector', ... + params.ToleranceType, params.Tolerance, params.FloorTolerance) + + tolerance_message = sprintf('Inputs are not equal within %s vector tolerance: %g', ... + params.ToleranceType, params.Tolerance); + message = xunit.utils.comparisonMessage(params.Message, tolerance_message, ... + params.A, params.B); + throwAsCaller(MException('assertVectorsAlmostEqual:tolExceeded', ... + '%s', message)); +end diff --git a/ext/matlab_xunit/info.xml b/ext/matlab_xunit/info.xml new file mode 100644 index 000000000..046189b0b --- /dev/null +++ b/ext/matlab_xunit/info.xml @@ -0,0 +1,28 @@ + + + +R2009a +MATLAB xUnit Test Framework +Other +$toolbox/matlab/icons/matlabicon.gif +../doc/ + + + + + +doc xunit +$toolbox/matlab/icons/book_mat.gif + + + + +web http://www.mathworks.com/matlabcentral/fileexchange/22846 -browser; +$toolbox/matlab/icons/webicon.gif + + + + + diff --git a/ext/matlab_xunit/initTestSuite.m b/ext/matlab_xunit/initTestSuite.m new file mode 100644 index 000000000..9af3f7f6e --- /dev/null +++ b/ext/matlab_xunit/initTestSuite.m @@ -0,0 +1,58 @@ +%findSubfunctionTests Utility script used for subfunction-based tests +% This file is a script that is called at the top of M-files containing +% subfunction-based tests. +% +% The top of a typical M-file using this script looks like this: +% +% function test_suite = testFeatureA +% +% findSubfunctionTests; +% +% IMPORTANT NOTE +% -------------- +% The output variable name for an M-file using this script must be test_suite. + +% Steven L. Eddins +% Copyright 2008-2009 The MathWorks, Inc. + +[ST,I] = dbstack('-completenames'); +caller_name = ST(I + 1).name; +caller_file = ST(I + 1).file; +subFcns = which('-subfun', caller_file); + +setup_fcn_name = subFcns(xunit.utils.isSetUpString(subFcns)); +if numel(setup_fcn_name) > 1 + error('findSubfunctionTests:tooManySetupFcns', ... + 'Found more than one setup subfunction.') +elseif isempty(setup_fcn_name) + setup_fcn = []; +else + setup_fcn = str2func(setup_fcn_name{1}); +end + +teardown_fcn_name = subFcns(xunit.utils.isTearDownString(subFcns)); +if numel(teardown_fcn_name) > 1 + error('findSubfunctionTests:tooManyTeardownFcns', ... + 'Found more than one teardown subfunction.') +elseif isempty(teardown_fcn_name) + teardown_fcn = []; +else + teardown_fcn = str2func(teardown_fcn_name{1}); +end + +test_fcns = cellfun(@str2func, subFcns(xunit.utils.isTestString(subFcns)), ... + 'UniformOutput', false); + +suite = TestSuite; +suite.Name = caller_name; +suite.Location = which(caller_file); +for k = 1:numel(test_fcns) + suite.add(FunctionHandleTestCase(test_fcns{k}, setup_fcn, teardown_fcn)); +end + +if nargout > 0 + test_suite = suite; +else + suite.run(); +end + diff --git a/ext/matlab_xunit/license.txt b/ext/matlab_xunit/license.txt new file mode 100644 index 000000000..2401e6db6 --- /dev/null +++ b/ext/matlab_xunit/license.txt @@ -0,0 +1,27 @@ +Copyright (c) 2010, The MathWorks, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution + * Neither the name of the The MathWorks, Inc. nor the names + of its contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/ext/matlab_xunit/runtests.m b/ext/matlab_xunit/runtests.m new file mode 100644 index 000000000..74a2414a6 --- /dev/null +++ b/ext/matlab_xunit/runtests.m @@ -0,0 +1,160 @@ +function out = runtests(varargin) +%runtests Run unit tests +% runtests 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: +% +% * An M-file function whose name starts or ends with "test" or +% "Test" and that returns no output arguments. +% +% * An M-file function whose name starts or ends with "test" or +% "Test" and that contains subfunction tests and uses the +% initTestSuite script to return a TestSuite object. +% +% * An M-file defining a subclass of TestCase. +% +% runtests(dirname) runs all the test cases found in the specified directory. +% +% runtests(packagename) runs all the test cases found in the specified +% package. (This option requires R2009a or later). +% +% runtests(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. +% +% runtests('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 runtests, as in runtests(name1, name2, ...) or +% runtests({name1, name2, ...}, ...) +% +% runtests(..., '-verbose') displays the name and result, result, and time +% taken for each test case to the Command Window. +% +% runtests(..., '-logfile', filename) directs the output of runtests to +% the specified log file instead of to the Command Window. +% +% out = runtests(...) returns a logical value that is true if all the +% tests passed. +% +% Examples +% -------- +% Find and run all the test cases in the current directory. +% +% runtests +% +% Find and run all the test cases in the current directory. Display more +% detailed information to the Command Window as the test cases are run. +% +% runtests -verbose +% +% Save verbose runtests output to a log file. +% +% runtests -verbose -logfile my_test_log.txt +% +% Find and run all the test cases contained in the M-file myfunc. +% +% runtests myfunc +% +% Find and run all the test cases contained in the TestCase subclass +% MyTestCase. +% +% runtests MyTestCase +% +% Run the test case named 'testFeature' contained in the M-file myfunc. +% +% runtests myfunc:testFeature +% +% Run all the tests in a specific directory. +% +% runtests c:\Work\MyProject\tests +% +% Run all the tests in two directories. +% +% runtests c:\Work\MyProject\tests c:\Work\Book\tests + +% Steven L. Eddins +% Copyright 2009-2010 The MathWorks, Inc. + +verbose = false; +logfile = ''; +if nargin < 1 + suite = TestSuite.fromPwd(); +else + [name_list, verbose, logfile] = getInputNames(varargin{:}); + if numel(name_list) == 0 + suite = TestSuite.fromPwd(); + elseif numel(name_list) == 1 + suite = TestSuite.fromName(name_list{1}); + else + suite = TestSuite(); + for k = 1:numel(name_list) + suite.add(TestSuite.fromName(name_list{k})); + end + end +end + +if isempty(suite.TestComponents) + error('xunit:runtests:noTestCasesFound', 'No test cases found.'); +end + +if isempty(logfile) + logfile_handle = 1; % File handle corresponding to Command Window +else + logfile_handle = fopen(logfile, 'w'); + if logfile_handle < 0 + error('xunit:runtests:FileOpenFailed', ... + 'Could not open "%s" for writing.', logfile); + else + cleanup = onCleanup(@() fclose(logfile_handle)); + end +end + +fprintf(logfile_handle, 'Test suite: %s\n', suite.Name); +if ~strcmp(suite.Name, suite.Location) + fprintf(logfile_handle, 'Test suite location: %s\n', suite.Location); +end +fprintf(logfile_handle, '%s\n\n', datestr(now)); + +if verbose + monitor = VerboseTestRunDisplay(logfile_handle); +else + monitor = TestRunDisplay(logfile_handle); +end +did_pass = suite.run(monitor); + +if nargout > 0 + out = did_pass; +end + +function [name_list, verbose, logfile] = getInputNames(varargin) +name_list = {}; +verbose = false; +logfile = ''; +k = 1; +while k <= numel(varargin) + arg = varargin{k}; + if iscell(arg) + name_list = [name_list; arg]; + elseif ~isempty(arg) && (arg(1) == '-') + if strcmp(arg, '-verbose') + verbose = true; + elseif strcmp(arg, '-logfile') + if k == numel(varargin) + error('xunit:runtests:MissingLogfile', ... + 'The option -logfile must be followed by a filename.'); + else + logfile = varargin{k+1}; + k = k + 1; + end + else + warning('runtests:unrecognizedOption', 'Unrecognized option: %s', arg); + end + else + name_list{end+1} = arg; + end + k = k + 1; +end + diff --git a/interfaces/matlab/testpath.m b/interfaces/matlab/testpath.m index 1f16b0f03..c03a9cf6e 100644 --- a/interfaces/matlab/testpath.m +++ b/interfaces/matlab/testpath.m @@ -6,6 +6,9 @@ % cd ~/src/cantera % run interfaces/matlab/testpath.m +% Unload the mex file so copying the DLL will work +clear ctmethods + % get the list of directories on the Matlab path dirs = regexp(path, ['([^' pathsep ']*)'], 'match'); @@ -29,13 +32,18 @@ cantera_root = fullfile(pwd, '..', '..'); % Copy the Cantera shared library from the build directory if necessary if strcmp(getenv('OS'), 'Windows_NT') copyfile(fullfile(cantera_root, 'build', 'lib', 'cantera_shared.dll'), ... - fullfile(pwd, 'toolbox')) + fullfile(pwd, 'toolbox')) end % Set path to Python module setenv('PYTHONPATH', fullfile(cantera_root, 'interfaces', 'python')) % A simple test to make sure that the ctmethods.mex file is present and working -f = Func('polynomial', 3, [1,2,3,4]) +f = Func('polynomial', 3, [1,2,3,4]); +if f(1) == 10 + disp('Cantera MEX file successfully loaded.') +else + disp('Something is wrong with the Cantera MEX file.') +end adddir(fullfile(cantera_root, 'data', 'inputs')) diff --git a/src/matlab/SConscript b/src/matlab/SConscript index b2bb136d0..e7fd3f281 100644 --- a/src/matlab/SConscript +++ b/src/matlab/SConscript @@ -64,6 +64,8 @@ ctmethods = build(localenv.SharedLibrary('#interfaces/matlab/toolbox/ctmethods', LIBS=linklibs, LINKFLAGS=linkflags)) +env['matlab_extension'] = ctmethods + ### Install the Matlab toolbox ### # 'ctpath.m' diff --git a/test/SConscript b/test/SConscript index d81d4bcf7..a357e50ea 100644 --- a/test/SConscript +++ b/test/SConscript @@ -24,20 +24,6 @@ def testRunner(target, source, env): return code -def scriptRunner(target, source, env): - """Scons Action to run a test script using the specified interpreter""" - interpreter = env['test_interpreter'] - workDir = Dir('#test/work').abspath - if not os.path.isdir(workDir): - os.mkdir(workDir) - code = subprocess.call([interpreter, source[0].abspath], - env=env['ENV'], - cwd=workDir) - if not code: - open(target[0].path, 'w').write(time.asctime()+'\n') - return code - - def addTestProgram(subdir, progName): """ Compile a test program and create a targets for running @@ -57,8 +43,19 @@ def addTestScript(subdir, script, interpreter, dependencies): """ Create targets for running and resetting a test script. """ + def scriptRunner(target, source, env): + """Scons Action to run a test script using the specified interpreter""" + workDir = Dir('#test/work').abspath + if not os.path.isdir(workDir): + os.mkdir(workDir) + code = subprocess.call([interpreter, source[0].abspath], + env=env['ENV'], + cwd=workDir) + if not code: + open(target[0].path, 'w').write(time.asctime()+'\n') + return code + testenv = localenv.Clone() - testenv['test_interpreter'] = interpreter passedFile = File(pjoin(subdir, '%s.passed' % (script))) run_program = testenv.Command(passedFile, pjoin(subdir, script), scriptRunner) for dep in dependencies: @@ -70,8 +67,56 @@ def addTestScript(subdir, script, interpreter, dependencies): Alias('newtest-reset', testenv.Command('reset-%s%s' % (subdir, script), [], [Delete(passedFile.abspath)])) + +def addMatlabTest(script, dependencies=None): + def matlabRunner(target, source, env): + workDir = Dir('#test/work').abspath + if not os.path.isdir(workDir): + os.mkdir(workDir) + outfile = pjoin(workDir, 'matlab-results.txt') + runCommand = "%s('%s'); exit" % (source[0].name[:-2], outfile) + if os.name == 'nt': + matlabOptions = ['-nojvm','-nosplash','-wait'] + else: + matlabOptions = ['-nojvm','-nodisplay'] + os.remove(outfile) + code = subprocess.call([pjoin(env['matlab_path'], 'bin', 'matlab')] + + matlabOptions + ['-r', runCommand], + env=env['ENV'], cwd=Dir('#test/matlab').abspath) + results = open(outfile).read() + print '-------- Matlab test results --------' + print results + print '------ end Matlab test results ------' + if 'FAILED' in results: + return 1 + else: + open(target[0].path, 'w').write(time.asctime()+'\n') + + testenv = localenv.Clone() + passedFile = File(pjoin('matlab', '%s.passed' % (script))) + + run_program = testenv.Command(passedFile, pjoin('matlab', script), matlabRunner) + + dependencies = (dependencies or []) + localenv['matlab_extension'] + for dep in dependencies: + if isinstance(dep, str): + dep = File(pjoin('matlab', dep)) + testenv.Depends(run_program, dep) + + Alias('newtest', run_program) + if os.path.exists(passedFile.abspath): + Alias('newtest-reset', testenv.Command('reset-%s%s' % ('matlab', script), + [], [Delete(passedFile.abspath)])) + + # Instantiate tests addTestProgram('thermo', 'nasapoly') -addTestScript('python', 'runTests.py', - interpreter=sys.executable, - dependencies=['testSolution.py', localenv['python_module']]) + +if localenv['python_package'] == 'full': + addTestScript('python', 'runTests.py', + interpreter=sys.executable, + dependencies=['testSolution.py', localenv['python_module']]) + +if localenv['matlab_toolbox'] == 'y': + addMatlabTest('runCanteraTests.m', + dependencies=mglob(localenv, 'matlab', 'm')) diff --git a/test/matlab/runCanteraTests.m b/test/matlab/runCanteraTests.m new file mode 100644 index 000000000..7816f021a --- /dev/null +++ b/test/matlab/runCanteraTests.m @@ -0,0 +1,16 @@ +function runCanteraTests(varargin) +% Set Cantera paths +run ../../interfaces/matlab/testpath.m + +% Set test framework path +path('../../ext/matlab_xunit', path) + +% run the tests +if nargin == 0 + runtests -verbose +else + runtests('-verbose','-logfile',varargin{1}) +end + +% unload the MEX file +clear ctmethods diff --git a/test/matlab/testImport.m b/test/matlab/testImport.m new file mode 100644 index 000000000..348efe101 --- /dev/null +++ b/test/matlab/testImport.m @@ -0,0 +1,12 @@ +function test_suite = testImport +initTestSuite; + +function testImportXML +gas = importPhase('gri30.xml', 'gri30_mix'); +dkm = mixDiffCoeffs(gas); +assertEqual(length(dkm), nSpecies(gas)) + +function testImportCTI +gas = importPhase('h2o2.cti'); +assertEqual(temperature(gas), 300) +delete('h2o2.xml');