| 1 | #!/usr/bin/env python2
 | 
| 2 | # Copyright 2016 Andy Chu. All rights reserved.
 | 
| 3 | # Licensed under the Apache License, Version 2.0 (the "License");
 | 
| 4 | # you may not use this file except in compliance with the License.
 | 
| 5 | # You may obtain a copy of the License at
 | 
| 6 | #
 | 
| 7 | #   http://www.apache.org/licenses/LICENSE-2.0
 | 
| 8 | """completion_test.py: Tests for completion.py."""
 | 
| 9 | from __future__ import print_function
 | 
| 10 | 
 | 
| 11 | import os
 | 
| 12 | import unittest
 | 
| 13 | import sys
 | 
| 14 | 
 | 
| 15 | from _devbuild.gen.option_asdl import option_i
 | 
| 16 | from _devbuild.gen.runtime_asdl import comp_action_e
 | 
| 17 | from _devbuild.gen.syntax_asdl import proc_sig
 | 
| 18 | from _devbuild.gen.value_asdl import (value, value_e)
 | 
| 19 | from core import completion  # module under test
 | 
| 20 | from core import comp_ui
 | 
| 21 | from core import state
 | 
| 22 | from core import test_lib
 | 
| 23 | from core import util
 | 
| 24 | from mycpp.mylib import log
 | 
| 25 | from frontend import flag_def  # side effect: flags are defined!
 | 
| 26 | 
 | 
| 27 | _ = flag_def
 | 
| 28 | from frontend import parse_lib
 | 
| 29 | from testdata.completion import bash_oracle
 | 
| 30 | 
 | 
| 31 | # guard some tests that fail on Darwin
 | 
| 32 | IS_DARWIN = sys.platform == 'darwin'
 | 
| 33 | 
 | 
| 34 | A1 = completion.TestAction(['foo.py', 'foo', 'bar.py'])
 | 
| 35 | U1 = completion.UserSpec([A1], [], [], completion.DefaultPredicate(), '', '')
 | 
| 36 | 
 | 
| 37 | BASE_OPTS = {}
 | 
| 38 | 
 | 
| 39 | FIRST = completion.TestAction(['grep', 'sed', 'test'])
 | 
| 40 | U2 = completion.UserSpec([FIRST], [], [], completion.DefaultPredicate(), '',
 | 
| 41 |                          '')
 | 
| 42 | 
 | 
| 43 | OPT_ARRAY = [False] * option_i.ARRAY_SIZE
 | 
| 44 | 
 | 
| 45 | 
 | 
| 46 | def MockApi(line):
 | 
| 47 |     """Match readline's get_begidx() / get_endidx()."""
 | 
| 48 |     return completion.Api(line=line, begin=0, end=len(line))
 | 
| 49 | 
 | 
| 50 | 
 | 
| 51 | def _MakeRootCompleter(parse_ctx=None, comp_lookup=None):
 | 
| 52 |     compopt_state = completion.OptionState()
 | 
| 53 |     comp_ui_state = comp_ui.State()
 | 
| 54 |     comp_lookup = comp_lookup or completion.Lookup()
 | 
| 55 | 
 | 
| 56 |     mem = state.Mem('', [], None, [])
 | 
| 57 |     parse_opts, exec_opts, mutable_opts = state.MakeOpts(mem, None)
 | 
| 58 |     mem.exec_opts = exec_opts
 | 
| 59 | 
 | 
| 60 |     state.InitMem(mem, {}, '0.1')
 | 
| 61 |     mutable_opts.Init()
 | 
| 62 | 
 | 
| 63 |     if not parse_ctx:
 | 
| 64 |         parse_ctx = test_lib.InitParseContext(parse_opts=parse_opts,
 | 
| 65 |                                               do_lossless=True)
 | 
| 66 |         parse_ctx.Init_Trail(parse_lib.Trail())
 | 
| 67 | 
 | 
| 68 |     if 1:  # enable for details
 | 
| 69 |         debug_f = util.DebugFile(sys.stdout)
 | 
| 70 |     else:
 | 
| 71 |         debug_f = util.NullDebugFile()
 | 
| 72 | 
 | 
| 73 |     ev = test_lib.InitWordEvaluator(exec_opts=exec_opts)
 | 
| 74 |     return completion.RootCompleter(ev, mem, comp_lookup, compopt_state,
 | 
| 75 |                                     comp_ui_state, parse_ctx, debug_f)
 | 
| 76 | 
 | 
| 77 | 
 | 
| 78 | class FunctionsTest(unittest.TestCase):
 | 
| 79 | 
 | 
| 80 |     def testAdjustArg(self):
 | 
| 81 |         AdjustArg = completion.AdjustArg
 | 
| 82 |         out = []
 | 
| 83 |         AdjustArg(':foo:=bar:', [':', '='], out)
 | 
| 84 |         self.assertEqual(out, [':', 'foo', ':=', 'bar', ':'])
 | 
| 85 | 
 | 
| 86 |         out = []
 | 
| 87 |         AdjustArg('==::==', [':', '='], out)
 | 
| 88 |         self.assertEqual(out, ['==::=='])
 | 
| 89 | 
 | 
| 90 |         out = []
 | 
| 91 |         AdjustArg('==::==', [':'], out)
 | 
| 92 |         self.assertEqual(out, ['==', '::', '=='])
 | 
| 93 | 
 | 
| 94 |         # This is like if you get [""] somehow, it should be [""].
 | 
| 95 |         out = []
 | 
| 96 |         AdjustArg('', [':', '='], out)
 | 
| 97 |         self.assertEqual(out, [''])
 | 
| 98 | 
 | 
| 99 | 
 | 
| 100 | class CompletionTest(unittest.TestCase):
 | 
| 101 | 
 | 
| 102 |     def _CompApi(self, partial_argv, index, to_complete):
 | 
| 103 |         comp = completion.Api('', 0, 0)
 | 
| 104 |         comp.Update('', to_complete, '', index, partial_argv)
 | 
| 105 |         return comp
 | 
| 106 | 
 | 
| 107 |     def testLookup(self):
 | 
| 108 |         c = completion.Lookup()
 | 
| 109 |         c.RegisterName('grep', BASE_OPTS, U1)
 | 
| 110 | 
 | 
| 111 |         _, user_spec = c.GetSpecForName('grep')
 | 
| 112 |         self.assertEqual(1, len(user_spec.actions))
 | 
| 113 | 
 | 
| 114 |         _, user_spec = c.GetSpecForName('/usr/bin/grep')
 | 
| 115 |         self.assertEqual(1, len(user_spec.actions))
 | 
| 116 | 
 | 
| 117 |         c.RegisterGlob('*.py', BASE_OPTS, U1)
 | 
| 118 |         base_opts, comp = c.GetSpecForName('/usr/bin/foo.py')
 | 
| 119 |         print('py', comp)
 | 
| 120 |         # NOTE: This is an implementation detail
 | 
| 121 |         self.assertEqual(1, len(comp.actions))
 | 
| 122 | 
 | 
| 123 |         comp_rb = c.GetSpecForName('foo.rb')
 | 
| 124 |         print('rb', comp_rb)
 | 
| 125 | 
 | 
| 126 |     def testExternalCommandAction(self):
 | 
| 127 |         mem = state.Mem('dummy', [], None, [])
 | 
| 128 |         parse_opts, exec_opts, mutable_opts = state.MakeOpts(mem, None)
 | 
| 129 |         mem.exec_opts = exec_opts
 | 
| 130 | 
 | 
| 131 |         a = completion.ExternalCommandAction(mem)
 | 
| 132 |         comp = self._CompApi([], 0, 'f')
 | 
| 133 |         print(list(a.Matches(comp)))
 | 
| 134 | 
 | 
| 135 |         # TODO: This should set up the file system and $PATH and assert that only
 | 
| 136 |         # executable files are accessed!
 | 
| 137 | 
 | 
| 138 |     def testFileSystemAction(self):
 | 
| 139 |         CASES = [
 | 
| 140 |             # Dirs and files
 | 
| 141 |             ('m', ['metrics', 'mycpp']),
 | 
| 142 |             ('opy/doc', ['opy/doc']),
 | 
| 143 |         ]
 | 
| 144 | 
 | 
| 145 |         a = completion.FileSystemAction(False, False, False)
 | 
| 146 |         for prefix, expected in CASES:
 | 
| 147 |             log('')
 | 
| 148 |             log('-- PREFIX %r', prefix)
 | 
| 149 |             comp = self._CompApi([], 0, prefix)
 | 
| 150 |             self.assertEqual(expected, sorted(a.Matches(comp)))
 | 
| 151 | 
 | 
| 152 |         os.system('mkdir -p /tmp/oil_comp_test')
 | 
| 153 |         os.system('bash -c "touch /tmp/oil_comp_test/{one,two,three}"')
 | 
| 154 | 
 | 
| 155 |         # This test depends on actual file system content.  But we choose things
 | 
| 156 |         # that shouldn't go away.
 | 
| 157 |         ADD_SLASH_CASES = [
 | 
| 158 |             # Dirs and files
 | 
| 159 |             ('o', ['oil-version.txt', 'opy/', 'osh/']),
 | 
| 160 |             ('nonexistent/', []),
 | 
| 161 |             ('README.', ['README.md']),
 | 
| 162 |             # Directory should be completed to core/ ?
 | 
| 163 |             ('core', ['core/']),
 | 
| 164 |             ('asdl/R', ['asdl/README.md']),
 | 
| 165 |             ('opy/doc', ['opy/doc/']),
 | 
| 166 |             ('opy/doc/', ['opy/doc/opcodes.md']),
 | 
| 167 |             ('/bi', ['/bin/']),
 | 
| 168 |             ('/tmp/oil_comp_test/', [
 | 
| 169 |                 '/tmp/oil_comp_test/one',
 | 
| 170 |                 '/tmp/oil_comp_test/three',
 | 
| 171 |                 '/tmp/oil_comp_test/two',
 | 
| 172 |             ]),
 | 
| 173 |             ('./o', ['./oil-version.txt', './opy/', './osh/']),
 | 
| 174 |         ]
 | 
| 175 | 
 | 
| 176 |         a = completion.FileSystemAction(False, False, True)
 | 
| 177 |         for prefix, expected in ADD_SLASH_CASES:
 | 
| 178 |             log('')
 | 
| 179 |             log('-- PREFIX %s', prefix)
 | 
| 180 |             comp = self._CompApi([], 0, prefix)
 | 
| 181 |             self.assertEqual(expected, sorted(a.Matches(comp)))
 | 
| 182 | 
 | 
| 183 |         # A bunch of repos in oilshell
 | 
| 184 |         comp = completion.Api('', 0, 0)
 | 
| 185 |         comp.Update('', '../o', '', 0, None)
 | 
| 186 |         print(list(a.Matches(comp)))
 | 
| 187 | 
 | 
| 188 |         EXEC_ONLY_CASES = [('i', ['install'])]
 | 
| 189 | 
 | 
| 190 |         a = completion.FileSystemAction(False, True, False)
 | 
| 191 |         for prefix, expected in EXEC_ONLY_CASES:
 | 
| 192 |             log('')
 | 
| 193 |             log('-- PREFIX %s', prefix)
 | 
| 194 |             comp = self._CompApi([], 0, prefix)
 | 
| 195 |             self.assertEqual(expected, sorted(a.Matches(comp)))
 | 
| 196 | 
 | 
| 197 |     def testShellFuncExecution(self):
 | 
| 198 |         arena = test_lib.MakeArena('testShellFuncExecution')
 | 
| 199 |         c_parser = test_lib.InitCommandParser("""\
 | 
| 200 |     f() {
 | 
| 201 |       COMPREPLY=(f1 f2)
 | 
| 202 |     }
 | 
| 203 |     """,
 | 
| 204 |                                               arena=arena)
 | 
| 205 |         node = c_parser.ParseLogicalLine()
 | 
| 206 |         proc = value.Proc(node.name, node.name_tok, proc_sig.Open, node.body,
 | 
| 207 |                           [], True)
 | 
| 208 | 
 | 
| 209 |         cmd_ev = test_lib.InitCommandEvaluator(arena=arena)
 | 
| 210 | 
 | 
| 211 |         comp_lookup = completion.Lookup()
 | 
| 212 |         a = completion.ShellFuncAction(cmd_ev, proc, comp_lookup)
 | 
| 213 |         comp = self._CompApi(['f'], 0, 'f')
 | 
| 214 |         matches = list(a.Matches(comp))
 | 
| 215 |         self.assertEqual(['f1', 'f2'], matches)
 | 
| 216 | 
 | 
| 217 |     def testUserSpec(self):
 | 
| 218 |         comp = self._CompApi(['f'], 0, 'f')
 | 
| 219 |         matches = list(U1.AllMatches(comp))
 | 
| 220 |         self.assertEqual([('foo.py', comp_action_e.Other),
 | 
| 221 |                           ('foo', comp_action_e.Other)], matches)
 | 
| 222 | 
 | 
| 223 |         predicate = completion.GlobPredicate(False, '*.py')
 | 
| 224 |         c2 = completion.UserSpec([A1], [], [], predicate, '', '')
 | 
| 225 |         comp = self._CompApi(['f'], 0, 'f')
 | 
| 226 |         matches = list(c2.AllMatches(comp))
 | 
| 227 |         self.assertEqual([('foo.py', comp_action_e.Other)], matches)
 | 
| 228 | 
 | 
| 229 | 
 | 
| 230 | class RootCompleterTest(unittest.TestCase):
 | 
| 231 | 
 | 
| 232 |     def testCompletesWords(self):
 | 
| 233 |         comp_lookup = completion.Lookup()
 | 
| 234 | 
 | 
| 235 |         comp_lookup.RegisterName('grep', BASE_OPTS, U1)
 | 
| 236 |         comp_lookup.RegisterName('__first', BASE_OPTS, U2)
 | 
| 237 |         r = _MakeRootCompleter(comp_lookup=comp_lookup)
 | 
| 238 | 
 | 
| 239 |         comp = MockApi('grep f')
 | 
| 240 |         m = list(r.Matches(comp))
 | 
| 241 |         self.assertEqual(['grep foo.py ', 'grep foo '], m)
 | 
| 242 | 
 | 
| 243 |         comp = MockApi('grep g')
 | 
| 244 |         m = list(r.Matches(comp))
 | 
| 245 |         self.assertEqual([], m)
 | 
| 246 | 
 | 
| 247 |         # Complete first word
 | 
| 248 |         m = list(r.Matches(MockApi('g')))
 | 
| 249 |         self.assertEqual(['grep '], m)
 | 
| 250 | 
 | 
| 251 |         # Empty completer
 | 
| 252 |         m = list(r.Matches(MockApi('')))
 | 
| 253 |         self.assertEqual(['grep ', 'sed ', 'test '], m)
 | 
| 254 | 
 | 
| 255 |         # Test compound commands. These PARSE
 | 
| 256 |         m = list(r.Matches(MockApi('echo hi || grep f')))
 | 
| 257 |         m = list(r.Matches(MockApi('echo hi; grep f')))
 | 
| 258 | 
 | 
| 259 |         # Brace -- does NOT parse
 | 
| 260 |         m = list(r.Matches(MockApi('{ echo hi; grep f')))
 | 
| 261 |         # TODO: Test if/for/while/case/etc.
 | 
| 262 | 
 | 
| 263 |         m = list(r.Matches(MockApi('var=$v')))
 | 
| 264 |         m = list(r.Matches(MockApi('local var=$v')))
 | 
| 265 | 
 | 
| 266 |     def testCompletesHomeDirs(self):
 | 
| 267 |         r = _MakeRootCompleter()
 | 
| 268 | 
 | 
| 269 |         comp = MockApi(line='echo ~r')
 | 
| 270 |         print(comp)
 | 
| 271 |         m = list(r.Matches(comp))
 | 
| 272 | 
 | 
| 273 |         # This test isn't hermetic, but I think root should be on all systems.
 | 
| 274 |         self.assert_('echo ~root/' in m, 'Got %s' % m)
 | 
| 275 | 
 | 
| 276 |         comp = MockApi(line='echo ~')
 | 
| 277 |         print(comp)
 | 
| 278 |         m = list(r.Matches(comp))
 | 
| 279 |         self.assert_('echo ~root/' in m, 'Got %s' % m)
 | 
| 280 | 
 | 
| 281 |         # Don't be overly aggressive!
 | 
| 282 |         comp = MockApi(line='echo a~')
 | 
| 283 |         m = list(r.Matches(comp))
 | 
| 284 |         self.assertEqual(0, len(m))
 | 
| 285 | 
 | 
| 286 |     def testCompletesVarNames(self):
 | 
| 287 |         r = _MakeRootCompleter()
 | 
| 288 | 
 | 
| 289 |         # Complete ALL variables
 | 
| 290 |         comp = MockApi('echo $')
 | 
| 291 |         self.assertEqual(0, comp.begin)  # what readline does
 | 
| 292 |         self.assertEqual(6, comp.end)
 | 
| 293 |         print(comp)
 | 
| 294 |         m = list(r.Matches(comp))
 | 
| 295 | 
 | 
| 296 |         # Just test for a subset
 | 
| 297 |         self.assert_('echo $HOSTNAME' in m, m)
 | 
| 298 |         self.assert_('echo $IFS' in m, m)
 | 
| 299 | 
 | 
| 300 |         # Now it has a prefix
 | 
| 301 |         comp = MockApi(line='echo $P')
 | 
| 302 |         self.assertEqual(0, comp.begin)  # what readline does
 | 
| 303 |         self.assertEqual(7, comp.end)
 | 
| 304 |         print(comp)
 | 
| 305 |         m = list(r.Matches(comp))
 | 
| 306 |         self.assert_('echo $PWD' in m, 'Got %s' % m)
 | 
| 307 |         self.assert_('echo $PS4' in m, 'Got %s' % m)
 | 
| 308 | 
 | 
| 309 |         #
 | 
| 310 |         # BracedVarSub
 | 
| 311 |         #
 | 
| 312 | 
 | 
| 313 |         # Complete ALL variables
 | 
| 314 |         comp = MockApi(line='echo _${')
 | 
| 315 |         print(comp)
 | 
| 316 |         m = list(r.Matches(comp))
 | 
| 317 |         # Just test for a subset
 | 
| 318 |         self.assert_('echo _${HOSTNAME' in m, 'Got %s' % m)
 | 
| 319 |         self.assert_('echo _${IFS' in m, 'Got %s' % m)
 | 
| 320 | 
 | 
| 321 |         # Now it has a prefix
 | 
| 322 |         comp = MockApi(line='echo ${P')
 | 
| 323 |         print(comp)
 | 
| 324 |         m = list(r.Matches(comp))
 | 
| 325 |         self.assert_('echo ${PWD' in m, 'Got %s' % m)
 | 
| 326 |         self.assert_('echo ${PS4' in m, 'Got %s' % m)
 | 
| 327 | 
 | 
| 328 |         # Odd word break
 | 
| 329 |         # NOTE: We use VSub_Name both for $FOO and ${FOO.  Might be bad?
 | 
| 330 |         comp = MockApi(line='echo ${undef:-$P')
 | 
| 331 |         print(comp)
 | 
| 332 |         m = list(r.Matches(comp))
 | 
| 333 |         self.assert_('echo ${undef:-$PWD' in m, 'Got %s' % m)
 | 
| 334 |         self.assert_('echo ${undef:-$PS4' in m, 'Got %s' % m)
 | 
| 335 | 
 | 
| 336 |         comp = MockApi(line='echo ${undef:-$')
 | 
| 337 |         print(comp)
 | 
| 338 |         m = list(r.Matches(comp))
 | 
| 339 |         self.assert_('echo ${undef:-$HOSTNAME' in m, 'Got %s' % m)
 | 
| 340 |         self.assert_('echo ${undef:-$IFS' in m, 'Got %s' % m)
 | 
| 341 | 
 | 
| 342 |         #
 | 
| 343 |         # Double Quoted
 | 
| 344 |         #
 | 
| 345 |         # NOTE: GNU readline seems to complete closing quotes?  We don't want that.
 | 
| 346 | 
 | 
| 347 |         comp = MockApi(line='echo "$')
 | 
| 348 |         print(comp)
 | 
| 349 |         m = list(r.Matches(comp))
 | 
| 350 |         self.assert_('echo "$HOSTNAME' in m, 'Got %s' % m)
 | 
| 351 |         self.assert_('echo "$IFS' in m, 'Got %s' % m)
 | 
| 352 | 
 | 
| 353 |         comp = MockApi(line='echo "$P')
 | 
| 354 |         print(comp)
 | 
| 355 |         m = list(r.Matches(comp))
 | 
| 356 |         self.assert_('echo "$PWD' in m, 'Got %s' % m)
 | 
| 357 |         self.assert_('echo "$PS4' in m, 'Got %s' % m)
 | 
| 358 | 
 | 
| 359 |         #
 | 
| 360 |         # Prefix operator
 | 
| 361 |         #
 | 
| 362 | 
 | 
| 363 |         if 0:  # Here you need to handle VSub_Pound
 | 
| 364 |             comp = MockApi(line='echo ${#')
 | 
| 365 |             print(comp)
 | 
| 366 |             m = list(r.Matches(comp))
 | 
| 367 |             self.assert_('${#HOSTNAME' in m, 'Got %s' % m)
 | 
| 368 |             self.assert_('${#IFS' in m, 'Got %s' % m)
 | 
| 369 | 
 | 
| 370 |         comp = MockApi(line='echo "${#P')
 | 
| 371 |         print(comp)
 | 
| 372 |         m = list(r.Matches(comp))
 | 
| 373 |         self.assert_('echo "${#PWD' in m, 'Got %s' % m)
 | 
| 374 |         self.assert_('echo "${#PS4' in m, 'Got %s' % m)
 | 
| 375 | 
 | 
| 376 |         #
 | 
| 377 |         # Arithmetic Context
 | 
| 378 |         #
 | 
| 379 | 
 | 
| 380 |         comp = MockApi(line='echo "$((PWD +P')  # bare word
 | 
| 381 |         print(comp)
 | 
| 382 |         m = list(r.Matches(comp))
 | 
| 383 |         self.assert_('echo "$((PWD +PWD' in m, 'Got %s' % m)
 | 
| 384 |         self.assert_('echo "$((PWD +PS4' in m, 'Got %s' % m)
 | 
| 385 | 
 | 
| 386 |         comp = MockApi(line='echo "$(( $P')
 | 
| 387 |         print(comp)
 | 
| 388 |         m = list(r.Matches(comp))
 | 
| 389 |         self.assert_('echo "$(( $PWD' in m, 'Got %s' % m)  # word with $
 | 
| 390 |         self.assert_('echo "$(( $PS4' in m, 'Got %s' % m)
 | 
| 391 | 
 | 
| 392 |     def testCompletesCommandSubs(self):
 | 
| 393 |         comp_lookup = completion.Lookup()
 | 
| 394 |         comp_lookup.RegisterName('grep', BASE_OPTS, U1)
 | 
| 395 |         comp_lookup.RegisterName('__first', BASE_OPTS, U2)
 | 
| 396 |         r = _MakeRootCompleter(comp_lookup=comp_lookup)
 | 
| 397 | 
 | 
| 398 |         # Normal completion
 | 
| 399 |         comp = MockApi('gre')
 | 
| 400 |         m = list(r.Matches(comp))
 | 
| 401 |         self.assertEqual(['grep '], m)
 | 
| 402 | 
 | 
| 403 |         # $(command sub)
 | 
| 404 |         comp = MockApi('echo $(gre')
 | 
| 405 |         m = list(r.Matches(comp))
 | 
| 406 |         self.assertEqual(['echo $(grep '], m)
 | 
| 407 | 
 | 
| 408 |         # `backticks`
 | 
| 409 |         comp = MockApi('echo `gre')
 | 
| 410 |         m = list(r.Matches(comp))
 | 
| 411 |         self.assertEqual(['echo `grep '], m)
 | 
| 412 | 
 | 
| 413 |         # Args inside `backticks
 | 
| 414 |         comp = MockApi('echo `grep f')
 | 
| 415 |         m = list(r.Matches(comp))
 | 
| 416 |         self.assertEqual(['echo `grep foo.py ', 'echo `grep foo '], m)
 | 
| 417 | 
 | 
| 418 |     def testCompletesRedirectArguments(self):
 | 
| 419 |         r = _MakeRootCompleter()
 | 
| 420 | 
 | 
| 421 |         comp = MockApi('cat < b')
 | 
| 422 |         m = list(r.Matches(comp))
 | 
| 423 |         # Some B subdirs of the repo!
 | 
| 424 |         self.assert_('cat < bin/' in m, 'Got %s' % m)
 | 
| 425 |         self.assert_('cat < build/' in m, 'Got %s' % m)
 | 
| 426 |         self.assert_('cat < benchmarks/' in m, 'Got %s' % m)
 | 
| 427 | 
 | 
| 428 |         # This redirect does NOT take a path argument!
 | 
| 429 |         comp = MockApi('echo >&')
 | 
| 430 |         m = list(r.Matches(comp))
 | 
| 431 |         self.assertEqual(0, len(m))
 | 
| 432 | 
 | 
| 433 |     def testRunsUserDefinedFunctions(self):
 | 
| 434 |         # This is here because it's hard to test readline with the spec tests.
 | 
| 435 |         with open('testdata/completion/osh-unit.bash') as f:
 | 
| 436 |             code_str = f.read()
 | 
| 437 | 
 | 
| 438 |         parse_ctx = test_lib.InitParseContext()
 | 
| 439 |         parse_ctx.Init_Trail(parse_lib.Trail())
 | 
| 440 | 
 | 
| 441 |         comp_lookup = completion.Lookup()
 | 
| 442 |         cmd_ev = test_lib.EvalCode(code_str,
 | 
| 443 |                                    parse_ctx,
 | 
| 444 |                                    comp_lookup=comp_lookup)
 | 
| 445 | 
 | 
| 446 |         r = _MakeRootCompleter(comp_lookup=comp_lookup)
 | 
| 447 | 
 | 
| 448 |         # By default, we get a space on the end.
 | 
| 449 |         m = list(r.Matches(MockApi('mywords t')))
 | 
| 450 |         self.assertEqual(['mywords three ', 'mywords two '], sorted(m))
 | 
| 451 | 
 | 
| 452 |         # No space
 | 
| 453 |         m = list(r.Matches(MockApi('mywords_nospace t')))
 | 
| 454 |         self.assertEqual(['mywords_nospace three', 'mywords_nospace two'],
 | 
| 455 |                          sorted(m))
 | 
| 456 | 
 | 
| 457 |         # next 3 fail on darwin
 | 
| 458 |         if not IS_DARWIN:
 | 
| 459 |             # Filtered out two and bin
 | 
| 460 |             m = list(r.Matches(MockApi('flagX ')))
 | 
| 461 |             self.assertEqual(['flagX one ', 'flagX three '], sorted(m))
 | 
| 462 | 
 | 
| 463 |             # Filter out everything EXCEPT two and bin
 | 
| 464 |             m = list(r.Matches(MockApi('flagX_bang ')))
 | 
| 465 |             self.assertEqual(['flagX_bang bin ', 'flagX_bang two '], sorted(m))
 | 
| 466 | 
 | 
| 467 |             # -X with -P
 | 
| 468 |             m = list(r.Matches(MockApi('flagX_prefix ')))
 | 
| 469 |             self.assertEqual(['flagX_prefix __one ', 'flagX_prefix __three '],
 | 
| 470 |                              sorted(m))
 | 
| 471 | 
 | 
| 472 |         # -P with plusdirs
 | 
| 473 |         m = list(r.Matches(MockApi('prefix_plusdirs b')))
 | 
| 474 |         self.assertEqual([
 | 
| 475 |             'prefix_plusdirs __bin ',
 | 
| 476 |             'prefix_plusdirs benchmarks/',
 | 
| 477 |             'prefix_plusdirs bin/',
 | 
| 478 |             'prefix_plusdirs build/',
 | 
| 479 |             'prefix_plusdirs builtin/',
 | 
| 480 |         ], sorted(m))
 | 
| 481 | 
 | 
| 482 |         if not IS_DARWIN:
 | 
| 483 |             # -X with plusdirs.  We're filtering out bin/, and then it's added back by
 | 
| 484 |             # plusdirs.  The filter doesn't kill it.
 | 
| 485 |             m = list(r.Matches(MockApi('flagX_plusdirs b')))
 | 
| 486 |             self.assertEqual([
 | 
| 487 |                 'flagX_plusdirs benchmarks/',
 | 
| 488 |                 'flagX_plusdirs bin/',
 | 
| 489 |                 'flagX_plusdirs build/',
 | 
| 490 |                 'flagX_plusdirs builtin/',
 | 
| 491 |             ], sorted(m))
 | 
| 492 | 
 | 
| 493 |         # -P with dirnames.  -P is NOT respected.
 | 
| 494 |         m = list(r.Matches(MockApi('prefix_dirnames b')))
 | 
| 495 |         self.assertEqual([
 | 
| 496 |             'prefix_dirnames benchmarks/',
 | 
| 497 |             'prefix_dirnames bin/',
 | 
| 498 |             'prefix_dirnames build/',
 | 
| 499 |             'prefix_dirnames builtin/',
 | 
| 500 |         ], sorted(m))
 | 
| 501 | 
 | 
| 502 |     def testCompletesAliases(self):
 | 
| 503 |         # I put some aliases in this file.
 | 
| 504 |         with open('testdata/completion/osh-unit.bash') as f:
 | 
| 505 |             code_str = f.read()
 | 
| 506 |         aliases = {}
 | 
| 507 |         parse_ctx = test_lib.InitParseContext(aliases=aliases)
 | 
| 508 |         parse_ctx.Init_Trail(parse_lib.Trail())
 | 
| 509 |         comp_lookup = completion.Lookup()
 | 
| 510 | 
 | 
| 511 |         cmd_ev = test_lib.EvalCode(code_str,
 | 
| 512 |                                    parse_ctx,
 | 
| 513 |                                    comp_lookup=comp_lookup,
 | 
| 514 |                                    aliases=aliases)
 | 
| 515 | 
 | 
| 516 |         r = _MakeRootCompleter(parse_ctx=parse_ctx, comp_lookup=comp_lookup)
 | 
| 517 | 
 | 
| 518 |         # The original command
 | 
| 519 |         m = list(r.Matches(MockApi('ls ')))
 | 
| 520 |         self.assertEqual(['ls one ', 'ls two '], sorted(m))
 | 
| 521 | 
 | 
| 522 |         # Alias for the command
 | 
| 523 |         m = list(r.Matches(MockApi('ll ')))
 | 
| 524 |         self.assertEqual(['ll one ', 'll two '], sorted(m))
 | 
| 525 | 
 | 
| 526 |         # DOUBLE alias expansion goes back to original
 | 
| 527 |         m = list(r.Matches(MockApi('ll_classify ')))
 | 
| 528 |         self.assertEqual(['ll_classify one ', 'll_classify two '], sorted(m))
 | 
| 529 | 
 | 
| 530 |         # Trailing space
 | 
| 531 |         m = list(r.Matches(MockApi('ll_trailing ')))
 | 
| 532 |         self.assertEqual(['ll_trailing one ', 'll_trailing two '], sorted(m))
 | 
| 533 | 
 | 
| 534 |         # It should NOT clobber completio registered for aliases
 | 
| 535 |         m = list(r.Matches(MockApi('ll_own_completion ')))
 | 
| 536 |         self.assertEqual(
 | 
| 537 |             ['ll_own_completion own ', 'll_own_completion words '], sorted(m))
 | 
| 538 | 
 | 
| 539 |     def testNoInfiniteLoop(self):
 | 
| 540 |         # This was ONE place where we got an infinite loop.
 | 
| 541 | 
 | 
| 542 |         with open('testdata/completion/return-124.bash') as f:
 | 
| 543 |             code_str = f.read()
 | 
| 544 |         parse_ctx = test_lib.InitParseContext()
 | 
| 545 |         parse_ctx.Init_Trail(parse_lib.Trail())
 | 
| 546 | 
 | 
| 547 |         comp_lookup = completion.Lookup()
 | 
| 548 |         cmd_ev = test_lib.EvalCode(code_str,
 | 
| 549 |                                    parse_ctx,
 | 
| 550 |                                    comp_lookup=comp_lookup)
 | 
| 551 | 
 | 
| 552 |         r = _MakeRootCompleter(parse_ctx=parse_ctx, comp_lookup=comp_lookup)
 | 
| 553 | 
 | 
| 554 |         m = list(r.Matches(MockApi('bad ')))
 | 
| 555 |         self.assertEqual([], sorted(m))
 | 
| 556 | 
 | 
| 557 |         # Error: spec not changed
 | 
| 558 |         m = list(r.Matches(MockApi('both ')))
 | 
| 559 |         self.assertEqual([], sorted(m))
 | 
| 560 | 
 | 
| 561 |         # Redefines completions
 | 
| 562 |         m = list(r.Matches(MockApi('both2 ')))
 | 
| 563 |         self.assertEqual(['both2 b1 ', 'both2 b2 '], sorted(m))
 | 
| 564 | 
 | 
| 565 |     def testCompletesShAssignment(self):
 | 
| 566 |         # OSH doesn't do this.  Here is noticed about bash --norc (which is
 | 
| 567 |         # undoubtedly different from bash_completion):
 | 
| 568 |         #
 | 
| 569 |         # foo=/ho<TAB> completes directory
 | 
| 570 |         # foo=/home/:/ho<TAB> completes directory
 | 
| 571 |         #
 | 
| 572 |         # foo='/ho<TAB> completes directory
 | 
| 573 |         # foo='/home/:/ho<TAB> does NOT complete
 | 
| 574 |         #
 | 
| 575 |         # Ditto for ".  The first path is completed, but nothing after :.
 | 
| 576 |         #
 | 
| 577 |         # Ditto for echo foo=/ho
 | 
| 578 |         #           echo foo='/ho
 | 
| 579 |         #           echo foo="/ho
 | 
| 580 |         #
 | 
| 581 |         # It doesn't distinguish by position.
 | 
| 582 |         #
 | 
| 583 |         # TODO:
 | 
| 584 |         # - test with an image created with debootstrap
 | 
| 585 |         # - test with an Alpine image
 | 
| 586 |         return
 | 
| 587 | 
 | 
| 588 | 
 | 
| 589 | _INIT_TEMPLATE = """
 | 
| 590 | argv() {
 | 
| 591 |   python -c 'import sys; print(sys.argv[1:])' "$@"
 | 
| 592 | }
 | 
| 593 | 
 | 
| 594 | fail() {
 | 
| 595 |   echo "Non-fatal assertion failed: $@" >&2
 | 
| 596 | }
 | 
| 597 | 
 | 
| 598 | arrays_equal() {
 | 
| 599 |   local n=$1
 | 
| 600 |   shift
 | 
| 601 | 
 | 
| 602 |   # Copy to avoid silly ${@ : : } semantics
 | 
| 603 |   local argv=("$@")
 | 
| 604 | 
 | 
| 605 |   local left=( "${argv[@]: 0 : n}" )
 | 
| 606 |   local right=( "${argv[@]: n : 2*n - 1}" )
 | 
| 607 | 
 | 
| 608 |   for (( i = 0; i < n; i++ )); do
 | 
| 609 |     if [[ ${left[i]} != ${right[i]} ]]; then
 | 
| 610 |       echo -n 'left : '; argv "${left[@]}"
 | 
| 611 |       echo -n 'right: '; argv "${right[@]}"
 | 
| 612 |       fail "Word $i differed: ${left[i]} != ${right[i]}"
 | 
| 613 |       return 1
 | 
| 614 |     fi
 | 
| 615 |   done
 | 
| 616 |   return 0
 | 
| 617 | }
 | 
| 618 | 
 | 
| 619 | _init_completion() {
 | 
| 620 |   compadjust "$@" cur prev words cword
 | 
| 621 | }
 | 
| 622 | 
 | 
| 623 | my_complete() {
 | 
| 624 |   local cur prev words cword split
 | 
| 625 | 
 | 
| 626 |   # Test this function
 | 
| 627 |   if arrays_equal 2 a b a b; then
 | 
| 628 |     echo ok
 | 
| 629 |   else
 | 
| 630 |     echo failed
 | 
| 631 |     return
 | 
| 632 |   fi
 | 
| 633 | 
 | 
| 634 |   PASSED=()
 | 
| 635 | 
 | 
| 636 |   # no quotes with [[
 | 
| 637 |   if [[ $COMP_LINE == $ORACLE_COMP_LINE ]]; then
 | 
| 638 |     PASSED+=(COMP_LINE)
 | 
| 639 |   fi
 | 
| 640 |   if [[ $COMP_POINT == $ORACLE_COMP_POINT ]]; then
 | 
| 641 |     PASSED+=(COMP_POINT)
 | 
| 642 |   fi
 | 
| 643 | 
 | 
| 644 |   if [[ ${#COMP_WORDS[@]} == ${#ORACLE_COMP_WORDS[@]} ]]; then
 | 
| 645 |     local n=${#COMP_WORDS[@]}
 | 
| 646 |     if arrays_equal "$n" "${COMP_WORDS[@]}" "${ORACLE_COMP_WORDS[@]}"; then
 | 
| 647 |       PASSED+=(COMP_WORDS)
 | 
| 648 |     fi
 | 
| 649 |   else
 | 
| 650 |     fail "COMP_WORDS: Expected ${ORACLE_COMP_WORDS[@]}, got ${COMP_WORDS[@]}"
 | 
| 651 |   fi
 | 
| 652 | 
 | 
| 653 |   # This doesn't pass because COMP_WORDS and COMP_CWORD are different.
 | 
| 654 |   if [[ $COMP_CWORD == $ORACLE_COMP_CWORD ]]; then
 | 
| 655 |     #echo "passed: COMP_CWORD = $COMP_CWORD"
 | 
| 656 |     PASSED+=(COMP_CWORD)
 | 
| 657 |   else
 | 
| 658 |     fail "COMP_CWORD: Expected $ORACLE_COMP_CWORD, got $COMP_CWORD"
 | 
| 659 |   fi
 | 
| 660 | 
 | 
| 661 |   #
 | 
| 662 |   # Now run _init_completion
 | 
| 663 |   #
 | 
| 664 |   _init_completion %(flags)s
 | 
| 665 | 
 | 
| 666 |   if [[ ${#words[@]} == ${#ORACLE_words[@]} ]]; then
 | 
| 667 |     local n=${#words[@]}
 | 
| 668 |     if arrays_equal "$n" "${words[@]}" "${ORACLE_words[@]}"; then
 | 
| 669 |       PASSED+=(words)
 | 
| 670 |     fi
 | 
| 671 |   else
 | 
| 672 |     fail "COMP_WORDS: Expected ${ORACLE_words[@]}, got ${words[@]}"
 | 
| 673 |   fi
 | 
| 674 | 
 | 
| 675 |   if [[ $cur == $ORACLE_cur ]]; then
 | 
| 676 |     PASSED+=(cur)
 | 
| 677 |   else
 | 
| 678 |     fail "cur: Expected $ORACLE_cur, got $cur"
 | 
| 679 |   fi
 | 
| 680 |   if [[ $prev == $ORACLE_prev ]]; then
 | 
| 681 |     PASSED+=(prev)
 | 
| 682 |   else
 | 
| 683 |     fail "prev: Expected $ORACLE_prev, got $prev"
 | 
| 684 |   fi
 | 
| 685 |   if [[ $cword == $ORACLE_cword ]]; then
 | 
| 686 |     PASSED+=(cword)
 | 
| 687 |   else
 | 
| 688 |     fail "cword: Expected $ORACLE_cword, got $cword"
 | 
| 689 |   fi
 | 
| 690 |   if [[ $split == $ORACLE_split ]]; then
 | 
| 691 |     PASSED+=(split)
 | 
| 692 |   else
 | 
| 693 |     fail "split: Expected $ORACLE_split, got $split"
 | 
| 694 |   fi
 | 
| 695 | 
 | 
| 696 |   COMPREPLY=(dummy)
 | 
| 697 | }
 | 
| 698 | complete -F my_complete %(command)s
 | 
| 699 | """
 | 
| 700 | 
 | 
| 701 | 
 | 
| 702 | class InitCompletionTest(unittest.TestCase):
 | 
| 703 | 
 | 
| 704 |     def testMatchesOracle(self):
 | 
| 705 |         for i, case in enumerate(bash_oracle.CASES):  # generated data
 | 
| 706 |             flags = case.get('_init_completion_flags')
 | 
| 707 |             if flags is None:
 | 
| 708 |                 continue
 | 
| 709 | 
 | 
| 710 |             # This was input
 | 
| 711 |             code_str = case['code']
 | 
| 712 |             assert code_str.endswith('\t')
 | 
| 713 | 
 | 
| 714 |             log('')
 | 
| 715 |             log('--- Case %d: %r with flags %s', i, code_str, flags)
 | 
| 716 |             log('')
 | 
| 717 |             #print(case)
 | 
| 718 | 
 | 
| 719 |             oracle_comp_words = case['COMP_WORDS']
 | 
| 720 |             oracle_comp_cword = case['COMP_CWORD']
 | 
| 721 |             oracle_comp_line = case['COMP_LINE']
 | 
| 722 |             oracle_comp_point = case['COMP_POINT']
 | 
| 723 | 
 | 
| 724 |             # Init completion data
 | 
| 725 |             oracle_words = case['words']
 | 
| 726 |             oracle_cur = case['cur']
 | 
| 727 |             oracle_prev = case['prev']
 | 
| 728 |             oracle_cword = case['cword']
 | 
| 729 |             oracle_split = case['split']
 | 
| 730 | 
 | 
| 731 |             #
 | 
| 732 |             # First test some invariants on the oracle's data.
 | 
| 733 |             #
 | 
| 734 | 
 | 
| 735 |             self.assertEqual(code_str[:-1], oracle_comp_line)
 | 
| 736 |             # weird invariant that always holds.  So isn't COMP_CWORD useless?
 | 
| 737 |             self.assertEqual(int(oracle_comp_cword),
 | 
| 738 |                              len(oracle_comp_words) - 1)
 | 
| 739 |             # Another weird invariant.  Note this is from the bash ORACLE, not from
 | 
| 740 |             # our mocks.
 | 
| 741 |             self.assertEqual(int(oracle_comp_point), len(code_str) - 1)
 | 
| 742 | 
 | 
| 743 |             #
 | 
| 744 |             # Now run a piece of code that compares OSH's actual data against the
 | 
| 745 |             # oracle.
 | 
| 746 |             #
 | 
| 747 | 
 | 
| 748 |             init_code = _INIT_TEMPLATE % {
 | 
| 749 |                 'flags': ' '.join(flags),
 | 
| 750 |                 'command': oracle_comp_words[0]
 | 
| 751 |             }
 | 
| 752 | 
 | 
| 753 |             arena = test_lib.MakeArena('<InitCompletionTest>')
 | 
| 754 |             parse_ctx = test_lib.InitParseContext(arena=arena)
 | 
| 755 |             mem = state.Mem('', [], arena, [])
 | 
| 756 |             parse_opts, exec_opts, mutable_opts = state.MakeOpts(mem, None)
 | 
| 757 |             mem.exec_opts = exec_opts
 | 
| 758 | 
 | 
| 759 |             mutable_opts.Init()
 | 
| 760 | 
 | 
| 761 |             #
 | 
| 762 |             # Allow our code to access oracle data
 | 
| 763 |             #
 | 
| 764 |             state.SetGlobalArray(mem, 'ORACLE_COMP_WORDS', oracle_comp_words)
 | 
| 765 |             state.SetGlobalString(mem, 'ORACLE_COMP_CWORD', oracle_comp_cword)
 | 
| 766 |             state.SetGlobalString(mem, 'ORACLE_COMP_LINE', oracle_comp_line)
 | 
| 767 |             state.SetGlobalString(mem, 'ORACLE_COMP_POINT', oracle_comp_point)
 | 
| 768 | 
 | 
| 769 |             state.SetGlobalArray(mem, 'ORACLE_words', oracle_words)
 | 
| 770 |             state.SetGlobalString(mem, 'ORACLE_cur', oracle_cur)
 | 
| 771 |             state.SetGlobalString(mem, 'ORACLE_prev', oracle_prev)
 | 
| 772 |             state.SetGlobalString(mem, 'ORACLE_cword', oracle_cword)
 | 
| 773 |             state.SetGlobalString(mem, 'ORACLE_split', oracle_split)
 | 
| 774 | 
 | 
| 775 |             comp_lookup = completion.Lookup()
 | 
| 776 |             cmd_ev = test_lib.EvalCode(init_code,
 | 
| 777 |                                        parse_ctx,
 | 
| 778 |                                        comp_lookup=comp_lookup,
 | 
| 779 |                                        mem=mem)
 | 
| 780 | 
 | 
| 781 |             r = _MakeRootCompleter(comp_lookup=comp_lookup)
 | 
| 782 |             comp = MockApi(code_str[:-1])
 | 
| 783 |             m = list(r.Matches(comp))
 | 
| 784 |             log('matches = %s', m)
 | 
| 785 | 
 | 
| 786 |             # Unterminated quote in case 5.  Nothing to complete.
 | 
| 787 |             # TODO: use a label
 | 
| 788 |             if i == 5:
 | 
| 789 |                 continue
 | 
| 790 | 
 | 
| 791 |             # Our test shell script records what passed in an array.
 | 
| 792 |             val = mem.GetValue('PASSED')
 | 
| 793 |             self.assertEqual(value_e.BashArray, val.tag(),
 | 
| 794 |                              "[case %d] Expected array, got %s" % (i, val))
 | 
| 795 |             actually_passed = val.strs
 | 
| 796 | 
 | 
| 797 |             should_pass = [
 | 
| 798 |                 'COMP_WORDS',
 | 
| 799 |                 'COMP_CWORD',
 | 
| 800 |                 'COMP_LINE',
 | 
| 801 |                 'COMP_POINT',  # old API
 | 
| 802 |                 'words',
 | 
| 803 |                 'cur',
 | 
| 804 |                 'prev',
 | 
| 805 |                 'cword',
 | 
| 806 |                 'split'  # new API
 | 
| 807 |             ]
 | 
| 808 | 
 | 
| 809 |             if i == 4:
 | 
| 810 |                 should_pass.remove('COMP_WORDS')
 | 
| 811 |                 should_pass.remove('COMP_CWORD')
 | 
| 812 |                 should_pass.remove('cword')
 | 
| 813 |                 should_pass.remove('words')  # double quotes aren't the same
 | 
| 814 | 
 | 
| 815 |             for t in should_pass:
 | 
| 816 |                 self.assert_(t in actually_passed,
 | 
| 817 |                              "%r was expected to pass (case %d)" % (t, i))
 | 
| 818 | 
 | 
| 819 |         log('Ran %d cases', len(bash_oracle.CASES))
 | 
| 820 | 
 | 
| 821 | 
 | 
| 822 | if __name__ == '__main__':
 | 
| 823 |     unittest.main()
 |