Skip to content

Commit 8fa77b3

Browse files
committed
gh-154007: Improve test coverage for the shlex module
1 parent 20b50f8 commit 8fa77b3

1 file changed

Lines changed: 201 additions & 1 deletion

File tree

Lib/test/test_shlex.py

Lines changed: 201 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import io
22
import itertools
3+
import os
34
import shlex
45
import string
6+
import tempfile
57
import unittest
8+
from unittest.mock import patch
69
from test.support import cpython_only
710
from test.support import import_helper
811

@@ -376,9 +379,206 @@ def testPunctuationCharsReadOnly(self):
376379
with self.assertRaises(AttributeError):
377380
shlex_instance.punctuation_chars = False
378381

382+
def testLinenoAfterNewLine(self):
383+
s = shlex.shlex("line 1\nline 2")
384+
self.assertEqual(s.lineno, 1) # before consumption
385+
list(s)
386+
self.assertEqual(s.lineno, 2)
387+
388+
def testLinenoAfterComment(self):
389+
"""Comment handler increments lineno even without a trailing newline."""
390+
s = shlex.shlex("line 1 # line 2")
391+
list(s)
392+
self.assertEqual(s.lineno, 2)
393+
394+
def testPushToken(self):
395+
s = shlex.shlex("b c")
396+
s.push_token("a")
397+
self.assertListEqual(list(s), ["a", "b", "c"])
398+
399+
def testPushTokenLifo(self):
400+
s = shlex.shlex("")
401+
s.push_token("first")
402+
s.push_token("last")
403+
self.assertListEqual(list(s), ["last", "first"])
404+
405+
def testPushTokenDebug(self):
406+
s = shlex.shlex("")
407+
s.debug = 1
408+
tok = "a"
409+
with patch("builtins.print") as mock_print:
410+
s.push_token(tok)
411+
mock_print.assert_called_once_with(f"shlex: pushing token {tok!r}")
412+
413+
def testPushSourceString(self):
414+
s = shlex.shlex("world")
415+
s.push_source("hello")
416+
self.assertListEqual(list(s), ["hello", "world"])
417+
418+
def testPushSourceStream(self):
419+
s = shlex.shlex("world")
420+
s.push_source(io.StringIO("hello"))
421+
self.assertListEqual(list(s), ["hello", "world"])
422+
423+
def testPushSourceStreamDebug(self):
424+
s = shlex.shlex("")
425+
stream = io.StringIO("hello")
426+
s.debug = 1
427+
with patch("builtins.print") as mock_print:
428+
s.push_source(stream)
429+
mock_print.assert_called_once_with(f"shlex: pushing to stream {stream}")
430+
431+
def testPushSourceNewfile(self):
432+
"""shlex.push_source sets infile to newfile; pop_source restores the original on exhaustion."""
433+
original_file = "original.sh"
434+
new_file = "new.sh"
435+
s = shlex.shlex("b", infile=original_file)
436+
s.debug = 1
437+
with patch("builtins.print") as mock_print:
438+
s.push_source("a", newfile=new_file)
439+
mock_print.assert_called_once_with(f"shlex: pushing to file {new_file}")
440+
self.assertEqual(s.infile, new_file)
441+
s.debug = 0
442+
list(s)
443+
self.assertEqual(s.infile, original_file)
444+
445+
def testPopSourceDebug(self):
446+
"""pop_source emits debug output when debug is set."""
447+
s = shlex.shlex("b")
448+
original_stream = s.instream
449+
s.push_source("a")
450+
s.debug = 1
451+
with patch("builtins.print") as mock_print:
452+
list(s) # exhausts pushed source and triggers pop_source internally
453+
mock_print.assert_any_call(f"shlex: popping to {original_stream}, line 1")
454+
455+
def testErrorLeaderTracksPosition(self):
456+
infile_label = "test.sh"
457+
s = shlex.shlex("line 1\nline 2", infile=infile_label)
458+
list(s)
459+
result = s.error_leader()
460+
self.assertEqual(result, f'"{infile_label}", line 2: ')
461+
462+
def testErrorLeaderOverrides(self):
463+
s = shlex.shlex("foo", infile="original.sh")
464+
infile_label_override = "override.sh"
465+
lineno_override = 42
466+
result = s.error_leader(infile=infile_label_override, lineno=lineno_override)
467+
self.assertEqual(result, f'"{infile_label_override}", line {lineno_override}: ')
468+
469+
def testNoClosingQuotation(self):
470+
s = shlex.shlex('"foo')
471+
with self.assertRaisesRegex(ValueError, "No closing quotation"):
472+
list(s)
473+
474+
def testNoEscapedCharacter(self):
475+
s = shlex.shlex("\\", posix=True)
476+
with self.assertRaisesRegex(ValueError, "No escaped character"):
477+
list(s)
478+
479+
def testSourcehookStripsQuotes(self):
480+
with tempfile.NamedTemporaryFile(mode="w", suffix=".sh", delete_on_close=False) as f:
481+
f.write("hello")
482+
f.close()
483+
s = shlex.shlex("")
484+
newfile, stream = s.sourcehook(f'"{f.name}"')
485+
stream.close()
486+
self.assertEqual(newfile, f.name)
487+
488+
def testSourcehookAbsolutePath(self):
489+
with tempfile.NamedTemporaryFile(mode="w", delete_on_close=False) as f:
490+
f.close()
491+
s = shlex.shlex("", infile="/some/dir/main.sh")
492+
newfile, stream = s.sourcehook(f.name)
493+
stream.close()
494+
self.assertEqual(newfile, f.name)
495+
496+
def testSourcehookRelativePath(self):
497+
with tempfile.TemporaryDirectory() as d:
498+
fpath = os.path.join(d, "included.sh")
499+
with open(fpath, "w"):
500+
pass
501+
s = shlex.shlex("", infile=os.path.join(d, "main.sh"))
502+
newfile, stream = s.sourcehook("included.sh")
503+
stream.close()
504+
self.assertEqual(newfile, fpath)
505+
506+
def testSourceInclusion(self):
507+
"""shlex.source sets a trigger keyword: when the lexer reads a token equal
508+
to it, the next token is consumed as a filename and passed to
509+
sourcehook, which returns a stream to push onto the input stack.
510+
Tokens flow from that stream first, then resume from the original.
511+
"""
512+
s = shlex.shlex("trigger filename remaining")
513+
s.source = "trigger"
514+
s.sourcehook = lambda f: (f, io.StringIO("included"))
515+
self.assertEqual(list(s), ["included", "remaining"])
516+
517+
def testGetTokenPopsPushbackDebug(self):
518+
s = shlex.shlex("")
519+
s.push_token("hello")
520+
s.debug = 1 # set after push_token to isolate the pop-token branch
521+
with patch("builtins.print") as mock_print:
522+
tok = s.get_token()
523+
self.assertEqual(tok, "hello")
524+
mock_print.assert_called_once_with("shlex: popping token 'hello'")
525+
526+
def testDebugWhitespaceInWhitespaceState(self):
527+
with patch("builtins.print") as mock_print:
528+
s = shlex.shlex(" a")
529+
s.debug = 2
530+
list(s)
531+
mock_print.assert_any_call("shlex: I see whitespace in whitespace state")
532+
533+
def testDebugWhitespaceInWordState(self):
534+
with patch("builtins.print") as mock_print:
535+
s = shlex.shlex("a b")
536+
s.debug = 2
537+
list(s)
538+
mock_print.assert_any_call("shlex: I see whitespace in word state")
539+
540+
def testDebugPunctuationInWordState(self):
541+
with patch("builtins.print") as mock_print:
542+
s = shlex.shlex("a(")
543+
s.debug = 2
544+
list(s)
545+
mock_print.assert_any_call("shlex: I see punctuation in word state")
546+
547+
def testDebugRawToken(self):
548+
with patch("builtins.print") as mock_print:
549+
s = shlex.shlex("hello")
550+
s.debug = 2
551+
list(s)
552+
mock_print.assert_any_call("shlex: raw token='hello'")
553+
554+
def testDebugEOFInQuote(self):
555+
s = shlex.shlex('"oops', posix=True)
556+
s.debug = 2
557+
with patch('builtins.print') as mock_print:
558+
with self.assertRaises(ValueError):
559+
list(s)
560+
msgs = [call.args[0] for call in mock_print.call_args_list]
561+
self.assertTrue(any("EOF in quotes" in m for m in msgs))
562+
563+
def testDebugEOFInEscape(self):
564+
s = shlex.shlex("oops\\", posix=True)
565+
s.debug = 2
566+
with patch("builtins.print") as mock_print:
567+
with self.assertRaises(ValueError):
568+
list(s)
569+
msgs = [call.args[0] for call in mock_print.call_args_list]
570+
self.assertTrue(any("EOF in escape" in m for m in msgs))
571+
572+
def testDebugStateTrace(self):
573+
s = shlex.shlex("a")
574+
s.debug = 3
575+
with patch("builtins.print") as mock_print:
576+
list(s)
577+
mock_print.assert_any_call("shlex: in state ' ' I see character: 'a'")
578+
379579
@cpython_only
380580
def test_lazy_imports(self):
381-
import_helper.ensure_lazy_imports('shlex', {'collections', 're', 'os'})
581+
import_helper.ensure_lazy_imports("shlex", {"collections", "re", "os"})
382582

383583

384584
# Allow this test to be used with old shlex.py

0 commit comments

Comments
 (0)