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 ']]>]]><![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]]>]]><![CDATA[\t\r\n]]>')
True
"""
return '<![CDATA[%s]]>' % _cdatasub(_cdatareplace, s)
def _quoteattrreplace(m):
"""Replace _quoteattrsub() regex match"""
return {'\t': '	',
'\n': ' ',
'\r': ' ',
'"': '"',
'&': '&',
'<': '<',
'>': '>'}.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<\'2\'>&"3\ufffd]]>	 "')
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
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
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.