Writing Tests

Mercurial contains a simple regression test framework that allows both Python unit tests and shell-script driven regression tests.

/!\ This page is intended for developers.

1. Running the test suite

To run the tests, do:

$ make tests
cd tests && ./run-tests.py
............................................
Ran 44 tests, 0 failed.

This finds all scripts in the tests/ directory named test-* and executes them. The scripts can be either unified tests, shell scripts, or Python. Each test is run in a temporary directory that is removed when the test is complete.

You can also run tests individually:

$ cd tests/
$ ./run-tests.py test-pull.t test-undo.t
..
Ran 2 tests, 0 failed.

A test-<x> succeeds if the script returns success and its output matches test-<x>.out. If the new output doesn't match, it is stored in test-<x>.err.

Also, 'run-tests.py' has some useful options:

See 'run-tests.py -h' for a full list.

One option that comes in handy when running tests repeatedly is '--local'. By default, 'run-tests.py' installs Mercurial into its temporary directory for each run of the test suite. You can save several seconds per run with '--local', which tells 'run-tests.py' simply to use the local 'hg' script and library. The catch: if you edit the code during a long test suite run, different tests will run with different code. It's best to use '--local' when you are running the same test script many times, as often happens during development.

{i} Note that tests won't run properly with an egg-based install of Mercurial; the system install of Mercurial will be used instead of the checked out version. Use a Mercurial installed from source instead to avoid conflicts.

2. Writing a shell script test

/!\ For performance reasons, it's generally best to extend an existing test script!

2.1. Basic example

Creating a regression test is easy. Simply create a *.t file which contains shell script commands prepended with '  $ '. Lines not starting with two spaces are comments.

Here's an example (test-x.t):

File replaced with directory:

  $ hg init a
  $ cd a
  $ echo a > a
  $ hg commit -Ama
  $ rm a
  $ mkdir a
  $ echo a > a/a

Should fail - would corrupt dirstate:

  $ hg add a/a

Then run this test for the first time

$ python run-tests.py -i test-x.t

ERROR: /home/adi/hgrepos/hg-crew/tests/test-x.t output changed
--- /home/adi/hgrepos/hg-crew/tests/test-x.t
+++ /home/adi/hgrepos/hg-crew/tests/test-x.t.err
@@ -4,6 +4,7 @@
   $ cd a
   $ echo a > a
   $ hg commit -Ama
+  adding a
   $ rm a
   $ mkdir a
   $ echo a > a/a
@@ -11,4 +12,6 @@
 Should fail - would corrupt dirstate:

   $ hg add a/a
+  abort: file 'a' in dirstate clashes with 'a/a'
+  [255]

!Accept this change? [n]

Check the output of the commands inserted into your test file and accept the modified test file with 'y'.

The test file now includes both command input interspersed with command output:

File replaced with directory:

  $ hg init a
  $ cd a
  $ echo a > a
  $ hg commit -Ama
  adding a
  $ rm a
  $ mkdir a
  $ echo a > a/a

Should fail - would corrupt dirstate:

  $ hg add a/a
  abort: file 'a' in dirstate clashes with 'a/a'
  [255]

Note how nonzero return values show up enclosed in squared brackets ("[255]" for "hg add a/a").

Running this test again will now pass

$ python run-tests.py test-x.t -i
.
# Ran 1 tests, 0 skipped, 0 failed.

This kind of test is also known as "unified test" (because it unifies input and output into the same file).

2.2. Filtering output

Such tests must be repeatable, that is, output generated by commands must not contain strings that change for each invocation (like the path of a temporary file).

To cope with this kind of variation, unified tests support filtering using (glob) or (re).

To enable glob filtering for an output line, append " (glob)" to the respective line like in the following example:

   $ hg version -q
   Mercurial Distributed SCM (version *) (glob)

(glob) filtering supports * for matching a string and ? for matching a single character. Example:

  $ hg diff
  diff -r ???????????? orphanchild (glob)
  --- /dev/null
  +++ b/orphanchild
  @@ -0,0 +1,1 @@
  +orphan

Literal * or ? on (glob) lines must be escaped with \ (backslash).

To use regular expression filtering on a line, append " (re)" to the output line:

   $ hg version -q
   Mercurial Distributed SCM \(version .*\) (re)

2.3. Format summary

The format in a nutshell (adapted from http://pypi.python.org/pypi/cram):

Anything else is a comment.

2.4. Making tests repeatable

There are some tricky points here that you should be aware of when writing tests:

#!/bin/sh
cat <<EOF > merge
echo merging for `basename $1`
EOF
chmod +x merge

env HGMERGE=./merge hg update -m 1

2.5. Making tests portable

{i} Most of these issues are caught by 'contrib/check-code.py'

You also need to be careful that the tests are portable from one platform to another. You're probably working on Linux, where the GNU toolchain has more (or different) functionality than on MacOS, *BSD, Solaris, AIX, etc. While testing on all platforms is the only sure-fire way to make sure that you've written portable code, here's a list of problems that have been found and fixed in the tests. Another, more comprehensive list may be found in the GNU Autoconf manual.

2.5.1. sh

The Bourne shell is a very basic shell. On Linux, /bin/sh is typically bash, which even in Bourne-shell mode has many features that Bourne shells on other Unix systems don't have. (Note however that on Linux /bin/sh isn't guaranteed to be bash; in particular, on Ubuntu, /bin/sh is dash, a small Posix-compliant shell that lacks many bash features). You'll need to be careful about constructs that seem ubiquitous, but are actually not available in the least common denominator. While using another shell (ksh, bash explicitly, posix shell, etc.) explicitly may seem like another option, these may not exist in a portable location, and so are generally probably not a good idea. You may find that rewriting the test in python will be easier.

2.5.2. grep

2.5.3. sed

sed -e 's/foo/bar/' a > a.new
mv a.new a

2.5.4. echo

2.5.5. false

2.5.6. diff

2.5.7. wc

head -c 20 foo > bar

dd if=foo of=bar bs=1 count=20 2>/dev/null

2.5.9. ls

2.5.10. tr

2.6. A naming scheme for test elements

Rather than use an ad-hoc mix of names like foo, bar, baz for generic names in tests, consider the following scheme when writing new test cases:

If you've only got one directory, one file, etc. in your test, you can drop the '1'.

3. Writing a Python unit test

A unit test operates much like a regression test, but is written in Python. Here's an example:

   1 #!/usr/bin/env python
   2 
   3 import sys
   4 from mercurial import bdiff, mpatch
   5 
   6 def test1(a, b):
   7     d = bdiff.bdiff(a, b)
   8     c = a
   9     if d:
  10         c = mpatch.patches(a, [d])
  11     if c != b:
  12         print "***", `a`, `b`
  13         print "bad:"
  14         print `c`[:200]
  15         print `d`
  16 
  17 def test(a, b):
  18     print "***", `a`, `b`
  19     test1(a, b)
  20     test1(b, a)
  21 
  22 test("a\nc\n\n\n\n", "a\nb\n\n\n")
  23 test("a\nb\nc\n", "a\nc\n")
  24 test("", "")
  25 test("a\nb\nc", "a\nb\nc")
  26 test("a\nb\nc\nd\n", "a\nd\n")
  27 test("a\nb\nc\nd\n", "a\nc\ne\n")
  28 test("a\nb\nc\n", "a\nc\n")
  29 test("a\n", "c\na\nb\n")
  30 test("a\n", "")
  31 test("a\n", "b\nc\n")
  32 test("a\n", "c\na\n")
  33 test("", "adjfkjdjksdhfksj")
  34 test("", "ab")
  35 test("", "abc")
  36 test("a", "a")
  37 test("ab", "ab")
  38 test("abc", "abc")
  39 test("a\n", "a\n")
  40 test("a\nb", "a\nb")
  41 
  42 print "done"

4. Writing a Python doctest

The Mercurial test suite also supports running Python doctests from the docstrings in the source code. This can be useful for testing simple functions which don't work on complex data or repositories. Here's an example test from mercurial/changelog.py:

def _string_escape(text):
    """
    >>> d = {'nl': chr(10), 'bs': chr(92), 'cr': chr(13), 'nul': chr(0)}
    >>> s = "ab%(nl)scd%(bs)s%(bs)sn%(nul)sab%(cr)scd%(bs)s%(nl)s" % d
    >>> s
    'ab\\ncd\\\\\\\\n\\x00ab\\rcd\\\\\\n'
    >>> res = _string_escape(s)
    >>> s == res.decode('string_escape')
    True
    """
    # subset of the string_escape codec
    text = text.replace('\\', '\\\\').replace('\n', '\\n').replace('\r', '\\r')
    return text.replace('\0', '\\0')

This tests is run by tests/test-docstring.py, which contains a list of modules to search for docstring tests in.

5. See also


CategoryTesting CategoryDeveloper