Bug 1928709 - Remove 'cram' test harness from the tree, r=jmaher,mach-reviewers,ahochheiden

This is no longer being used and the upstream project is unmaintained.

Differential Revision: https://phabricator.services.mozilla.com/D227869
This commit is contained in:
Andrew Halberstadt
2024-11-06 19:03:16 +00:00
parent 3a434be9a2
commit 7da1da2f21
29 changed files with 2 additions and 1749 deletions

View File

@@ -85,7 +85,6 @@ MACH_COMMANDS = {
),
"configure": MachCommandReference("python/mozbuild/mozbuild/build_commands.py"),
"cppunittest": MachCommandReference("testing/mach_commands.py"),
"cramtest": MachCommandReference("testing/mach_commands.py"),
"crashtest": MachCommandReference("layout/tools/reftest/mach_commands.py"),
"data-review": MachCommandReference(
"toolkit/components/glean/build_scripts/mach_commands.py"

View File

@@ -2247,12 +2247,6 @@ VARIABLES = {
"""List of manifest files defining MozPerftest performance tests.
""",
),
"CRAMTEST_MANIFESTS": (
ManifestparserManifestList,
list,
"""List of manifest files defining cram unit tests.
""",
),
"TELEMETRY_TESTS_CLIENT_MANIFESTS": (
ManifestparserManifestList,
list,

View File

@@ -41,7 +41,6 @@ TEST_MANIFESTS = dict(
FIREFOX_UI_FUNCTIONAL=("firefox-ui-functional", "firefox-ui", ".", False),
FIREFOX_UI_UPDATE=("firefox-ui-update", "firefox-ui", ".", False),
PYTHON_UNITTEST=("python", "python", ".", False),
CRAMTEST=("cram", "cram", ".", False),
TELEMETRY_TESTS_CLIENT=(
"telemetry-tests-client",
"toolkit/components/telemetry/tests/marionette/",

View File

@@ -20,7 +20,6 @@ vendored:third_party/python/charset_normalizer
vendored:third_party/python/compare_locales
vendored:third_party/python/cookiecutter
vendored:third_party/python/cookies
vendored:third_party/python/cram
vendored:third_party/python/diskcache
vendored:third_party/python/dlmanager
vendored:third_party/python/ecdsa

View File

@@ -28,7 +28,6 @@ vendored:third_party/python/charset_normalizer
vendored:third_party/python/compare_locales
vendored:third_party/python/cookiecutter
vendored:third_party/python/cookies
vendored:third_party/python/cram
vendored:third_party/python/diskcache
vendored:third_party/python/dlmanager
vendored:third_party/python/ecdsa

View File

@@ -20,7 +20,6 @@ vendored:third_party/python/charset_normalizer
vendored:third_party/python/compare_locales
vendored:third_party/python/cookiecutter
vendored:third_party/python/cookies
vendored:third_party/python/cram
vendored:third_party/python/diskcache
vendored:third_party/python/dlmanager
vendored:third_party/python/ecdsa

View File

@@ -7,7 +7,6 @@ project-repo-param-prefix: ''
product-dir: 'browser'
treeherder:
group-names:
'cram': 'Cram tests'
'js-bench-sm': 'JavaScript shell benchmarks with Spidermonkey'
'js-bench-v8': 'JavaScript shell benchmarks with Google V8'
'node': 'Node tests'

View File

@@ -5,8 +5,8 @@
import argparse
import logging
import os
import subprocess
import sys
from datetime import date, timedelta
import requests
from mach.decorators import Command, CommandArgument, SubCommand
@@ -716,57 +716,6 @@ def run_jsshelltests(command_context, **kwargs):
return benchmark.run(**kwargs)
@Command(
"cramtest",
category="testing",
description="Mercurial style .t tests for command line applications.",
)
@CommandArgument(
"test_paths",
nargs="*",
metavar="N",
help="Test paths to run. Each path can be a test file or directory. "
"If omitted, the entire suite will be run.",
)
@CommandArgument(
"cram_args",
nargs=argparse.REMAINDER,
help="Extra arguments to pass down to the cram binary. See "
"'./mach python -m cram -- -h' for a list of available options.",
)
def cramtest(command_context, cram_args=None, test_paths=None, test_objects=None):
command_context.activate_virtualenv()
import mozinfo
from manifestparser import TestManifest
if test_objects is None:
from moztest.resolve import TestResolver
resolver = command_context._spawn(TestResolver)
if test_paths:
# If we were given test paths, try to find tests matching them.
test_objects = resolver.resolve_tests(paths=test_paths, flavor="cram")
else:
# Otherwise just run everything in CRAMTEST_MANIFESTS
test_objects = resolver.resolve_tests(flavor="cram")
if not test_objects:
message = "No tests were collected, check spelling of the test paths."
command_context.log(logging.WARN, "cramtest", {}, message)
return 1
mp = TestManifest()
mp.tests.extend(test_objects)
tests = mp.active_tests(disabled=False, **mozinfo.info)
python = command_context.virtualenv_manager.python_path
cmd = [python, "-m", "cram"] + cram_args + [t["relpath"] for t in tests]
return subprocess.call(cmd, cwd=command_context.topsrcdir)
from datetime import date, timedelta
@Command(
"test-info", category="testing", description="Display historical test results."
)

View File

@@ -1,9 +0,0 @@
#!python
import sys
import cram
try:
sys.exit(cram.main(sys.argv[1:]))
except KeyboardInterrupt:
pass

View File

@@ -1,227 +0,0 @@
======================
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`_::
The $PYTHON environment variable should be set when running this test
from Python.
$ [ -n "$PYTHON" ] || PYTHON="`which python`"
$ [ -n "$PYTHONPATH" ] || PYTHONPATH="$TESTDIR/.." && export PYTHONPATH
$ if [ -n "$COVERAGE" ]; then
> coverage erase
> alias cram="`which coverage` run --branch -a $TESTDIR/../scripts/cram"
> else
> alias cram="$PYTHON $TESTDIR/../scripts/cram"
> fi
$ command -v md5 > /dev/null || alias md5=md5sum
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://bitbucket.org/brodie/cram/src/default/tests/cram.t
.. _Perl-compatible regular expressions: https://en.wikipedia.org/wiki/Perl_Compatible_Regular_Expressions
Download
--------
* `cram-0.7.tar.gz`_ (32 KB, requires Python 2.4-2.7 or Python 3.1 or newer)
.. _cram-0.7.tar.gz: https://bitheap.org/cram/cram-0.7.tar.gz
Installation
------------
Install Cram using make::
$ wget https://bitheap.org/cram/cram-0.7.tar.gz
$ tar zxvf cram-0.7.tar.gz
$ cd cram-0.7
$ 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://bitbucket.org/brodie/cram/src/default/examples/
Development
-----------
Download the official development repository using Mercurial_::
hg clone https://bitbucket.org/brodie/cram
Or Git_::
git clone https://github.com/brodie/cram.git
Test Cram using Cram::
pip install -r requirements.txt
make test
Visit Bitbucket_ or GitHub_ if you'd like to fork the project, watch
for new changes, or report issues.
.. _Mercurial: http://mercurial.selenic.com/
.. _Git: http://git-scm.com/
.. _coverage.py: http://nedbatchelder.com/code/coverage/
.. _Bitbucket: https://bitbucket.org/brodie/cram
.. _GitHub: https://github.com/brodie/cram

View File

@@ -1,250 +0,0 @@
Metadata-Version: 2.0
Name: cram
Version: 0.7
Summary: A simple testing framework for command line applications
Home-page: https://bitheap.org/cram/
Author: Brodie Rao
Author-email: brodie@bitheap.org
License: GNU GPLv2 or any later version
Download-URL: https://bitheap.org/cram/cram-0.7.tar.gz
Keywords: automatic functional test framework
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: GNU General Public License (GPL)
Classifier: License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Unix Shell
Classifier: Topic :: Software Development :: Testing
======================
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`_::
The $PYTHON environment variable should be set when running this test
from Python.
$ [ -n "$PYTHON" ] || PYTHON="`which python`"
$ [ -n "$PYTHONPATH" ] || PYTHONPATH="$TESTDIR/.." && export PYTHONPATH
$ if [ -n "$COVERAGE" ]; then
> coverage erase
> alias cram="`which coverage` run --branch -a $TESTDIR/../scripts/cram"
> else
> alias cram="$PYTHON $TESTDIR/../scripts/cram"
> fi
$ command -v md5 > /dev/null || alias md5=md5sum
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://bitbucket.org/brodie/cram/src/default/tests/cram.t
.. _Perl-compatible regular expressions: https://en.wikipedia.org/wiki/Perl_Compatible_Regular_Expressions
Download
--------
* `cram-0.7.tar.gz`_ (32 KB, requires Python 2.4-2.7 or Python 3.1 or newer)
.. _cram-0.7.tar.gz: https://bitheap.org/cram/cram-0.7.tar.gz
Installation
------------
Install Cram using make::
$ wget https://bitheap.org/cram/cram-0.7.tar.gz
$ tar zxvf cram-0.7.tar.gz
$ cd cram-0.7
$ 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://bitbucket.org/brodie/cram/src/default/examples/
Development
-----------
Download the official development repository using Mercurial_::
hg clone https://bitbucket.org/brodie/cram
Or Git_::
git clone https://github.com/brodie/cram.git
Test Cram using Cram::
pip install -r requirements.txt
make test
Visit Bitbucket_ or GitHub_ if you'd like to fork the project, watch
for new changes, or report issues.
.. _Mercurial: http://mercurial.selenic.com/
.. _Git: http://git-scm.com/
.. _coverage.py: http://nedbatchelder.com/code/coverage/
.. _Bitbucket: https://bitbucket.org/brodie/cram
.. _GitHub: https://github.com/brodie/cram

View File

@@ -1,16 +0,0 @@
cram/__init__.py,sha256=80M3WLqeS6MAACoIZW89KZR4bOmFm7UcpoRPF6S-8jc,172
cram/__main__.py,sha256=AUlczSWsDtiA6srk4dsmdsz8cZXb1QXMdPkobAR-Ex0,152
cram/_cli.py,sha256=aIJE2BY0djuOqgtCHe9IVUIl7Vvvk-awsksdmMd1RNc,4345
cram/_diff.py,sha256=pXLlKb1UgQX17ayJpPQsGoMHW7bKLcACe9KEZlnMkx0,5630
cram/_encoding.py,sha256=PSPdcjenMvC0wabbPhWPkCxeUcohcQ6o3Rk58AC97Uo,2990
cram/_main.py,sha256=5gwaBNSyKCq9bwkRLKqNXcsB5Okf0sfxDpousd51CO4,7728
cram/_process.py,sha256=2JV6sRl_9p3DYu1IYN5_D-isln9vAh5ua6bAxAy8ytA,1805
cram/_run.py,sha256=X5fOy7TKxMdBcis0JczYZkNUoQdJ5wUqlDCM2sRJDm0,2292
cram/_test.py,sha256=9QYuf3DRuLs9O1QVP3MfoJlISBRfnC5ONhCL4uXGYG8,7904
cram/_xunit.py,sha256=KUAUokY3HhkgPYp0IjSl2m7KvztYdbwW7p1aqdaUJgA,6247
cram-0.7.data/scripts/cram,sha256=S3wCw9Ks2J4dtVftWZ8DU0eNtpb1ekf8Bz73Di3PvUs,112
cram-0.7.dist-info/DESCRIPTION.rst,sha256=ejwfPio_dRLrZ2PhWnsGbLW6lPyiDTjUAejg5MPG-kg,7080
cram-0.7.dist-info/METADATA,sha256=ExruW_6HNwqu-mVqvcCSUtund4CHxt5hb3019a3jLeo,8018
cram-0.7.dist-info/RECORD,,
cram-0.7.dist-info/WHEEL,sha256=o2k-Qa-RMNIJmUdIc7KU6VWR_ErNRbWNlxDIpl7lm34,110
cram-0.7.dist-info/metadata.json,sha256=cRTULRj1eXU8xWOtqLK8DMhu0vWJELulW_PI8O4ytPU,1063

View File

@@ -1,6 +0,0 @@
Wheel-Version: 1.0
Generator: bdist_wheel (0.29.0)
Root-Is-Purelib: true
Tag: py2-none-any
Tag: py3-none-any

View File

@@ -1 +0,0 @@
{"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 :: 2", "Programming Language :: Python :: 3", "Programming Language :: Unix Shell", "Topic :: Software Development :: Testing"], "download_url": "https://bitheap.org/cram/cram-0.7.tar.gz", "extensions": {"python.details": {"contacts": [{"email": "brodie@bitheap.org", "name": "Brodie Rao", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "https://bitheap.org/cram/"}}}, "generator": "bdist_wheel (0.29.0)", "keywords": ["automatic", "functional", "test", "framework"], "license": "GNU GPLv2 or any later version", "metadata_version": "2.0", "name": "cram", "summary": "A simple testing framework for command line applications", "version": "0.7"}

View File

@@ -1,6 +0,0 @@
"""Functional testing framework for command line applications"""
from cram._main import main
from cram._test import test, testfile
__all__ = ['main', 'test', 'testfile']

View File

@@ -1,10 +0,0 @@
"""Main module (invoked by "python -m cram")"""
import sys
import cram
try:
sys.exit(cram.main(sys.argv[1:]))
except KeyboardInterrupt:
pass

View File

@@ -1,134 +0,0 @@
"""The command line interface implementation"""
import os
import sys
from cram._encoding import b, bytestype, stdoutb
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, bytestype):
stdoutb.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:
stdoutb.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]))

View File

@@ -1,158 +0,0 @@
"""Utilities for diffing test files and their output"""
import codecs
import difflib
import re
from cram._encoding import b
__all__ = ['esc', 'glob', 'regex', 'unified_diff']
def _regex(pattern, s):
"""Match a regular expression or return False if invalid.
>>> from cram._encoding import b
>>> [bool(_regex(r, b('foobar'))) for r in (b('foo.*'), b('***'))]
[True, False]
"""
try:
return re.match(pattern + b(r'\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.
>>> from cram._encoding import b
>>> bool(_glob(b(r'\* \\ \? 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('re', _regex, el, l)
def glob(el, l):
"""Apply a glob match to a line annotated with '(glob)'"""
return _matchannotation('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.
>>> from cram._encoding import b
>>> 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

View File

@@ -1,106 +0,0 @@
"""Encoding utilities"""
import os
import sys
try:
import builtins
except ImportError:
import __builtin__ as builtins
__all__ = ['b', 'bchr', 'bytestype', 'envencode', 'fsdecode', 'fsencode',
'stdoutb', 'stderrb', 'u', 'ul', 'unicodetype']
bytestype = getattr(builtins, 'bytes', str)
unicodetype = getattr(builtins, 'unicode', str)
if getattr(os, 'fsdecode', None) is not None:
fsdecode = os.fsdecode
fsencode = os.fsencode
elif bytestype is not str:
if sys.platform == 'win32':
def fsdecode(s):
"""Decode a filename from the filesystem encoding"""
if isinstance(s, unicodetype):
return s
encoding = sys.getfilesystemencoding()
if encoding == 'mbcs':
return s.decode(encoding)
else:
return s.decode(encoding, 'surrogateescape')
def fsencode(s):
"""Encode a filename to the filesystem encoding"""
if isinstance(s, bytestype):
return s
encoding = sys.getfilesystemencoding()
if encoding == 'mbcs':
return s.encode(encoding)
else:
return s.encode(encoding, 'surrogateescape')
else:
def fsdecode(s):
"""Decode a filename from the filesystem encoding"""
if isinstance(s, unicodetype):
return s
return s.decode(sys.getfilesystemencoding(), 'surrogateescape')
def fsencode(s):
"""Encode a filename to the filesystem encoding"""
if isinstance(s, bytestype):
return s
return s.encode(sys.getfilesystemencoding(), 'surrogateescape')
else:
def fsdecode(s):
"""Decode a filename from the filesystem encoding"""
return s
def fsencode(s):
"""Encode a filename to the filesystem encoding"""
return s
if bytestype is str:
def envencode(s):
"""Encode a byte string to the os.environ encoding"""
return s
else:
envencode = fsdecode
if getattr(sys.stdout, 'buffer', None) is not None:
stdoutb = sys.stdout.buffer
stderrb = sys.stderr.buffer
else:
stdoutb = sys.stdout
stderrb = sys.stderr
if bytestype is str:
def b(s):
"""Convert an ASCII string literal into a bytes object"""
return s
bchr = chr
def u(s):
"""Convert an ASCII string literal into a unicode object"""
return s.decode('ascii')
else:
def b(s):
"""Convert an ASCII string literal into a bytes object"""
return s.encode('ascii')
def bchr(i):
"""Return a bytes character for a given integer value"""
return bytestype([i])
def u(s):
"""Convert an ASCII string literal into a unicode object"""
return s
try:
eval(r'u""')
except SyntaxError:
ul = eval
else:
def ul(e):
"""Evaluate e as a unicode string literal"""
return eval('u' + e)

View File

@@ -1,211 +0,0 @@
"""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._encoding import b, fsencode, stderrb, stdoutb
from cram._run import runtests
from cram._xunit import runxunit
def _which(cmd):
"""Return the path to cmd or None if not found"""
cmd = fsencode(cmd)
for p in os.environ['PATH'].split(os.pathsep):
path = os.path.join(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 = [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.7)
Copyright (C) 2010-2016 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:
stderrb.write(b('shell not found: ') + fsencode(opts.shell) + b('\n'))
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:
stderrb.write(b('no such file: ') + badpaths[0] + b('\n'))
return 2
if opts.yes:
answer = 'y'
elif opts.no:
answer = 'n'
else:
answer = None
tmpdir = os.environ['CRAMTMP'] = tempfile.mkdtemp('', 'cramtests-')
tmpdirb = 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:
stdoutb.write(b('# Kept temporary directory: ') + tmpdirb +
b('\n'))
else:
shutil.rmtree(tmpdir)

View File

@@ -1,54 +0,0 @@
"""Utilities for running subprocesses"""
import os
import signal
import subprocess
import sys
from cram._encoding import fsdecode
__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 = [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

View File

@@ -1,77 +0,0 @@
"""The test runner"""
import os
import sys
from cram._encoding import b, fsdecode, fsencode
from cram._test import testfile
__all__ = ['runtests']
if sys.platform == 'win32': # pragma: nocover
def _walk(top):
top = fsdecode(top)
for root, dirs, files in os.walk(top):
yield (fsencode(root),
[fsencode(p) for p in dirs],
[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('-%s' % 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)

View File

@@ -1,230 +0,0 @@
"""Utilities for running individual tests"""
import itertools
import os
import re
import time
from cram._encoding import b, bchr, bytestype, envencode, unicodetype
from cram._diff import esc, glob, regex, unified_diff
from cram._process import PIPE, STDOUT, execute
__all__ = ['test', 'testfile']
_needescape = re.compile(b(r'[\x00-\x09\x0b-\x1f\x7f-\xff]')).search
_escapesub = re.compile(b(r'[\x00-\x09\x0b-\x1f\\\x7f-\xff]')).sub
_escapemap = dict((bchr(i), b(r'\x%02x' % i)) for i in range(256))
_escapemap.update({b('\\'): b('\\\\'), b('\r'): b(r'\r'), b('\t'): b(r'\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:
>>> from cram._encoding import b
>>> 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
Note that the b() function is internal to Cram. If you're using Python 2,
use normal string literals instead. If you're using Python 3, use bytes
literals.
: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('> ')
usalt = 'CRAM%s' % time.time()
salt = b(usalt)
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, bytestype):
lines = lines.splitlines(True)
if isinstance(shell, (bytestype, unicodetype)):
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 %s $?\n' % (usalt, 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 %s $?\n' % (usalt, 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('[%s]\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'] = envencode(os.path.dirname(abspath))
env['TESTFILE'] = envencode(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()

View File

@@ -1,173 +0,0 @@
"""xUnit XML output"""
import locale
import os
import re
import socket
import sys
import time
from cram._encoding import u, ul
__all__ = ['runxunit']
_widecdataregex = ul(r"'(?:[^\x09\x0a\x0d\x20-\ud7ff\ue000-\ufffd"
r"\U00010000-\U0010ffff]|]]>)'")
_narrowcdataregex = ul(r"'(?:[^\x09\x0a\x0d\x20-\ud7ff\ue000-\ufffd]"
r"|]]>)'")
_widequoteattrregex = ul(r"'[^\x20\x21\x23-\x25\x27-\x3b\x3d"
r"\x3f-\ud7ff\ue000-\ufffd"
r"\U00010000-\U0010ffff]'")
_narrowquoteattrregex = ul(r"'[^\x20\x21\x23-\x25\x27-\x3b\x3d"
r"\x3f-\ud7ff\ue000-\ufffd]'")
_replacementchar = ul(r"'\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) == u(']]>'):
return u(']]>]]&gt;<![CDATA[')
else:
return _replacementchar
def _cdata(s):
r"""Escape a string as an XML CDATA block.
>>> from cram._encoding import ul
>>> (_cdata('1<\'2\'>&"3\x00]]>\t\r\n') ==
... ul(r"'<![CDATA[1<\'2\'>&\"3\ufffd]]>]]&gt;<![CDATA[\t\r\n]]>'"))
True
"""
return u('<![CDATA[%s]]>') % _cdatasub(_cdatareplace, s)
def _quoteattrreplace(m):
"""Replace _quoteattrsub() regex match"""
return {u('\t'): u('&#9;'),
u('\n'): u('&#10;'),
u('\r'): u('&#13;'),
u('"'): u('&quot;'),
u('&'): u('&amp;'),
u('<'): u('&lt;'),
u('>'): u('&gt;')}.get(m.group(0), _replacementchar)
def _quoteattr(s):
r"""Escape a string for use as an XML attribute value.
>>> from cram._encoding import ul
>>> (_quoteattr('1<\'2\'>&"3\x00]]>\t\r\n') ==
... ul(r"'\"1&lt;\'2\'&gt;&amp;&quot;3\ufffd]]&gt;&#9;&#13;&#10;\"'"))
True
"""
return u('"%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 += u('%+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 = (u(' <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 = u('').join(l.decode(locale.getpreferredencoding(),
'replace')
for l in diff)
testcase = (u(' <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 = (u(' <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 = (u('<?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 = u('</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()

View File

@@ -15,7 +15,6 @@ dependencies = [
"colorama==0.4.6",
"compare-locales==9.0.1",
"cookies==2.2.1",
"cram==0.7",
"distro==1.8.0",
"ecdsa==0.15",
"esprima==4.0.1",

View File

@@ -191,9 +191,6 @@ cookiecutter==2.6.0 \
cookies==2.2.1 \
--hash=sha256:d6b698788cae4cfa4e62ef8643a9ca332b79bd96cb314294b864ae8d7eb3ee8e \
--hash=sha256:15bee753002dff684987b8df8c235288eb8d45f8191ae056254812dfd42c81d3
cram==0.7 \
--hash=sha256:7da7445af2ce15b90aad5ec4792f857cef5786d71f14377e9eb994d8b8337f2f \
--hash=sha256:008e4e8b4d325cf040964b5f62460535b004a7bc816d54f8527a4d299edfe4a3
diskcache==5.6.3 \
--hash=sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc \
--hash=sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19

11
third_party/python/uv.lock generated vendored
View File

@@ -355,15 +355,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/60/557f84aa2db629e5124aa05408b975b1b5d0e1cec16cde0bfa06aae097d3/cookies-2.2.1-py2.py3-none-any.whl", hash = "sha256:15bee753002dff684987b8df8c235288eb8d45f8191ae056254812dfd42c81d3", size = 44423 },
]
[[package]]
name = "cram"
version = "0.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/38/85/5a8a3397b2ccb2ffa3ba871f76a4d72c16531e43d0e58fc89a0f2983adbd/cram-0.7.tar.gz", hash = "sha256:7da7445af2ce15b90aad5ec4792f857cef5786d71f14377e9eb994d8b8337f2f", size = 33527 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a2/09/c119252f12b35c21b030c01d38d292936660c167e725ce07517fae2b01a0/cram-0.7-py2.py3-none-any.whl", hash = "sha256:008e4e8b4d325cf040964b5f62460535b004a7bc816d54f8527a4d299edfe4a3", size = 22141 },
]
[[package]]
name = "diskcache"
version = "5.6.3"
@@ -751,7 +742,6 @@ dependencies = [
{ name = "colorama" },
{ name = "compare-locales" },
{ name = "cookies" },
{ name = "cram" },
{ name = "distro" },
{ name = "ecdsa" },
{ name = "esprima" },
@@ -812,7 +802,6 @@ requires-dist = [
{ name = "colorama", specifier = "==0.4.6" },
{ name = "compare-locales", specifier = "==9.0.1" },
{ name = "cookies", specifier = "==2.2.1" },
{ name = "cram", specifier = "==0.7" },
{ name = "distro", specifier = "==1.8.0" },
{ name = "ecdsa", specifier = "==0.15" },
{ name = "esprima", specifier = "==4.0.1" },

View File

@@ -1 +1 @@
8f3b27ce4a9f0bcab71b21d5133a3ef5b65ec655c3a696d6587955942fc95ec5
35232ec676dd469de7810dbc063f1a4d1831d97adc9a05236e5a7645fe148b90

View File

@@ -19,7 +19,6 @@ test-manifest-toml:
- '**/clippy.toml'
- '**/config-lock.toml'
- '**/config.toml'
- '**/cram.toml'
- '**/empty.toml'
- '**/generated-mochitest.toml'
- '**/l10n.toml'