Full Code of brodie/cram for AI

main d245cca8d121 cached
49 files
97.4 KB
28.9k tokens
40 symbols
1 requests
Download .txt
Repository: brodie/cram
Branch: main
Commit: d245cca8d121
Files: 49
Total size: 97.4 KB

Directory structure:
gitextract_gm2l0gze/

├── .coveragerc
├── .gitignore
├── .hgignore
├── .hgtags
├── .pylintrc
├── .travis.yml
├── COPYING.txt
├── MANIFEST.in
├── Makefile
├── NEWS.rst
├── README.rst
├── TODO.md
├── contrib/
│   ├── PKGBUILD
│   └── cram.vim
├── cram/
│   ├── __init__.py
│   ├── __main__.py
│   ├── _cli.py
│   ├── _diff.py
│   ├── _main.py
│   ├── _process.py
│   ├── _run.py
│   ├── _test.py
│   └── _xunit.py
├── examples/
│   ├── .hidden/
│   │   └── hidden.t
│   ├── .hidden.t
│   ├── bare.t
│   ├── empty.t
│   ├── env.t
│   ├── fail.t
│   ├── missingeol.t
│   ├── skip.t
│   └── test.t
├── requirements.txt
├── scripts/
│   └── cram
├── setup.cfg
├── setup.py
└── tests/
    ├── config.t
    ├── debug.t
    ├── dist.t
    ├── doctest.t
    ├── encoding.t
    ├── interactive.t
    ├── pep8.t
    ├── pyflakes.t
    ├── run-doctests.py
    ├── setup.sh
    ├── test.t
    ├── usage.t
    └── xunit.t

================================================
FILE CONTENTS
================================================

================================================
FILE: .coveragerc
================================================
[run]
omit =
    */cram/__main__.py
source = cram


================================================
FILE: .gitignore
================================================
*.orig
*.rej
*~
*.mergebackup
*.o
*.so
*.dll
*.py[cdo]
*$py.class
__pycache__
*.swp
*.prof
\#*\#
.\#*
.coverage
*,cover
htmlcov
tests/*.err
examples/*.err
build
dist
MANIFEST
cram.egg-info
.DS_Store
tags
cscope.*
.idea


================================================
FILE: .hgignore
================================================
syntax: glob

*.orig
*.rej
*~
*.mergebackup
*.o
*.so
*.dll
*.py[cdo]
*$py.class
__pycache__
*.swp
*.prof
\#*\#
.\#*
.coverage
*,cover
htmlcov
tests/*.err
examples/*.err
build
dist
MANIFEST
cram.egg-info
.DS_Store
tags
cscope.*
.idea

syntax: regexp
^\.pc/
^\.(pydev)?project


================================================
FILE: .hgtags
================================================
931859fdd3e0d5af442a3e9b5fe6ac0dbfed2309 0.1
3c471f7a16b435095b98525e7b851b17e871a2ce 0.2
3c471f7a16b435095b98525e7b851b17e871a2ce 0.2
995a287114b0a2a0bcd79b9c5ce8ff98765e7c8a 0.2
924d14e0636a7ff5815c2412409115a69dfc63f0 0.3
3ba61fadf306c63ec4bc3254522f286a27ac974a 0.4
112e96e43892344954a98b0f05a32819f2b6c20d 0.5
05669fd0420dc0cd52f48bc2f2379a61732d14e0 0.6
e230eb00d4668508766fc32da154ba46c358ff5f 0.7


================================================
FILE: .pylintrc
================================================
[MESSAGES CONTROL]
# C0330: bad continuation
# The design check gives mostly useless advice.
# R0201: method could be a function
# W0123: eval used
# W0141: used range
# W0142: * or ** arguments
# W0201: attribute defined outside of __init__
# W0640: unreliable closure/loop variable checking
disable=C0330,design,R0201,W0123,W0141,W0142,W0201,W0640

[REPORTS]
reports=no

[TYPECHECK]
ignored-classes=
generated-members=

[BASIC]
const-rgx=(([a-zA-Z_][a-zA-Z0-9_]{2,30})|(__[a-z0-9_]{2,30}__))$
class-rgx=[a-zA-Z_][a-zA-Z0-9]{2,30}$
function-rgx=[a-z_][a-z0-9_]{2,30}$
method-rgx=[a-z_][a-z0-9_]{2,30}$
attr-rgx=[a-z_][a-z0-9_]{0,30}$
argument-rgx=[a-z_][a-z0-9_]{0,30}$
variable-rgx=[a-z_][a-z0-9_]{0,30}$
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$

[CLASSES]
ignore-iface-methods=
defining-attr-methods=__init__,__new__

[IMPORTS]
deprecated-modules=regsub,TERMIOS,Bastion,rexec

[FORMAT]
max-line-length=79
max-module-lines=5000

[MISCELLANEOUS]
notes=FIXME,XXX,TODO


================================================
FILE: .travis.yml
================================================
language: python

matrix:
  allow_failures:
    - python: nightly
      env: TESTOPTS=--shell=dash
    - python: pypy
      env: TESTOPTS=--shell=dash
    - python: pypy3
      env: TESTOPTS=--shell=dash
  include:
    - python: "3.5"
      env: TESTOPTS=--shell=dash
    - python: "3.5"
      env: TESTOPTS=--shell=bash
    - python: "3.5"
      env: TESTOPTS=--shell=zsh
      addons:
        apt:
          packages:
            - zsh
    - python: "3.4"
      env: TESTOPTS=--shell=dash
    - python: "3.3"
      env: TESTOPTS=--shell=dash
    - python: "3.2"
      env: TESTOPTS=--shell=dash
    - python: "2.7"
      env: TESTOPTS=--shell=dash
    - python: "2.6"
      env: TESTOPTS=--shell=dash
    - env: PYTHON=2.5 TESTOPTS=--shell=dash
      addons:
        apt:
          sources:
            - deadsnakes
          packages:
            - python2.5
    - env: PYTHON=2.4 TESTOPTS=--shell=dash
      addons:
        apt:
          sources:
            - deadsnakes
          packages:
            - python2.4
    - python: nightly
      env: TESTOPTS=--shell=dash
    - python: pypy
      env: TESTOPTS=--shell=dash
    - python: pypy3
      env: TESTOPTS=--shell=dash
  fast_finish: true

install: |
  if [ -z "$PYTHON" ]
  then
    [ "$TRAVIS_PYTHON_VERSION" = "3.2" ] && pip install coverage==3.7.1
    pip install -r requirements.txt
  fi

script: |
  if [ -z "$PYTHON" ]
  then
    make test TESTOPTS="$TESTOPTS"
  else
    make quicktest PYTHON="python$PYTHON"
  fi


================================================
FILE: COPYING.txt
================================================
                    GNU GENERAL PUBLIC LICENSE
                       Version 2, June 1991

 Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The licenses for most software are designed to take away your
freedom to share and change it.  By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users.  This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it.  (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.)  You can apply it to
your programs, too.

  When we speak of free software, we are referring to freedom, not
price.  Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.

  To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.

  For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have.  You must make sure that they, too, receive or can get the
source code.  And you must show them these terms so they know their
rights.

  We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.

  Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software.  If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.

  Finally, any free program is threatened constantly by software
patents.  We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary.  To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.

  The precise terms and conditions for copying, distribution and
modification follow.

                    GNU GENERAL PUBLIC LICENSE
   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION

  0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License.  The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language.  (Hereinafter, translation is included without limitation in
the term "modification".)  Each licensee is addressed as "you".

Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope.  The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.

  1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.

You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.

  2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:

    a) You must cause the modified files to carry prominent notices
    stating that you changed the files and the date of any change.

    b) You must cause any work that you distribute or publish, that in
    whole or in part contains or is derived from the Program or any
    part thereof, to be licensed as a whole at no charge to all third
    parties under the terms of this License.

    c) If the modified program normally reads commands interactively
    when run, you must cause it, when started running for such
    interactive use in the most ordinary way, to print or display an
    announcement including an appropriate copyright notice and a
    notice that there is no warranty (or else, saying that you provide
    a warranty) and that users may redistribute the program under
    these conditions, and telling the user how to view a copy of this
    License.  (Exception: if the Program itself is interactive but
    does not normally print such an announcement, your work based on
    the Program is not required to print an announcement.)

These requirements apply to the modified work as a whole.  If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works.  But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.

Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.

In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.

  3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:

    a) Accompany it with the complete corresponding machine-readable
    source code, which must be distributed under the terms of Sections
    1 and 2 above on a medium customarily used for software interchange; or,

    b) Accompany it with a written offer, valid for at least three
    years, to give any third party, for a charge no more than your
    cost of physically performing source distribution, a complete
    machine-readable copy of the corresponding source code, to be
    distributed under the terms of Sections 1 and 2 above on a medium
    customarily used for software interchange; or,

    c) Accompany it with the information you received as to the offer
    to distribute corresponding source code.  (This alternative is
    allowed only for noncommercial distribution and only if you
    received the program in object code or executable form with such
    an offer, in accord with Subsection b above.)

The source code for a work means the preferred form of the work for
making modifications to it.  For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable.  However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.

If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.

  4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License.  Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.

  5. You are not required to accept this License, since you have not
signed it.  However, nothing else grants you permission to modify or
distribute the Program or its derivative works.  These actions are
prohibited by law if you do not accept this License.  Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.

  6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions.  You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.

  7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License.  If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all.  For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.

If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.

It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices.  Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.

This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.

  8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded.  In such case, this License incorporates
the limitation as if written in the body of this License.

  9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time.  Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

Each version is given a distinguishing version number.  If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation.  If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.

  10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission.  For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this.  Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.

                            NO WARRANTY

  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.

  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.

                     END OF TERMS AND CONDITIONS

            How to Apply These Terms to Your New Programs

  If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.

  To do so, attach the following notices to the program.  It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

    <one line to give the program's name and a brief idea of what it does.>
    Copyright (C) <year>  <name of author>

    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 2 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, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

Also add information on how to contact you by electronic and paper mail.

If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:

    Gnomovision version 69, Copyright (C) year name of author
    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
    This is free software, and you are welcome to redistribute it
    under certain conditions; type `show c' for details.

The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License.  Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.

You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary.  Here is a sample; alter the names:

  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
  `Gnomovision' (which makes passes at compilers) written by James Hacker.

  <signature of Ty Coon>, 1 April 1989
  Ty Coon, President of Vice

This General Public License does not permit incorporating your program into
proprietary programs.  If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library.  If this is what you want to do, use the GNU Lesser General
Public License instead of this License.


================================================
FILE: MANIFEST.in
================================================
include .coveragerc .pylintrc .travis.yml Makefile MANIFEST.in
include *.md *.rst *.txt contrib/* scripts/*
exclude contrib/PKGBUILD
recursive-include examples *.t
recursive-include tests *.py *.sh *.t


================================================
FILE: Makefile
================================================
COVERAGE=coverage
PREFIX=/usr/local
export PREFIX
PYTHON=python3

all: build

build:
	$(PYTHON) setup.py build

check: test

clean:
	-$(PYTHON) setup.py clean --all
	find . -not \( -path '*/.hg/*' -o -path '*/.git/*' \) \
		\( -name '*.py[cdo]' -o -name '*.err' -o \
		-name '*,cover' -o -name __pycache__ \) -prune \
		-exec rm -rf '{}' ';'
	rm -rf dist build htmlcov
	rm -f MANIFEST .coverage cram.xml

dist:
	TAR_OPTIONS="--owner=root --group=root --mode=u+w,go-w,a+rX-s" \
		$(PYTHON) setup.py -q sdist

install: build
	$(PYTHON) setup.py install --prefix="$(PREFIX)" --force

quicktest:
	PYTHON=$(PYTHON) PYTHONPATH=`pwd` scripts/cram $(TESTOPTS) tests

test:
	$(COVERAGE) erase
	COVERAGE=$(COVERAGE) PYTHON=$(PYTHON) PYTHONPATH=`pwd` scripts/cram \
		$(TESTOPTS) tests
	$(COVERAGE) report --fail-under=100

.PHONY: all build check clean install dist quicktest test


================================================
FILE: NEWS.rst
================================================
======
 News
======

Version 0.7 (Feb. 24, 2016)
---------------------------

* Added the ``-d``/``--debug`` flag that disables diffing of
  expected/actual output and instead passes through script output to
  ``stdout``/``stderr``.

* Added the ``--shell-opts`` flag for specifying flags to invoke the
  shell with. By setting ``--shell-opts='-x'`` and ``--debug``
  together, this can be used to see shell commands as they're run and
  their output in real time which can be useful for debugging slow or
  hanging tests.

* Added xUnit XML output support (for better integration of test
  results with Bamboo and other continuous integration tools).

* Added support for using (esc) on expected out lines that aren't
  automatically escaped in actual output.

* Added the ``$TESTSHELL`` environment variable. This allows a test to
  portably check what shell it was invoked with.

* Added an error message for when no tests are found in a directory.

* Changed ``Makefile`` to install into ``/usr/local`` by default.

* Simplified the ``Makefile``'s targets. The targets available now are
  ``all``, ``build``, ``check``/``test``, ``clean``, ``dist``,
  ``install``, and ``quicktest`` (for running the test suite without
  checking test coverage).

* Fixed non-ASCII strings not being escaped with ``(esc)`` on Python 3.

* Fixed a crash on tests that don't have a trailing newline.

* Fixed a crash when using ``set -x`` with zsh.


Version 0.6 (Aug. 1, 2013)
--------------------------

* Added the long option ``--preserve-env`` for ``-E``.

* Added support for specifying options in ``.cramrc`` (configurable
  with the ``CRAMRC`` environment variable).

* Added a ``--shell`` option to change the shell tests are run
  with. Contributed by `Kamil Kisiel`_.

* Added Arch Linux package metadata (in ``contrib/``). Contributed by
  `Andrey Vlasovskikh`_.

* Fixed shell commands unintentionally inheriting Python's ``SIGPIPE``
  handler (causing commands that close pipes to print ``broken pipe``
  messages).

* Fixed ``EPIPE`` under PyPy when applying patches in
  ``--interactive`` mode.

* Added ``TESTFILE`` test environment variable (set to the name of the
  current test).

* Fixed GNU patch 2.7 compatibility by using relative paths instead of
  absolute paths. Contributed by `Douglas Creager`_.

* Fixed name clashes in temporary test directories (e.g., when running
  two tests with the same name in different folders).

* **Backwards compatibility:** Fixed improper usage of the subprocess
  library under Python 3. This fixes Python 3.3 support, but breaks
  support for Python 3.1-3.2.3 due to a bug in Python. If you're using
  Python 3.0-3.2, you must upgrade to Python 3.2.4 or newer.

.. _Kamil Kisiel: http://kamilkisiel.net/
.. _Andrey Vlasovskikh: https://twitter.com/vlasovskikh
.. _Douglas Creager: http://dcreager.net/


Version 0.5 (Jan. 8, 2011)
--------------------------

* **The test format has changed:** Matching output not ending in a
  newline now requires the ``(no-eol)`` keyword instead of ending the
  line in ``%``.

* Matching output containing unprintable characters now requires the
  ``(esc)`` keyword. Real output containing unprintable characters
  will automatically receive ``(esc)``.

* If an expected line matches its real output line exactly, special
  matching like ``(re)`` or ``(glob)`` will be ignored.

* Regular expressions ending in a trailing backslash are now
  considered invalid.

* Added an ``--indent`` option for changing the default amount of
  indentation required to specify commands and output.

* Added support for specifying command line options in the ``CRAM``
  environment variable.

* The ``--quiet`` and ``--verbose`` options can now be used together.

* When running Cram under Python 3, Unicode-specific line break
  characters will no longer be parsed as newlines.

* Tests are no longer required to end in a trailing newline.


Version 0.4 (Sep. 28, 2010)
---------------------------

* **The test format has changed:** Output lines containing regular
  expressions must now end in ``(re)`` or they'll be matched
  literally. Lines ending with keywords are matched literally first,
  however.

* Regular expressions are now matched from beginning to end. In other
  words ``\d (re)`` is matched as ``^\d$``.

* In addition to ``(re)``, ``(glob)`` has been added. It supports
  ``*``, ``?``, and escaping both characters (and backslashes) using
  ``\``.

* **Environment settings have changed:** The ``-D`` flag has been
  removed, ``$TESTDIR`` is now set to the directory containing the
  ``.t`` file, and ``$CRAMTMP`` is set to the test runner's temporary
  directory.

* ``-i``/``--interactive`` now requires ``patch(1)``. Instead of
  ``.err`` files replacing ``.t`` files during merges, diffs are
  applied using ``patch(1)``. This prevents matching regular
  expressions and globs from getting clobbered.

* Previous ``.err`` files are now removed when tests pass.

* Cram now exits with return code ``1`` if any tests failed.

* If a test exits with return code ``80``, it's considered a skipped a
  test. This is useful for intentionally disabling tests when they
  only work on certain platforms or in certain settings.

* The number of tests, the number of skipped tests, and the number of
  failed tests are now printed after all tests are finished.

* Added ``-q``/``--quiet`` to suppress diff output.

* Added `contrib/cram.vim`_ syntax file for Vim. Contributed by `Steve
  Losh`_.

.. _contrib/cram.vim: https://github.com/brodie/cram/blob/0.7/contrib/cram.vim
.. _Steve Losh: http://stevelosh.com/


Version 0.3 (Sep. 20, 2010)
---------------------------

* Implemented resetting of common environment variables. This behavior
  can be disabled using the ``-E`` flag.

* Changed the test runner to first make its own overall random
  temporary directory, make ``tmp`` inside of it and set ``TMPDIR``,
  etc. to its path, and run each test with a random temporary working
  directory inside of that.

* Added ``--keep-tmpdir``. Temporary directories are named by test
  filename (along with a random string).

* Added ``-i``/``--interactive`` to merge actual output back to into
  tests interactively.

* Added ability to match command output not ending in a newline by
  suffixing output in the test with ``%``.


Version 0.2 (Sep. 19, 2010)
---------------------------

* Changed the test runner to run tests with a random temporary working
  directory.


Version 0.1 (Sep. 19, 2010)
---------------------------

* Initial release.


================================================
FILE: README.rst
================================================
======================
 Cram: It's test time
======================

Cram is a functional testing framework for command line applications.
Cram tests look like snippets of interactive shell sessions. Cram runs
each command and compares the command output in the test with the
command's actual output.

Here's a snippet from `Cram's own test suite`_::

    Set up cram alias and example tests:

      $ . "$TESTDIR"/setup.sh

    Usage:

      $ cram -h
      [Uu]sage: cram \[OPTIONS\] TESTS\.\.\. (re)

      [Oo]ptions: (re)
        -h, --help          show this help message and exit
        -V, --version       show version information and exit
        -q, --quiet         don't print diffs
        -v, --verbose       show filenames and test status
        -i, --interactive   interactively merge changed test output
        -d, --debug         write script output directly to the terminal
        -y, --yes           answer yes to all questions
        -n, --no            answer no to all questions
        -E, --preserve-env  don't reset common environment variables
        --keep-tmpdir       keep temporary directories
        --shell=PATH        shell to use for running tests (default: /bin/sh)
        --shell-opts=OPTS   arguments to invoke shell with
        --indent=NUM        number of spaces to use for indentation (default: 2)
        --xunit-file=PATH   path to write xUnit XML output

The format in a nutshell:

* Cram tests use the ``.t`` file extension.

* Lines beginning with two spaces, a dollar sign, and a space are run
  in the shell.

* Lines beginning with two spaces, a greater than sign, and a space
  allow multi-line commands.

* All other lines beginning with two spaces are considered command
  output.

* Output lines ending with a space and the keyword ``(re)`` are
  matched as `Perl-compatible regular expressions`_.

* Lines ending with a space and the keyword ``(glob)`` are matched
  with a glob-like syntax. The only special characters supported are
  ``*`` and ``?``. Both characters can be escaped using ``\``, and the
  backslash can be escaped itself.

* Output lines ending with either of the above keywords are always
  first matched literally with actual command output.

* Lines ending with a space and the keyword ``(no-eol)`` will match
  actual output that doesn't end in a newline.

* Actual output lines containing unprintable characters are escaped
  and suffixed with a space and the keyword ``(esc)``. Lines matching
  unprintable output must also contain the keyword.

* Anything else is a comment.

.. _Cram's own test suite: https://github.com/brodie/cram/blob/master/tests/usage.t
.. _Perl-compatible regular expressions: https://en.wikipedia.org/wiki/Perl_Compatible_Regular_Expressions


Download
--------

* `cram-0.8.tar.gz`_ (32 KB, requires Python 3.3 or newer)

.. _cram-0.8.tar.gz: https://bitheap.org/cram/cram-0.8.tar.gz


Installation
------------

Install Cram using make::

    $ wget https://bitheap.org/cram/cram-0.8.tar.gz
    $ tar zxvf cram-0.8.tar.gz
    $ cd cram-0.8
    $ make install


Usage
-----

Cram will print a dot for each passing test. If a test fails, a
`unified context diff`_ is printed showing the test's expected output
and the actual output. Skipped tests (empty tests and tests that exit
with return code ``80``) are marked with ``s`` instead of a dot.

For example, if we run Cram on `its own example tests`_::

    .s.!
    --- examples/fail.t
    +++ examples/fail.t.err
    @@ -3,21 +3,22 @@
       $ echo 1
       1
       $ echo 1
    -  2
    +  1
       $ echo 1
       1

     Invalid regex:

       $ echo 1
    -  +++ (re)
    +  1

     Offset regular expression:

       $ printf 'foo\nbar\nbaz\n\n1\nA\n@\n'
       foo
    +  bar
       baz

       \d (re)
       [A-Z] (re)
    -  #
    +  @
    s.
    # Ran 6 tests, 2 skipped, 1 failed.

Cram will also write the test with its actual output to
``examples/fail.t.err``, allowing you to use other diff tools. This
file is automatically removed the next time the test passes.

When you're first writing a test, you might just write the commands
and run the test to see what happens. If you run Cram with ``-i`` or
``--interactive``, you'll be prompted to merge the actual output back
into the test. This makes it easy to quickly prototype new tests.

You can specify a default set of options by creating a ``.cramrc``
file. For example::

    [cram]
    verbose = True
    indent = 4

Is the same as invoking Cram with ``--verbose`` and ``--indent=4``.

To change what configuration file Cram loads, you can set the
``CRAMRC`` environment variable. You can also specify command line
options in the ``CRAM`` environment variable.

Note that the following environment variables are reset before tests
are run:

* ``TMPDIR``, ``TEMP``, and ``TMP`` are set to the test runner's
  ``tmp`` directory.

* ``LANG``, ``LC_ALL``, and ``LANGUAGE`` are set to ``C``.

* ``TZ`` is set to ``GMT``.

* ``COLUMNS`` is set to ``80``. (Note: When using ``--shell=zsh``,
  this cannot be reset. It will reflect the actual terminal's width.)

* ``CDPATH`` and ``GREP_OPTIONS`` are set to an empty string.

Cram also provides the following environment variables to tests:

* ``CRAMTMP``, set to the test runner's temporary directory.

* ``TESTDIR``, set to the directory containing the test file.

* ``TESTFILE``, set to the basename of the current test file.

* ``TESTSHELL``, set to the value specified by ``--shell``.

Also note that care should be taken with commands that close the test
shell's ``stdin``. For example, if you're trying to invoke ``ssh`` in
a test, try adding the ``-n`` option to prevent it from closing
``stdin``. Similarly, if you invoke a daemon process that inherits
``stdout`` and fails to close it, it may cause Cram to hang while
waiting for the test shell's ``stdout`` to be fully closed.

.. _unified context diff: https://en.wikipedia.org/wiki/Diff#Unified_format
.. _its own example tests: https://github.com/brodie/cram/tree/master/examples


Development
-----------

Download the official development repository using Git_::

    git clone https://github.com/brodie/cram.git

Test Cram using Cram::

    pip install -r requirements.txt
    make test

Visit GitHub_ if you'd like to fork the project, watch for new changes, or
report issues.

.. _Git: http://git-scm.com/
.. _GitHub: https://github.com/brodie/cram


================================================
FILE: TODO.md
================================================
* Add more comments explaining how different parts of the code work.

* Add a man page.

* Implement string substitutions (e.g., --substitute=FOOPORT=123).

* Conditionals (e.g., --define=windows=1, #if windows ... #else ...
  #endif).

* Support #!/usr/bin/env cram

* Support .cramrc in test directories. Though, if I do this, what happens
  when there are multiple .cramrc files? Does the deepest one completely
  override the others? Do they merge together?

* Homebrew formula.

* Debian, Ubuntu, CentOS/RHEL repos.

* Implement a test that does stricter style guide testing.

* Write contributor guidelines.

* Get the test suite running on AppVeyor under MSYS2.

  - http://help.appveyor.com/discussions/suggestions/615-support-for-msys2
  - https://github.com/behdad/harfbuzz/pull/112/files
  - https://github.com/khaledhosny/ots/pull/67/files
  - https://github.com/appveyor/ci/issues/352#issuecomment-138149606
  - https://github.com/appveyor/ci/issues/597
  - http://www.appveyor.com

* Get the test suite fully passing with Python.org's Windows
  distribution.

* Global setup/teardown support.

* Local setup/teardown? This is technically already supported via
  sourcing scripts and using exit traps, but dedicated syntax might be
  nice (e.g., #setup ... #endsetup? or maybe just #teardown ...
  #endteardown or #finally ... #endfinally?).

* Implement -j flag for concurrency.

* Flexible indentation support (with an algorithm similar to Python's
  for detecting indentation on a per-block basis).

* Some sort of plugin system (one that doesn't require writing plugins
  in Python) that allows basic extension of Cram's functionality (and
  possibly even syntax, though perhaps limited to just "macros" like
  #foo, #bar, etc. and matchers like (baz), (quux), etc.).

* Be able to run the Mercurial test suite.

* Write cram plugins for other testing frameworks (nose, py.test,
  etc.).

* Somehow make it possible to specify tests in Python doc
  strings (and similar things in other languages like Perl, Ruby,
  etc.).

* Emacs mode.


================================================
FILE: contrib/PKGBUILD
================================================
# Maintainer: Andrey Vlasovskikh <andrey.vlasovskikh@gmail.com>

pkgname=cram
pkgver=0.7
pkgrel=1
pkgdesc="Functional tests for command line applications"
arch=(any)
url="https://bitheap.org/cram/"
license=('GPL')
depends=('python')
source=("https://pypi.python.org/packages/source/c/cram/cram-$pkgver.tar.gz")
md5sums=('2ea37ada5190526b9bcaac5e4099221c')

build() {
    cd "$srcdir/$pkgname-$pkgver"
    python setup.py install --root="$pkgdir/" --optimize=1
}


================================================
FILE: contrib/cram.vim
================================================
" Vim syntax file
" Language: Cram Tests
" Author: Steve Losh (steve@stevelosh.com)
"
" Add the following line to your ~/.vimrc to enable:
" au BufNewFile,BufRead *.t set filetype=cram
"
" If you want folding you'll need the following line as well:
" let cram_fold=1
"
" You might also want to set the starting foldlevel for Cram files:
" autocmd Syntax cram setlocal foldlevel=1

if exists("b:current_syntax")
  finish
endif

syn include @Shell syntax/sh.vim

syn match cramComment /^[^ ].*$/ contains=@Spell
syn region cramOutput start=/^  [^$>]/ start=/^  $/ end=/\v.(\n\n*[^ ])\@=/me=s end=/^  [$>]/me=e-3 end=/^$/ fold containedin=cramBlock
syn match cramCommandStart /^  \$ / containedin=cramCommand
syn region cramCommand start=/^  \$ /hs=s+4,rs=s+4 end=/^  [^>]/me=e-3 end=/^  $/me=e-2 containedin=cramBlock contains=@Shell keepend
syn region cramBlock start=/^  /ms=e-2 end=/\v.(\n\n*[^ ])\@=/me=s end=/^$/me=e-1 fold keepend

hi link cramCommandStart Keyword
hi link cramComment Normal
hi link cramOutput Comment

if exists("cram_fold")
  setlocal foldmethod=syntax
endif

syn sync match cramSync grouphere NONE "^$"
syn sync maxlines=200

" It's okay to set tab settings here, because an indent of two spaces is specified
" by the file format.
setlocal tabstop=2 softtabstop=2 shiftwidth=2 expandtab

let b:current_syntax = "cram"


================================================
FILE: cram/__init__.py
================================================
"""Functional testing framework for command line applications"""

from cram._main import main
from cram._test import test, testfile

__all__ = ['main', 'test', 'testfile']


================================================
FILE: cram/__main__.py
================================================
"""Main module (invoked by "python3 -m cram")"""

import sys

import cram

try:
    sys.exit(cram.main(sys.argv[1:]))
except (BrokenPipeError, KeyboardInterrupt):
    pass


================================================
FILE: cram/_cli.py
================================================
"""The command line interface implementation"""

import os
import sys

from cram._process import execute

__all__ = ['runcli']

def _prompt(question, answers, auto=None):
    """Write a prompt to stdout and ask for answer in stdin.

    answers should be a string, with each character a single
    answer. An uppercase letter is considered the default answer.

    If an invalid answer is given, this asks again until it gets a
    valid one.

    If auto is set, the question is answered automatically with the
    specified value.
    """
    default = [c for c in answers if c.isupper()]
    while True:
        sys.stdout.write('%s [%s] ' % (question, answers))
        sys.stdout.flush()
        if auto is not None:
            sys.stdout.write(auto + '\n')
            sys.stdout.flush()
            return auto

        answer = sys.stdin.readline().strip().lower()
        if not answer and default:
            return default[0]
        elif answer and answer in answers.lower():
            return answer

def _log(msg=None, verbosemsg=None, verbose=False):
    """Write msg to standard out and flush.

    If verbose is True, write verbosemsg instead.
    """
    if verbose:
        msg = verbosemsg
    if msg:
        if isinstance(msg, bytes):
            sys.stdout.buffer.write(msg)
        else: # pragma: nocover
            sys.stdout.write(msg)
        sys.stdout.flush()

def _patch(cmd, diff):
    """Run echo [lines from diff] | cmd -p0"""
    out, retcode = execute([cmd, '-p0'], stdin=b''.join(diff))
    return retcode == 0

def runcli(tests, quiet=False, verbose=False, patchcmd=None, answer=None):
    """Run tests with command line interface input/output.

    tests should be a sequence of 2-tuples containing the following:

        (test path, test function)

    This function yields a new sequence where each test function is wrapped
    with a function that handles CLI input/output.

    If quiet is True, diffs aren't printed. If verbose is True,
    filenames and status information are printed.

    If patchcmd is set, a prompt is written to stdout asking if
    changed output should be merged back into the original test. The
    answer is read from stdin. If 'y', the test is patched using patch
    based on the changed output.
    """
    total, skipped, failed = [0], [0], [0]

    for path, test in tests:
        def testwrapper():
            """Test function that adds CLI output"""
            total[0] += 1
            _log(None, path + b': ', verbose)

            refout, postout, diff = test()
            if refout is None:
                skipped[0] += 1
                _log('s', 'empty\n', verbose)
                return refout, postout, diff

            abspath = os.path.abspath(path)
            errpath = abspath + b'.err'

            if postout is None:
                skipped[0] += 1
                _log('s', 'skipped\n', verbose)
            elif not diff:
                _log('.', 'passed\n', verbose)
                if os.path.exists(errpath):
                    os.remove(errpath)
            else:
                failed[0] += 1
                _log('!', 'failed\n', verbose)
                if not quiet:
                    _log('\n', None, verbose)

                errfile = open(errpath, 'wb')
                try:
                    for line in postout:
                        errfile.write(line)
                finally:
                    errfile.close()

                if not quiet:
                    origdiff = diff
                    diff = []
                    for line in origdiff:
                        sys.stdout.buffer.write(line)
                        diff.append(line)

                    if (patchcmd and
                        _prompt('Accept this change?', 'yN', answer) == 'y'):
                        if _patch(patchcmd, diff):
                            _log(None, path + b': merged output\n', verbose)
                            os.remove(errpath)
                        else:
                            _log(path + b': merge failed\n')

            return refout, postout, diff

        yield (path, testwrapper)

    if total[0] > 0:
        _log('\n', None, verbose)
        _log('# Ran %s tests, %s skipped, %s failed.\n'
             % (total[0], skipped[0], failed[0]))


================================================
FILE: cram/_diff.py
================================================
"""Utilities for diffing test files and their output"""

import codecs
import difflib
import re

__all__ = ['esc', 'glob', 'regex', 'unified_diff']

def _regex(pattern, s):
    """Match a regular expression or return False if invalid.

    >>> [bool(_regex(r, b'foobar')) for r in (b'foo.*', b'***')]
    [True, False]
    """
    try:
        return re.match(pattern + br'\Z', s)
    except re.error:
        return False

def _glob(el, l):
    r"""Match a glob-like pattern.

    The only supported special characters are * and ?. Escaping is
    supported.

    >>> bool(_glob(br'\* \\ \? fo?b*', b'* \\ ? foobar'))
    True
    """
    i, n = 0, len(el)
    res = b''
    while i < n:
        c = el[i:i + 1]
        i += 1
        if c == b'\\' and el[i] in b'*?\\':
            res += el[i - 1:i + 1]
            i += 1
        elif c == b'*':
            res += b'.*'
        elif c == b'?':
            res += b'.'
        else:
            res += re.escape(c)
    return _regex(res, l)

def _matchannotation(keyword, matchfunc, el, l):
    """Apply match function based on annotation keyword"""
    ann = b' (%s)\n' % keyword
    return el.endswith(ann) and matchfunc(el[:-len(ann)], l[:-1])

def regex(el, l):
    """Apply a regular expression match to a line annotated with '(re)'"""
    return _matchannotation(b're', _regex, el, l)

def glob(el, l):
    """Apply a glob match to a line annotated with '(glob)'"""
    return _matchannotation(b'glob', _glob, el, l)

def esc(el, l):
    """Apply an escape match to a line annotated with '(esc)'"""
    ann = b' (esc)\n'

    if el.endswith(ann):
        el = codecs.escape_decode(el[:-len(ann)])[0] + b'\n'
    if el == l:
        return True

    if l.endswith(ann):
        l = codecs.escape_decode(l[:-len(ann)])[0] + b'\n'
    return el == l

class _SequenceMatcher(difflib.SequenceMatcher, object):
    """Like difflib.SequenceMatcher, but supports custom match functions"""
    def __init__(self, *args, **kwargs):
        self._matchers = kwargs.pop('matchers', [])
        super(_SequenceMatcher, self).__init__(*args, **kwargs)

    def _match(self, el, l):
        """Tests for matching lines using custom matchers"""
        for matcher in self._matchers:
            if matcher(el, l):
                return True
        return False

    def find_longest_match(self, alo, ahi, blo, bhi):
        """Find longest matching block in a[alo:ahi] and b[blo:bhi]"""
        # SequenceMatcher uses find_longest_match() to slowly whittle down
        # the differences between a and b until it has each matching block.
        # Because of this, we can end up doing the same matches many times.
        matches = []
        for n, (el, line) in enumerate(zip(self.a[alo:ahi], self.b[blo:bhi])):
            if el != line and self._match(el, line):
                # This fools the superclass's method into thinking that the
                # regex/glob in a is identical to b by replacing a's line (the
                # expected output) with b's line (the actual output).
                self.a[alo + n] = line
                matches.append((n, el))
        ret = super(_SequenceMatcher, self).find_longest_match(alo, ahi,
                                                               blo, bhi)
        # Restore the lines replaced above. Otherwise, the diff output
        # would seem to imply that the tests never had any regexes/globs.
        for n, el in matches:
            self.a[alo + n] = el
        return ret

def unified_diff(l1, l2, fromfile=b'', tofile=b'', fromfiledate=b'',
                 tofiledate=b'', n=3, lineterm=b'\n', matchers=None):
    r"""Compare two sequences of lines; generate the delta as a unified diff.

    This is like difflib.unified_diff(), but allows custom matchers.

    >>> l1 = [b'a\n', b'? (glob)\n']
    >>> l2 = [b'a\n', b'b\n']
    >>> (list(unified_diff(l1, l2, b'f1', b'f2', b'1970-01-01',
    ...                    b'1970-01-02')) ==
    ...  [b'--- f1\t1970-01-01\n', b'+++ f2\t1970-01-02\n',
    ...   b'@@ -1,2 +1,2 @@\n', b' a\n', b'-? (glob)\n', b'+b\n'])
    True

    >>> from cram._diff import glob
    >>> list(unified_diff(l1, l2, matchers=[glob]))
    []
    """
    if matchers is None:
        matchers = []
    started = False
    matcher = _SequenceMatcher(None, l1, l2, matchers=matchers)
    for group in matcher.get_grouped_opcodes(n):
        if not started:
            if fromfiledate:
                fromdate = b'\t' + fromfiledate
            else:
                fromdate = b''
            if tofiledate:
                todate = b'\t' + tofiledate
            else:
                todate = b''
            yield b'--- ' + fromfile + fromdate + lineterm
            yield b'+++ ' + tofile + todate + lineterm
            started = True
        i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4]
        yield (b'@@ -%d,%d +%d,%d @@' % (i1 + 1, i2 - i1, j1 + 1, j2 - j1) +
               lineterm)
        for tag, i1, i2, j1, j2 in group:
            if tag == 'equal':
                for line in l1[i1:i2]:
                    yield b' ' + line
                continue
            if tag == 'replace' or tag == 'delete':
                for line in l1[i1:i2]:
                    yield b'-' + line
            if tag == 'replace' or tag == 'insert':
                for line in l2[j1:j2]:
                    yield b'+' + line


================================================
FILE: cram/_main.py
================================================
"""Main entry point"""

import optparse
import os
import shlex
import shutil
import sys
import tempfile

try:
    import configparser
except ImportError: # pragma: nocover
    import ConfigParser as configparser

from cram._cli import runcli
from cram._run import runtests
from cram._xunit import runxunit

def _which(cmd):
    """Return the path to cmd or None if not found"""
    cmd = os.fsencode(cmd)
    for p in os.environ['PATH'].split(os.pathsep):
        path = os.path.join(os.fsencode(p), cmd)
        if os.path.isfile(path) and os.access(path, os.X_OK):
            return os.path.abspath(path)
    return None

def _expandpath(path):
    """Expands ~ and environment variables in path"""
    return os.path.expanduser(os.path.expandvars(path))

class _OptionParser(optparse.OptionParser):
    """Like optparse.OptionParser, but supports setting values through
    CRAM= and .cramrc."""

    def __init__(self, *args, **kwargs):
        self._config_opts = {}
        optparse.OptionParser.__init__(self, *args, **kwargs)

    def add_option(self, *args, **kwargs):
        option = optparse.OptionParser.add_option(self, *args, **kwargs)
        if option.dest and option.dest != 'version':
            key = option.dest.replace('_', '-')
            self._config_opts[key] = option.action == 'store_true'
        return option

    def parse_args(self, args=None, values=None):
        config = configparser.RawConfigParser()
        config.read(_expandpath(os.environ.get('CRAMRC', '.cramrc')))
        defaults = {}
        for key, isbool in self._config_opts.items():
            try:
                if isbool:
                    try:
                        value = config.getboolean('cram', key)
                    except ValueError:
                        value = config.get('cram', key)
                        self.error('--%s: invalid boolean value: %r'
                                   % (key, value))
                else:
                    value = config.get('cram', key)
            except (configparser.NoSectionError, configparser.NoOptionError):
                pass
            else:
                defaults[key] = value
        self.set_defaults(**defaults)

        eargs = os.environ.get('CRAM', '').strip()
        if eargs:
            args = args or []
            args += shlex.split(eargs)

        try:
            return optparse.OptionParser.parse_args(self, args, values)
        except optparse.OptionValueError:
            self.error(str(sys.exc_info()[1]))

def _parseopts(args):
    """Parse command line arguments"""
    p = _OptionParser(usage='cram [OPTIONS] TESTS...', prog='cram')
    p.add_option('-V', '--version', action='store_true',
                 help='show version information and exit')
    p.add_option('-q', '--quiet', action='store_true',
                 help="don't print diffs")
    p.add_option('-v', '--verbose', action='store_true',
                 help='show filenames and test status')
    p.add_option('-i', '--interactive', action='store_true',
                 help='interactively merge changed test output')
    p.add_option('-d', '--debug', action='store_true',
                 help='write script output directly to the terminal')
    p.add_option('-y', '--yes', action='store_true',
                 help='answer yes to all questions')
    p.add_option('-n', '--no', action='store_true',
                 help='answer no to all questions')
    p.add_option('-E', '--preserve-env', action='store_true',
                 help="don't reset common environment variables")
    p.add_option('--keep-tmpdir', action='store_true',
                 help='keep temporary directories')
    p.add_option('--shell', action='store', default='/bin/sh', metavar='PATH',
                 help='shell to use for running tests (default: %default)')
    p.add_option('--shell-opts', action='store', metavar='OPTS',
                 help='arguments to invoke shell with')
    p.add_option('--indent', action='store', default=2, metavar='NUM',
                 type='int', help=('number of spaces to use for indentation '
                                   '(default: %default)'))
    p.add_option('--xunit-file', action='store', metavar='PATH',
                 help='path to write xUnit XML output')
    opts, paths = p.parse_args(args)
    paths = [os.fsencode(path) for path in paths]
    return opts, paths, p.get_usage

def main(args):
    """Main entry point.

    If you're thinking of using Cram in other Python code (e.g., unit tests),
    consider using the test() or testfile() functions instead.

    :param args: Script arguments (excluding script name)
    :type args: str
    :return: Exit code (non-zero on failure)
    :rtype: int
    """
    opts, paths, getusage = _parseopts(args)
    if opts.version:
        sys.stdout.write("""Cram CLI testing framework (version 0.8)

Copyright (C) 2010-2021 Brodie Rao <brodie@bitheap.org> and others
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
""")
        return

    conflicts = [('--yes', opts.yes, '--no', opts.no),
                 ('--quiet', opts.quiet, '--interactive', opts.interactive),
                 ('--debug', opts.debug, '--quiet', opts.quiet),
                 ('--debug', opts.debug, '--interactive', opts.interactive),
                 ('--debug', opts.debug, '--verbose', opts.verbose),
                 ('--debug', opts.debug, '--xunit-file', opts.xunit_file)]
    for s1, o1, s2, o2 in conflicts:
        if o1 and o2:
            sys.stderr.write('options %s and %s are mutually exclusive\n'
                             % (s1, s2))
            return 2

    shellcmd = _which(opts.shell)
    if not shellcmd:
        sys.stderr.buffer.write(b'shell not found: %s\n' %
                                os.fsencode(opts.shell))
        return 2
    shell = [shellcmd]
    if opts.shell_opts:
        shell += shlex.split(opts.shell_opts)

    patchcmd = None
    if opts.interactive:
        patchcmd = _which('patch')
        if not patchcmd:
            sys.stderr.write('patch(1) required for -i\n')
            return 2

    if not paths:
        sys.stdout.write(getusage())
        return 2

    badpaths = [path for path in paths if not os.path.exists(path)]
    if badpaths:
        sys.stderr.buffer.write(b'no such file: %s\n' % badpaths[0])
        return 2

    if opts.yes:
        answer = 'y'
    elif opts.no:
        answer = 'n'
    else:
        answer = None

    tmpdir = os.environ['CRAMTMP'] = tempfile.mkdtemp('', 'cramtests-')
    tmpdirb = os.fsencode(tmpdir)
    proctmp = os.path.join(tmpdir, 'tmp')
    for s in ('TMPDIR', 'TEMP', 'TMP'):
        os.environ[s] = proctmp

    os.mkdir(proctmp)
    try:
        tests = runtests(paths, tmpdirb, shell, indent=opts.indent,
                         cleanenv=not opts.preserve_env, debug=opts.debug)
        if not opts.debug:
            tests = runcli(tests, quiet=opts.quiet, verbose=opts.verbose,
                           patchcmd=patchcmd, answer=answer)
            if opts.xunit_file is not None:
                tests = runxunit(tests, opts.xunit_file)

        hastests = False
        failed = False
        for path, test in tests:
            hastests = True
            refout, postout, diff = test()
            if diff:
                failed = True

        if not hastests:
            sys.stderr.write('no tests found\n')
            return 2

        return int(failed)
    finally:
        if opts.keep_tmpdir:
            sys.stdout.buffer.write(b'# Kept temporary directory: %s\n'
                                    % tmpdirb)
        else:
            shutil.rmtree(tmpdir)


================================================
FILE: cram/_process.py
================================================
"""Utilities for running subprocesses"""

import os
import signal
import subprocess
import sys

__all__ = ['PIPE', 'STDOUT', 'execute']

PIPE = subprocess.PIPE
STDOUT = subprocess.STDOUT

def _makeresetsigpipe():
    """Make a function to reset SIGPIPE to SIG_DFL (for use in subprocesses).

    Doing subprocess.Popen(..., preexec_fn=makeresetsigpipe()) will prevent
    Python's SIGPIPE handler (SIG_IGN) from being inherited by the
    child process.
    """
    if (sys.platform == 'win32' or
        getattr(signal, 'SIGPIPE', None) is None): # pragma: nocover
        return None
    return lambda: signal.signal(signal.SIGPIPE, signal.SIG_DFL)

def execute(args, stdin=None, stdout=None, stderr=None, cwd=None, env=None):
    """Run a process and return its output and return code.

    stdin may either be None or a string to send to the process.

    stdout may either be None or PIPE. If set to PIPE, the process's output
    is returned as a string.

    stderr may either be None or STDOUT. If stdout is set to PIPE and stderr
    is set to STDOUT, the process's stderr output will be interleaved with
    stdout and returned as a string.

    cwd sets the process's current working directory.

    env can be set to a dictionary to override the process's environment
    variables.

    This function returns a 2-tuple of (output, returncode).
    """
    if sys.platform == 'win32': # pragma: nocover
        args = [os.fsdecode(arg) for arg in args]

    p = subprocess.Popen(args, stdin=PIPE, stdout=stdout, stderr=stderr,
                         cwd=cwd, env=env, bufsize=-1,
                         preexec_fn=_makeresetsigpipe(),
                         close_fds=os.name == 'posix')
    out, err = p.communicate(stdin)
    return out, p.returncode


================================================
FILE: cram/_run.py
================================================
"""The test runner"""

import os
import sys

from cram._test import testfile

__all__ = ['runtests']

if sys.platform == 'win32': # pragma: nocover
    def _walk(top):
        top = os.fsdecode(top)
        for root, dirs, files in os.walk(top):
            yield (os.fsencode(root),
                   [os.fsencode(p) for p in dirs],
                   [os.fsencode(p) for p in files])
else:
    _walk = os.walk

def _findtests(paths):
    """Yield tests in paths in sorted order"""
    for p in paths:
        if os.path.isdir(p):
            for root, dirs, files in _walk(p):
                if os.path.basename(root).startswith(b'.'):
                    continue
                for f in sorted(files):
                    if not f.startswith(b'.') and f.endswith(b'.t'):
                        yield os.path.normpath(os.path.join(root, f))
        else:
            yield os.path.normpath(p)

def runtests(paths, tmpdir, shell, indent=2, cleanenv=True, debug=False):
    """Run tests and yield results.

    This yields a sequence of 2-tuples containing the following:

        (test path, test function)

    The test function, when called, runs the test in a temporary directory
    and returns a 3-tuple:

        (list of lines in the test, same list with actual output, diff)
    """
    cwd = os.getcwd()
    seen = set()
    basenames = set()
    for i, path in enumerate(_findtests(paths)):
        abspath = os.path.abspath(path)
        if abspath in seen:
            continue
        seen.add(abspath)

        if not os.stat(path).st_size:
            yield (path, lambda: (None, None, None))
            continue

        basename = os.path.basename(path)
        if basename in basenames:
            basename = basename + b'-%d' % i
        else:
            basenames.add(basename)

        def test():
            """Run test file"""
            testdir = os.path.join(tmpdir, basename)
            os.mkdir(testdir)
            try:
                os.chdir(testdir)
                return testfile(abspath, shell, indent=indent,
                                cleanenv=cleanenv, debug=debug,
                                testname=path)
            finally:
                os.chdir(cwd)

        yield (path, test)


================================================
FILE: cram/_test.py
================================================
"""Utilities for running individual tests"""

import itertools
import os
import re
import time

from cram._diff import esc, glob, regex, unified_diff
from cram._process import PIPE, STDOUT, execute

__all__ = ['test', 'testfile']

_needescape = re.compile(br'[\x00-\x09\x0b-\x1f\x7f-\xff]').search
_escapesub = re.compile(br'[\x00-\x09\x0b-\x1f\\\x7f-\xff]').sub
_escapemap = dict((bytes([i]), br'\x%02x' % i) for i in range(256))
_escapemap.update({b'\\': b'\\\\', b'\r': br'\r', b'\t': br'\t'})

def _escape(s):
    """Like the string-escape codec, but doesn't escape quotes"""
    return (_escapesub(lambda m: _escapemap[m.group(0)], s[:-1]) +
            b' (esc)\n')

def test(lines, shell='/bin/sh', indent=2, testname=None, env=None,
         cleanenv=True, debug=False):
    r"""Run test lines and return input, output, and diff.

    This returns a 3-tuple containing the following:

        (list of lines in test, same list with actual output, diff)

    diff is a generator that yields the diff between the two lists.

    If a test exits with return code 80, the actual output is set to
    None and diff is set to [].

    Note that the TESTSHELL environment variable is available in the
    test (set to the specified shell). However, the TESTDIR and
    TESTFILE environment variables are not available. To run actual
    test files, see testfile().

    Example usage:

    >>> refout, postout, diff = test([b'  $ echo hi\n',
    ...                               b'  [a-z]{2} (re)\n'])
    >>> refout == [b'  $ echo hi\n', b'  [a-z]{2} (re)\n']
    True
    >>> postout == [b'  $ echo hi\n', b'  hi\n']
    True
    >>> bool(diff)
    False

    lines may also be a single bytes string:

    >>> refout, postout, diff = test(b'  $ echo hi\n  bye\n')
    >>> refout == [b'  $ echo hi\n', b'  bye\n']
    True
    >>> postout == [b'  $ echo hi\n', b'  hi\n']
    True
    >>> bool(diff)
    True
    >>> (b''.join(diff) ==
    ...  b'--- \n+++ \n@@ -1,2 +1,2 @@\n   $ echo hi\n-  bye\n+  hi\n')
    True

    :param lines: Test input
    :type lines: bytes or collections.Iterable[bytes]
    :param shell: Shell to run test in
    :type shell: bytes or str or list[bytes] or list[str]
    :param indent: Amount of indentation to use for shell commands
    :type indent: int
    :param testname: Optional test file name (used in diff output)
    :type testname: bytes or None
    :param env: Optional environment variables for the test shell
    :type env: dict or None
    :param cleanenv: Whether or not to sanitize the environment
    :type cleanenv: bool
    :param debug: Whether or not to run in debug mode (don't capture stdout)
    :type debug: bool
    return: Input, output, and diff iterables
    :rtype: (list[bytes], list[bytes], collections.Iterable[bytes])
    """
    indent = b' ' * indent
    cmdline = indent + b'$ '
    conline = indent + b'> '
    salt = b'CRAM%.5f' % time.time()

    if env is None:
        env = os.environ.copy()

    if cleanenv:
        for s in ('LANG', 'LC_ALL', 'LANGUAGE'):
            env[s] = 'C'
        env['TZ'] = 'GMT'
        env['CDPATH'] = ''
        env['COLUMNS'] = '80'
        env['GREP_OPTIONS'] = ''

    if isinstance(lines, bytes):
        lines = lines.splitlines(True)

    if isinstance(shell, (bytes, str)):
        shell = [shell]
    env['TESTSHELL'] = shell[0]

    if debug:
        stdin = []
        for line in lines:
            if not line.endswith(b'\n'):
                line += b'\n'
            if line.startswith(cmdline):
                stdin.append(line[len(cmdline):])
            elif line.startswith(conline):
                stdin.append(line[len(conline):])

        execute(shell + ['-'], stdin=b''.join(stdin), env=env)
        return ([], [], [])

    after = {}
    refout, postout = [], []
    i = pos = prepos = -1
    stdin = []
    for i, line in enumerate(lines):
        if not line.endswith(b'\n'):
            line += b'\n'
        refout.append(line)
        if line.startswith(cmdline):
            after.setdefault(pos, []).append(line)
            prepos = pos
            pos = i
            stdin.append(b'echo %s %d $?\n' % (salt, i))
            stdin.append(line[len(cmdline):])
        elif line.startswith(conline):
            after.setdefault(prepos, []).append(line)
            stdin.append(line[len(conline):])
        elif not line.startswith(indent):
            after.setdefault(pos, []).append(line)
    stdin.append(b'echo %s %d $?\n' % (salt, i + 1))

    output, retcode = execute(shell + ['-'], stdin=b''.join(stdin),
                              stdout=PIPE, stderr=STDOUT, env=env)
    if retcode == 80:
        return (refout, None, [])

    pos = -1
    ret = 0
    for i, line in enumerate(output[:-1].splitlines(True)):
        out, cmd = line, None
        if salt in line:
            out, cmd = line.split(salt, 1)

        if out:
            if not out.endswith(b'\n'):
                out += b' (no-eol)\n'

            if _needescape(out):
                out = _escape(out)
            postout.append(indent + out)

        if cmd:
            ret = int(cmd.split()[1])
            if ret != 0:
                postout.append(indent + b'[%d]\n' % ret)
            postout += after.pop(pos, [])
            pos = int(cmd.split()[0])

    postout += after.pop(pos, [])

    if testname:
        diffpath = testname
        errpath = diffpath + b'.err'
    else:
        diffpath = errpath = b''
    diff = unified_diff(refout, postout, diffpath, errpath,
                        matchers=[esc, glob, regex])
    for firstline in diff:
        return refout, postout, itertools.chain([firstline], diff)
    return refout, postout, []

def testfile(path, shell='/bin/sh', indent=2, env=None, cleanenv=True,
             debug=False, testname=None):
    """Run test at path and return input, output, and diff.

    This returns a 3-tuple containing the following:

        (list of lines in test, same list with actual output, diff)

    diff is a generator that yields the diff between the two lists.

    If a test exits with return code 80, the actual output is set to
    None and diff is set to [].

    Note that the TESTDIR, TESTFILE, and TESTSHELL environment
    variables are available to use in the test.

    :param path: Path to test file
    :type path: bytes or str
    :param shell: Shell to run test in
    :type shell: bytes or str or list[bytes] or list[str]
    :param indent: Amount of indentation to use for shell commands
    :type indent: int
    :param env: Optional environment variables for the test shell
    :type env: dict or None
    :param cleanenv: Whether or not to sanitize the environment
    :type cleanenv: bool
    :param debug: Whether or not to run in debug mode (don't capture stdout)
    :type debug: bool
    :param testname: Optional test file name (used in diff output)
    :type testname: bytes or None
    :return: Input, output, and diff iterables
    :rtype: (list[bytes], list[bytes], collections.Iterable[bytes])
    """
    f = open(path, 'rb')
    try:
        abspath = os.path.abspath(path)
        env = env or os.environ.copy()
        env['TESTDIR'] = os.fsdecode(os.path.dirname(abspath))
        env['TESTFILE'] = os.fsdecode(os.path.basename(abspath))
        if testname is None: # pragma: nocover
            testname = os.path.basename(abspath)
        return test(f, shell, indent=indent, testname=testname, env=env,
                    cleanenv=cleanenv, debug=debug)
    finally:
        f.close()


================================================
FILE: cram/_xunit.py
================================================
"""xUnit XML output"""

import locale
import os
import re
import socket
import sys
import time

__all__ = ['runxunit']

_widecdataregex = (r'(?:[^\x09\x0a\x0d\x20-\ud7ff\ue000-\ufffd'
                   r'\U00010000-\U0010ffff]|]]>)')
_narrowcdataregex = (r'(?:[^\x09\x0a\x0d\x20-\ud7ff\ue000-\ufffd]'
                     r'|]]>)')
_widequoteattrregex = (r'[^\x20\x21\x23-\x25\x27-\x3b\x3d'
                       r'\x3f-\ud7ff\ue000-\ufffd'
                       r'\U00010000-\U0010ffff]')
_narrowquoteattrregex = (r'[^\x20\x21\x23-\x25\x27-\x3b\x3d'
                         r'\x3f-\ud7ff\ue000-\ufffd]')
_replacementchar = '\N{REPLACEMENT CHARACTER}'

if sys.maxunicode >= 0x10ffff: # pragma: nocover
    _cdatasub = re.compile(_widecdataregex).sub
    _quoteattrsub = re.compile(_widequoteattrregex).sub
else: # pragma: nocover
    _cdatasub = re.compile(_narrowcdataregex).sub
    _quoteattrsub = re.compile(_narrowquoteattrregex).sub

def _cdatareplace(m):
    """Replace _cdatasub() regex match"""
    if m.group(0) == ']]>':
        return ']]>]]&gt;<![CDATA['
    else:
        return _replacementchar

def _cdata(s):
    r"""Escape a string as an XML CDATA block.

    >>> (_cdata('1<\'2\'>&"3\x00]]>\t\r\n') ==
    ...  '<![CDATA[1<\'2\'>&\"3\ufffd]]>]]&gt;<![CDATA[\t\r\n]]>')
    True
    """
    return '<![CDATA[%s]]>' % _cdatasub(_cdatareplace, s)

def _quoteattrreplace(m):
    """Replace _quoteattrsub() regex match"""
    return {'\t': '&#9;',
            '\n': '&#10;',
            '\r': '&#13;',
            '"': '&quot;',
            '&': '&amp;',
            '<': '&lt;',
            '>': '&gt;'}.get(m.group(0), _replacementchar)

def _quoteattr(s):
    r"""Escape a string for use as an XML attribute value.

    >>> (_quoteattr('1<\'2\'>&"3\x00]]>\t\r\n') ==
    ...  '"1&lt;\'2\'&gt;&amp;&quot;3\ufffd]]&gt;&#9;&#13;&#10;"')
    True
    """
    return '"%s"' % _quoteattrsub(_quoteattrreplace, s)

def _timestamp():
    """Return the current time in ISO 8601 format"""
    tm = time.localtime()
    if tm.tm_isdst == 1: # pragma: nocover
        tz = time.altzone
    else: # pragma: nocover
        tz = time.timezone

    timestamp = time.strftime('%Y-%m-%dT%H:%M:%S', tm)
    tzhours = int(-tz / 60 / 60)
    tzmins = int(abs(tz) / 60 % 60)
    timestamp += '%+03d:%02d' % (tzhours, tzmins)
    return timestamp

def runxunit(tests, xmlpath):
    """Run tests with xUnit XML output.

    tests should be a sequence of 2-tuples containing the following:

        (test path, test function)

    This function yields a new sequence where each test function is wrapped
    with a function that writes test results to an xUnit XML file.
    """
    suitestart = time.time()
    timestamp = _timestamp()
    hostname = socket.gethostname()
    total, skipped, failed = [0], [0], [0]
    testcases = []

    for path, test in tests:
        def testwrapper():
            """Run test and collect XML output"""
            total[0] += 1

            start = time.time()
            refout, postout, diff = test()
            testtime = time.time() - start

            classname = path.decode(locale.getpreferredencoding(), 'replace')
            name = os.path.basename(classname)

            if postout is None:
                skipped[0] += 1
                testcase = (('  <testcase classname=%(classname)s\n'
                             '            name=%(name)s\n'
                             '            time="%(time).6f">\n'
                             '    <skipped/>\n'
                             '  </testcase>\n') %
                            {'classname': _quoteattr(classname),
                             'name': _quoteattr(name),
                             'time': testtime})
            elif diff:
                failed[0] += 1
                diff = list(diff)
                diffu = ''.join(l.decode(locale.getpreferredencoding(),
                                         'replace')
                                for l in diff)
                testcase = (('  <testcase classname=%(classname)s\n'
                             '            name=%(name)s\n'
                             '            time="%(time).6f">\n'
                             '    <failure>%(diff)s</failure>\n'
                             '  </testcase>\n') %
                            {'classname': _quoteattr(classname),
                             'name': _quoteattr(name),
                             'time': testtime,
                             'diff': _cdata(diffu)})
            else:
                testcase = (('  <testcase classname=%(classname)s\n'
                             '            name=%(name)s\n'
                             '            time="%(time).6f"/>\n') %
                            {'classname': _quoteattr(classname),
                             'name': _quoteattr(name),
                             'time': testtime})
            testcases.append(testcase)

            return refout, postout, diff

        yield path, testwrapper

    suitetime = time.time() - suitestart
    header = (('<?xml version="1.0" encoding="utf-8"?>\n'
               '<testsuite name="cram"\n'
               '           tests="%(total)d"\n'
               '           failures="%(failed)d"\n'
               '           skipped="%(skipped)d"\n'
               '           timestamp=%(timestamp)s\n'
               '           hostname=%(hostname)s\n'
               '           time="%(time).6f">\n') %
              {'total': total[0],
               'failed': failed[0],
               'skipped': skipped[0],
               'timestamp': _quoteattr(timestamp),
               'hostname': _quoteattr(hostname),
               'time': suitetime})
    footer = '</testsuite>\n'

    xmlfile = open(xmlpath, 'wb')
    try:
        xmlfile.write(header.encode('utf-8'))
        for testcase in testcases:
            xmlfile.write(testcase.encode('utf-8'))
        xmlfile.write(footer.encode('utf-8'))
    finally:
        xmlfile.close()


================================================
FILE: examples/.hidden/hidden.t
================================================
This test is ignored because it's hidden.


================================================
FILE: examples/.hidden.t
================================================
This test is ignored because it's hidden.


================================================
FILE: examples/bare.t
================================================
  $ true


================================================
FILE: examples/empty.t
================================================


================================================
FILE: examples/env.t
================================================
Check environment variables:

  $ echo "$LANG"
  C
  $ echo "$LC_ALL"
  C
  $ echo "$LANGUAGE"
  C
  $ echo "$TZ"
  GMT
  $ echo "$CDPATH"
  
  $ echo "$GREP_OPTIONS"
  
  $ echo "$CRAMTMP"
  .+ (re)
  $ echo "$TESTDIR"
  */examples (glob)
  $ ls "$TESTDIR"
  bare.t
  empty.t
  env.t
  fail.t
  missingeol.t
  skip.t
  test.t
  $ echo "$TESTFILE"
  env.t
  $ pwd
  */cramtests*/env.t (glob)


================================================
FILE: examples/fail.t
================================================
Output needing escaping:

  $ printf '\00\01\02\03\04\05\06\07\010\011\013\014\016\017\020\021\022\n'
  foo
  $ printf '\023\024\025\026\027\030\031\032\033\034\035\036\037\040\047\n'
  bar

Wrong output and bad regexes:

  $ echo 1
  2
  $ printf '1\nfoo\n1\n'
  +++ (re)
  foo\ (re)
   (re)

Filler to force a second diff hunk:


Offset regular expression:

  $ printf 'foo\n\n1\n'
  
  \d (re)


================================================
FILE: examples/missingeol.t
================================================
  $ printf foo
  foo (no-eol)

================================================
FILE: examples/skip.t
================================================
This test is considered "skipped" because it exits with return code
80. This is useful for skipping tests that only work on certain
platforms or in certain settings.

  $ exit 80


================================================
FILE: examples/test.t
================================================
Simple commands:

  $ echo foo
  foo
  $ printf 'bar\nbaz\n' | cat
  bar
  baz

Multi-line command:

  $ foo() {
  >     echo bar
  > }
  $ foo
  bar

Regular expression:

  $ echo foobarbaz
  foobar.* (re)

Glob:

  $ printf '* \\foobarbaz {10}\n'
  \* \\fo?bar* {10} (glob)

Literal match ending in (re) and (glob):

  $ echo 'foo\Z\Z\Z bar (re)'
  foo\Z\Z\Z bar (re)
  $ echo 'baz??? quux (glob)'
  baz??? quux (glob)

Exit code:

  $ (exit 1)
  [1]

Write to stderr:

  $ echo foo >&2
  foo

No newline:

  $ printf foo
  foo (no-eol)
  $ printf 'foo\nbar'
  foo
  bar (no-eol)
  $ printf '  '
     (no-eol)
  $ printf '  \n  '
    
     (no-eol)
  $ echo foo
  foo
  $ printf foo
  foo (no-eol)

Escaped output:

  $ printf '\00\01\02\03\04\05\06\07\010\011\013\014\016\017\020\021\022\n'
  \x00\x01\x02\x03\x04\x05\x06\x07\x08\t\x0b\x0c\x0e\x0f\x10\x11\x12 (esc)
  $ printf '\023\024\025\026\027\030\031\032\033\034\035\036\037\040\047\n'
  \x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f ' (esc)
  $ echo hi
  \x68\x69 (esc)
  $ echo '(esc) in output (esc)'
  (esc) in output (esc)
  $ echo '(esc) in output (esc)'
  (esc) in output \x28esc\x29 (esc)

Command that closes a pipe:

  $ cat /dev/urandom | head -1 > /dev/null

If Cram let Python's SIGPIPE handler get inherited by this script, we
might see broken pipe messages.


================================================
FILE: requirements.txt
================================================
check-manifest
coverage
pycodestyle
pyflakes


================================================
FILE: scripts/cram
================================================
#!/usr/bin/env python3
import sys

import cram

try:
    sys.exit(cram.main(sys.argv[1:]))
except (BrokenPipeError, KeyboardInterrupt):
    pass


================================================
FILE: setup.cfg
================================================
[bdist_wheel]
universal = true

[pycodestyle]
# E129: indentation between lines in conditions
# E261: two spaces before inline comment
# E302/E305: two new lines between functions/etc.
# E741: ambiguous variable name 'l'
# W504: line break after binary operator
ignore = E129,E261,E302,E305,E741,W504


================================================
FILE: setup.py
================================================
#!/usr/bin/env python
"""Installs cram"""

import os
import sys
from distutils.core import setup

COMMANDS = {}
CRAM_DIR = os.path.abspath(os.path.dirname(__file__))

try:
    from wheel.bdist_wheel import bdist_wheel
except ImportError:
    pass
else:
    COMMANDS['bdist_wheel'] = bdist_wheel

def long_description():
    """Get the long description from the README"""
    return open(os.path.join(sys.path[0], 'README.rst')).read()

setup(
    author='Brodie Rao',
    author_email='brodie@bitheap.org',
    classifiers=[
        'Development Status :: 5 - Production/Stable',
        'Environment :: Console',
        'Intended Audience :: Developers',
        'License :: OSI Approved :: GNU General Public License (GPL)',
        ('License :: OSI Approved :: GNU General Public License v2 '
         'or later (GPLv2+)'),
        'Natural Language :: English',
        'Operating System :: OS Independent',
        'Programming Language :: Python :: 3',
        'Programming Language :: Unix Shell',
        'Topic :: Software Development :: Testing',
    ],
    cmdclass=COMMANDS,
    description='Functional tests for command line applications',
    download_url='https://bitheap.org/cram/cram-0.8.tar.gz',
    keywords='automatic functional test framework',
    license='GNU GPLv2 or any later version',
    long_description=long_description(),
    name='cram',
    packages=['cram'],
    scripts=['scripts/cram'],
    url='https://bitheap.org/cram/',
    version='0.8',
)


================================================
FILE: tests/config.t
================================================
Set up cram alias and example tests:

  $ . "$TESTDIR"/setup.sh

Options in .cramrc:

  $ cat > .cramrc <<EOF
  > [cram]
  > yes = True
  > no = 1
  > indent = 4
  > EOF
  $ cram
  options --yes and --no are mutually exclusive
  [2]
  $ mv .cramrc config
  $ CRAMRC=config cram
  options --yes and --no are mutually exclusive
  [2]
  $ rm config

Invalid option in .cramrc:

  $ cat > .cramrc <<EOF
  > [cram]
  > indent = hmm
  > EOF
  $ cram
  [Uu]sage: cram \[OPTIONS\] TESTS\.\.\. (re)
  
  cram: error: option --indent: invalid integer value: 'hmm'
  [2]
  $ rm .cramrc
  $ cat > .cramrc <<EOF
  > [cram]
  > verbose = hmm
  > EOF
  $ cram
  [Uu]sage: cram \[OPTIONS\] TESTS\.\.\. (re)
  
  cram: error: --verbose: invalid boolean value: 'hmm'
  [2]
  $ rm .cramrc

Options in an environment variable:

  $ CRAM='-y -n' cram
  options --yes and --no are mutually exclusive
  [2]


================================================
FILE: tests/debug.t
================================================
Set up cram alias and example tests:

  $ . "$TESTDIR"/setup.sh

Debug mode:

  $ printf '  $ echo hi\n  > echo bye' > debug.t
  $ cram -d -v debug.t
  options --debug and --verbose are mutually exclusive
  [2]
  $ cram -d -q debug.t
  options --debug and --quiet are mutually exclusive
  [2]
  $ cram -d -i debug.t
  options --debug and --interactive are mutually exclusive
  [2]
  $ cram -d --xunit-file==cram.xml debug.t
  options --debug and --xunit-file are mutually exclusive
  [2]
  $ cram -d debug.t
  hi
  bye
  $ cram -d examples/empty.t

Debug mode with extra shell arguments:

  $ cram --shell-opts='-s' -d debug.t
  hi
  bye

Test debug mode with set -x:

  $ cat > set-x.t <<EOF
  >   $ echo 1
  >   1
  >   $ set -x
  >   $ echo 2
  > EOF
  $ cram -d set-x.t
  1
  \+.*echo 2 (re)
  2

Test set -x without debug mode:

  $ cram set-x.t
  !
  --- set-x.t
  +++ set-x.t.err
  @@ -1,4 +1,8 @@
     $ echo 1
     1
     $ set -x
  \+  \+.*echo  \(no-eol\) (re)
     $ echo 2
  \+  \+.*echo 2 (re)
  +  2
  \+  \+.*echo  \(no-eol\) (re)
  
  # Ran 1 tests, 0 skipped, 1 failed.
  [1]

Note that the "+ echo  (no-eol)" lines are artifacts of the echo commands
that Cram inserts into the test in order to track output. It'd be nice if
Cram could avoid removing salt/line number/return code information from those
lines, but it isn't possible to distinguish between set -x output and normal
output.


================================================
FILE: tests/dist.t
================================================
Skip this test if check-manifest isn't available:

  $ command -v check-manifest > /dev/null || exit 80

Confirm that "make dist" isn't going to miss any files:

  $ check-manifest "$TESTDIR/.."
  lists of files in version control and sdist match


================================================
FILE: tests/doctest.t
================================================
Set up cram alias and example tests:

  $ . "$TESTDIR"/setup.sh

Run doctests:

  $ doctest "$TESTDIR"/../cram


================================================
FILE: tests/encoding.t
================================================
Set up cram alias and example tests:

  $ . "$TESTDIR"/setup.sh

Test with Windows newlines:

  $ printf "  $ echo hi\r\n  hi\r\n" > windows-newlines.t
  $ cram windows-newlines.t
  .
  # Ran 1 tests, 0 skipped, 0 failed.

Test with Latin-1 encoding:

  $ cat > good-latin-1.t <<EOF
  >   $ printf "hola se\361or\n"
  >   hola se\xf1or (esc)
  > EOF

  $ cat > bad-latin-1.t <<EOF
  >   $ printf "hola se\361or\n"
  >   hey
  > EOF

  $ cram good-latin-1.t bad-latin-1.t
  .!
  --- bad-latin-1.t
  +++ bad-latin-1.t.err
  @@ -1,2 +1,2 @@
     $ printf "hola se\361or\n"
  -  hey
  +  hola se\xf1or (esc)
  
  # Ran 2 tests, 0 skipped, 1 failed.
  [1]

Test with UTF-8 encoding:

  $ cat > good-utf-8.t <<EOF
  >   $ printf "hola se\303\261or\n"
  >   hola se\xc3\xb1or (esc)
  > EOF

  $ cat > bad-utf-8.t <<EOF
  >   $ printf "hola se\303\261or\n"
  >   hey
  > EOF

  $ cram good-utf-8.t bad-utf-8.t
  .!
  --- bad-utf-8.t
  +++ bad-utf-8.t.err
  @@ -1,2 +1,2 @@
     $ printf "hola se\303\261or\n"
  -  hey
  +  hola se\xc3\xb1or (esc)
  
  # Ran 2 tests, 0 skipped, 1 failed.
  [1]

Test file missing trailing newline:

  $ printf '  $ true' > passing-with-no-newline.t
  $ cram passing-with-no-newline.t
  .
  # Ran 1 tests, 0 skipped, 0 failed.

  $ printf '  $ false' > failing-with-no-newline.t
  $ cram failing-with-no-newline.t
  !
  --- failing-with-no-newline.t
  +++ failing-with-no-newline.t.err
  @@ -1,1 +1,2 @@
     $ false
  +  [1]
  
  # Ran 1 tests, 0 skipped, 1 failed.
  [1]


================================================
FILE: tests/interactive.t
================================================
Set up cram alias and example tests:

  $ . "$TESTDIR"/setup.sh

Interactive mode (don't merge):

  $ cram -n -i examples/fail.t
  !
  --- examples/fail.t
  +++ examples/fail.t.err
  @@ -1,18 +1,18 @@
   Output needing escaping:
   
     $ printf '\00\01\02\03\04\05\06\07\010\011\013\014\016\017\020\021\022\n'
  -  foo
  +  \x00\x01\x02\x03\x04\x05\x06\x07\x08\t\x0b\x0c\x0e\x0f\x10\x11\x12 (esc)
     $ printf '\023\024\025\026\027\030\031\032\033\034\035\036\037\040\047\n'
  -  bar
  +  \x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f ' (esc)
   
   Wrong output and bad regexes:
   
     $ echo 1
  -  2
  +  1
     $ printf '1\nfoo\n1\n'
  -  +++ (re)
  -  foo\ (re)
  -   (re)
  +  1
  +  foo
  +  1
   
   Filler to force a second diff hunk:
   
  @@ -20,5 +20,6 @@
   Offset regular expression:
   
     $ printf 'foo\n\n1\n'
  +  foo
     
     \d (re)
  Accept this change? [yN] n
  
  # Ran 1 tests, 0 skipped, 1 failed.
  [1]
  $ md5 examples/fail.t examples/fail.t.err
  .*\b0f598c2b7b8ca5bcb8880e492ff6b452\b.* (re)
  .*\b7a23dfa85773c77648f619ad0f9df554\b.* (re)

Interactive mode (merge):

  $ cp examples/fail.t examples/fail.t.orig
  $ cram -y -i examples/fail.t
  !
  --- examples/fail.t
  +++ examples/fail.t.err
  @@ -1,18 +1,18 @@
   Output needing escaping:
   
     $ printf '\00\01\02\03\04\05\06\07\010\011\013\014\016\017\020\021\022\n'
  -  foo
  +  \x00\x01\x02\x03\x04\x05\x06\x07\x08\t\x0b\x0c\x0e\x0f\x10\x11\x12 (esc)
     $ printf '\023\024\025\026\027\030\031\032\033\034\035\036\037\040\047\n'
  -  bar
  +  \x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f ' (esc)
   
   Wrong output and bad regexes:
   
     $ echo 1
  -  2
  +  1
     $ printf '1\nfoo\n1\n'
  -  +++ (re)
  -  foo\ (re)
  -   (re)
  +  1
  +  foo
  +  1
   
   Filler to force a second diff hunk:
   
  @@ -20,5 +20,6 @@
   Offset regular expression:
   
     $ printf 'foo\n\n1\n'
  +  foo
     
     \d (re)
  Accept this change? [yN] y
  patching file examples/fail.t
  
  # Ran 1 tests, 0 skipped, 1 failed.
  [1]
  $ md5 examples/fail.t
  .*\b1d9e5b527f01fbf2d9b1c121d005108c\b.* (re)
  $ mv examples/fail.t.orig examples/fail.t

Verbose interactive mode (answer manually and don't merge):

  $ printf 'bad\nn\n' | cram -v -i examples/fail.t
  examples/fail.t: failed
  --- examples/fail.t
  +++ examples/fail.t.err
  @@ -1,18 +1,18 @@
   Output needing escaping:
   
     $ printf '\00\01\02\03\04\05\06\07\010\011\013\014\016\017\020\021\022\n'
  -  foo
  +  \x00\x01\x02\x03\x04\x05\x06\x07\x08\t\x0b\x0c\x0e\x0f\x10\x11\x12 (esc)
     $ printf '\023\024\025\026\027\030\031\032\033\034\035\036\037\040\047\n'
  -  bar
  +  \x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f ' (esc)
   
   Wrong output and bad regexes:
   
     $ echo 1
  -  2
  +  1
     $ printf '1\nfoo\n1\n'
  -  +++ (re)
  -  foo\ (re)
  -   (re)
  +  1
  +  foo
  +  1
   
   Filler to force a second diff hunk:
   
  @@ -20,5 +20,6 @@
   Offset regular expression:
   
     $ printf 'foo\n\n1\n'
  +  foo
     
     \d (re)
  Accept this change? [yN] Accept this change? [yN] # Ran 1 tests, 0 skipped, 1 failed.
  [1]
  $ md5 examples/fail.t examples/fail.t.err
  .*\b0f598c2b7b8ca5bcb8880e492ff6b452\b.* (re)
  .*\b7a23dfa85773c77648f619ad0f9df554\b.* (re)
  $ printf 'bad\n\n' | cram -v -i examples/fail.t
  examples/fail.t: failed
  --- examples/fail.t
  +++ examples/fail.t.err
  @@ -1,18 +1,18 @@
   Output needing escaping:
   
     $ printf '\00\01\02\03\04\05\06\07\010\011\013\014\016\017\020\021\022\n'
  -  foo
  +  \x00\x01\x02\x03\x04\x05\x06\x07\x08\t\x0b\x0c\x0e\x0f\x10\x11\x12 (esc)
     $ printf '\023\024\025\026\027\030\031\032\033\034\035\036\037\040\047\n'
  -  bar
  +  \x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f ' (esc)
   
   Wrong output and bad regexes:
   
     $ echo 1
  -  2
  +  1
     $ printf '1\nfoo\n1\n'
  -  +++ (re)
  -  foo\ (re)
  -   (re)
  +  1
  +  foo
  +  1
   
   Filler to force a second diff hunk:
   
  @@ -20,5 +20,6 @@
   Offset regular expression:
   
     $ printf 'foo\n\n1\n'
  +  foo
     
     \d (re)
  Accept this change? [yN] Accept this change? [yN] # Ran 1 tests, 0 skipped, 1 failed.
  [1]
  $ md5 examples/fail.t examples/fail.t.err
  .*\b0f598c2b7b8ca5bcb8880e492ff6b452\b.* (re)
  .*\b7a23dfa85773c77648f619ad0f9df554\b.* (re)

Verbose interactive mode (answer manually and merge):

  $ cp examples/fail.t examples/fail.t.orig
  $ printf 'bad\ny\n' | cram -v -i examples/fail.t
  examples/fail.t: failed
  --- examples/fail.t
  +++ examples/fail.t.err
  @@ -1,18 +1,18 @@
   Output needing escaping:
   
     $ printf '\00\01\02\03\04\05\06\07\010\011\013\014\016\017\020\021\022\n'
  -  foo
  +  \x00\x01\x02\x03\x04\x05\x06\x07\x08\t\x0b\x0c\x0e\x0f\x10\x11\x12 (esc)
     $ printf '\023\024\025\026\027\030\031\032\033\034\035\036\037\040\047\n'
  -  bar
  +  \x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f ' (esc)
   
   Wrong output and bad regexes:
   
     $ echo 1
  -  2
  +  1
     $ printf '1\nfoo\n1\n'
  -  +++ (re)
  -  foo\ (re)
  -   (re)
  +  1
  +  foo
  +  1
   
   Filler to force a second diff hunk:
   
  @@ -20,5 +20,6 @@
   Offset regular expression:
   
     $ printf 'foo\n\n1\n'
  +  foo
     
     \d (re)
  Accept this change? [yN] Accept this change? [yN] patching file examples/fail.t
  examples/fail.t: merged output
  # Ran 1 tests, 0 skipped, 1 failed.
  [1]
  $ md5 examples/fail.t
  .*\b1d9e5b527f01fbf2d9b1c121d005108c\b.* (re)
  $ mv examples/fail.t.orig examples/fail.t

Test missing patch(1) and patch(1) error:

  $ PATH=. cram -i examples/fail.t
  patch(1) required for -i
  [2]
  $ cat > patch <<EOF
  > #!/bin/sh
  > echo "patch failed" 1>&2
  > exit 1
  > EOF
  $ chmod +x patch
  $ PATH=. cram -y -i examples/fail.t
  !
  --- examples/fail.t
  +++ examples/fail.t.err
  @@ -1,18 +1,18 @@
   Output needing escaping:
   
     $ printf '\00\01\02\03\04\05\06\07\010\011\013\014\016\017\020\021\022\n'
  -  foo
  +  \x00\x01\x02\x03\x04\x05\x06\x07\x08\t\x0b\x0c\x0e\x0f\x10\x11\x12 (esc)
     $ printf '\023\024\025\026\027\030\031\032\033\034\035\036\037\040\047\n'
  -  bar
  +  \x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f ' (esc)
   
   Wrong output and bad regexes:
   
     $ echo 1
  -  2
  +  1
     $ printf '1\nfoo\n1\n'
  -  +++ (re)
  -  foo\ (re)
  -   (re)
  +  1
  +  foo
  +  1
   
   Filler to force a second diff hunk:
   
  @@ -20,5 +20,6 @@
   Offset regular expression:
   
     $ printf 'foo\n\n1\n'
  +  foo
     
     \d (re)
  Accept this change? [yN] y
  patch failed
  examples/fail.t: merge failed
  
  # Ran 1 tests, 0 skipped, 1 failed.
  [1]
  $ md5 examples/fail.t examples/fail.t.err
  .*\b0f598c2b7b8ca5bcb8880e492ff6b452\b.* (re)
  .*\b7a23dfa85773c77648f619ad0f9df554\b.* (re)
  $ rm patch examples/fail.t.err


================================================
FILE: tests/pep8.t
================================================
Skip this test if pycodestyle isn't available:

  $ command -v pycodestyle > /dev/null || exit 80

Check that the Python source code style is PEP 8 compliant:

  $ pycodestyle --config="$TESTDIR/.."/setup.cfg --repeat "$TESTDIR/.."


================================================
FILE: tests/pyflakes.t
================================================
Skip this test if pyflakes isn't available:

  $ command -v pyflakes > /dev/null || exit 80

Check that there are no obvious Python source code errors:

  $ pyflakes "$TESTDIR/.."


================================================
FILE: tests/run-doctests.py
================================================
#!/usr/bin/env python

import doctest
import os
import sys

def _getmodules(pkgdir):
    """Import and yield modules in pkgdir"""
    for root, dirs, files in os.walk(pkgdir):
        if '__pycache__' in dirs:
            dirs.remove('__pycache__')
        for fn in files:
            if not fn.endswith('.py') or fn == '__main__.py':
                continue

            modname = fn.replace(os.sep, '.')[:-len('.py')]
            if modname.endswith('.__init__'):
                modname = modname[:-len('.__init__')]
            modname = '.'.join(['cram', modname])
            if '.' in modname:
                fromlist = [modname.rsplit('.', 1)[1]]
            else:
                fromlist = []

            yield __import__(modname, {}, {}, fromlist)

def rundoctests(pkgdir):
    """Run doctests in the given package directory"""
    totalfailures = totaltests = 0
    for module in _getmodules(pkgdir):
        failures, tests = doctest.testmod(module)
        totalfailures += failures
        totaltests += tests
    return totalfailures != 0

if __name__ == '__main__':
    try:
        sys.exit(rundoctests(sys.argv[1]))
    except KeyboardInterrupt:
        pass


================================================
FILE: tests/setup.sh
================================================
#!/bin/sh

# Bash doesn't expand aliases by default in non-interactive mode, so
# we enable it manually if the test is run with --shell=/bin/bash.
[ "$TESTSHELL" = "/bin/bash" ] && shopt -s expand_aliases

# The $PYTHON environment variable should be set when running this test
# from Python.
[ -n "$PYTHON" ] || PYTHON="`which python3`"
[ -n "$PYTHONPATH" ] || PYTHONPATH="$TESTDIR/.." && export PYTHONPATH
if [ -n "$COVERAGE" ]; then
  if [ -z "$COVERAGE_FILE" ]; then
    COVERAGE_FILE="$TESTDIR/../.coverage"
    export COVERAGE_FILE
  fi

  alias cram="`which "$COVERAGE"` run -a --rcfile=$TESTDIR/../.coveragerc \
$TESTDIR/../scripts/cram --shell=$TESTSHELL"
  alias doctest="`which "$COVERAGE"` run -a --rcfile=$TESTDIR/../.coveragerc \
$TESTDIR/run-doctests.py"
else
  PYTHON="`command -v "$PYTHON" || echo "$PYTHON"`"
  alias cram="$PYTHON $TESTDIR/../scripts/cram --shell=$TESTSHELL"
  alias doctest="$PYTHON $TESTDIR/run-doctests.py"
fi
command -v md5 > /dev/null || alias md5=md5sum

# Copy in example tests
cp -R "$TESTDIR"/../examples .
find . -name '*.err' -exec rm '{}' \;


================================================
FILE: tests/test.t
================================================
Set up cram alias and example tests:

  $ . "$TESTDIR"/setup.sh

Run cram examples:

  $ cram -q examples examples/fail.t
  .s.!.s.
  # Ran 7 tests, 2 skipped, 1 failed.
  [1]
  $ md5 examples/fail.t examples/fail.t.err
  .*\b0f598c2b7b8ca5bcb8880e492ff6b452\b.* (re)
  .*\b7a23dfa85773c77648f619ad0f9df554\b.* (re)
  $ rm examples/fail.t.err

Run examples with bash:

  $ cram --shell=/bin/bash -q examples examples/fail.t
  .s.!.s.
  # Ran 7 tests, 2 skipped, 1 failed.
  [1]
  $ md5 examples/fail.t examples/fail.t.err
  .*\b0f598c2b7b8ca5bcb8880e492ff6b452\b.* (re)
  .*\b7a23dfa85773c77648f619ad0f9df554\b.* (re)
  $ rm examples/fail.t.err

Verbose mode:

  $ cram -q -v examples examples/fail.t
  examples/bare.t: passed
  examples/empty.t: empty
  examples/env.t: passed
  examples/fail.t: failed
  examples/missingeol.t: passed
  examples/skip.t: skipped
  examples/test.t: passed
  # Ran 7 tests, 2 skipped, 1 failed.
  [1]
  $ md5 examples/fail.t examples/fail.t.err
  .*\b0f598c2b7b8ca5bcb8880e492ff6b452\b.* (re)
  .*\b7a23dfa85773c77648f619ad0f9df554\b.* (re)
  $ rm examples/fail.t.err

Test that a fixed .err file is deleted:

  $ echo "  $ echo 1" > fixed.t
  $ cram fixed.t
  !
  --- fixed.t
  +++ fixed.t.err
  @@ -1,1 +1,2 @@
     $ echo 1
  +  1
  
  # Ran 1 tests, 0 skipped, 1 failed.
  [1]
  $ cp fixed.t.err fixed.t
  $ cram fixed.t
  .
  # Ran 1 tests, 0 skipped, 0 failed.
  $ test \! -f fixed.t.err
  $ rm fixed.t

Don't sterilize environment:

  $ TZ=foo; export TZ
  $ CDPATH=foo; export CDPATH
  $ GREP_OPTIONS=foo; export GREP_OPTIONS
  $ cram -E examples/env.t
  !
  \-\-\- examples/env\.t\s* (re)
  \+\+\+ examples/env\.t\.err\s* (re)
  @@ -7,11 +7,11 @@
     $ echo "$LANGUAGE"
     C
     $ echo "$TZ"
  -  GMT
  +  foo
     $ echo "$CDPATH"
  -  
  +  foo
     $ echo "$GREP_OPTIONS"
  -  
  +  foo
     $ echo "$CRAMTMP"
     .+ (re)
     $ echo "$TESTDIR"
  
  # Ran 1 tests, 0 skipped, 1 failed.
  [1]
  $ rm examples/env.t.err

Note: We can't set the locale to foo because some shells will issue
warnings for invalid locales.

Test --keep-tmpdir:

  $ cram -q --keep-tmpdir examples/test.t | while read line; do
  >   echo "$line" 1>&2
  >   msg=`echo "$line" | cut -d ' ' -f 1-4`
  >   if [ "$msg" = '# Kept temporary directory:' ]; then
  >     echo "$line" | cut -d ' ' -f 5
  >   fi
  > done > keeptmp
  .
  # Ran 1 tests, 0 skipped, 0 failed.
  # Kept temporary directory: */cramtests-* (glob)
  $ ls "`cat keeptmp`" | sort
  test.t
  tmp

Custom indentation:

  $ cat > indent.t <<EOF
  > Indented by 4 spaces:
  > 
  >     $ echo foo
  >     foo
  > 
  > Not part of the test:
  > 
  >   $ echo foo
  >   bar
  > EOF
  $ cram --indent=4 indent.t
  .
  # Ran 1 tests, 0 skipped, 0 failed.

Test running tests with the same filename in different directories:

  $ mkdir subdir1 subdir2
  $ cat > subdir1/test.t <<EOF
  >   $ echo 1
  > EOF
  $ cat > subdir2/test.t <<EOF
  >   $ echo 2
  > EOF
  $ cram subdir1 subdir2
  !
  --- subdir1/test.t
  +++ subdir1/test.t.err
  @@ -1,1 +1,2 @@
     $ echo 1
  +  1
  !
  --- subdir2/test.t
  +++ subdir2/test.t.err
  @@ -1,1 +1,2 @@
     $ echo 2
  +  2
  
  # Ran 2 tests, 0 skipped, 2 failed.
  [1]


================================================
FILE: tests/usage.t
================================================
Set up cram alias and example tests:

  $ . "$TESTDIR"/setup.sh

Usage:

  $ cram -h
  [Uu]sage: cram \[OPTIONS\] TESTS\.\.\. (re)
  
  [Oo]ptions: (re)
    -h, --help          show this help message and exit
    -V, --version       show version information and exit
    -q, --quiet         don't print diffs
    -v, --verbose       show filenames and test status
    -i, --interactive   interactively merge changed test output
    -d, --debug         write script output directly to the terminal
    -y, --yes           answer yes to all questions
    -n, --no            answer no to all questions
    -E, --preserve-env  don't reset common environment variables
    --keep-tmpdir       keep temporary directories
    --shell=PATH        shell to use for running tests (default: /bin/sh)
    --shell-opts=OPTS   arguments to invoke shell with
    --indent=NUM        number of spaces to use for indentation (default: 2)
    --xunit-file=PATH   path to write xUnit XML output
  $ cram -V
  Cram CLI testing framework (version 0.8)
  
  Copyright (C) 2010-2021 Brodie Rao <brodie@bitheap.org> and others
  This is free software; see the source for copying conditions. There is NO
  warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  $ cram
  [Uu]sage: cram \[OPTIONS\] TESTS\.\.\. (re)
  [2]
  $ cram -y -n
  options --yes and --no are mutually exclusive
  [2]
  $ cram non-existent also-not-here
  no such file: non-existent
  [2]
  $ mkdir empty
  $ cram empty
  no tests found
  [2]
  $ cram --shell=./badsh
  shell not found: ./badsh
  [2]


================================================
FILE: tests/xunit.t
================================================
Set up cram alias and example tests:

  $ . "$TESTDIR"/setup.sh

xUnit XML output:

  $ cram -q -v --xunit-file=cram.xml examples
  examples/bare.t: passed
  examples/empty.t: empty
  examples/env.t: passed
  examples/fail.t: failed
  examples/missingeol.t: passed
  examples/skip.t: skipped
  examples/test.t: passed
  # Ran 7 tests, 2 skipped, 1 failed.
  [1]
  $ cat cram.xml
  <?xml version="1.0" encoding="utf-8"?>
  <testsuite name="cram"
             tests="7"
             failures="1"
             skipped="2"
             timestamp="\d+-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+\d{2}:\d{2}" (re)
             hostname="[^"]+" (re)
             time="\d+\.\d{6}"> (re)
    <testcase classname="examples/bare.t"
              name="bare.t"
              time="\d+\.\d{6}"/> (re)
    <testcase classname="examples/empty.t"
              name="empty.t"
              time="\d+\.\d{6}"> (re)
      <skipped/>
    </testcase>
    <testcase classname="examples/env.t"
              name="env.t"
              time="\d+\.\d{6}"/> (re)
    <testcase classname="examples/fail.t"
              name="fail.t"
              time="\d+\.\d{6}"> (re)
      <failure><![CDATA[--- examples/fail.t
  +++ examples/fail.t.err
  @@ -1,18 +1,18 @@
   Output needing escaping:
   
     $ printf '\00\01\02\03\04\05\06\07\010\011\013\014\016\017\020\021\022\n'
  -  foo
  +  \x00\x01\x02\x03\x04\x05\x06\x07\x08\t\x0b\x0c\x0e\x0f\x10\x11\x12 (esc)
     $ printf '\023\024\025\026\027\030\031\032\033\034\035\036\037\040\047\n'
  -  bar
  +  \x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f ' (esc)
   
   Wrong output and bad regexes:
   
     $ echo 1
  -  2
  +  1
     $ printf '1\nfoo\n1\n'
  -  +++ (re)
  -  foo\ (re)
  -   (re)
  +  1
  +  foo
  +  1
   
   Filler to force a second diff hunk:
   
  @@ -20,5 +20,6 @@
   Offset regular expression:
   
     $ printf 'foo\n\n1\n'
  +  foo
     
     \d (re)
  ]]></failure>
    </testcase>
    <testcase classname="examples/missingeol.t"
              name="missingeol.t"
              time="\d+\.\d{6}"/> (re)
    <testcase classname="examples/skip.t"
              name="skip.t"
              time="\d+\.\d{6}"> (re)
      <skipped/>
    </testcase>
    <testcase classname="examples/test.t"
              name="test.t"
              time="\d+\.\d{6}"/> (re)
  </testsuite>
  $ rm cram.xml examples/fail.t.err
Download .txt
gitextract_gm2l0gze/

├── .coveragerc
├── .gitignore
├── .hgignore
├── .hgtags
├── .pylintrc
├── .travis.yml
├── COPYING.txt
├── MANIFEST.in
├── Makefile
├── NEWS.rst
├── README.rst
├── TODO.md
├── contrib/
│   ├── PKGBUILD
│   └── cram.vim
├── cram/
│   ├── __init__.py
│   ├── __main__.py
│   ├── _cli.py
│   ├── _diff.py
│   ├── _main.py
│   ├── _process.py
│   ├── _run.py
│   ├── _test.py
│   └── _xunit.py
├── examples/
│   ├── .hidden/
│   │   └── hidden.t
│   ├── .hidden.t
│   ├── bare.t
│   ├── empty.t
│   ├── env.t
│   ├── fail.t
│   ├── missingeol.t
│   ├── skip.t
│   └── test.t
├── requirements.txt
├── scripts/
│   └── cram
├── setup.cfg
├── setup.py
└── tests/
    ├── config.t
    ├── debug.t
    ├── dist.t
    ├── doctest.t
    ├── encoding.t
    ├── interactive.t
    ├── pep8.t
    ├── pyflakes.t
    ├── run-doctests.py
    ├── setup.sh
    ├── test.t
    ├── usage.t
    └── xunit.t
Download .txt
SYMBOL INDEX (40 symbols across 9 files)

FILE: cram/_cli.py
  function _prompt (line 10) | def _prompt(question, answers, auto=None):
  function _log (line 37) | def _log(msg=None, verbosemsg=None, verbose=False):
  function _patch (line 51) | def _patch(cmd, diff):
  function runcli (line 56) | def runcli(tests, quiet=False, verbose=False, patchcmd=None, answer=None):

FILE: cram/_diff.py
  function _regex (line 9) | def _regex(pattern, s):
  function _glob (line 20) | def _glob(el, l):
  function _matchannotation (line 45) | def _matchannotation(keyword, matchfunc, el, l):
  function regex (line 50) | def regex(el, l):
  function glob (line 54) | def glob(el, l):
  function esc (line 58) | def esc(el, l):
  class _SequenceMatcher (line 71) | class _SequenceMatcher(difflib.SequenceMatcher, object):
    method __init__ (line 73) | def __init__(self, *args, **kwargs):
    method _match (line 77) | def _match(self, el, l):
    method find_longest_match (line 84) | def find_longest_match(self, alo, ahi, blo, bhi):
  function unified_diff (line 105) | def unified_diff(l1, l2, fromfile=b'', tofile=b'', fromfiledate=b'',

FILE: cram/_main.py
  function _which (line 19) | def _which(cmd):
  function _expandpath (line 28) | def _expandpath(path):
  class _OptionParser (line 32) | class _OptionParser(optparse.OptionParser):
    method __init__ (line 36) | def __init__(self, *args, **kwargs):
    method add_option (line 40) | def add_option(self, *args, **kwargs):
    method parse_args (line 47) | def parse_args(self, args=None, values=None):
  function _parseopts (line 78) | def _parseopts(args):
  function main (line 112) | def main(args):

FILE: cram/_process.py
  function _makeresetsigpipe (line 13) | def _makeresetsigpipe():
  function execute (line 25) | def execute(args, stdin=None, stdout=None, stderr=None, cwd=None, env=No...

FILE: cram/_run.py
  function _walk (line 11) | def _walk(top):
  function _findtests (line 20) | def _findtests(paths):
  function runtests (line 33) | def runtests(paths, tmpdir, shell, indent=2, cleanenv=True, debug=False):

FILE: cram/_test.py
  function _escape (line 18) | def _escape(s):
  function test (line 23) | def test(lines, shell='/bin/sh', indent=2, testname=None, env=None,
  function testfile (line 179) | def testfile(path, shell='/bin/sh', indent=2, env=None, cleanenv=True,

FILE: cram/_xunit.py
  function _cdatareplace (line 30) | def _cdatareplace(m):
  function _cdata (line 37) | def _cdata(s):
  function _quoteattrreplace (line 46) | def _quoteattrreplace(m):
  function _quoteattr (line 56) | def _quoteattr(s):
  function _timestamp (line 65) | def _timestamp():
  function runxunit (line 79) | def runxunit(tests, xmlpath):

FILE: setup.py
  function long_description (line 18) | def long_description():

FILE: tests/run-doctests.py
  function _getmodules (line 7) | def _getmodules(pkgdir):
  function rundoctests (line 27) | def rundoctests(pkgdir):
Condensed preview — 49 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (107K chars).
[
  {
    "path": ".coveragerc",
    "chars": 50,
    "preview": "[run]\nomit =\n    */cram/__main__.py\nsource = cram\n"
  },
  {
    "path": ".gitignore",
    "chars": 219,
    "preview": "*.orig\n*.rej\n*~\n*.mergebackup\n*.o\n*.so\n*.dll\n*.py[cdo]\n*$py.class\n__pycache__\n*.swp\n*.prof\n\\#*\\#\n.\\#*\n.coverage\n*,cover\n"
  },
  {
    "path": ".hgignore",
    "chars": 275,
    "preview": "syntax: glob\n\n*.orig\n*.rej\n*~\n*.mergebackup\n*.o\n*.so\n*.dll\n*.py[cdo]\n*$py.class\n__pycache__\n*.swp\n*.prof\n\\#*\\#\n.\\#*\n.cov"
  },
  {
    "path": ".hgtags",
    "chars": 405,
    "preview": "931859fdd3e0d5af442a3e9b5fe6ac0dbfed2309 0.1\n3c471f7a16b435095b98525e7b851b17e871a2ce 0.2\n3c471f7a16b435095b98525e7b851b"
  },
  {
    "path": ".pylintrc",
    "chars": 965,
    "preview": "[MESSAGES CONTROL]\n# C0330: bad continuation\n# The design check gives mostly useless advice.\n# R0201: method could be a "
  },
  {
    "path": ".travis.yml",
    "chars": 1484,
    "preview": "language: python\n\nmatrix:\n  allow_failures:\n    - python: nightly\n      env: TESTOPTS=--shell=dash\n    - python: pypy\n  "
  },
  {
    "path": "COPYING.txt",
    "chars": 18092,
    "preview": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 2, June 1991\n\n Copyright (C) 1989, 1991 Fr"
  },
  {
    "path": "MANIFEST.in",
    "chars": 202,
    "preview": "include .coveragerc .pylintrc .travis.yml Makefile MANIFEST.in\ninclude *.md *.rst *.txt contrib/* scripts/*\nexclude cont"
  },
  {
    "path": "Makefile",
    "chars": 871,
    "preview": "COVERAGE=coverage\nPREFIX=/usr/local\nexport PREFIX\nPYTHON=python3\n\nall: build\n\nbuild:\n\t$(PYTHON) setup.py build\n\ncheck: t"
  },
  {
    "path": "NEWS.rst",
    "chars": 6532,
    "preview": "======\n News\n======\n\nVersion 0.7 (Feb. 24, 2016)\n---------------------------\n\n* Added the ``-d``/``--debug`` flag that d"
  },
  {
    "path": "README.rst",
    "chars": 6397,
    "preview": "======================\n Cram: It's test time\n======================\n\nCram is a functional testing framework for command "
  },
  {
    "path": "TODO.md",
    "chars": 2054,
    "preview": "* Add more comments explaining how different parts of the code work.\n\n* Add a man page.\n\n* Implement string substitution"
  },
  {
    "path": "contrib/PKGBUILD",
    "chars": 462,
    "preview": "# Maintainer: Andrey Vlasovskikh <andrey.vlasovskikh@gmail.com>\n\npkgname=cram\npkgver=0.7\npkgrel=1\npkgdesc=\"Functional te"
  },
  {
    "path": "contrib/cram.vim",
    "chars": 1342,
    "preview": "\" Vim syntax file\n\" Language: Cram Tests\n\" Author: Steve Losh (steve@stevelosh.com)\n\"\n\" Add the following line to your ~"
  },
  {
    "path": "cram/__init__.py",
    "chars": 172,
    "preview": "\"\"\"Functional testing framework for command line applications\"\"\"\n\nfrom cram._main import main\nfrom cram._test import tes"
  },
  {
    "path": "cram/__main__.py",
    "chars": 172,
    "preview": "\"\"\"Main module (invoked by \"python3 -m cram\")\"\"\"\n\nimport sys\n\nimport cram\n\ntry:\n    sys.exit(cram.main(sys.argv[1:]))\nex"
  },
  {
    "path": "cram/_cli.py",
    "chars": 4302,
    "preview": "\"\"\"The command line interface implementation\"\"\"\n\nimport os\nimport sys\n\nfrom cram._process import execute\n\n__all__ = ['ru"
  },
  {
    "path": "cram/_diff.py",
    "chars": 5399,
    "preview": "\"\"\"Utilities for diffing test files and their output\"\"\"\n\nimport codecs\nimport difflib\nimport re\n\n__all__ = ['esc', 'glob"
  },
  {
    "path": "cram/_main.py",
    "chars": 7734,
    "preview": "\"\"\"Main entry point\"\"\"\n\nimport optparse\nimport os\nimport shlex\nimport shutil\nimport sys\nimport tempfile\n\ntry:\n    import"
  },
  {
    "path": "cram/_process.py",
    "chars": 1771,
    "preview": "\"\"\"Utilities for running subprocesses\"\"\"\n\nimport os\nimport signal\nimport subprocess\nimport sys\n\n__all__ = ['PIPE', 'STDO"
  },
  {
    "path": "cram/_run.py",
    "chars": 2247,
    "preview": "\"\"\"The test runner\"\"\"\n\nimport os\nimport sys\n\nfrom cram._test import testfile\n\n__all__ = ['runtests']\n\nif sys.platform =="
  },
  {
    "path": "cram/_test.py",
    "chars": 7518,
    "preview": "\"\"\"Utilities for running individual tests\"\"\"\n\nimport itertools\nimport os\nimport re\nimport time\n\nfrom cram._diff import e"
  },
  {
    "path": "cram/_xunit.py",
    "chars": 5998,
    "preview": "\"\"\"xUnit XML output\"\"\"\n\nimport locale\nimport os\nimport re\nimport socket\nimport sys\nimport time\n\n__all__ = ['runxunit']\n\n"
  },
  {
    "path": "examples/.hidden/hidden.t",
    "chars": 42,
    "preview": "This test is ignored because it's hidden.\n"
  },
  {
    "path": "examples/.hidden.t",
    "chars": 42,
    "preview": "This test is ignored because it's hidden.\n"
  },
  {
    "path": "examples/bare.t",
    "chars": 9,
    "preview": "  $ true\n"
  },
  {
    "path": "examples/empty.t",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "examples/env.t",
    "chars": 392,
    "preview": "Check environment variables:\n\n  $ echo \"$LANG\"\n  C\n  $ echo \"$LC_ALL\"\n  C\n  $ echo \"$LANGUAGE\"\n  C\n  $ echo \"$TZ\"\n  GMT\n"
  },
  {
    "path": "examples/fail.t",
    "chars": 397,
    "preview": "Output needing escaping:\n\n  $ printf '\\00\\01\\02\\03\\04\\05\\06\\07\\010\\011\\013\\014\\016\\017\\020\\021\\022\\n'\n  foo\n  $ printf '"
  },
  {
    "path": "examples/missingeol.t",
    "chars": 29,
    "preview": "  $ printf foo\n  foo (no-eol)"
  },
  {
    "path": "examples/skip.t",
    "chars": 179,
    "preview": "This test is considered \"skipped\" because it exits with return code\n80. This is useful for skipping tests that only work"
  },
  {
    "path": "examples/test.t",
    "chars": 1339,
    "preview": "Simple commands:\n\n  $ echo foo\n  foo\n  $ printf 'bar\\nbaz\\n' | cat\n  bar\n  baz\n\nMulti-line command:\n\n  $ foo() {\n  >    "
  },
  {
    "path": "requirements.txt",
    "chars": 45,
    "preview": "check-manifest\ncoverage\npycodestyle\npyflakes\n"
  },
  {
    "path": "scripts/cram",
    "chars": 145,
    "preview": "#!/usr/bin/env python3\nimport sys\n\nimport cram\n\ntry:\n    sys.exit(cram.main(sys.argv[1:]))\nexcept (BrokenPipeError, Keyb"
  },
  {
    "path": "setup.cfg",
    "chars": 301,
    "preview": "[bdist_wheel]\nuniversal = true\n\n[pycodestyle]\n# E129: indentation between lines in conditions\n# E261: two spaces before "
  },
  {
    "path": "setup.py",
    "chars": 1482,
    "preview": "#!/usr/bin/env python\n\"\"\"Installs cram\"\"\"\n\nimport os\nimport sys\nfrom distutils.core import setup\n\nCOMMANDS = {}\nCRAM_DIR"
  },
  {
    "path": "tests/config.t",
    "chars": 884,
    "preview": "Set up cram alias and example tests:\n\n  $ . \"$TESTDIR\"/setup.sh\n\nOptions in .cramrc:\n\n  $ cat > .cramrc <<EOF\n  > [cram]"
  },
  {
    "path": "tests/debug.t",
    "chars": 1406,
    "preview": "Set up cram alias and example tests:\n\n  $ . \"$TESTDIR\"/setup.sh\n\nDebug mode:\n\n  $ printf '  $ echo hi\\n  > echo bye' > d"
  },
  {
    "path": "tests/dist.t",
    "chars": 247,
    "preview": "Skip this test if check-manifest isn't available:\n\n  $ command -v check-manifest > /dev/null || exit 80\n\nConfirm that \"m"
  },
  {
    "path": "tests/doctest.t",
    "chars": 111,
    "preview": "Set up cram alias and example tests:\n\n  $ . \"$TESTDIR\"/setup.sh\n\nRun doctests:\n\n  $ doctest \"$TESTDIR\"/../cram\n"
  },
  {
    "path": "tests/encoding.t",
    "chars": 1497,
    "preview": "Set up cram alias and example tests:\n\n  $ . \"$TESTDIR\"/setup.sh\n\nTest with Windows newlines:\n\n  $ printf \"  $ echo hi\\r\\"
  },
  {
    "path": "tests/interactive.t",
    "chars": 6784,
    "preview": "Set up cram alias and example tests:\n\n  $ . \"$TESTDIR\"/setup.sh\n\nInteractive mode (don't merge):\n\n  $ cram -n -i example"
  },
  {
    "path": "tests/pep8.t",
    "chars": 232,
    "preview": "Skip this test if pycodestyle isn't available:\n\n  $ command -v pycodestyle > /dev/null || exit 80\n\nCheck that the Python"
  },
  {
    "path": "tests/pyflakes.t",
    "chars": 180,
    "preview": "Skip this test if pyflakes isn't available:\n\n  $ command -v pyflakes > /dev/null || exit 80\n\nCheck that there are no obv"
  },
  {
    "path": "tests/run-doctests.py",
    "chars": 1182,
    "preview": "#!/usr/bin/env python\n\nimport doctest\nimport os\nimport sys\n\ndef _getmodules(pkgdir):\n    \"\"\"Import and yield modules in "
  },
  {
    "path": "tests/setup.sh",
    "chars": 1089,
    "preview": "#!/bin/sh\n\n# Bash doesn't expand aliases by default in non-interactive mode, so\n# we enable it manually if the test is r"
  },
  {
    "path": "tests/test.t",
    "chars": 3189,
    "preview": "Set up cram alias and example tests:\n\n  $ . \"$TESTDIR\"/setup.sh\n\nRun cram examples:\n\n  $ cram -q examples examples/fail."
  },
  {
    "path": "tests/usage.t",
    "chars": 1571,
    "preview": "Set up cram alias and example tests:\n\n  $ . \"$TESTDIR\"/setup.sh\n\nUsage:\n\n  $ cram -h\n  [Uu]sage: cram \\[OPTIONS\\] TESTS\\"
  },
  {
    "path": "tests/xunit.t",
    "chars": 2347,
    "preview": "Set up cram alias and example tests:\n\n  $ . \"$TESTDIR\"/setup.sh\n\nxUnit XML output:\n\n  $ cram -q -v --xunit-file=cram.xml"
  }
]

About this extraction

This page contains the full source code of the brodie/cram GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 49 files (97.4 KB), approximately 28.9k tokens, and a symbol index with 40 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!