Skip to content

Commit 0d3ec66

Browse files
[3.14] gh-154007: Improve test coverage for the shlex module (GH-154009) (#154446)
gh-154007: Improve test coverage for the `shlex` module (GH-154009) (cherry picked from commit 0eac28e) Co-authored-by: Piotr Kaznowski <piotr@kazno.dev>
1 parent 6d61cc9 commit 0d3ec66

1 file changed

Lines changed: 200 additions & 0 deletions

File tree

Lib/test/test_shlex.py

Lines changed: 200 additions & 0 deletions
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

@@ -368,6 +371,203 @@ def testPunctuationCharsReadOnly(self):
368371
with self.assertRaises(AttributeError):
369372
shlex_instance.punctuation_chars = False
370373

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

0 commit comments

Comments
 (0)