[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 [![Build Status](https://travis-ci.org/catch22/octave-doctest.svg?branch=master)](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