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