Skip to content

Commit 61304a3

Browse files
fedonmanvstinner
andauthored
gh-153953: Increase test coverage for the wave module (#153954)
Add tests for previously-uncovered paths in Lib/wave.py, all reachable through the public API: * Wave_write parameter validation: rejecting bad channel counts, sample widths, compression types and formats; the "not set" errors from the getters; the "cannot change parameters after starting to write" guards on every setter; and tell(). * Wave_read error handling: rejecting an unknown WAVE_FORMAT_EXTENSIBLE subformat, raising EOFError on a truncated fmt chunk, skipping unknown chunks, getfp(), and closing the file when opening a malformed path fails. * wave.open() rejecting an invalid mode. This raises line coverage of Lib/wave.py under test_wave from 317 to 345 of 449 executable lines. Test-only change; no behavior change. Co-authored-by: Victor Stinner <vstinner@python.org>
1 parent 63bf95b commit 61304a3

1 file changed

Lines changed: 191 additions & 0 deletions

File tree

Lib/test/test_wave.py

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,19 @@ def test__all__(self):
172172
not_exported = {'KSDATAFORMAT_SUBTYPE_PCM'}
173173
support.check__all__(self, wave, not_exported=not_exported)
174174

175+
def test_getfp(self):
176+
fp = io.BytesIO()
177+
with wave.open(fp, 'wb') as w:
178+
w.setnchannels(1)
179+
w.setsampwidth(1)
180+
w.setframerate(11025)
181+
fp.seek(0)
182+
with wave.open(fp) as r:
183+
chunk = r.getfp()
184+
self.assertIsNotNone(chunk)
185+
self.assertIs(chunk.file, fp)
186+
self.assertEqual(chunk.chunkname, b'RIFF')
187+
175188

176189
class WaveLowLevelTest(unittest.TestCase):
177190

@@ -474,6 +487,184 @@ def test_open_pathlike(self):
474487
with wave.open(fake_path, 'rb') as f:
475488
pass
476489

490+
def test_open_invalid_mode(self):
491+
with self.assertRaisesRegex(wave.Error, "mode must be"):
492+
wave.open(io.BytesIO(), 'xb')
493+
494+
495+
class WaveReadErrorTest(unittest.TestCase):
496+
"""Cover error and edge paths of Wave_read, and wave.open()."""
497+
498+
FMT_PCM = struct.pack('<HHLLHH', wave.WAVE_FORMAT_PCM, 1, 11025, 11025, 1, 8)
499+
500+
@staticmethod
501+
def _wave_file(*chunks):
502+
"""Build in-memory WAVE bytes from (name, payload) chunks.
503+
504+
Each chunk stores its real payload length and is padded to an even
505+
number of bytes, and the RIFF size is computed to match.
506+
"""
507+
body = b'WAVE'
508+
for name, payload in chunks:
509+
body += name + struct.pack('<L', len(payload)) + payload
510+
if len(payload) & 1:
511+
body += b'\x00'
512+
return b'RIFF' + struct.pack('<L', len(body)) + body
513+
514+
def test_read_unknown_extensible_subformat(self):
515+
# A WAVE_FORMAT_EXTENSIBLE fmt chunk whose SubFormat GUID is not
516+
# KSDATAFORMAT_SUBTYPE_PCM must be rejected.
517+
fmt = struct.pack('<HHLLH', wave.WAVE_FORMAT_EXTENSIBLE, 2, 11025,
518+
11025 * 2 * 3, 6)
519+
fmt += struct.pack('<H', 24) # bits per sample
520+
fmt += struct.pack('<HHL', 22, 24, 3) # cbSize, valid bits, channel mask
521+
fmt += b'\xff' * 16 # bogus SubFormat GUID
522+
b = self._wave_file((b'fmt ', fmt), (b'data', b''))
523+
with self.assertRaisesRegex(wave.Error, 'unknown extended format'):
524+
wave.open(io.BytesIO(b))
525+
526+
def test_read_truncated_fmt_chunk_header(self):
527+
# fmt chunk too short for the fixed 14-byte header.
528+
fmt = struct.pack('<H', wave.WAVE_FORMAT_PCM) + b'\x00' * 8
529+
b = self._wave_file((b'fmt ', fmt))
530+
with self.assertRaises(EOFError):
531+
wave.open(io.BytesIO(b))
532+
533+
def test_read_truncated_fmt_chunk_sampwidth(self):
534+
# fmt chunk holds the 14-byte header but is missing the sample width.
535+
fmt = struct.pack('<HHLLH', wave.WAVE_FORMAT_PCM, 1, 11025, 11025, 1)
536+
b = self._wave_file((b'fmt ', fmt))
537+
with self.assertRaises(EOFError):
538+
wave.open(io.BytesIO(b))
539+
540+
def test_read_skips_unknown_chunk(self):
541+
# An unknown, odd-sized chunk between fmt and data must be skipped
542+
# (including its pad byte) so the data chunk is still found.
543+
data = b'\x01\x02\x03\x04'
544+
b = self._wave_file((b'fmt ', self.FMT_PCM),
545+
(b'LIST', b'abc'), # odd size, forces alignment
546+
(b'data', data))
547+
with wave.open(io.BytesIO(b)) as r:
548+
self.assertEqual(r.getnframes(), 4)
549+
self.assertEqual(r.readframes(4), data)
550+
551+
552+
class WaveWriteValidationTest(unittest.TestCase):
553+
"""Cover parameter-validation paths of Wave_write."""
554+
555+
@staticmethod
556+
def _close(w):
557+
try:
558+
# Make sure that all parameters are set
559+
w.setnchannels(1)
560+
w.setsampwidth(2)
561+
w.setframerate(44100)
562+
except wave.Error:
563+
# Ignore "cannot change parameters after starting to write" error
564+
pass
565+
566+
w.close()
567+
568+
def open_writer(self):
569+
w = wave.open(io.BytesIO(), 'wb')
570+
self.addCleanup(self._close, w)
571+
return w
572+
573+
def test_get(self):
574+
w = self.open_writer()
575+
self.assertEqual(w.getformat(), wave.WAVE_FORMAT_PCM)
576+
self.assertEqual(w.getnframes(), 0)
577+
# getcomptype() and getcompname() raise AttributeError
578+
# until setcomptype() is called
579+
580+
with self.assertRaisesRegex(wave.Error, 'number of channels not set'):
581+
w.getnchannels()
582+
with self.assertRaisesRegex(wave.Error, 'sample width not set'):
583+
w.getsampwidth()
584+
with self.assertRaisesRegex(wave.Error, 'frame rate not set'):
585+
w.getframerate()
586+
with self.assertRaisesRegex(wave.Error, 'not all parameters set'):
587+
w.getparams()
588+
589+
def test_set(self):
590+
w = self.open_writer()
591+
592+
w.setnchannels(1)
593+
self.assertEqual(w.getnchannels(), 1)
594+
with self.assertRaisesRegex(wave.Error, 'bad # of channels'):
595+
w.setnchannels(0)
596+
597+
w.setsampwidth(2)
598+
self.assertEqual(w.getsampwidth(), 2)
599+
for width in (0, 5):
600+
with self.subTest(width=width):
601+
with self.assertRaisesRegex(wave.Error, 'bad sample width'):
602+
w.setsampwidth(width)
603+
604+
w.setframerate(44100)
605+
self.assertEqual(w.getframerate(), 44100)
606+
with self.assertRaisesRegex(wave.Error, 'bad frame rate'):
607+
w.setframerate(0)
608+
609+
w.setnframes(10)
610+
self.assertEqual(w.getnframes(), 0)
611+
612+
w.setcomptype('NONE', 'not compressed')
613+
self.assertEqual(w.getcomptype(), 'NONE')
614+
self.assertEqual(w.getcompname(), 'not compressed')
615+
with self.assertRaisesRegex(wave.Error, 'unsupported compression type'):
616+
w.setcomptype('ADPCM', 'unsupported')
617+
618+
w.setformat(wave.WAVE_FORMAT_PCM)
619+
self.assertEqual(w.getformat(), wave.WAVE_FORMAT_PCM)
620+
with self.assertRaisesRegex(wave.Error, 'unsupported wave format'):
621+
w.setformat(0x1234)
622+
623+
w.setparams((1, 2, 44100, 0, 'NONE', 'not compressed'))
624+
self.assertEqual(w.getparams(),
625+
(1, 2, 44100, 0, 'NONE', 'not compressed'))
626+
with self.assertRaisesRegex(wave.Error, 'bad # of channels'):
627+
w.setparams((0, 2, 44100, 0, 'NONE', 'not compressed'))
628+
629+
def test_tell(self):
630+
def check_nframes(nframes):
631+
self.assertEqual(w.tell(), nframes)
632+
self.assertEqual(w.getnframes(), nframes)
633+
634+
w = self.open_writer()
635+
w.setnchannels(1)
636+
w.setsampwidth(2)
637+
w.setframerate(44100)
638+
check_nframes(0)
639+
640+
frame = b'\x00\x00'
641+
w.writeframes(frame * 5)
642+
check_nframes(5)
643+
644+
w.writeframes(frame * 3)
645+
check_nframes(8)
646+
647+
def test_cannot_change_params_after_write(self):
648+
w = self.open_writer()
649+
w.setnchannels(1)
650+
w.setsampwidth(2)
651+
w.setframerate(44100)
652+
w.writeframes(b'\x00\x00')
653+
654+
setters = (
655+
('setnchannels', (1,)),
656+
('setsampwidth', (2,)),
657+
('setframerate', (44100,)),
658+
('setnframes', (10,)),
659+
('setcomptype', ('NONE', 'not compressed')),
660+
('setformat', (wave.WAVE_FORMAT_PCM,)),
661+
('setparams', ((1, 2, 44100, 0, 'NONE', 'not compressed'),)),
662+
)
663+
for name, args in setters:
664+
with self.subTest(setter=name):
665+
with self.assertRaisesRegex(wave.Error,
666+
'cannot change parameters'):
667+
getattr(w, name)(*args)
477668

478669
if __name__ == '__main__':
479670
unittest.main()

0 commit comments

Comments
 (0)