| 1 | """
 | 
| 2 | spec_lib.py
 | 
| 3 | 
 | 
| 4 | Shared between sh_spec.py (Python 2) and spec/stateful/harness.py (Python 3)!
 | 
| 5 | """
 | 
| 6 | from __future__ import print_function
 | 
| 7 | 
 | 
| 8 | import os
 | 
| 9 | import re
 | 
| 10 | import sys
 | 
| 11 | 
 | 
| 12 | 
 | 
| 13 | def log(msg, *args):
 | 
| 14 |   # type: (str, *Any) -> None
 | 
| 15 |   if args:
 | 
| 16 |     msg = msg % args
 | 
| 17 |   print(msg, file=sys.stderr)
 | 
| 18 | 
 | 
| 19 | 
 | 
| 20 | # Note that devtools/release.sh spec-all runs with bin/osh and $DIR/_bin/osh,
 | 
| 21 | # which should NOT match
 | 
| 22 | 
 | 
| 23 | OSH_CPP_RE = re.compile(r'_bin/\w+-\w+(-sh)?/osh')  # e.g. $PWD/_bin/cxx-dbg/osh
 | 
| 24 | YSH_CPP_RE = re.compile(r'_bin/\w+-\w+(-sh)?/ysh')  # e.g. $PWD/_bin/cxx-dbg/ysh
 | 
| 25 | OIL_CPP_RE = re.compile(r'_bin/\w+-\w+(-sh)?/oil')
 | 
| 26 | 
 | 
| 27 | # e.g. bash-4.4   bash 5.2.21
 | 
| 28 | BASH_RE = re.compile(r'(bash-\d)[\d.]+$')
 | 
| 29 | 
 | 
| 30 | def MakeShellPairs(shells):
 | 
| 31 |   shell_pairs = []
 | 
| 32 | 
 | 
| 33 |   saw_osh = False
 | 
| 34 |   saw_ysh = False
 | 
| 35 |   saw_oil = False
 | 
| 36 | 
 | 
| 37 |   for path in shells:
 | 
| 38 |     m = BASH_RE.match(path)
 | 
| 39 |     if m:
 | 
| 40 |       label = m.group(1)  # bash-4 or to fit
 | 
| 41 |     else:
 | 
| 42 |       first, _ = os.path.splitext(path)
 | 
| 43 |       label = os.path.basename(first)
 | 
| 44 | 
 | 
| 45 |     if label == 'osh':
 | 
| 46 |       # change the second 'osh' to 'osh_ALT' so it's distinct
 | 
| 47 |       if saw_osh:
 | 
| 48 |         if OSH_CPP_RE.search(path):
 | 
| 49 |           label = 'osh-cpp'
 | 
| 50 |         else:
 | 
| 51 |           label = 'osh_ALT'
 | 
| 52 |       saw_osh = True
 | 
| 53 | 
 | 
| 54 |     elif label == 'ysh':
 | 
| 55 |       if saw_ysh:
 | 
| 56 |         if YSH_CPP_RE.search(path):
 | 
| 57 |           label = 'ysh-cpp'
 | 
| 58 |         else:
 | 
| 59 |           label = 'ysh_ALT'
 | 
| 60 | 
 | 
| 61 |       saw_ysh = True
 | 
| 62 | 
 | 
| 63 |     elif label == 'oil':  # TODO: remove this
 | 
| 64 |       if saw_oil:
 | 
| 65 |         if OIL_CPP_RE.search(path):
 | 
| 66 |           label = 'oil-cpp'
 | 
| 67 |         else:
 | 
| 68 |           label = 'oil_ALT'
 | 
| 69 | 
 | 
| 70 |       saw_oil = True
 | 
| 71 | 
 | 
| 72 |     shell_pairs.append((label, path))
 | 
| 73 |   return shell_pairs
 | 
| 74 | 
 | 
| 75 | 
 | 
| 76 | RANGE_RE = re.compile('(\d+) \s* - \s* (\d+)', re.VERBOSE)
 | 
| 77 | 
 | 
| 78 | 
 | 
| 79 | def ParseRange(range_str):
 | 
| 80 |   try:
 | 
| 81 |     d = int(range_str)
 | 
| 82 |     return d, d  # singleton range
 | 
| 83 |   except ValueError:
 | 
| 84 |     m = RANGE_RE.match(range_str)
 | 
| 85 |     if not m:
 | 
| 86 |       raise RuntimeError('Invalid range %r' % range_str)
 | 
| 87 |     b, e = m.groups()
 | 
| 88 |     return int(b), int(e)
 | 
| 89 | 
 | 
| 90 | 
 | 
| 91 | class RangePredicate(object):
 | 
| 92 |   """Zero-based indexing, inclusive ranges."""
 | 
| 93 | 
 | 
| 94 |   def __init__(self, begin, end):
 | 
| 95 |     self.begin = begin
 | 
| 96 |     self.end = end
 | 
| 97 | 
 | 
| 98 |   def __call__(self, i, case):
 | 
| 99 |     return self.begin <= i <= self.end
 | 
| 100 | 
 | 
| 101 | 
 | 
| 102 | class RegexPredicate(object):
 | 
| 103 |   """Filter by name."""
 | 
| 104 | 
 | 
| 105 |   def __init__(self, desc_re):
 | 
| 106 |     self.desc_re = desc_re
 | 
| 107 | 
 | 
| 108 |   def __call__(self, i, case):
 | 
| 109 |     return bool(self.desc_re.search(case['desc']))
 | 
| 110 | 
 | 
| 111 | 
 | 
| 112 | 
 | 
| 113 | def DefineCommon(p):
 | 
| 114 |   """Flags shared between sh_spec.py and stateful/harness.py."""
 | 
| 115 |   p.add_option(
 | 
| 116 |       '-v', '--verbose', dest='verbose', action='store_true', default=False,
 | 
| 117 |       help='Show details about test failures')
 | 
| 118 |   p.add_option(
 | 
| 119 |       '-r', '--range', dest='range', default=None,
 | 
| 120 |       help='Execute only a given test range, e.g. 5-10, 5-, -10, or 5')
 | 
| 121 |   p.add_option(
 | 
| 122 |       '--regex', dest='regex', default=None,
 | 
| 123 |       help='Execute only tests whose description matches a given regex '
 | 
| 124 |            '(case-insensitive)')
 | 
| 125 |   p.add_option(
 | 
| 126 |       '--list', dest='do_list', action='store_true', default=None,
 | 
| 127 |       help='Just list tests')
 | 
| 128 |   p.add_option(
 | 
| 129 |       '--oils-failures-allowed', dest='oils_failures_allowed', type='int',
 | 
| 130 |       default=0, help="Allow this number of Oils failures")
 | 
| 131 | 
 | 
| 132 |   # Select what shells to run
 | 
| 133 |   p.add_option(
 | 
| 134 |       '--oils-bin-dir', dest='oils_bin_dir', default=None,
 | 
| 135 |       help="Directory that osh and ysh live in")
 | 
| 136 |   p.add_option(
 | 
| 137 |       '--oils-cpp-bin-dir', dest='oils_cpp_bin_dir', default=None,
 | 
| 138 |       help="Directory that native C++ osh and ysh live in")
 | 
| 139 |   p.add_option(
 | 
| 140 |       '--ovm-bin-dir', dest='ovm_bin_dir', default=None,
 | 
| 141 |       help="Directory of the legacy OVM/CPython build")
 | 
| 142 |   p.add_option(
 | 
| 143 |       '--compare-shells', dest='compare_shells', action='store_true',
 | 
| 144 |       help="Compare against shells specified at the top of each file")
 | 
| 145 | 
 | 
| 146 | 
 | 
| 147 | def DefineStateful(p):
 | 
| 148 |   p.add_option(
 | 
| 149 |       '--num-retries', dest='num_retries', 
 | 
| 150 |       type='int', default=4, 
 | 
| 151 |       help='Number of retries (for spec/stateful only)')
 | 
| 152 |   p.add_option(
 | 
| 153 |       '--pexpect-timeout', dest='pexpect_timeout', 
 | 
| 154 |       type='float', default=1.0, 
 | 
| 155 |       help='In seconds')
 | 
| 156 |   p.add_option(
 | 
| 157 |       '--results-file', dest='results_file', default=None,
 | 
| 158 |       help='Write table of results to this file.  Default is stdout.')
 | 
| 159 | 
 | 
| 160 | 
 | 
| 161 | def DefineShSpec(p):
 | 
| 162 |   p.add_option(
 | 
| 163 |       '-d', '--details', dest='details', action='store_true', default=False,
 | 
| 164 |       help='Show details even for successful cases (requires -v)')
 | 
| 165 |   p.add_option(
 | 
| 166 |       '-t', '--trace', dest='trace', action='store_true', default=False,
 | 
| 167 |       help='trace execution of shells to diagnose hangs')
 | 
| 168 | 
 | 
| 169 |   # Execution modes
 | 
| 170 |   p.add_option(
 | 
| 171 |       '-p', '--print', dest='do_print', action='store_true', default=None,
 | 
| 172 |       help="Print test code, but don't run it")
 | 
| 173 |   p.add_option(
 | 
| 174 |       '--print-spec-suite', dest='print_spec_suite', action='store_true', default=None,
 | 
| 175 |       help="Print suite this file belongs to")
 | 
| 176 |   p.add_option(
 | 
| 177 |       '--print-table', dest='print_table', action='store_true', default=None,
 | 
| 178 |       help="Print table of test files")
 | 
| 179 |   p.add_option(
 | 
| 180 |       '--print-tagged', dest='print_tagged',
 | 
| 181 |       help="Print spec files tagged with a certain string")
 | 
| 182 | 
 | 
| 183 |   # Output control
 | 
| 184 |   p.add_option(
 | 
| 185 |       '--format', dest='format', choices=['ansi', 'html'],
 | 
| 186 |       default='ansi', help="Output format (default 'ansi')")
 | 
| 187 |   p.add_option(
 | 
| 188 |       '--stats-file', dest='stats_file', default=None,
 | 
| 189 |       help="File to write stats to")
 | 
| 190 |   p.add_option(
 | 
| 191 |       '--tsv-output', dest='tsv_output', default=None,
 | 
| 192 |       help="Write a TSV log to this file.  Subsumes --stats-file.")
 | 
| 193 |   p.add_option(
 | 
| 194 |       '--stats-template', dest='stats_template', default='',
 | 
| 195 |       help="Python format string for stats")
 | 
| 196 | 
 | 
| 197 |   p.add_option(
 | 
| 198 |       '--path-env', dest='path_env', default='',
 | 
| 199 |       help="The full PATH, for finding binaries used in tests.")
 | 
| 200 |   p.add_option(
 | 
| 201 |       '--tmp-env', dest='tmp_env', default='',
 | 
| 202 |       help="A temporary directory that the tests can use.")
 | 
| 203 | 
 | 
| 204 |   # Notes:
 | 
| 205 |   # - utf-8 is the Ubuntu default
 | 
| 206 |   # - this flag has limited usefulness.  It may be better to simply export LANG=
 | 
| 207 |   #   in this test case itself.
 | 
| 208 |   if 0:
 | 
| 209 |       p.add_option(
 | 
| 210 |           '--lang-env', dest='lang_env', default='en_US.UTF-8',
 | 
| 211 |           help="The LANG= setting, which affects various libc functions.")
 | 
| 212 |   p.add_option(
 | 
| 213 |       '--env-pair', dest='env_pair', default=[], action='append',
 | 
| 214 |       help='A key=value pair to add to the environment')
 | 
| 215 | 
 | 
| 216 |   p.add_option(
 | 
| 217 |       '--timeout', dest='timeout', default='',
 | 
| 218 |       help="Prefix shell invocation with 'timeout N'")
 | 
| 219 |   p.add_option(
 | 
| 220 |       '--timeout-bin', dest='timeout_bin', default=None,
 | 
| 221 |       help="Use the smoosh timeout binary at this location.")
 | 
| 222 | 
 | 
| 223 |   p.add_option(
 | 
| 224 |       '--posix', dest='posix', default=False, action='store_true',
 | 
| 225 |       help='Pass -o posix to the shell (when applicable)')
 | 
| 226 | 
 | 
| 227 |   p.add_option(
 | 
| 228 |       '--sh-env-var-name', dest='sh_env_var_name', default='SH',
 | 
| 229 |       help="Set this environment variable to the path of the shell")
 | 
| 230 | 
 | 
| 231 |   p.add_option(
 | 
| 232 |       '--pyann-out-dir', dest='pyann_out_dir', default=None,
 | 
| 233 |       help='Run OSH with PYANN_OUT=$dir/$case_num.json')
 |