Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
pytest / testing / test_terminal.py
Size: Mime:
"""
terminal reporting of the full testing process.
"""
import collections
import sys

import _pytest._pluggy as pluggy
import _pytest._code
import py
import pytest
from _pytest import runner
from _pytest.main import EXIT_NOTESTSCOLLECTED
from _pytest.terminal import TerminalReporter, repr_pythonversion, getreportopt
from _pytest.terminal import build_summary_stats_line, _plugin_nameversions


def basic_run_report(item):
    runner.call_and_report(item, "setup", log=False)
    return runner.call_and_report(item, "call", log=False)

DistInfo = collections.namedtuple('DistInfo', ['project_name', 'version'])


class Option:
    def __init__(self, verbose=False, fulltrace=False):
        self.verbose = verbose
        self.fulltrace = fulltrace

    @property
    def args(self):
        l = []
        if self.verbose:
            l.append('-v')
        if self.fulltrace:
            l.append('--fulltrace')
        return l

def pytest_generate_tests(metafunc):
    if "option" in metafunc.fixturenames:
        metafunc.addcall(id="default",
                         funcargs={'option': Option(verbose=False)})
        metafunc.addcall(id="verbose",
                         funcargs={'option': Option(verbose=True)})
        metafunc.addcall(id="quiet",
                         funcargs={'option': Option(verbose= -1)})
        metafunc.addcall(id="fulltrace",
                         funcargs={'option': Option(fulltrace=True)})


@pytest.mark.parametrize('input,expected', [
    ([DistInfo(project_name='test', version=1)], ['test-1']),
    ([DistInfo(project_name='pytest-test', version=1)], ['test-1']),
    ([
        DistInfo(project_name='test', version=1),
        DistInfo(project_name='test', version=1)
    ], ['test-1']),
], ids=['normal', 'prefix-strip', 'deduplicate'])
def test_plugin_nameversion(input, expected):
    pluginlist = [(None, x) for x in input]
    result = _plugin_nameversions(pluginlist)
    assert result == expected


class TestTerminal:
    def test_pass_skip_fail(self, testdir, option):
        testdir.makepyfile("""
            import pytest
            def test_ok():
                pass
            def test_skip():
                pytest.skip("xx")
            def test_func():
                assert 0
        """)
        result = testdir.runpytest(*option.args)
        if option.verbose:
            result.stdout.fnmatch_lines([
                "*test_pass_skip_fail.py::test_ok PASS*",
                "*test_pass_skip_fail.py::test_skip SKIP*",
                "*test_pass_skip_fail.py::test_func FAIL*",
            ])
        else:
            result.stdout.fnmatch_lines([
            "*test_pass_skip_fail.py .sF"
        ])
        result.stdout.fnmatch_lines([
            "    def test_func():",
            ">       assert 0",
            "E       assert 0",
        ])

    def test_internalerror(self, testdir, linecomp):
        modcol = testdir.getmodulecol("def test_one(): pass")
        rep = TerminalReporter(modcol.config, file=linecomp.stringio)
        excinfo = pytest.raises(ValueError, "raise ValueError('hello')")
        rep.pytest_internalerror(excinfo.getrepr())
        linecomp.assert_contains_lines([
            "INTERNALERROR> *ValueError*hello*"
        ])

    def test_writeline(self, testdir, linecomp):
        modcol = testdir.getmodulecol("def test_one(): pass")
        rep = TerminalReporter(modcol.config, file=linecomp.stringio)
        rep.write_fspath_result(modcol.nodeid, ".")
        rep.write_line("hello world")
        lines = linecomp.stringio.getvalue().split('\n')
        assert not lines[0]
        assert lines[1].endswith(modcol.name + " .")
        assert lines[2] == "hello world"

    def test_show_runtest_logstart(self, testdir, linecomp):
        item = testdir.getitem("def test_func(): pass")
        tr = TerminalReporter(item.config, file=linecomp.stringio)
        item.config.pluginmanager.register(tr)
        location = item.reportinfo()
        tr.config.hook.pytest_runtest_logstart(nodeid=item.nodeid,
            location=location, fspath=str(item.fspath))
        linecomp.assert_contains_lines([
            "*test_show_runtest_logstart.py*"
        ])

    def test_runtest_location_shown_before_test_starts(self, testdir):
        testdir.makepyfile("""
            def test_1():
                import time
                time.sleep(20)
        """)
        child = testdir.spawn_pytest("")
        child.expect(".*test_runtest_location.*py")
        child.sendeof()
        child.kill(15)

    def test_itemreport_subclasses_show_subclassed_file(self, testdir):
        testdir.makepyfile(test_p1="""
            class BaseTests:
                def test_p1(self):
                    pass
            class TestClass(BaseTests):
                pass
        """)
        p2 = testdir.makepyfile(test_p2="""
            from test_p1 import BaseTests
            class TestMore(BaseTests):
                pass
        """)
        result = testdir.runpytest(p2)
        result.stdout.fnmatch_lines([
            "*test_p2.py .",
            "*1 passed*",
        ])
        result = testdir.runpytest("-v", p2)
        result.stdout.fnmatch_lines([
            "*test_p2.py::TestMore::test_p1* <- *test_p1.py*PASSED",
        ])

    def test_itemreport_directclasses_not_shown_as_subclasses(self, testdir):
        a = testdir.mkpydir("a123")
        a.join("test_hello123.py").write(_pytest._code.Source("""
            class TestClass:
                def test_method(self):
                    pass
        """))
        result = testdir.runpytest("-v")
        assert result.ret == 0
        result.stdout.fnmatch_lines([
            "*a123/test_hello123.py*PASS*",
        ])
        assert " <- " not in result.stdout.str()

    def test_keyboard_interrupt(self, testdir, option):
        testdir.makepyfile("""
            def test_foobar():
                assert 0
            def test_spamegg():
                import py; pytest.skip('skip me please!')
            def test_interrupt_me():
                raise KeyboardInterrupt   # simulating the user
        """)

        result = testdir.runpytest(*option.args, no_reraise_ctrlc=True)
        result.stdout.fnmatch_lines([
            "    def test_foobar():",
            ">       assert 0",
            "E       assert 0",
            "*_keyboard_interrupt.py:6: KeyboardInterrupt*",
        ])
        if option.fulltrace:
            result.stdout.fnmatch_lines([
                "*raise KeyboardInterrupt   # simulating the user*",
            ])
        else:
            result.stdout.fnmatch_lines([
                "to show a full traceback on KeyboardInterrupt use --fulltrace"
            ])
        result.stdout.fnmatch_lines(['*KeyboardInterrupt*'])

    def test_keyboard_in_sessionstart(self, testdir):
        testdir.makeconftest("""
            def pytest_sessionstart():
                raise KeyboardInterrupt
        """)
        testdir.makepyfile("""
            def test_foobar():
                pass
        """)

        result = testdir.runpytest(no_reraise_ctrlc=True)
        assert result.ret == 2
        result.stdout.fnmatch_lines(['*KeyboardInterrupt*'])


class TestCollectonly:
    def test_collectonly_basic(self, testdir):
        testdir.makepyfile("""
            def test_func():
                pass
        """)
        result = testdir.runpytest("--collect-only",)
        result.stdout.fnmatch_lines([
           "<Module 'test_collectonly_basic.py'>",
           "  <Function 'test_func'>",
        ])

    def test_collectonly_skipped_module(self, testdir):
        testdir.makepyfile("""
            import pytest
            pytest.skip("hello")
        """)
        result = testdir.runpytest("--collect-only", "-rs")
        result.stdout.fnmatch_lines([
            "SKIP*hello*",
            "*1 skip*",
        ])

    def test_collectonly_failed_module(self, testdir):
        testdir.makepyfile("""raise ValueError(0)""")
        result = testdir.runpytest("--collect-only")
        result.stdout.fnmatch_lines([
            "*raise ValueError*",
            "*1 error*",
        ])

    def test_collectonly_fatal(self, testdir):
        testdir.makeconftest("""
            def pytest_collectstart(collector):
                assert 0, "urgs"
        """)
        result = testdir.runpytest("--collect-only")
        result.stdout.fnmatch_lines([
            "*INTERNAL*args*"
        ])
        assert result.ret == 3

    def test_collectonly_simple(self, testdir):
        p = testdir.makepyfile("""
            def test_func1():
                pass
            class TestClass:
                def test_method(self):
                    pass
        """)
        result = testdir.runpytest("--collect-only", p)
        #assert stderr.startswith("inserting into sys.path")
        assert result.ret == 0
        result.stdout.fnmatch_lines([
            "*<Module '*.py'>",
            "* <Function 'test_func1'*>",
            "* <Class 'TestClass'>",
            #"*  <Instance '()'>",
            "*   <Function 'test_method'*>",
        ])

    def test_collectonly_error(self, testdir):
        p = testdir.makepyfile("import Errlkjqweqwe")
        result = testdir.runpytest("--collect-only", p)
        assert result.ret == 1
        result.stdout.fnmatch_lines(_pytest._code.Source("""
            *ERROR*
            *import Errlk*
            *ImportError*
            *1 error*
        """).strip())

    def test_collectonly_missing_path(self, testdir):
        """this checks issue 115,
            failure in parseargs will cause session
            not to have the items attribute
        """
        result = testdir.runpytest("--collect-only", "uhm_missing_path")
        assert result.ret == 4
        result.stderr.fnmatch_lines([
            '*ERROR: file not found*',
        ])

    def test_collectonly_quiet(self, testdir):
        testdir.makepyfile("def test_foo(): pass")
        result = testdir.runpytest("--collect-only", "-q")
        result.stdout.fnmatch_lines([
            '*test_foo*',
        ])

    def test_collectonly_more_quiet(self, testdir):
        testdir.makepyfile(test_fun="def test_foo(): pass")
        result = testdir.runpytest("--collect-only", "-qq")
        result.stdout.fnmatch_lines([
            '*test_fun.py: 1*',
        ])


def test_repr_python_version(monkeypatch):
    try:
        monkeypatch.setattr(sys, 'version_info', (2, 5, 1, 'final', 0))
        assert repr_pythonversion() == "2.5.1-final-0"
        py.std.sys.version_info = x = (2, 3)
        assert repr_pythonversion() == str(x)
    finally:
        monkeypatch.undo() # do this early as pytest can get confused

class TestFixtureReporting:
    def test_setup_fixture_error(self, testdir):
        testdir.makepyfile("""
            def setup_function(function):
                print ("setup func")
                assert 0
            def test_nada():
                pass
        """)
        result = testdir.runpytest()
        result.stdout.fnmatch_lines([
            "*ERROR at setup of test_nada*",
            "*setup_function(function):*",
            "*setup func*",
            "*assert 0*",
            "*1 error*",
        ])
        assert result.ret != 0

    def test_teardown_fixture_error(self, testdir):
        testdir.makepyfile("""
            def test_nada():
                pass
            def teardown_function(function):
                print ("teardown func")
                assert 0
        """)
        result = testdir.runpytest()
        result.stdout.fnmatch_lines([
            "*ERROR at teardown*",
            "*teardown_function(function):*",
            "*assert 0*",
            "*Captured stdout*",
            "*teardown func*",
            "*1 passed*1 error*",
        ])

    def test_teardown_fixture_error_and_test_failure(self, testdir):
        testdir.makepyfile("""
            def test_fail():
                assert 0, "failingfunc"

            def teardown_function(function):
                print ("teardown func")
                assert False
        """)
        result = testdir.runpytest()
        result.stdout.fnmatch_lines([
            "*ERROR at teardown of test_fail*",
            "*teardown_function(function):*",
            "*assert False*",
            "*Captured stdout*",
            "*teardown func*",

            "*test_fail*",
            "*def test_fail():",
            "*failingfunc*",
            "*1 failed*1 error*",
         ])

class TestTerminalFunctional:
    def test_deselected(self, testdir):
        testpath = testdir.makepyfile("""
                def test_one():
                    pass
                def test_two():
                    pass
                def test_three():
                    pass
           """
        )
        result = testdir.runpytest("-k", "test_two:", testpath)
        result.stdout.fnmatch_lines([
            "*test_deselected.py ..",
            "=* 1 test*deselected by*test_two:*=",
        ])
        assert result.ret == 0

    def test_no_skip_summary_if_failure(self, testdir):
        testdir.makepyfile("""
            import pytest
            def test_ok():
                pass
            def test_fail():
                assert 0
            def test_skip():
                pytest.skip("dontshow")
        """)
        result = testdir.runpytest()
        assert result.stdout.str().find("skip test summary") == -1
        assert result.ret == 1

    def test_passes(self, testdir):
        p1 = testdir.makepyfile("""
            def test_passes():
                pass
            class TestClass:
                def test_method(self):
                    pass
        """)
        old = p1.dirpath().chdir()
        try:
            result = testdir.runpytest()
        finally:
            old.chdir()
        result.stdout.fnmatch_lines([
            "test_passes.py ..",
            "* 2 pass*",
        ])
        assert result.ret == 0

    def test_header_trailer_info(self, testdir):
        testdir.makepyfile("""
            def test_passes():
                pass
        """)
        result = testdir.runpytest()
        verinfo = ".".join(map(str, py.std.sys.version_info[:3]))
        result.stdout.fnmatch_lines([
            "*===== test session starts ====*",
            "platform %s -- Python %s*pytest-%s*py-%s*pluggy-%s" % (
                py.std.sys.platform, verinfo,
                pytest.__version__, py.__version__, pluggy.__version__),
            "*test_header_trailer_info.py .",
            "=* 1 passed*in *.[0-9][0-9] seconds *=",
        ])
        if pytest.config.pluginmanager.list_plugin_distinfo():
            result.stdout.fnmatch_lines([
                "plugins: *",
            ])

    def test_showlocals(self, testdir):
        p1 = testdir.makepyfile("""
            def test_showlocals():
                x = 3
                y = "x" * 5000
                assert 0
        """)
        result = testdir.runpytest(p1, '-l')
        result.stdout.fnmatch_lines([
            #"_ _ * Locals *",
            "x* = 3",
            "y* = 'xxxxxx*"
        ])

    def test_verbose_reporting(self, testdir, pytestconfig):
        p1 = testdir.makepyfile("""
            import pytest
            def test_fail():
                raise ValueError()
            def test_pass():
                pass
            class TestClass:
                def test_skip(self):
                    pytest.skip("hello")
            def test_gen():
                def check(x):
                    assert x == 1
                yield check, 0
        """)
        result = testdir.runpytest(p1, '-v')
        result.stdout.fnmatch_lines([
            "*test_verbose_reporting.py::test_fail *FAIL*",
            "*test_verbose_reporting.py::test_pass *PASS*",
            "*test_verbose_reporting.py::TestClass::test_skip *SKIP*",
            "*test_verbose_reporting.py::test_gen*0* *FAIL*",
        ])
        assert result.ret == 1

        if not pytestconfig.pluginmanager.get_plugin("xdist"):
            pytest.skip("xdist plugin not installed")

        result = testdir.runpytest(p1, '-v', '-n 1')
        result.stdout.fnmatch_lines([
            "*FAIL*test_verbose_reporting.py::test_fail*",
        ])
        assert result.ret == 1

    def test_quiet_reporting(self, testdir):
        p1 = testdir.makepyfile("def test_pass(): pass")
        result = testdir.runpytest(p1, '-q')
        s = result.stdout.str()
        assert 'test session starts' not in s
        assert p1.basename not in s
        assert "===" not in s
        assert "passed" in s

    def test_more_quiet_reporting(self, testdir):
        p1 = testdir.makepyfile("def test_pass(): pass")
        result = testdir.runpytest(p1, '-qq')
        s = result.stdout.str()
        assert 'test session starts' not in s
        assert p1.basename not in s
        assert "===" not in s
        assert "passed" not in s


def test_fail_extra_reporting(testdir):
    testdir.makepyfile("def test_this(): assert 0")
    result = testdir.runpytest()
    assert 'short test summary' not in result.stdout.str()
    result = testdir.runpytest('-rf')
    result.stdout.fnmatch_lines([
        "*test summary*",
        "FAIL*test_fail_extra_reporting*",
    ])

def test_fail_reporting_on_pass(testdir):
    testdir.makepyfile("def test_this(): assert 1")
    result = testdir.runpytest('-rf')
    assert 'short test summary' not in result.stdout.str()

def test_pass_extra_reporting(testdir):
    testdir.makepyfile("def test_this(): assert 1")
    result = testdir.runpytest()
    assert 'short test summary' not in result.stdout.str()
    result = testdir.runpytest('-rp')
    result.stdout.fnmatch_lines([
        "*test summary*",
        "PASS*test_pass_extra_reporting*",
    ])

def test_pass_reporting_on_fail(testdir):
    testdir.makepyfile("def test_this(): assert 0")
    result = testdir.runpytest('-rp')
    assert 'short test summary' not in result.stdout.str()

def test_pass_output_reporting(testdir):
    testdir.makepyfile("""
        def test_pass_output():
            print("Four score and seven years ago...")
    """)
    result = testdir.runpytest()
    assert 'Four score and seven years ago...' not in result.stdout.str()
    result = testdir.runpytest('-rP')
    result.stdout.fnmatch_lines([
        "Four score and seven years ago...",
    ])

def test_color_yes(testdir):
    testdir.makepyfile("def test_this(): assert 1")
    result = testdir.runpytest('--color=yes')
    assert 'test session starts' in result.stdout.str()
    assert '\x1b[1m' in result.stdout.str()


def test_color_no(testdir):
    testdir.makepyfile("def test_this(): assert 1")
    result = testdir.runpytest('--color=no')
    assert 'test session starts' in result.stdout.str()
    assert '\x1b[1m' not in result.stdout.str()


@pytest.mark.parametrize('verbose', [True, False])
def test_color_yes_collection_on_non_atty(testdir, verbose):
    """skip collect progress report when working on non-terminals.
    #1397
    """
    testdir.makepyfile("""
        import pytest
        @pytest.mark.parametrize('i', range(10))
        def test_this(i):
            assert 1
    """)
    args = ['--color=yes']
    if verbose:
        args.append('-vv')
    result = testdir.runpytest(*args)
    assert 'test session starts' in result.stdout.str()
    assert '\x1b[1m' in result.stdout.str()
    assert 'collecting 10 items' not in result.stdout.str()
    if verbose:
        assert 'collecting ...' in result.stdout.str()
    assert 'collected 10 items' in result.stdout.str()


def test_getreportopt():
    class config:
        class option:
            reportchars = ""
    config.option.report = "xfailed"
    assert getreportopt(config) == "x"

    config.option.report = "xfailed,skipped"
    assert getreportopt(config) == "xs"

    config.option.report = "skipped,xfailed"
    assert getreportopt(config) == "sx"

    config.option.report = "skipped"
    config.option.reportchars = "sf"
    assert getreportopt(config) == "sf"

    config.option.reportchars = "sfx"
    assert getreportopt(config) == "sfx"

def test_terminalreporter_reportopt_addopts(testdir):
    testdir.makeini("[pytest]\naddopts=-rs")
    testdir.makepyfile("""
        def pytest_funcarg__tr(request):
            tr = request.config.pluginmanager.getplugin("terminalreporter")
            return tr
        def test_opt(tr):
            assert tr.hasopt('skipped')
            assert not tr.hasopt('qwe')
    """)
    result = testdir.runpytest()
    result.stdout.fnmatch_lines([
        "*1 passed*"
    ])

def test_tbstyle_short(testdir):
    p = testdir.makepyfile("""
        def pytest_funcarg__arg(request):
            return 42
        def test_opt(arg):
            x = 0
            assert x
    """)
    result = testdir.runpytest("--tb=short")
    s = result.stdout.str()
    assert 'arg = 42' not in s
    assert 'x = 0' not in s
    result.stdout.fnmatch_lines([
        "*%s:5*" % p.basename,
        "    assert x",
        "E   assert*",
    ])
    result = testdir.runpytest()
    s = result.stdout.str()
    assert 'x = 0' in s
    assert 'assert x' in s

def test_traceconfig(testdir, monkeypatch):
    result = testdir.runpytest("--traceconfig")
    result.stdout.fnmatch_lines([
        "*active plugins*"
    ])
    assert result.ret == EXIT_NOTESTSCOLLECTED


class TestGenericReporting:
    """ this test class can be subclassed with a different option
        provider to run e.g. distributed tests.
    """
    def test_collect_fail(self, testdir, option):
        testdir.makepyfile("import xyz\n")
        result = testdir.runpytest(*option.args)
        result.stdout.fnmatch_lines([
            "?   import xyz",
            "E   ImportError: No module named *xyz*",
            "*1 error*",
        ])

    def test_maxfailures(self, testdir, option):
        testdir.makepyfile("""
            def test_1():
                assert 0
            def test_2():
                assert 0
            def test_3():
                assert 0
        """)
        result = testdir.runpytest("--maxfail=2", *option.args)
        result.stdout.fnmatch_lines([
            "*def test_1():*",
            "*def test_2():*",
            "*!! Interrupted: stopping after 2 failures*!!*",
            "*2 failed*",
        ])


    def test_tb_option(self, testdir, option):
        testdir.makepyfile("""
            import pytest
            def g():
                raise IndexError
            def test_func():
                print (6*7)
                g()  # --calling--
        """)
        for tbopt in ["long", "short", "no"]:
            print('testing --tb=%s...' % tbopt)
            result = testdir.runpytest('--tb=%s' % tbopt)
            s = result.stdout.str()
            if tbopt == "long":
                assert 'print (6*7)' in s
            else:
                assert 'print (6*7)' not in s
            if tbopt != "no":
                assert '--calling--' in s
                assert 'IndexError' in s
            else:
                assert 'FAILURES' not in s
                assert '--calling--' not in s
                assert 'IndexError' not in s

    def test_tb_crashline(self, testdir, option):
        p = testdir.makepyfile("""
            import pytest
            def g():
                raise IndexError
            def test_func1():
                print (6*7)
                g()  # --calling--
            def test_func2():
                assert 0, "hello"
        """)
        result = testdir.runpytest("--tb=line")
        bn = p.basename
        result.stdout.fnmatch_lines([
            "*%s:3: IndexError*" % bn,
            "*%s:8: AssertionError: hello*" % bn,
        ])
        s = result.stdout.str()
        assert "def test_func2" not in s

    def test_pytest_report_header(self, testdir, option):
        testdir.makeconftest("""
            def pytest_sessionstart(session):
                session.config._somevalue = 42
            def pytest_report_header(config):
                return "hello: %s" % config._somevalue
        """)
        testdir.mkdir("a").join("conftest.py").write("""
def pytest_report_header(config, startdir):
    return ["line1", str(startdir)]
""")
        result = testdir.runpytest("a")
        result.stdout.fnmatch_lines([
            "*hello: 42*",
            "line1",
            str(testdir.tmpdir),
        ])

@pytest.mark.xfail("not hasattr(os, 'dup')")
def test_fdopen_kept_alive_issue124(testdir):
    testdir.makepyfile("""
        import os, sys
        k = []
        def test_open_file_and_keep_alive(capfd):
            stdout = os.fdopen(1, 'w', 1)
            k.append(stdout)

        def test_close_kept_alive_file():
            stdout = k.pop()
            stdout.close()
    """)
    result = testdir.runpytest()
    result.stdout.fnmatch_lines([
        "*2 passed*"
    ])

def test_tbstyle_native_setup_error(testdir):
    testdir.makepyfile("""
        import pytest
        @pytest.fixture
        def setup_error_fixture():
            raise Exception("error in exception")

        def test_error_fixture(setup_error_fixture):
            pass
    """)
    result = testdir.runpytest("--tb=native")
    result.stdout.fnmatch_lines([
            '*File *test_tbstyle_native_setup_error.py", line *, in setup_error_fixture*'
            ])

def test_terminal_summary(testdir):
    testdir.makeconftest("""
        def pytest_terminal_summary(terminalreporter):
            w = terminalreporter
            w.section("hello")
            w.line("world")
    """)
    result = testdir.runpytest()
    result.stdout.fnmatch_lines("""
        *==== hello ====*
        world
    """)


def test_terminal_summary_warnings_are_displayed(testdir):
    """Test that warnings emitted during pytest_terminal_summary are displayed.
    (#1305).
    """
    testdir.makeconftest("""
        def pytest_terminal_summary(terminalreporter):
            config = terminalreporter.config
            config.warn('C1', 'internal warning')
    """)
    result = testdir.runpytest('-rw')
    result.stdout.fnmatch_lines([
        '*C1*internal warning',
        '*== 1 pytest-warnings in *',
    ])


@pytest.mark.parametrize("exp_color, exp_line, stats_arg", [
    # The method under test only cares about the length of each
    # dict value, not the actual contents, so tuples of anything
    # suffice

    # Important statuses -- the highest priority of these always wins
    ("red", "1 failed", {"failed": (1,)}),
    ("red", "1 failed, 1 passed", {"failed": (1,), "passed": (1,)}),

    ("red", "1 error", {"error": (1,)}),
    ("red", "1 passed, 1 error", {"error": (1,), "passed": (1,)}),

    # (a status that's not known to the code)
    ("yellow", "1 weird", {"weird": (1,)}),
    ("yellow", "1 passed, 1 weird", {"weird": (1,), "passed": (1,)}),

    ("yellow", "1 pytest-warnings", {"warnings": (1,)}),
    ("yellow", "1 passed, 1 pytest-warnings", {"warnings": (1,),
                                               "passed": (1,)}),

    ("green", "5 passed", {"passed": (1,2,3,4,5)}),


    # "Boring" statuses.  These have no effect on the color of the summary
    # line.  Thus, if *every* test has a boring status, the summary line stays
    # at its default color, i.e. yellow, to warn the user that the test run
    # produced no useful information
    ("yellow", "1 skipped", {"skipped": (1,)}),
    ("green", "1 passed, 1 skipped", {"skipped": (1,), "passed": (1,)}),

    ("yellow", "1 deselected", {"deselected": (1,)}),
    ("green", "1 passed, 1 deselected", {"deselected": (1,), "passed": (1,)}),

    ("yellow", "1 xfailed", {"xfailed": (1,)}),
    ("green", "1 passed, 1 xfailed", {"xfailed": (1,), "passed": (1,)}),

    ("yellow", "1 xpassed", {"xpassed": (1,)}),
    ("green", "1 passed, 1 xpassed", {"xpassed": (1,), "passed": (1,)}),

    # Likewise if no tests were found at all
    ("yellow", "no tests ran", {}),

    # Test the empty-key special case
    ("yellow", "no tests ran", {"": (1,)}),
    ("green", "1 passed", {"": (1,), "passed": (1,)}),


    # A couple more complex combinations
    ("red", "1 failed, 2 passed, 3 xfailed",
        {"passed": (1,2), "failed": (1,), "xfailed": (1,2,3)}),

    ("green", "1 passed, 2 skipped, 3 deselected, 2 xfailed",
        {"passed": (1,),
        "skipped": (1,2),
        "deselected": (1,2,3),
        "xfailed": (1,2)}),
])
def test_summary_stats(exp_line, exp_color, stats_arg):
    print("Based on stats: %s" % stats_arg)
    print("Expect summary: \"%s\"; with color \"%s\"" % (exp_line, exp_color))
    (line, color) = build_summary_stats_line(stats_arg)
    print("Actually got:   \"%s\"; with color \"%s\"" % (line, color))
    assert line == exp_line
    assert color == exp_color