1"""The command line interface implementation"""
2
3import os
4import sys
5
6from cram._encoding import b, bytestype, stdoutb
7from cram._process import execute
8
9__all__ = ['runcli']
10
11def _prompt(question, answers, auto=None):
12    """Write a prompt to stdout and ask for answer in stdin.
13
14    answers should be a string, with each character a single
15    answer. An uppercase letter is considered the default answer.
16
17    If an invalid answer is given, this asks again until it gets a
18    valid one.
19
20    If auto is set, the question is answered automatically with the
21    specified value.
22    """
23    default = [c for c in answers if c.isupper()]
24    while True:
25        sys.stdout.write('%s [%s] ' % (question, answers))
26        sys.stdout.flush()
27        if auto is not None:
28            sys.stdout.write(auto + '\n')
29            sys.stdout.flush()
30            return auto
31
32        answer = sys.stdin.readline().strip().lower()
33        if not answer and default:
34            return default[0]
35        elif answer and answer in answers.lower():
36            return answer
37
38def _log(msg=None, verbosemsg=None, verbose=False):
39    """Write msg to standard out and flush.
40
41    If verbose is True, write verbosemsg instead.
42    """
43    if verbose:
44        msg = verbosemsg
45    if msg:
46        if isinstance(msg, bytestype):
47            stdoutb.write(msg)
48        else: # pragma: nocover
49            sys.stdout.write(msg)
50        sys.stdout.flush()
51
52def _patch(cmd, diff):
53    """Run echo [lines from diff] | cmd -p0"""
54    out, retcode = execute([cmd, '-p0'], stdin=b('').join(diff))
55    return retcode == 0
56
57def runcli(tests, quiet=False, verbose=False, patchcmd=None, answer=None,
58           noerrfiles=False):
59    """Run tests with command line interface input/output.
60
61    tests should be a sequence of 2-tuples containing the following:
62
63        (test path, test function)
64
65    This function yields a new sequence where each test function is wrapped
66    with a function that handles CLI input/output.
67
68    If quiet is True, diffs aren't printed. If verbose is True,
69    filenames and status information are printed.
70
71    If patchcmd is set, a prompt is written to stdout asking if
72    changed output should be merged back into the original test. The
73    answer is read from stdin. If 'y', the test is patched using patch
74    based on the changed output.
75    """
76    total, skipped, failed = [0], [0], [0]
77
78    for path, test in tests:
79        def testwrapper():
80            """Test function that adds CLI output"""
81            total[0] += 1
82            _log(None, path + b(': '), verbose)
83
84            refout, postout, diff = test()
85            if refout is None:
86                skipped[0] += 1
87                _log('s', 'empty\n', verbose)
88                return refout, postout, diff
89
90            abspath = os.path.abspath(path)
91            errpath = abspath + b('.err')
92
93            if postout is None:
94                skipped[0] += 1
95                _log('s', 'skipped\n', verbose)
96            elif not diff:
97                _log('.', 'passed\n', verbose)
98                if os.path.exists(errpath):
99                    os.remove(errpath)
100            else:
101                failed[0] += 1
102                _log('!', 'failed\n', verbose)
103                if not quiet:
104                    _log('\n', None, verbose)
105
106                if not noerrfiles:
107                    errfile = open(errpath, 'wb')
108                    try:
109                        for line in postout:
110                            errfile.write(line)
111                    finally:
112                        errfile.close()
113
114                if not quiet:
115                    origdiff = diff
116                    diff = []
117                    for line in origdiff:
118                        stdoutb.write(line)
119                        diff.append(line)
120
121                    if (patchcmd and
122                        _prompt('Accept this change?', 'yN', answer) == 'y'):
123                        if _patch(patchcmd, diff):
124                            _log(None, path + b(': merged output\n'), verbose)
125                            if not noerrfiles:
126                                os.remove(errpath)
127                        else:
128                            _log(path + b(': merge failed\n'))
129
130            return refout, postout, diff
131
132        yield (path, testwrapper)
133
134    if total[0] > 0:
135        _log('\n', None, verbose)
136        _log('# Ran %s tests, %s skipped, %s failed.\n'
137             % (total[0], skipped[0], failed[0]))
138