[Pkg-octave-commit] [octave-doctest] 01/02: Imported Upstream version 0.4.1
Rafael Laboissière
rlaboiss-guest at moszumanska.debian.org
Fri Sep 9 18:00:21 UTC 2016
This is an automated email from the git hooks/post-receive script.
rlaboiss-guest pushed a commit to branch master
in repository octave-doctest.
commit 11fa1634aafd3b07073c1989e2abca88fda26d17
Author: Rafael Laboissiere <rafael at debian.org>
Date: Sat Aug 20 16:19:11 2016 -0300
Imported Upstream version 0.4.1
---
CONTRIBUTORS | 9 +
COPYING | 29 ++
DESCRIPTION | 13 +
INDEX | 3 +
NEWS | 84 ++++++
README.md | 38 +++
inst/doctest.m | 304 +++++++++++++++++++++
inst/private/doctest_collect.m | 437 ++++++++++++++++++++++++++++++
inst/private/doctest_colors.m | 25 ++
inst/private/doctest_compare.m | 61 +++++
inst/private/doctest_default_directives.m | 36 +++
inst/private/doctest_run.m | 173 ++++++++++++
inst/private/is_octave.m | 24 ++
octave-doctest.metainfo.xml | 11 +
src/Makefile | 5 +
src/doctest_evalc.cc | 87 ++++++
test/@test_class/test_class.m | 16 ++
test/@test_class/test_method.m | 8 +
test/@test_classdef/amethod.m | 5 +
test/@test_classdef/test_classdef.m | 44 +++
test/@test_shadow/test_shadow.m | 9 +
test/examples/greet.m | 10 +
test/private/test_in_private_dir.m | 3 +
test/test_angle_brackets.m | 9 +
test/test_ans.m | 22 ++
test/test_ans.texinfo | 29 ++
test/test_blank_match.m | 5 +
test/test_comments.texinfo | 22 ++
test/test_compare_backspace.m | 19 ++
test/test_compare_hyperlinks.m | 7 +
test/test_diary_style.texinfo | 40 +++
test/test_diary_style_mixed.texinfo | 11 +
test/test_ellipsis.m | 20 ++
test/test_long_rows.m | 4 +
test/test_long_rows.texinfo | 4 +
test/test_multi_result.texinfo | 65 +++++
test/test_multi_return.texinfo | 46 ++++
test/test_no_docs.m | 4 +
test/test_not_an_mfile.txt | 9 +
test/test_shadow.m | 15 +
test/test_shadow/test_in_shadow_dir.m | 12 +
test/test_skip.m | 31 +++
test/test_skip_comments.texinfo | 23 ++
test/test_skip_if.m | 31 +++
test/test_skip_if_multiple.m | 19 ++
test/test_skip_malformed.texinfo | 7 +
test/test_skip_only_one.m | 4 +
test/test_skip_unless.m | 31 +++
test/test_var.texinfo | 10 +
test/test_warning.m | 6 +
test/test_whitespace.m | 53 ++++
test/test_windows_eol.texinfo | 20 ++
test/test_xfail.m | 14 +
test/test_xfail.texinfo | 5 +
test/test_xfail_if.m | 24 ++
test/test_xfail_if_multiple.m | 25 ++
test/test_xfail_unless.m | 24 ++
util/convert_comments.m | 314 +++++++++++++++++++++
58 files changed, 2418 insertions(+)
diff --git a/CONTRIBUTORS b/CONTRIBUTORS
new file mode 100644
index 0000000..c24f5fc
--- /dev/null
+++ b/CONTRIBUTORS
@@ -0,0 +1,9 @@
+Authors and Contributors
+========================
+
+Thomas Smith
+Michael Walter
+Colin B. Macdonald
+Oliver Heimlich
+
+(Please contact the developers if your name should be here but isn't!)
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..83ac386
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,29 @@
+Copyright (c) 2010 Thomas Grenfell Smith
+Copyright (c) 2011, 2013-2015 Michael Walter
+Copyright (c) 2015 Colin B. Macdonald
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+
+2. 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.
+
+3. Neither the name of the copyright holder 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 HOLDER 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/DESCRIPTION b/DESCRIPTION
new file mode 100644
index 0000000..ffd5a1e
--- /dev/null
+++ b/DESCRIPTION
@@ -0,0 +1,13 @@
+Name: doctest
+Version: 0.4.1
+Date: 2016-01-04
+Author: various authors
+Maintainer: Colin B. Macdonald <cbm at m.fsf.org>, Michael Walter <michael.walter at gmail.com>
+Title: Documentation tests
+Description: The Octave-Forge Doctest package finds specially-formatted
+ blocks of example code within documentation files. It then executes
+ the code and confirms the output is correct. This can be useful as part of
+ a testing framework or simply to ensure that documentation stays up-to-date
+ during software development.
+Url: https://github.com/catch22/octave-doctest
+License: modified BSD
diff --git a/INDEX b/INDEX
new file mode 100644
index 0000000..92c9176
--- /dev/null
+++ b/INDEX
@@ -0,0 +1,3 @@
+doctest >> Documentation tests
+testing
+ doctest
diff --git a/NEWS b/NEWS
new file mode 100644
index 0000000..0fbd5d8
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,84 @@
+doctest 0.4.1 (2016-01-04)
+==========================
+
+ * Added conditional variants of SKIP and XFAIL directives to control test
+ execution based on runtime conditions:
+
+ - "% doctest: +SKIP_IF(condition)"
+ - "% doctest: +SKIP_UNLESS(condition)"
+ - "% doctest: +XFAIL_IF(condition)"
+ - "% doctest: +XFAIL_UNLESS(condition)"
+
+ * Added constants DOCTEST_OCTAVE and DOCTEST_MATLAB that can be used as
+ conditions in SKIP_IF etc.
+
+ * Improved handling of example code in TexInfo documentation.
+
+ - Added support for @print{} macros, which may be used for output that
+ is not part of a returned value.
+
+ - Examples without ">>" markers use code indentation together with
+ @result{} / @print{} macros to classify input and output lines in a
+ natural way. It is no longer necessary to split code into several
+ @example / @group blocks.
+
+ - Allow arbitrary TexInfo macros. The documentation is interpreted
+ by makeinfo before running the code examples.
+
+ - Fixed handling of TexInfo files with Windows line endings.
+
+ * Improved folder/directory traversals:
+
+ - Ignore hidden (dot) directories.
+
+ - Ignore files that are neither m-files nor texinfo.
+
+
+
+doctest 0.4.0 (2015-07-02)
+==========================
+
+ * Change doctest interface to be closer to Octave's test function.
+
+ * Change wildcard string from '***' to '...'.
+
+ * Doctests can be influenced with directives:
+
+ - mark tests to be skipped by appending "% doctest: +SKIP".
+
+ - mark tests expected to fail with "% doctest: +XFAIL".
+
+ - stricter whitespace matching: "% doctest: -NORMALIZE_WHITESPACE".
+
+ - disable "..." wildcard matching with "% doctest: -ELLIPSIS".
+
+ * Support "doctest foldername" to run tests on the files/classes within
+ the folder/directory "foldername". With optional recursion.
+
+ * Improve evalc implementation on Octave.
+
+ * Other bug fixes.
+
+
+
+doctest 0.3.0 (2015-05-12)
+==========================
+
+ * Multiline input now works (e.g., a matrix split across lines).
+
+ * Allow "ans = " to be omitted.
+
+ * Pure texinfo files can be tested: "doctest myfile.texinfo".
+
+ * Other bug fixes.
+
+ * Support and directory structure for being an Octave package.
+
+
+
+doctest 0.2.0 (2015-04-06)
+==========================
+
+ * Octave support, including examples in Texinfo blocks.
+
+ * Return the number of tests and number failed.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ff6ab90
--- /dev/null
+++ b/README.md
@@ -0,0 +1,38 @@
+Doctest [](https://travis-ci.org/catch22/octave-doctest)
+=======
+
+The [Octave-Forge Doctest](http://octave.sourceforge.net/doctest/) package finds specially-formatted blocks of example code within documentation files.
+It then executes the code and confirms the output is correct.
+This can be useful as part of a testing framework or simply to ensure that documentation stays up-to-date during software development.
+
+To get started, here is a simple example:
+
+~~~matlab
+function greeting = greet(user)
+% Returns a greeting.
+%
+% >> greet World
+%
+% Hello, World!
+
+greeting = ['Hello, ' user '!'];
+
+end
+~~~
+
+We can test it by invoking `doctest greet` at the Octave prompt, which will give the following output:
+
+~~~
+greet .................................................. PASS 1/1
+
+Summary:
+
+ PASS 1/1
+
+1/1 targets passed, 0 without tests.
+~~~
+
+Doctest also supports Texinfo markup, which is [quite popular](https://www.gnu.org/software/octave/doc/interpreter/Documentation-Tips.html) in the Octave world, and it provides various toggles and switches for customizing its behavior.
+The [Doctest documentation](http://octave.sourceforge.net/doctest/function/doctest.html) contains information on all this.
+Quite appropriately, Doctest can test its own documentation.
+We also maintain a [list of software](https://github.com/catch22/octave-doctest/wiki/WhoIsUsingDoctest) that is using Doctest.
diff --git a/inst/doctest.m b/inst/doctest.m
new file mode 100644
index 0000000..bf4854e
--- /dev/null
+++ b/inst/doctest.m
@@ -0,0 +1,304 @@
+%% Copyright (c) 2010 Thomas Grenfell Smith
+%% Copyright (c) 2011, 2013-2015 Michael Walter
+%% Copyright (c) 2015-2016 Colin B. Macdonald
+%%
+%% Redistribution and use in source and binary forms, with or without
+%% modification, are permitted provided that the following conditions are met:
+%%
+%% 1. Redistributions of source code must retain the above copyright notice,
+%% this list of conditions and the following disclaimer.
+%%
+%% 2. 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.
+%%
+%% 3. Neither the name of the copyright holder 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 HOLDER 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.
+
+%% -*- texinfo -*-
+%% @documentencoding UTF-8
+%% @deftypefn {Function File} {} doctest @var{target}
+%% @deftypefnx {Function File} {} doctest @var{target} -recursive
+%% @deftypefnx {Function File} {} doctest @var{target} -DIRECTIVE
+%% @deftypefnx {Function File} {} doctest @var{target} +DIRECTIVE
+%% @deftypefnx {Function File} {@var{success} =} doctest (@var{target}, @dots{})
+%% @deftypefnx {Function File} {[@var{numpass}, @var{numtests}, @var{summary]} =} doctest (@dots{})
+%% Run examples embedded in documentation.
+%%
+%% Doctest finds and runs code found in @var{target}, which can be a:
+%% @itemize
+%% @item function;
+%% @item class;
+%% @item Texinfo file;
+%% @item directory/folder (pass @code{-recursive} to descend
+%% into subfolders);
+%% @item cell array of such items.
+%% @end itemize
+%% When called with a single return value, return whether all tests have
+%% succeeded (@var{success}).
+%%
+%% When called with two or more return values, return the number of tests
+%% passed (@var{numpass}), the total number of tests (@var{numtests}) and a
+%% structure @var{summary} with various fields.
+%%
+%%
+%% Doctest finds example blocks, executes the code and verifies that the
+%% results match the expected output. For example, running
+%% @code{doctest doctest} will execute this code:
+%%
+%% @example
+%% @group
+%% >> 1 + 3
+%% ans =
+%% 4
+%% @end group
+%% @end example
+%%
+%% If there's no output, just put the next line right after the one with
+%% no output. If the line does produce output (for instance, an error),
+%% this will be recorded as a test failure.
+%%
+%% @example
+%% @group
+%% >> x = 3 + 4;
+%% >> x
+%% x =
+%% 7
+%% @end group
+%% @end example
+%%
+%%
+%% @strong{Wildcards}
+%% You can use a wildcard to match unpredictable output:
+%%
+%% @example
+%% @group
+%% >> datestr(now, 'yyyy-mm-dd')
+%% 2...
+%% @end group
+%% @end example
+%%
+%% @strong{Expecting an error}
+%% Doctest can deal with errors, to some extent. For instance, this case is
+%% handled correctly:
+%%
+%% @example
+%% @group
+%% >> not_a_real_function(42)
+%% ??? ...ndefined ...
+%% @end group
+%% @end example
+%% (Note use of wildcards here; MATLAB spells this 'Undefined', while Octave
+%% uses 'undefined').
+%%
+%% However, currently this does not work if the code emits other output
+%% @strong{before} the error message. Warnings are different; they work
+%% fine.
+%%
+%%
+%% @strong{Multiple lines of code}
+%% Code spanning multiple lines can be entered by prefixing all subsequent
+%% lines with @code{..}, e.g.,
+%%
+%% @example
+%% @group
+%% >> for i = 1:3
+%% .. i
+%% .. end
+%% i = 1
+%% i = 2
+%% i = 3
+%% @end group
+%% @end example
+%% (But note this is not required when writing texinfo documentation,
+%% see below).
+%%
+%%
+%% @strong{Shortcuts}
+%% You can optionally omit @code{ans = } when the output is unassigned. But
+%% actual variable names (such as @code{x = }) must be included. Leading
+%% and trailing whitespace on each line of output will be discarded which
+%% gives some freedom to, e.g., indent the code output as you wish.
+%%
+%%
+%% @strong{Directives}
+%% You can skip certain tests by marking them with a special comment. This
+%% can be used, for example, for a test not expected to pass or to avoid
+%% opening a figure window during automated testing.
+%%
+%% @example
+%% @group
+%% >> a = 6 % doctest: +SKIP
+%% b = 42
+%% >> plot(...) % doctest: +SKIP
+%% @end group
+%% @end example
+%%
+%%
+%% These special comments act as directives for modifying test behaviour.
+%% You can also mark tests that you expect to fail:
+%%
+%% @example
+%% @group
+%% >> a = 6 % doctest: +XFAIL
+%% b = 42
+%% @end group
+%% @end example
+%%
+%% Both the @code{+SKIP} and the @code{+XFAIL} directives have conditional
+%% variants (e.g., @code{+SKIP_IF} and @code{+SKIP_UNLESS}) that control
+%% test execution and expectations based on runtime conditions, such as
+%% the platform, operating systems, or installed packages:
+%%
+%% @example
+%% @group
+%% >> "shiny Octave feature" % doctest: +XFAIL_IF(DOCTEST_MATLAB)
+%% ans = shiny Octave feature
+%% @end group
+%% @end example
+%%
+%% Doctest provides the default flags @code{DOCTEST_OCTAVE} and
+%% @code{DOCTEST_MATLAB}, but you can access arbitrary variables and
+%% (nullary) functions.
+%%
+%%
+%% By default, all adjacent white space is collapsed into a single space
+%% before comparison. A stricter mode where ``internal whitespace'' must
+%% match is available:
+%%
+%% @example
+%% @group
+%% >> fprintf('a b\nc d\n') % doctest: -NORMALIZE_WHITESPACE
+%% a b
+%% c d
+%%
+%% >> fprintf('a b\nc d\n') % doctest: +NORMALIZE_WHITESPACE
+%% a b
+%% c d
+%% @end group
+%% @end example
+%%
+%%
+%% To disable the @code{...} wildcard, use the @code{-ELLIPSIS} directive.
+%%
+%% The default directives can be overridden on the command line using, for
+%% example, @code{doctest target -NORMALIZE_WHITESPACE +ELLIPSIS}. Note that
+%% directives local to a test still take precident over these.
+%%
+%%
+%% @strong{Diary Style}
+%% When the m-file contains plaintext documentation, doctest finds tests
+%% by searching for lines that begin with @code{>>}. It then finds the
+%% expected output by searching for the next @code{>>} or two blank lines.
+%%
+%% @strong{Octave/Texinfo Style}
+%% If your m-file contains Texinfo markup, then doctest finds code inside
+%% @code{@@example @dots{} @@end example} blocks. Some comments:
+%% @itemize
+%% @item The two-blank-lines convention is not required.
+%% @item The use of @code{>>} is not required as Octave documentation
+%% conventionally indicates output with @code{@@print} and
+%% @code{@@result}. Ambiguities are resolving by assuming output
+%% is indented further than input.
+%% @item You are free to use diary-style doctests inside
+%% @code{@@example} blocks.
+%% @end itemize
+%%
+%% @seealso{test}
+%% @end deftypefn
+
+function varargout = doctest(what, varargin)
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Process parameters.
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+% print usage?
+if nargin < 1
+ help doctest;
+ return;
+end
+
+% if given a single object, wrap it in a cell array
+if ~iscell(what)
+ what = {what};
+end
+
+% input parsing for options and directives
+recursive = false;
+directives = doctest_default_directives();
+for i = 1:(nargin-1)
+ assert(ischar(varargin{i}))
+ pm = varargin{i}(1);
+ directive = varargin{i}(2:end);
+ switch directive
+ case 'recursive'
+ assert(strcmp(pm, '-'))
+ recursive = true;
+ otherwise
+ assert(strcmp(pm, '+') || strcmp(pm, '-'))
+ enable = strcmp(varargin{i}(1), '+');
+ directives = doctest_default_directives(directives, directive, enable);
+ end
+end
+
+% for now, always print to stdout
+fid = 1;
+
+% get terminal color codes
+[color_ok, color_err, color_warn, reset] = doctest_colors(fid);
+
+% print banner
+fprintf(fid, 'Doctest v0.4.1: this is Free Software without warranty, see source.\n\n');
+
+
+summary = struct();
+summary.num_targets = 0;
+summary.num_targets_passed = 0;
+summary.num_targets_without_tests = 0;
+summary.num_targets_with_extraction_errors = 0;
+summary.num_tests = 0;
+summary.num_tests_passed = 0;
+
+
+for i=1:numel(what)
+ summary = doctest_collect(what{i}, directives, summary, recursive, fid);
+end
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Report summary
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+fprintf(fid, '\nSummary:\n\n');
+if (summary.num_tests_passed == summary.num_tests)
+ fprintf(fid, [' ' color_ok 'PASS %4d/%-4d' reset '\n\n'], summary.num_tests_passed, summary.num_tests);
+else
+ fprintf(fid, [' ' color_err 'FAIL %4d/%-4d' reset '\n\n'], summary.num_tests - summary.num_tests_passed, summary.num_tests);
+end
+
+fprintf(fid, '%d/%d targets passed, %d without tests', summary.num_targets_passed, summary.num_targets, summary.num_targets_without_tests);
+if summary.num_targets_with_extraction_errors > 0
+ fprintf(fid, [', ' color_err '%d with extraction errors' reset], summary.num_targets_with_extraction_errors);
+end
+fprintf(fid, '.\n\n');
+
+if nargout == 1
+ varargout = {summary.num_targets_passed == summary.num_targets};
+elseif nargout > 1
+ varargout = {summary.num_tests_passed, summary.num_tests, summary};
+end
+
+end
diff --git a/inst/private/doctest_collect.m b/inst/private/doctest_collect.m
new file mode 100644
index 0000000..76bf74a
--- /dev/null
+++ b/inst/private/doctest_collect.m
@@ -0,0 +1,437 @@
+function summary = doctest_collect(what, directives, summary, recursive, fid)
+% Find and run doctests.
+%
+% The parameter WHAT is the name of a class, directory, function or filename:
+% * For a directory, calls itself on the contents, recursively if
+% RECURSIVE is true;
+% * For a class, all methods are tested;
+% * When running Octave, it can also be the filename of a Texinfo file.
+%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+% TODO: methods('logical') octave/matlab differ: which behaviour do we want?
+% TODO: what about builtin "test" versus dir "test/"? Do we prefer dir?
+
+% determine type of target
+if is_octave()
+ % Note: ripe for refactoring once "exist(what, 'class')" works in Octave.
+ [~, ~, ext] = fileparts(what);
+ if any(strcmpi(ext, {'.texinfo' '.texi' '.txi' '.tex'}))
+ type = 'texinfo';
+ elseif (exist(what, 'file') && ~exist(what, 'dir')) || exist(what, 'builtin');
+ if (exist(['@' what], 'dir'))
+ % special case, e.g., @logical is class, logical is builtin
+ type = 'class';
+ else
+ type = 'function';
+ end
+ elseif (strcmp(what(1), '@'))
+ % comes after 'file' above for "doctest @class/method"
+ type = 'class';
+ elseif (exist(what, 'dir'))
+ type = 'dir';
+ elseif exist(what) == 2 || exist(what) == 103
+ % Notes:
+ % * exist('@class', 'dir') only works if pwd is the parent of
+ % '@class', having it in the path is not sufficient.
+ % * Return 2 on Octave 3.8 and 103 on Octave 4.
+ type = 'class';
+ else
+ % classdef classes are not detected by any of the above
+ try
+ temp = methods(what);
+ type = 'class';
+ catch
+ type = 'unknown';
+ end
+ end
+else % Matlab
+ if (strcmp(what(1), '@')) && ~isempty(methods(what(2:end)))
+ % covers "doctest @class", but not "doctest @class/method"
+ type = 'class';
+ elseif ~isempty(methods(what))
+ % covers "doctest class"
+ type = 'class';
+ elseif (exist(what, 'dir'))
+ type = 'dir';
+ elseif exist(what, 'file') || exist(what, 'builtin');
+ type = 'function';
+ elseif ~isempty(help(what))
+ % covers "doctest class.method" and "doctest class/method"
+ type = 'function'
+ else
+ type = 'unknown';
+ end
+ % Note: ambiguous what happens for "doctest @class/method"... as it is
+ % for "help @class/method", e.g., "help @class/class" does not give the
+ % constructor's help.
+end
+
+
+% Deal with directories
+if (strcmp(type, 'dir'))
+ %if (~ strcmp(what, '.'))
+ % fprintf(fid, 'Descending into directory "%s"\n', what);
+ %end
+ oldcwd = chdir(what);
+ files = dir('.');
+ for i=1:numel(files)
+ f = files(i).name;
+ if (exist(f, 'dir'))
+ if (strcmp(f, '.') || strcmp(f, '..'))
+ % skip "." and ".."
+ continue
+ elseif (strcmp(f(1), '@'))
+ % class, don't skip if nonrecursive
+ elseif (~ recursive)
+ % skip all directories
+ continue
+ elseif (strcmp(f(1), '.'))
+ %fprintf(fid, 'Ignoring hidden directory "%s"\n', f)
+ continue
+ end
+ else
+ [~, ~, ext] = fileparts(f);
+ if (~ any(strcmpi(ext, {'.m' '.texinfo' '.texi' '.txi' '.tex'})))
+ %fprintf(fid, 'Debug: ignoring file "%s"\n', f)
+ continue
+ end
+ end
+ summary = doctest_collect(f, directives, summary, recursive, fid);
+ end
+ chdir(oldcwd);
+ return
+end
+
+
+
+% Build structure array with the following fields:
+% TARGETS(i).name Human-readable name of test.
+% TARGETS(i).link Hyperlink to test for use in Matlab.
+% TARGETS(i).docstring Associated docstring.
+% TARGETS(i).error: Contains error string if extraction failed.
+
+if strcmp(type, 'function')
+ targets = collect_targets_function(what);
+elseif strcmp(type, 'class')
+ targets = collect_targets_class(what);
+elseif strcmp(type, 'texinfo')
+ target = struct();
+ target.name = what;
+ target.link = '';
+ [target.docstring, target.error] = parse_texinfo(fileread(what));
+ targets = [target];
+else
+ target = struct();
+ target.name = what;
+ target.link = '';
+ target.docstring = '';
+ target.error = 'Function or class not found.';
+ targets = [target];
+end
+
+
+% update summary
+summary.num_targets = summary.num_targets + numel(targets);
+
+% get terminal color codes
+[color_ok, color_err, color_warn, reset] = doctest_colors(fid);
+
+
+for i=1:numel(targets)
+ % run doctests for target and update statistics
+ target = targets(i);
+ fprintf(fid, '%s %s ', target.name, repmat('.', 1, 55 - numel(target.name)));
+
+ % extraction error?
+ if target.error
+ summary.num_targets_with_extraction_errors = summary.num_targets_with_extraction_errors + 1;
+ fprintf(fid, [color_err 'EXTRACTION ERROR' reset '\n\n']);
+ fprintf(fid, ' %s\n\n', target.error);
+ continue;
+ end
+
+ % run doctest
+ results = doctest_run(target.docstring, directives);
+
+ % determine number of tests passed
+ num_tests = numel(results);
+ num_tests_passed = 0;
+ for j=1:num_tests
+ if results(j).passed
+ num_tests_passed = num_tests_passed + 1;
+ end
+ end
+
+ % update summary
+ summary.num_tests = summary.num_tests + num_tests;
+ summary.num_tests_passed = summary.num_tests_passed + num_tests_passed;
+ if num_tests_passed == num_tests
+ summary.num_targets_passed = summary.num_targets_passed + 1;
+ end
+ if num_tests == 0
+ summary.num_targets_without_tests = summary.num_targets_without_tests + 1;
+ end
+
+ % pretty print outcome
+ if num_tests == 0
+ fprintf(fid, 'NO TESTS\n');
+ elseif num_tests_passed == num_tests
+ fprintf(fid, [color_ok 'PASS %4d/%-4d' reset '\n'], num_tests_passed, num_tests);
+ else
+ fprintf(fid, [color_err 'FAIL %4d/%-4d' reset '\n\n'], num_tests - num_tests_passed, num_tests);
+ for j = 1:num_tests
+ if ~results(j).passed
+ fprintf(fid, ' >> %s\n\n', results(j).source);
+ fprintf(fid, [ ' expected: ' '%s' '\n' ], results(j).want);
+ fprintf(fid, [ ' got : ' color_err '%s' reset '\n' ], results(j).got);
+ if results(j).xfail
+ fprintf(fid, ' expected failure, but test succeeded!');
+ end
+ fprintf(fid, '\n');
+ end
+ end
+ end
+end
+
+end
+
+
+
+function target = collect_targets_function(what)
+ target = struct();
+ target.name = what;
+ if is_octave()
+ target.link = '';
+ else
+ target.link = sprintf('<a href="matlab:editorservices.openAndGoToLine(''%s'', 1);">%s</a>', which(what), what);
+ end
+ [target.docstring, target.error] = extract_docstring(target.name);
+end
+
+
+function targets = collect_targets_class(what)
+ if (strcmp(what(1), '@'))
+ % Octave methods('@foo') gives java error, Matlab just says "No methods"
+ what = what(2:end);
+ end
+ % First, "help class". For classdef, this differs from "help class.class"
+ % (general class help vs constructor help). For old-style classes we will
+ % probably end up testing the constructor twice but... meh.
+ target.name = what;
+ if is_octave()
+ target.link = '';
+ else
+ target.link = sprintf('<a href="matlab:editorservices.openAndGoToLine(''%s'', 1);">%s</a>', which(what), what);
+ end
+ [target.docstring, target.error] = extract_docstring(target.name);
+ targets = target;
+
+ % Next, add targets for all class methods
+ meths = methods(what);
+ for i=1:numel(meths)
+ target = struct();
+ if is_octave()
+ target.name = sprintf('@%s%s%s', what, filesep(), meths{i});
+ target.link = '';
+ else
+ target.name = sprintf('%s.%s', what, meths{i});
+ target.link = sprintf('<a href="matlab:editorservices.openAndGoToFunction(''%s'', ''%s'');">%s</a>', which(what), meths{i}, target.name);
+ end
+ [target.docstring, target.error] = extract_docstring(target.name);
+ targets = [targets; target];
+ end
+end
+
+
+function [docstring, error] = extract_docstring(name)
+ if is_octave()
+ [docstring, format] = get_help_text(name);
+ if strcmp(format, 'texinfo')
+ [docstring, error] = parse_texinfo(docstring);
+ elseif strcmp(format, 'plain text')
+ error = '';
+ elseif strcmp(format, 'Not documented')
+ assert (isempty (docstring))
+ error = '';
+ elseif strcmp(format, 'Not found')
+ % looks like "doctest test_no_docs.m" gets us here: octave bug?
+ if (regexp(name,'\.m$'))
+ assert (isempty (docstring))
+ error = '';
+ else
+ assert (isempty (docstring))
+ error = 'Not an m file.';
+ end
+ else
+ format
+ warning('Unexpected format in that file/function');
+ error = '';
+ end
+ else
+ docstring = help(name);
+ error = '';
+ end
+end
+
+
+function [docstring, error] = parse_texinfo(str)
+ docstring = '';
+ error = '';
+
+ % no example blocks? not an error, but nothing to do
+ if (isempty(strfind(str, '@example')))
+ % error = 'no @example blocks';
+ return
+ end
+
+ % Normalize line endings in files which have been edited in Windows
+ % This simplifies the regular expressions below.
+ str = strrep (str, sprintf ('\r\n'), sprintf ('\n'));
+
+ % The subsequent regexprep would fail if the example block is located right
+ % at the beginning of the file. This is probably a bug in regexprep and is
+ % only possible inside included texinfo files.
+ if (isempty (regexp (str, '^\s', 'once')))
+ str = cstrcat (sprintf ('\n'), str);
+ end
+
+ % Mark the occurrence of “@example” and “@end example” to be able to find
+ % example blocks after conversion from texi to plain text. Also consider
+ % indentation, so we can later correctly unindent the example's content.
+ str = regexprep (str, ...
+ '^([ \t]*)(@example)(.*)$', ...
+ [ '$1$2$3\n', ... % retain original line
+ '$1###### EXAMPLE START ######'], ...
+ 'lineanchors', 'dotexceptnewline', 'emptymatch');
+ str = regexprep (str, ...
+ '^([ \t]*)(@end example)(.*)$', ...
+ [ '$1###### EXAMPLE STOP ######\n', ...
+ '$1$2$3'], ... % retain original line
+ 'lineanchors', 'dotexceptnewline', 'emptymatch');
+
+ % special comments "@c doctest: cmd" are translated
+ % FIXME the expression would also match @@c doctest: ...
+ re = [ '(?:@c(?:omment)?\s' ... % @c or @comment, ?: means no token
+ '|#|%)\s*' ... % or one of #,%
+ '(doctest:\s*.*)' ]; % want the doctest token
+ str = regexprep (str, re, '% $1', 'dotexceptnewline');
+
+ % We use eval to not produce compile errors in Matlab,
+ % the __makeinfo__ function exists in Octave only.
+ [str, err] = eval('__makeinfo__ (str, ''plain text'')');
+ if (err ~= 0)
+ error = '__makeinfo__ returned with error code'
+ return
+ end
+
+ % Normalize end of line characters again. __makeinfo__ returns end of line
+ % characters depending on the current OS. Since we want Unix line endings,
+ % the conversion is only required under Windows.
+ if (ispc ())
+ str = strrep (str, sprintf ('\r\n'), sprintf ('\n'));
+ end
+
+ % extract examples and discard everything else
+ T = regexp (str, ...
+ [ '(^[ \t]*###### EXAMPLE START ######', ...
+ '.*?', ...
+ '###### EXAMPLE STOP ######$)'], ...
+ 'tokens', 'lineanchors');
+ if (isempty (T))
+ error = 'malformed @example blocks';
+ return
+ end
+
+ % post-process each example block
+ for i = 1 : length (T)
+ % flatten
+ assert (numel (T{i}), 1);
+ T{i} = T{i}{1};
+
+ % unindent
+ indent = regexp (T{i}, '#', 'once') - 1;
+ T{i} = regexprep (T{i}, sprintf ('^[ \t]{%d}', indent), '', 'lineanchors');
+
+ % remove EXAMPLE markers
+ T{i} = regexprep (T{i}, ...
+ '[ \t]*###### EXAMPLE ST(?:ART|OP) ######(?:\n|$)', ...
+ '');
+
+ if (regexp (T{i}, '^\s*$', 'once', 'emptymatch'))
+ error = 'empty @example blocks';
+ return
+ end
+
+ % split into lines
+ L = strsplit (T{i}, '\n');
+
+ if (regexp (T{i}, '^\s*>>', 'once'))
+ % First nonblank line starts with '>>': assume diary style. However,
+ % we strip @result and @print macros (TODO: perhaps unwisely?)
+ L = regexprep (L, '^(\s*)(?:⇒|=>|⊣|-\|)', '$1', 'once', 'lineanchors');
+ T{i} = strjoin (L, '\n');
+ continue
+ end
+
+ % Categorize input and output lines in the example using
+ % @result and @print macros. Everything else, including comment lines and
+ % empty lines, is categorized as input (for now).
+ Linput = cellfun ('isempty', regexp (L, '^\s*(⇒|=>|⊣|-\|)', 'once'));
+
+ if (not (Linput (1)))
+ error = 'no command: @result on first line?';
+ return
+ end
+
+ % Output lines may be wrapped or output goes over several lines and not
+ % every line is preceded by “=>”.
+ indent = regexp(L, '\S', 'once');
+ indent(cellfun ('isempty', indent)) = inf;
+ indent = [indent{:}] - 1;
+ row = 1;
+ while (row < numel (L))
+ begin_of_input = row;
+ begin_of_output = row + find (not (Linput(row + 1 : end)), 1);
+ if (isempty (begin_of_output))
+ begin_of_output = numel (L) + 1;
+ end
+ end_of_input = begin_of_output - 1;
+
+ % determine minimum indentation of input lines
+ min_indent = min (indent(begin_of_input : end_of_input));
+
+ % Find next input line with an equal or less indentation to determine the
+ % end of the output.
+ row = begin_of_output ...
+ + find (Linput(begin_of_output + 1: end) ...
+ & (indent(begin_of_output + 1: end) <= min_indent), ...
+ 1);
+ if (isempty (row))
+ row = numel (L) + 1;
+ end
+ end_of_output = row - 1;
+
+ if (end_of_output <= numel (L))
+ Linput (begin_of_output : end_of_output) = false;
+ end
+
+ % Mark verified input lines as such
+ L{begin_of_input} = ['>> ' L{begin_of_input}];
+ L(begin_of_input + 1 : end_of_input) = ...
+ cellfun (@(s) ['.. ' s], L(begin_of_input + 1 : end_of_input), ...
+ 'UniformOutput', false);
+ end
+
+ % strip @result and @print macro output
+ Loutput = not (Linput);
+ L(Loutput) = regexprep (L(Loutput), ...
+ '^(\s*)(?:⇒|=>|⊣|-\|)', ...
+ '$1', ...
+ 'once', 'lineanchors');
+
+ T{i} = strjoin (L, '\n');
+ end
+
+ docstring = strjoin (T, '\n');
+end
diff --git a/inst/private/doctest_colors.m b/inst/private/doctest_colors.m
new file mode 100644
index 0000000..1b643a7
--- /dev/null
+++ b/inst/private/doctest_colors.m
@@ -0,0 +1,25 @@
+function [color_ok, color_err, color_warn, reset] = doctest_colors(fid)
+% Return terminal color codes to use for current invocation of doctest.
+%
+% FIXME: Shouldn't use colors if stdout is not a TTY.
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+% by default, no colors
+color_ok = '';
+color_err = '';
+color_warn = '';
+reset = '';
+
+% only use colors in Octave, when printing to stdout, and when terminal supports colors
+if (is_octave())
+ have_colorterm = index(getenv('TERM'), 'color') > 0;
+ if fid == stdout && have_colorterm
+ % hide terminal escapes from Matlab
+ color_ok = eval('"\033[1;32m"'); % green
+ color_err = eval('"\033[1;31m"'); % red
+ color_warn = eval('"\033[1;35m"'); % purple
+ reset = eval('"\033[m"');
+ end
+end
+
+end
diff --git a/inst/private/doctest_compare.m b/inst/private/doctest_compare.m
new file mode 100644
index 0000000..91a8087
--- /dev/null
+++ b/inst/private/doctest_compare.m
@@ -0,0 +1,61 @@
+function match = doctest_compare(want, got, normalize_whitespace, ellipsis)
+% Matches two strings together. They should be identical, except:
+%
+% * multiple spaces are collapsed (if NORMALIZE_WHITESPACE is true);
+% * the first one can contain '...', which matches anything in the
+% second (if ELLIPSIS is true);
+% * they might match after putting "ans = " on the first;
+% * various other nonsense of unknown current relevance.
+%
+
+% This looks bad, like hardcoding for lower-case "a href"
+% and a double quote... but that's what MATLAB looks for too.
+got = regexprep(got, '<a +href=".*?>', '');
+got = regexprep(got, '</a>', '');
+
+% WHY do they need backspaces? huh.
+got = regexprep(got, '.\x08', '');
+
+% collapse multiple spaces to one
+if normalize_whitespace
+ want = strtrim(regexprep(want, '\s+', ' '));
+ got = strtrim(regexprep(got, '\s+', ' '));
+else
+ want = strtrim(strtrim_lines_discard_empties(want));
+ got = strtrim(strtrim_lines_discard_empties(got));
+end
+
+if isempty(got) && (isempty(want) || (ellipsis && strcmp(want, '...')))
+ match = 1;
+ return
+end
+
+want_re = regexptranslate('escape', want);
+if ellipsis
+ want_re = regexprep(want_re, '(\\\.){3}', '.*');
+end
+
+% allow "ans = " to be missing
+want_re = ['^(ans\s*=\s*)?' want_re '$'];
+
+
+result = regexp(got, want_re, 'once');
+
+match = ~ isempty(result);
+
+end
+
+
+function r = strtrim_lines_discard_empties(s)
+ lines = strsplit(s, '\n');
+
+ keep = true(size(lines));
+ for j = 1:length(lines)
+ lines{j} = strtrim(lines{j});
+ if (isempty(lines{j}))
+ keep(j) = false;
+ end
+ end
+ lines = lines(keep);
+ r = strjoin(lines, '');
+end
diff --git a/inst/private/doctest_default_directives.m b/inst/private/doctest_default_directives.m
new file mode 100644
index 0000000..5891b9a
--- /dev/null
+++ b/inst/private/doctest_default_directives.m
@@ -0,0 +1,36 @@
+function d = doctest_default_directives(varargin)
+%DOCTEST_DEFAULT_DIRECTIVES Return/set defaults directives
+% Possible calling forms:
+% dirs = doctest_default_directives()
+% dirs = doctest_default_directives('ellipsis', true)
+% dirs = doctest_default_directives(dirs, 'ellipsis', true)
+% See source/documentation for valid directives.
+
+ defaults.normalize_whitespace = true;
+ defaults.ellipsis = true;
+
+ if (nargin == 0)
+ d = defaults;
+ return
+ elseif (nargin == 2)
+ d = defaults;
+ directive = varargin{1};
+ enable = varargin{2};
+ elseif (nargin == 3)
+ d = varargin{1};
+ directive = varargin{2};
+ enable = varargin{3};
+ else
+ error('invalid input')
+ end
+
+ switch directive
+ case 'ELLIPSIS'
+ d.ellipsis = enable;
+ case 'NORMALIZE_WHITESPACE'
+ d.normalize_whitespace = enable;
+ otherwise
+ error('invalid directive "%s"', directive)
+ end
+
+end
diff --git a/inst/private/doctest_run.m b/inst/private/doctest_run.m
new file mode 100644
index 0000000..a7ef8f9
--- /dev/null
+++ b/inst/private/doctest_run.m
@@ -0,0 +1,173 @@
+function results = doctest_run(docstring, defaults)
+%DOCTEST_RUN - used internally by doctest
+%
+% Usage:
+% doctest_run(docstring)
+% Runs all the examples in the given docstring and returns a
+% structure with the results from running.
+%
+% The return value is a structure with the following fields:
+%
+% results.source: the source code that was run
+% results.want: the desired output
+% results.got: the output that was recieved
+% results.passed: whether .want and .got match each other according to
+% doctest_compare.
+%
+
+% extract tests from docstring
+TEST_RE = [ % loosely based on Python 2.6 doctest.py, line 510
+ '(?m)(?-s)' ... % options
+ '(?:^ *>> )' ... % ">> "
+ '(.*(?:\n *\.\. .*)*)\n' ... % rest of line + ".. " lines
+ '((?:(?:^ *$\n)?(?!\s*>>).*\S.*\n)*)']; % the output
+
+tests = [];
+test_matches = regexp(docstring, TEST_RE, 'tokens');
+for i=1:length(test_matches)
+ % each block should be split into source and desired output
+ source = test_matches{i}{1};
+ tests(i).want = test_matches{i}{2};
+
+ % replace initial '..' by ' ' in subsequent lines
+ lines = strsplit(source, '\n');
+ source = lines{1};
+ for j = 2:length(lines)
+ T = regexp(lines{j}, '^\s*(\.\.)(.*)$', 'tokens');
+ assert(length(T) == 1);
+ T = T{1};
+ assert(length(T) == 2);
+ source = sprintf('%s\n %s', source, T{2});
+ end
+ tests(i).source = source;
+
+ % set default options
+ tests(i).normalize_whitespace = defaults.normalize_whitespace;
+ tests(i).ellipsis = defaults.ellipsis;
+ tests(i).skip = {};
+ tests(i).xfail = {};
+
+ % find and process directives
+ directive_matches = regexp(tests(i).source, '(?:#|%)\s*doctest:\s+([(\+|\-)][\w]+)(\([\w]+\))?', 'tokens');
+ for j = 1:length(directive_matches)
+ directive = directive_matches{j}{1};
+ if (strcmp('+SKIP_IF', directive) || strcmp('+SKIP_UNLESS', directive) || strcmp('+XFAIL_IF', directive) || strcmp('+XFAIL_UNLESS', directive))
+ if length(directive_matches{j}) == 2
+ condition = directive_matches{j}{2}(2:end - 1);
+ else
+ error('doctest: syntax error, expected %s(varname)', directive);
+ end
+ end
+
+ if strcmp('NORMALIZE_WHITESPACE', directive(2:end))
+ tests(i).normalize_whitespace = strcmp(directive(1), '+');
+ elseif strcmp('ELLIPSIS', directive(2:end))
+ tests(i).ellipsis = strcmp(directive(1), '+');
+ elseif strcmp('+SKIP', directive)
+ tests(i).skip{end + 1} = 'true';
+ elseif strcmp('+SKIP_IF', directive)
+ tests(i).skip{end + 1} = condition;
+ elseif strcmp('+SKIP_UNLESS', directive)
+ tests(i).skip{end + 1} = sprintf('~(%s)', condition);
+ elseif strcmp('+XFAIL', directive)
+ tests(i).xfail{end + 1} = 'true';
+ elseif strcmp('+XFAIL_IF', directive)
+ tests(i).xfail{end + 1} = condition;
+ elseif strcmp('+XFAIL_UNLESS', directive)
+ tests(i).xfail{end + 1} = sprintf('~(%s)', condition);
+ else
+ error('doctest: unexpected directive %s', directive);
+ end
+ end
+end
+
+% run tests in a local namespace
+results = DOCTEST__run_impl(tests);
+
+end
+
+
+% given a cell array of conditions (represented as strings to be eval'ed),
+% return the string that corresponds to their logical "or".
+function result = DOCTEST__join_conditions(conditions)
+ if isempty(conditions)
+ result = 'false';
+ else
+ result = strcat('(', strjoin(conditions, ') || ('), ')');
+ end
+end
+
+% the following function is used to evaluate all lines of code in same
+% namespace (the one of this invocation of DOCTEST__run_impl)
+function DOCTEST__results = DOCTEST__run_impl(DOCTEST__tests)
+
+% do not split long rows (TODO: how to do this on MATLAB?)
+if is_octave()
+ split_long_rows(0, 'local')
+end
+
+% define test-global constants
+DOCTEST_OCTAVE = is_octave();
+DOCTEST_MATLAB = ~DOCTEST_OCTAVE;
+
+% Octave has [no evalc command](https://savannah.gnu.org/patch/?8033)
+DOCTEST__has_builtin_evalc = exist('evalc', 'builtin');
+
+DOCTEST__results = [];
+for DOCTEST__i = 1:numel(DOCTEST__tests)
+ DOCTEST__result = DOCTEST__tests(DOCTEST__i);
+
+ % determine whether test should be skipped
+ % (careful about Octave bug #46397 to not change the current value of “ans”)
+ eval (strcat ('DOCTEST__result.skip = ', ...
+ DOCTEST__join_conditions (DOCTEST__result.skip), ...
+ ';'));
+ if (DOCTEST__result.skip)
+ continue
+ end
+
+ % determine whether test is expected to fail
+ % (careful about Octave bug #46397 to not change the current value of “ans”)
+ eval (strcat ('DOCTEST__result.xfail = ', ...
+ DOCTEST__join_conditions (DOCTEST__result.xfail), ...
+ ';'));
+
+ % evaluate input (structure adapted from a StackOverflow answer by user Amro, see http://stackoverflow.com/questions/3283586 and http://stackoverflow.com/users/97160/amro)
+ try
+ if (DOCTEST__has_builtin_evalc)
+ DOCTEST__result.got = evalc(DOCTEST__result.source);
+ else
+ DOCTEST__result.got = doctest_evalc(DOCTEST__result.source);
+ end
+ catch DOCTEST__exception
+ DOCTEST__result.got = DOCTEST__format_exception(DOCTEST__exception);
+ end
+
+ % determine if test has passed
+ DOCTEST__result.passed = doctest_compare(DOCTEST__result.want, DOCTEST__result.got, DOCTEST__result.normalize_whitespace, DOCTEST__result.ellipsis);
+ if DOCTEST__result.xfail
+ DOCTEST__result.passed = ~DOCTEST__result.passed;
+ end
+
+ DOCTEST__results = [DOCTEST__results; DOCTEST__result];
+end
+
+end
+
+
+function formatted = DOCTEST__format_exception(ex)
+
+ if is_octave()
+ formatted = ['??? ' ex.message];
+ return
+ end
+
+ if strcmp(ex.stack(1).name, 'DOCTEST__run_impl')
+ % we don't want the report, we just want the message
+ % otherwise it'll talk about evalc, which is not what the user got on
+ % the command line.
+ formatted = ['??? ' ex.message];
+ else
+ formatted = ['??? ' ex.getReport('basic')];
+ end
+end
diff --git a/inst/private/is_octave.m b/inst/private/is_octave.m
new file mode 100644
index 0000000..b87aed2
--- /dev/null
+++ b/inst/private/is_octave.m
@@ -0,0 +1,24 @@
+function r = is_octave()
+%IS_OCTAVE Return true if we are running Octave, false for Matlab.
+
+% Timings for different implementations, 10000 calls
+%
+% test Matlab Octave
+% ----------------------------
+% try-catch 1.2s 0.18s
+% if-exist 0.14s 0.22s
+% dummy 0.13s 0.13s
+%
+% Conclusions: "if-exist" only twice as slow as "dummy" (always return
+% true), so no need to bother with a persistent variable.
+
+ r = exist('OCTAVE_VERSION', 'builtin') ~= 0;
+
+ %try
+ % OCTAVE_VERSION;
+ % r = true;
+ %catch
+ % r = false;
+ %end
+
+end
diff --git a/octave-doctest.metainfo.xml b/octave-doctest.metainfo.xml
new file mode 100644
index 0000000..81af57f
--- /dev/null
+++ b/octave-doctest.metainfo.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<component type="addon">
+ <id>octave-doctest</id>
+ <extends>www.octave.org-octave.desktop</extends>
+ <name>Doctest</name>
+ <summary>Finds and tests example code within documentation</summary>
+ <url type="homepage">http://octave.sourceforge.net/doctest</url>
+ <url type="bugtracker">https://github.com/catch22/octave-doctest/issues/new</url>
+ <metadata_license>CC0-1.0</metadata_license>
+ <project_license>BSD-3-Clause</project_license>
+</component>
diff --git a/src/Makefile b/src/Makefile
new file mode 100644
index 0000000..a8d99b8
--- /dev/null
+++ b/src/Makefile
@@ -0,0 +1,5 @@
+all: doctest_evalc.oct
+
+%.oct: %.cc
+ $(MKOCTFILE) $<
+
diff --git a/src/doctest_evalc.cc b/src/doctest_evalc.cc
new file mode 100644
index 0000000..ce19972
--- /dev/null
+++ b/src/doctest_evalc.cc
@@ -0,0 +1,87 @@
+/*
+ Copyright 2015 Oliver Heimlich
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include <octave/oct.h>
+#include <octave/parse.h>
+
+DEFUN_DLD (doctest_evalc, args, nargout,
+ "-*- texinfo -*-\n"
+ "@documentencoding UTF-8\n"
+ "@deftypefn {Loadable Function} {@var{S} =} doctest_evalc (@var{TRY})\n"
+ "@deftypefnx {Loadable Function} {@var{S} =} doctest_evalc (@var{TRY}, @var{CATCH})\n"
+ "\n"
+ "Parse the string @var{TRY} and evaluate it as if it were an Octave "
+ "program. If that fails, evaluate the optional string @var{CATCH}. The "
+ "string @var{TRY} is evaluated in the current context, so any results "
+ "remain available after @command{doctest_evalc} returns."
+ "\n\n"
+ "This function is like @command{eval}, except any output that would "
+ "normally be written in the console is captured and returned as string "
+ "@var{S}."
+ "\n\n"
+ "@example\n"
+ "@group\n"
+ "s = doctest_evalc (\"t = 42\"), t\n"
+ " @result{}\n"
+ " s = t = 42\n\n"
+ " t = 42\n"
+ "@end group\n"
+ "@end example\n"
+ "@seealso{eval, evalin}\n"
+ "@end deftypefn"
+ )
+{
+ octave_value_list retval;
+
+ int nargin = args.length ();
+
+ if (nargin > 0)
+ {
+ // Redirect stdout to capturing buffer
+ std::ostream & out_stream = octave_stdout;
+ std::ostream & err_stream = std::cerr;
+ out_stream.flush ();
+ err_stream.flush ();
+ std::ostringstream buffer;
+ std::streambuf* old_out_buf = out_stream.rdbuf (buffer.rdbuf ());
+ std::streambuf* old_err_buf = err_stream.rdbuf (buffer.rdbuf ());
+
+ int parse_status = 0;
+
+ octave_value_list tmp = eval_string (args(0).string_value (), false,
+ parse_status, 0);
+
+ if (nargin > 1 && (parse_status != 0 || error_state))
+ {
+ error_state = 0;
+
+ tmp = eval_string (args(1).string_value (), false,
+ parse_status, 0);
+ }
+
+ // Stop capturing buffer and restore stdout
+ out_stream.flush ();
+ err_stream.flush ();
+ retval (0) = buffer.str ();
+ out_stream.rdbuf (old_out_buf);
+ err_stream.rdbuf (old_err_buf);
+ }
+ else
+ print_usage ();
+
+ return retval;
+}
diff --git a/test/@test_class/test_class.m b/test/@test_class/test_class.m
new file mode 100644
index 0000000..a0b1e5f
--- /dev/null
+++ b/test/@test_class/test_class.m
@@ -0,0 +1,16 @@
+function obj = test_class
+%
+% >> class(test_class)
+% ans = test_class
+%
+%
+% >> methods test_class
+% Methods for class test_class:
+% test_class test_method
+
+obj = struct;
+obj.name = 'Default Name';
+obj.age = 42;
+obj = class(obj, 'test_class');
+
+end
diff --git a/test/@test_class/test_method.m b/test/@test_class/test_method.m
new file mode 100644
index 0000000..e34080f
--- /dev/null
+++ b/test/@test_class/test_method.m
@@ -0,0 +1,8 @@
+function result = test_method(obj, varargin)
+%
+% >> m = test_class; test_method(m)
+% ans = Default Name is 42 years old.
+
+result = sprintf('%s is %d years old.', obj.name, obj.age);
+
+end
diff --git a/test/@test_classdef/amethod.m b/test/@test_classdef/amethod.m
new file mode 100644
index 0000000..f61c548
--- /dev/null
+++ b/test/@test_classdef/amethod.m
@@ -0,0 +1,5 @@
+function amethod(obj)
+% This method is stored in a separate file.
+% >> b = 2 + 2
+% b = 4
+end
diff --git a/test/@test_classdef/test_classdef.m b/test/@test_classdef/test_classdef.m
new file mode 100644
index 0000000..1766228
--- /dev/null
+++ b/test/@test_classdef/test_classdef.m
@@ -0,0 +1,44 @@
+classdef test_classdef
+%TEST_CLASSDEF A test for classdef classes
+%
+% Some tests:
+% >> 6 + 7
+% ans = 13
+%
+% >> a = test_classdef()
+% a =
+% class name = "default", age = 42
+%
+%
+% This general help text should be shown for "help test_classdef".
+%
+% There are also tests in the methods below.
+
+ properties
+ name
+ age
+ end
+
+ methods
+
+ function obj = test_classdef(n, a)
+ % constructor
+ % >> a = 13 + 1
+ % a = 14
+ if (nargin ~= 2)
+ obj.name = 'default';
+ obj.age = 42;
+ else
+ obj.name = n;
+ obj.age = a;
+ end
+ end
+ end
+ methods
+ function disp(obj)
+ % >> a = 30 + 2
+ % a = 32
+ fprintf('class name = "%s", age = %d\n', obj.name, obj.age)
+ end
+ end
+end
diff --git a/test/@test_shadow/test_shadow.m b/test/@test_shadow/test_shadow.m
new file mode 100644
index 0000000..9e95ad9
--- /dev/null
+++ b/test/@test_shadow/test_shadow.m
@@ -0,0 +1,9 @@
+function obj = test_shadow()
+% >> 6 + 7
+% ans = 13
+
+ obj = struct();
+ obj.name = 'Do not shadow me bro';
+ obj = class(obj, 'test_shadow');
+
+end
diff --git a/test/examples/greet.m b/test/examples/greet.m
new file mode 100644
index 0000000..e40ca3f
--- /dev/null
+++ b/test/examples/greet.m
@@ -0,0 +1,10 @@
+function greeting = greet(user)
+% Returns a greeting.
+%
+% >> greet World
+%
+% Hello, World!
+
+greeting = ['Hello, ' user '!'];
+
+end
diff --git a/test/private/test_in_private_dir.m b/test/private/test_in_private_dir.m
new file mode 100644
index 0000000..9236733
--- /dev/null
+++ b/test/private/test_in_private_dir.m
@@ -0,0 +1,3 @@
+function test_in_private_dir()
+% >> a = 5
+% a = 5
diff --git a/test/test_angle_brackets.m b/test/test_angle_brackets.m
new file mode 100644
index 0000000..03447cd
--- /dev/null
+++ b/test/test_angle_brackets.m
@@ -0,0 +1,9 @@
+function s = test_angle_brackets()
+% https://savannah.gnu.org/bugs/?45084 (Fixed in Octave 4.0)
+%
+% >> s = test_angle_brackets()
+% s = I <3 U
+% >> s = '<p>I heart you</p>'
+% s = <p>I heart you</p>
+
+s = 'I <3 U';
diff --git a/test/test_ans.m b/test/test_ans.m
new file mode 100644
index 0000000..c46d137
--- /dev/null
+++ b/test/test_ans.m
@@ -0,0 +1,22 @@
+% >> 4
+% ans = 4
+%
+%
+% >> ans
+% ans = 4
+%
+%
+% >> 5 % doctest: +SKIP
+% ans = 5
+%
+%
+% >> ans
+% ans = 4
+%
+%
+% >> 6 % doctest: +XFAIL
+% ans = 7
+%
+%
+% >> ans
+% ans = 6
diff --git a/test/test_ans.texinfo b/test/test_ans.texinfo
new file mode 100644
index 0000000..3f0a920
--- /dev/null
+++ b/test/test_ans.texinfo
@@ -0,0 +1,29 @@
+ at example
+>> 4
+ans = 4
+ at end example
+
+ at example
+>> ans
+ans = 4
+ at end example
+
+ at example
+>> 5 @c doctest: +SKIP
+ans = 5
+ at end example
+
+ at example
+>> ans
+ans = 4
+ at end example
+
+ at example
+>> 6 @c doctest: +XFAIL
+ans = 7
+ at end example
+
+ at example
+>> ans
+ans = 6
+ at end example
diff --git a/test/test_blank_match.m b/test/test_blank_match.m
new file mode 100644
index 0000000..53c178c
--- /dev/null
+++ b/test/test_blank_match.m
@@ -0,0 +1,5 @@
+function s = test_blank_match()
+% Issue #46
+%
+% >> a = 3 + 4;
+% ...
diff --git a/test/test_comments.texinfo b/test/test_comments.texinfo
new file mode 100644
index 0000000..82070a4
--- /dev/null
+++ b/test/test_comments.texinfo
@@ -0,0 +1,22 @@
+ at example
+ at group
+A = 5
+ at result{} A = 5
+ at comment Don't break my test
+ at end group
+ at end example
+
+ at example
+ at group
+A = 6
+ @comment My achy breaky test
+ at result{} A = 6
+ at end group
+ at end example
+
+ at example
+ at group
+A = 7 @*@c I just don't think my
+ at result{} A = 7 @c test would understand
+ at end group
+ at end example
diff --git a/test/test_compare_backspace.m b/test/test_compare_backspace.m
new file mode 100644
index 0000000..62c3dfb
--- /dev/null
+++ b/test/test_compare_backspace.m
@@ -0,0 +1,19 @@
+function test_compare_backspace()
+% Matlab appears to emit backspace characters (0x08) for no apparent reason.
+% This doctest verifies that backspace characters are correctly processed
+% before comparison. Note a bit of fuss here because Octave needs this escape
+% sequence in double quotes which Matlab won't parse.
+%
+% >> if (exist('OCTAVE_VERSION'))
+% .. eval('sprintf("Hi, no question mark here?\x08 goodbye")')
+% .. else
+% .. sprintf('Hi, no question mark here?\x08 goodbye')
+% .. end
+%
+% ans =
+%
+% Hi, no question mark here goodbye
+%
+%
+% All of the doctests should pass, and they manipulate this function.
+%
diff --git a/test/test_compare_hyperlinks.m b/test/test_compare_hyperlinks.m
new file mode 100644
index 0000000..2dbbb0c
--- /dev/null
+++ b/test/test_compare_hyperlinks.m
@@ -0,0 +1,7 @@
+function test_compare_hyperlinks()
+% There are some tricky things that Matlab does to strings, such as adding
+% hyperlinks to help. We remove those before comparison, as verified by the
+% following doctest:
+%
+% >> disp('Hi there! <a href="matlab:help help">foo</a>')
+% Hi there! foo
diff --git a/test/test_diary_style.texinfo b/test/test_diary_style.texinfo
new file mode 100644
index 0000000..ba56a5e
--- /dev/null
+++ b/test/test_diary_style.texinfo
@@ -0,0 +1,40 @@
+One is allowed to put diary-style tests within texinfo:
+
+ at example
+ at group
+>> a = 6
+a = 6
+ at end group
+ at end example
+
+
+ at example
+ at group
+>> % comments should
+>> # not interfere
+>> a = 7
+ a = 7
+ at end group
+ at end example
+
+
+ at example
+ at group
+ at c even these comments
+>> a = 7
+ a = 7
+ at end group
+ at end example
+
+
+Blank lines ok:
+
+ at example
+ at group
+
+>> a = 7
+
+ a = 7
+
+ at end group
+ at end example
diff --git a/test/test_diary_style_mixed.texinfo b/test/test_diary_style_mixed.texinfo
new file mode 100644
index 0000000..74e3d7b
--- /dev/null
+++ b/test/test_diary_style_mixed.texinfo
@@ -0,0 +1,11 @@
+Mixed, with print and result macros (do we want to allow this?)
+
+ at example
+ at group
+>> x = 8
+ @result{} x = 8
+>> disp('abc'), s = disp('xyz')
+ @print{} abc
+ @result{} s = xyz
+ at end group
+ at end example
diff --git a/test/test_ellipsis.m b/test/test_ellipsis.m
new file mode 100644
index 0000000..dd96646
--- /dev/null
+++ b/test/test_ellipsis.m
@@ -0,0 +1,20 @@
+function test_ellipsis()
+% >> '...' % doctest: -ELLIPSIS
+%
+% ...
+%
+%
+% >> 1 + 2
+%
+% ...
+%
+%
+% >> 1 + 2 % doctest: -ELLIPSIS % doctest: +XFAIL
+%
+% ...
+%
+%
+% >> 1 + 2 % doctest: -ELLIPSIS
+%
+% ans = 3
+%
diff --git a/test/test_long_rows.m b/test/test_long_rows.m
new file mode 100644
index 0000000..6156a54
--- /dev/null
+++ b/test/test_long_rows.m
@@ -0,0 +1,4 @@
+function y = test_long_rows()
+% >> repmat(1, 1, 50) % doctest: +XFAIL_IF(DOCTEST_MATLAB)
+%
+% ans = 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
diff --git a/test/test_long_rows.texinfo b/test/test_long_rows.texinfo
new file mode 100644
index 0000000..04a41e2
--- /dev/null
+++ b/test/test_long_rows.texinfo
@@ -0,0 +1,4 @@
+ at example
+repmat(1, 1, 50)
+ @result{} ans = 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
+ at end example
diff --git a/test/test_multi_result.texinfo b/test/test_multi_result.texinfo
new file mode 100644
index 0000000..10974c2
--- /dev/null
+++ b/test/test_multi_result.texinfo
@@ -0,0 +1,65 @@
+Current approach works for this block because the last line before
+a @@result becomes a command:
+ at example
+ at group
+[a, b] = deal (1, 2)
+ at result{}
+ a = 1
+ b = 2
+c = 3
+ at result{} c = 3
+d = 4, e = 5
+ at result{}
+ d = 4
+ e = 5
+ at end group
+ at end example
+
+
+But here we have trouble:
+ at example
+ at group
+[a, b] = deal (1, 2)
+ at result{}
+ a = 1
+ b = 2
+c = 3 % I should be a command
+d = 4
+ at result{}
+ c = 3
+ d = 4
+ at end group
+ at end example
+
+A reasonable solution would be to use the intending level to help
+detect/classify lines as input/output.
+
+Here are some challenges for that:
+ at example
+ at group
+ [a, b] = deal (1, 2)
+ @result{}
+ a = 1
+ b = 2
+ c = 3
+ d = 4
+ @result{}
+ c = 3
+ d = 4
+ at end group
+ at end example
+
+ at example
+ at group
+ [a, b] = deal (1, 2)
+ @result{}
+ a = 1
+ b = 2
+ at comment don't mess up because of this outdented comment
+ c = 3
+ d = 4
+ @result{}
+ c = 3
+ d = 4
+ at end group
+ at end example
diff --git a/test/test_multi_return.texinfo b/test/test_multi_return.texinfo
new file mode 100644
index 0000000..a1032ca
--- /dev/null
+++ b/test/test_multi_return.texinfo
@@ -0,0 +1,46 @@
+Here we have no return value but only output:
+
+ at example
+disp ('abc')
+ at print{} abc
+ at end example
+
+Here it is an return value:
+
+ at example
+x = disp ('abc')
+ at result{} x = abc
+ at end example
+
+Here we have a combination of both:
+
+ at example
+disp ('abc'); x = disp ('def')
+ at print{} abc
+ at result{} x = def
+ at end example
+
+Here the warning is not part of the result:
+
+ at example
+inv (0)
+ at print{} warning: ...matrix singular to machine precision...
+ at result{} ans = Inf
+ at end example
+
+Here we have two results:
+
+ at example
+[m, n] = size (zeros (1, 2))
+ at result{} m = 1
+ at result{} n = 2
+ at end example
+
+Intermediate results and printing:
+
+ at example
+[m, n] = size (zeros (1, 2)), disp ('abc')
+ at result{} m = 1
+ at result{} n = 2
+ at print{} abc
+ at end example
diff --git a/test/test_no_docs.m b/test/test_no_docs.m
new file mode 100644
index 0000000..5154b47
--- /dev/null
+++ b/test/test_no_docs.m
@@ -0,0 +1,4 @@
+function test_no_docs()
+
+ s = 'note this function has no docs: this should not cause an error';
+
diff --git a/test/test_not_an_mfile.txt b/test/test_not_an_mfile.txt
new file mode 100644
index 0000000..23d7005
--- /dev/null
+++ b/test/test_not_an_mfile.txt
@@ -0,0 +1,9 @@
+% hello, I am not an m-file so I should not be doctested
+%
+% >> a = 6
+% a = 7
+%
+%
+% This test would fail if doctest finds it when traversing
+% a directory. If you run doctest on it directly, you should
+% get an error.
diff --git a/test/test_shadow.m b/test/test_shadow.m
new file mode 100644
index 0000000..9fa76bf
--- /dev/null
+++ b/test/test_shadow.m
@@ -0,0 +1,15 @@
+% Note this file shadows the class @test_shadow. The class will
+% take precedence: i.e., `doctest test_shadow` will test the class.
+% You can always override with `doctest test_shadow.m`.
+%
+% >> 2 + 2
+% ans = 4
+% >> 3 + 3
+% ans = 6
+%
+%
+% Note: in Matlab, help('test_shadow.m') returns the help for
+% '@test_shadow' even when the .m if explicitly given. So this
+% test will never run on Matlab.
+
+not_the_class = 42
diff --git a/test/test_shadow/test_in_shadow_dir.m b/test/test_shadow/test_in_shadow_dir.m
new file mode 100644
index 0000000..c0d1520
--- /dev/null
+++ b/test/test_shadow/test_in_shadow_dir.m
@@ -0,0 +1,12 @@
+function test_in_shadow_dir(x)
+% this test is in a directory shadowed/shadowing both a class
+% and an m-file. Its probably not entirely well-posed what
+% should happen here but on Octave at least, you can doctest
+% all three.
+%
+% >> a = 4
+% a = 4
+% >> a = 5
+% a = 5
+% >> a = 6
+% a = 6
diff --git a/test/test_skip.m b/test/test_skip.m
new file mode 100644
index 0000000..39bc7d5
--- /dev/null
+++ b/test/test_skip.m
@@ -0,0 +1,31 @@
+function test_skip()
+% This file should have 3 passed tests
+%
+% A test that would fail:
+% >> a = 5 % doctest: +SKIP
+% b = 7
+%
+%
+% And a passing one:
+% >> a = 6
+% a = 6
+%
+%
+% Multiline input:
+% >> A = [1 2;
+% .. 3 4] % doctest: +SKIP
+% A = 42
+%
+%
+% Put it on any line of multiline input:
+% >> A = [1 2; % doctest: +SKIP
+% .. 3 4]
+% A = 42
+%
+%
+% Skip means not evaluated
+% >> a = 6
+% a = 6
+% >> a = 5 % doctest: +SKIP
+% >> a
+% a = 6
diff --git a/test/test_skip_comments.texinfo b/test/test_skip_comments.texinfo
new file mode 100644
index 0000000..67989e4
--- /dev/null
+++ b/test/test_skip_comments.texinfo
@@ -0,0 +1,23 @@
+First actually set A (we'll check later that nobody messed it up)
+ at example
+A = 5
+ at result{} A = 5
+ at end example
+
+ at example
+A = 10 # doctest: +SKIP
+ at result{}
+ at end example
+
+ at example
+A = 10 @c doctest: +SKIP
+ at result{}
+A = 10 @comment doctest: +SKIP
+ at result{}
+ at end example
+
+Ensure A is still 5 (none of the skipped tests ran)
+ at example
+A
+ at result{} A = 5
+ at end example
diff --git a/test/test_skip_if.m b/test/test_skip_if.m
new file mode 100644
index 0000000..7a9788a
--- /dev/null
+++ b/test/test_skip_if.m
@@ -0,0 +1,31 @@
+function test_skip_if()
+% This test should have 4 passing tests.
+%
+% Set up flags that determine test skipping.
+% >> my_true_flag = 1;
+% >> my_false_flag = 0;
+%
+%
+% A test that would fail:
+% >> a = 5 % doctest: +SKIP_IF(my_true_flag)
+% b = 7
+%
+%
+% And a passing one:
+% >> a = 6 % doctest: +SKIP_IF(my_false_flag)
+% a = 6
+%
+%
+% Check that it was indeed not skipped:
+% >> a
+% a = 6
+%
+%
+% Multiline examples (put it on any line)
+% >> A = [1 2;
+% .. 3 4] % doctest: +SKIP_IF(my_true_flag)
+% A = 42
+%
+% >> A = [1 2; % doctest: +SKIP_IF(my_true_flag)
+% .. 3 4]
+% A = 42
diff --git a/test/test_skip_if_multiple.m b/test/test_skip_if_multiple.m
new file mode 100644
index 0000000..3ccd0ab
--- /dev/null
+++ b/test/test_skip_if_multiple.m
@@ -0,0 +1,19 @@
+function test_skip_if_multiple()
+% Set up flags that determine test skipping.
+% >> false_flag = 0;
+% >> true_flag = 1;
+% >> z = 3;
+%
+%
+% The following test should not be skipped
+% >> z = 5 % doctest: +SKIP_IF(false_flag)
+% z = 5
+%
+%
+% The following test should be skipped (thanks to the second condition)
+% >> z = 7 % doctest: +SKIP_IF(false_flag) % doctest: +SKIP_IF(true_flag)
+% w = 9
+%
+% Check that it was indeed skipped:
+% >> z
+% z = 5
diff --git a/test/test_skip_malformed.texinfo b/test/test_skip_malformed.texinfo
new file mode 100644
index 0000000..697b908
--- /dev/null
+++ b/test/test_skip_malformed.texinfo
@@ -0,0 +1,7 @@
+ at example
+ at comment doctest: +SKIP
+Some verbatim text masquerading as an example (probably should live inside
+an @@verbatim block, but isn't).
+
+Should not raise an extraction error, because it is marked to skip.
+ at end example
diff --git a/test/test_skip_only_one.m b/test/test_skip_only_one.m
new file mode 100644
index 0000000..605e8ae
--- /dev/null
+++ b/test/test_skip_only_one.m
@@ -0,0 +1,4 @@
+function test_skip_only_one()
+% A file with just one skipped test:
+% >> a = 5 % doctest: +SKIP
+% b = 7
diff --git a/test/test_skip_unless.m b/test/test_skip_unless.m
new file mode 100644
index 0000000..e19b2a4
--- /dev/null
+++ b/test/test_skip_unless.m
@@ -0,0 +1,31 @@
+function test_skip_unless()
+% This test should have 4 passing tests.
+%
+% Set up flags that determine test skipping.
+% >> my_true_flag = 1;
+% >> my_false_flag = 0;
+%
+%
+% A test that would fail:
+% >> a = 5 % doctest: +SKIP_UNLESS(my_false_flag)
+% b = 7
+%
+%
+% And a passing one:
+% >> a = 6 % doctest: +SKIP_UNLESS(my_true_flag)
+% a = 6
+%
+%
+% Check that it was indeed not skipped:
+% >> a
+% a = 6
+%
+%
+% Multiline examples (put it on any line)
+% >> A = [1 2;
+% .. 3 4] % doctest: +SKIP_UNLESS(my_false_flag)
+% A = 42
+%
+% >> A = [1 2; % doctest: +SKIP_UNLESS(my_false_flag)
+% .. 3 4]
+% A = 42
diff --git a/test/test_var.texinfo b/test/test_var.texinfo
new file mode 100644
index 0000000..0dbf53d
--- /dev/null
+++ b/test/test_var.texinfo
@@ -0,0 +1,10 @@
+Make sure var macro can be used:
+
+ at example
+ at group
+ at var{ABC} = 6
+ at result{} @var{ABC} = 6
+ at var{aBc} = 7
+ at result{} @var{aBc} = 7
+ at end group
+ at end example
diff --git a/test/test_warning.m b/test/test_warning.m
new file mode 100644
index 0000000..adc55cd
--- /dev/null
+++ b/test/test_warning.m
@@ -0,0 +1,6 @@
+function test_warning()
+% >> toeplitz ([1 2], [2 3])
+% ...arning: ...olumn wins ...diagonal conflict...
+% ans =
+% 1 3
+% 2 1
diff --git a/test/test_whitespace.m b/test/test_whitespace.m
new file mode 100644
index 0000000..4a23fba
--- /dev/null
+++ b/test/test_whitespace.m
@@ -0,0 +1,53 @@
+function test_whitespace()
+% >> disp('a b') % doctest: -NORMALIZE_WHITESPACE
+% a b
+%
+% >> disp('a b') % doctest: +NORMALIZE_WHITESPACE
+% a b
+% >> disp('a b') % doctest: +NORMALIZE_WHITESPACE
+% a b
+%
+%
+% Indenting is ok:
+%
+% >> disp('a b') % doctest: -NORMALIZE_WHITESPACE
+%
+% a b
+%
+%
+% But this should fail:
+%
+% >> disp('a b') % doctest: -NORMALIZE_WHITESPACE % doctest: +XFAIL
+%
+% a b
+%
+%
+% Multiline: Matlab and Octave format matrices differently but a
+% column vector is safe to use in cross-platform tests.
+%
+% >> A = [1; 2; -3] % doctest: -NORMALIZE_WHITESPACE
+% A =
+% 1
+% 2
+% -3
+%
+% >> A % doctest: -NORMALIZE_WHITESPACE
+%
+% A =
+%
+% 1
+%
+% 2
+%
+% -3
+%
+%
+% Matlab and Octave format differently, even for scalars, so
+% make sure our auto "ans = " bit still works.
+%
+% >> 42 % doctest: -NORMALIZE_WHITESPACE
+% 42
+%
+%
+% Note: even very simple scalar tests like "x = 5" are difficult to
+% pass in both Octave and Matlab when using -NORMALIZE_WHITESPACE.
diff --git a/test/test_windows_eol.texinfo b/test/test_windows_eol.texinfo
new file mode 100644
index 0000000..2882843
--- /dev/null
+++ b/test/test_windows_eol.texinfo
@@ -0,0 +1,20 @@
+ at example
+ at group
+A = 5
+ at result{} A = 5
+ at end group
+ at end example
+
+ at example
+ at group
+A = 6
+ at result{} A = 6
+ at end group
+ at end example
+
+ at example
+ at group
+>> A = 7
+A = 7
+ at end group
+ at end example
diff --git a/test/test_xfail.m b/test/test_xfail.m
new file mode 100644
index 0000000..e2262aa
--- /dev/null
+++ b/test/test_xfail.m
@@ -0,0 +1,14 @@
+function test_xfail()
+% Let's initialize our dummy variable a.
+% >> a = 3
+% a = 3
+%
+%
+% The following test will fail:
+% >> a = 5 % doctest: +XFAIL
+% b = 7
+%
+%
+% Check that it has been executed, though:
+% >> a
+% a = 5
diff --git a/test/test_xfail.texinfo b/test/test_xfail.texinfo
new file mode 100644
index 0000000..de2dd9c
--- /dev/null
+++ b/test/test_xfail.texinfo
@@ -0,0 +1,5 @@
+This test is supposed to fail
+ at example
+a = 5 @c doctest: +XFAIL
+ at result{} b = 7
+ at end example
diff --git a/test/test_xfail_if.m b/test/test_xfail_if.m
new file mode 100644
index 0000000..a1afa87
--- /dev/null
+++ b/test/test_xfail_if.m
@@ -0,0 +1,24 @@
+function test_xfail_if()
+% These flags control our expectations:
+% >> my_true_flag = 1;
+% >> my_false_flag = 0;
+%
+%
+% Let's initialize our dummy variable a:
+% >> a = 3
+% a = 3
+%
+%
+% The following test will fail:
+% >> a = 5 % doctest: +XFAIL_IF(my_true_flag)
+% b = 7
+%
+%
+% Check that it has been executed, though:
+% >> a
+% a = 5
+%
+%
+% This one should succeed, however:
+% >> a % doctest: +XFAIL_IF(my_false_flag)
+% a = 5
diff --git a/test/test_xfail_if_multiple.m b/test/test_xfail_if_multiple.m
new file mode 100644
index 0000000..4ee750d
--- /dev/null
+++ b/test/test_xfail_if_multiple.m
@@ -0,0 +1,25 @@
+function test_xfail_if_multiple()
+% Set up flags that determine test skipping.
+% >> false_flag = 0;
+% >> true_flag = 1;
+% >> z = 3;
+%
+%
+% The following test should succeed
+% >> z = 5 % doctest: +XFAIL_IF(false_flag)
+% z = 5
+%
+%
+% Check that the first test was executed
+% >> z
+% z = 5
+%
+%
+% The following test should be fail
+% >> z = 7 % doctest: +XFAIL_IF(false_flag) % doctest: +XFAIL_IF(true_flag)
+% w = 9
+%
+%
+% Check that the second test was indeed executed
+% >> z
+% z = 7
diff --git a/test/test_xfail_unless.m b/test/test_xfail_unless.m
new file mode 100644
index 0000000..34f2c4e
--- /dev/null
+++ b/test/test_xfail_unless.m
@@ -0,0 +1,24 @@
+function test_xfail_unless()
+% These flags control our expectations:
+% >> my_true_flag = 1;
+% >> my_false_flag = 0;
+%
+%
+% Let's initialize our dummy variable a:
+% >> a = 3
+% a = 3
+%
+%
+% The following test will fail:
+% >> a = 5 % doctest: +XFAIL_UNLESS(my_false_flag)
+% b = 7
+%
+%
+% Check that it has been executed, though:
+% >> a
+% a = 5
+%
+%
+% This one should succeed, however:
+% >> a % doctest: +XFAIL_UNLESS(my_true_flag)
+% a = 5
diff --git a/util/convert_comments.m b/util/convert_comments.m
new file mode 100644
index 0000000..49bd37b
--- /dev/null
+++ b/util/convert_comments.m
@@ -0,0 +1,314 @@
+%% Copyright (c) 2015 Colin B. Macdonald
+%%
+%% Redistribution and use in source and binary forms, with or without
+%% modification, are permitted provided that the following conditions are met:
+%%
+%% 1. Redistributions of source code must retain the above copyright notice,
+%% this list of conditions and the following disclaimer.
+%%
+%% 2. 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.
+%%
+%% 3. Neither the name of the copyright holder 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 HOLDER 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.
+
+function convert_comments (basedir, subdir, dirout)
+% this slightly strange way of doing things (basedir, subdir) is
+% b/c I must "chdir" into base, but get_first_help_sentence() must
+% not be in the class dir...
+
+ %basedir, subdir, dirout
+ files = dir([basedir subdir]);
+ chdir(basedir)
+
+ for i=1:length(files)
+ if (~files(i).isdir)
+ [dir, name, ext] = fileparts(files(i).name);
+ if (strcmp(ext, '.m'))
+ if isempty(subdir)
+ octname = [name ext];
+ else
+ octname = [subdir '/' name ext];
+ end
+ fprintf('Converting texinfo to Matlab-style documentation: %s\n', octname)
+ r = convert_oct_2_ml (octname, [dirout octname]);
+ if ~r
+ [status, msg, msgid] = copyfile (octname, [dirout octname], 'f');
+ if (status ~= 1)
+ error(msg)
+ end
+ fprintf('**** COPYING %s UNMODIFIED ****\n', octname)
+ end
+ end
+ end
+ end
+end
+
+
+
+
+function success = convert_oct_2_ml (fname, foutname)
+
+ [dir, fcn, ext] = fileparts(fname);
+
+ newl = sprintf('\n');
+
+ [fi,msg] = fopen(fname, 'r');
+ if (fi < 0)
+ error(msg)
+ end
+
+ ins = {}; i = 0;
+ while (1)
+ temp = fgets(fi);
+ if ~ischar(temp) && temp == -1
+ break
+ end
+ i = i + 1;
+ ins{i} = temp;
+ % todo, possible strip newl
+ end
+
+ fclose(fi);
+
+ % trim newlines
+ ins = deblank(ins);
+
+
+ %% find the actual function [] = ... line
+ Nfcn = [];
+ for i = 1:length(ins)
+ I = strfind (ins{i}, 'function');
+ if ~isempty(I) && I(1) == 1
+ %disp ('found function header')
+ Nfcn = i;
+ break
+ end
+ end
+ if isempty(Nfcn)
+ disp('AFAICT, this is a script, not a function')
+ success = false;
+ return
+ end
+
+
+ %% copyright block
+ [cr,N] = findblock(ins, 1);
+ if (Nfcn < N)
+ warning('function header in first block (where copyright block should be), not converting')
+ success = false;
+ return
+ end
+ cr = ltrim(cr, 3);
+
+ % cut 2nd line if empty
+ if isempty(cr{2})
+ cr2 = cell(1,length(cr)-1);
+ cr2(1) = cr(1);
+ cr2(2:end) = cr(3:end);
+ cr = cr2;
+ end
+
+ cr = prepend_each_line(cr, '%', ' ');
+ cr{1} = ['%' cr{1}];
+ copyright_summary = 'This is free software, see .m file for license.';
+
+
+ %% use block
+ % we don't parse this, just call get_help_text
+ temp = ins{N};
+ if ~strcmp(temp, '%% -*- texinfo -*-')
+ error('can''t find the texinfo line, aborting')
+ %success = false;
+ %return
+ end
+
+ %% the "lookfor" line
+ lookforstr = get_first_help_sentence (fname);
+ if (~isempty(strfind(lookforstr, newl)))
+ lookforstr
+ error('lookfor string contains newline: missing period? too long? some other issue?')
+ %success = false;
+ %return
+ end
+ if (length(lookforstr) > 76)
+ error(sprintf('lookfor string of length %d deemed too long', length(lookforstr)))
+ end
+
+
+ %% get the texinfo source, and format it
+ [text, form] = get_help_text(fname);
+ if ~strcmp(form, 'texinfo')
+ text
+ form
+ error('formatted incorrectly, help text not texinfo')
+ end
+
+ % Doctest diary-mode compatibility: force two blank lines after example.
+ % Final "\n\n" is incase text immediately follows "@end example".
+ text = regexprep(text, '(^\s*)(@end example\n)', '$1$2 @*\n\n',
+ 'lineanchors');
+
+ usestr = __makeinfo__(text, 'plain text');
+
+
+ %% remove the lookforstr from the text
+ I = strfind(usestr, lookforstr);
+ if length(I) ~= 1
+ I
+ lookforstr
+ usestr
+ error('too many lookfor lines?')
+ end
+ len = length(lookforstr);
+ J = I + len;
+
+ % if usestr has only a lookfor line then no need to see what's next
+ if (J < length(usestr))
+ % find next non-empty char
+ %while isspace(usestr(J))
+ % J = J + 1;
+ %end
+
+ % let's be more conservative trim newline in usual case:
+ if ~isspace(usestr(J))
+ error('no space or newline after lookfor line?');
+ end
+ J = J + 1;
+ end
+
+ usestr = usestr([1:(I-1) J:end]);
+
+ use = strsplit(usestr, newl, 'CollapseDelimiters', false);
+
+ %% remove this string
+ % and make sure these lines have the correct function name
+ remstr = '-- Function File: ';
+ for i=1:length(use)
+ if strfind(use{i}, remstr);
+ if isempty(strfind(use{i}, [' ' fcn]))
+ error('function @deftypefn line doesn''t include function name')
+ end
+ end
+ use{i} = strrep(use{i}, remstr, ' ');
+ end
+ %usestr = strrep(usestr, lookforstr, '');
+
+ use = ltrim(use, 2);
+ while isempty(use{end})
+ use = use(1:end-1);
+ end
+
+
+ %% the rest
+ N = Nfcn;
+ fcn_line = ins{N};
+
+ % sanity checks
+ I = strfind(ins{N+1}, '%');
+ if ~isempty(I) && I(1) == 1
+ ins{N}
+ ins{N+1}
+ error('possible duplicate comment header following function')
+ end
+
+ therest = ins(N+1:end);
+
+
+
+ %% Output
+ f = fopen(foutname, 'w');
+
+ fdisp(f, fcn_line)
+
+ fprintf(f, '%%%s %s\n', upper(fcn), lookforstr)
+
+ for i=1:length(use)
+ fprintf(f, '%%%s\n', use{i});
+ end
+
+ fdisp(f, '%');
+ fprintf(f, '%% %s\n', copyright_summary);
+
+ %fdisp(f, '%');
+ %fdisp(f, '% [Genereated from a GNU Octave .m file, edit that instead.]');
+
+ %fprintf(f,(s)
+
+ fdisp(f, '');
+ fdisp(f, '%% Note for developers');
+ fdisp(f, '% This file is autogenerated from a GNU Octave .m file.');
+ fdisp(f, '% If you want to edit, please make changes to the original instead');
+
+ fdisp(f, '');
+ for i=1:length(cr)
+ fprintf(f, '%s\n', cr{i});
+ end
+
+ fdisp(f, '');
+
+ for i=1:length(therest)
+ fprintf(f, '%s\n', therest{i});
+ end
+
+ fclose(f);
+
+ success = true;
+
+end
+
+
+function [block,endl] = findblock(f, j)
+ block = {}; c = 0;
+ %newl = sprintf('\n');
+ for i = j:length(f)
+ temp = f{i};
+ %if (strcmp(temp, newl))
+ if (isempty(temp))
+ endl = i + 1;
+ break
+ end
+ c = c + 1;
+ block{c} = temp;
+ end
+end
+
+
+function g = ltrim(f, n)
+ g = {};
+ for i = 1:length(f)
+ temp = f{i};
+ if length(temp) < n
+ g{i} = '';
+ else
+ g{i} = substr(temp, n+1);
+ end
+ end
+end
+
+
+function g = prepend_each_line(f, pre, pad)
+ g = {};
+ for i = 1:length(f)
+ temp = f{i};
+ if isempty(temp)
+ g{i} = pre;
+ else
+ g{i} = [pre pad temp];
+ end
+ end
+end
--
Alioth's /home/groups/pkg-octave/bin/git-commit-notice on /srv/git.debian.org/git/pkg-octave/octave-doctest.git
More information about the Pkg-octave-commit
mailing list