gh-153953: Increase test coverage for the `wave` module (#153954)
vstinner <[email protected]> Mon, 10 Aug 2026 10:41:27 -0400 (EDT)
| Newsgroups | gmane.comp.python.cvs |
|---|---|
| Message-ID | <[email protected]> |
https://github.com/python/cpython/commit/61304a39dc4404da16d5435561263fdf5dfed0f9 commit: 61304a39dc4404da16d5435561263fdf5dfed0f9 branch: main author: Vyron Vasileiadis <[email protected]> committer: vstinner <[email protected]> date: 2026-08-10T14:41:10Z summary: 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 <[email protected]> files: M Lib/test/test_wave.py diff --git a/Lib/test/test_wave.py b/Lib/test/test_wave.py index d3723c04820d9d..c482de12f7829b 100644 --- a/Lib/test/test_wave.py +++ b/Lib/test/test_wave.py @@ -172,6 +172,19 @@ def test__all__(self): not_exported = {'KSDATAFORMAT_SUBTYPE_PCM'} support.check__all__(self, wave, not_exported=not_exported) + def test_getfp(self): + fp = io.BytesIO() + with wave.open(fp, 'wb') as w: + w.setnchannels(1) + w.setsampwidth(1) + w.setframerate(11025) + fp.seek(0) + with wave.open(fp) as r: + chunk = r.getfp() + self.assertIsNotNone(chunk) + self.assertIs(chunk.file, fp) + self.assertEqual(chunk.chunkname, b'RIFF') + class WaveLowLevelTest(unittest.TestCase): @@ -474,6 +487,184 @@ def test_open_pathlike(self): with wave.open(fake_path, 'rb') as f: pass + def test_open_invalid_mode(self): + with self.assertRaisesRegex(wave.Error, "mode must be"): + wave.open(io.BytesIO(), 'xb') + + +class WaveReadErrorTest(unittest.TestCase): + """Cover error and edge paths of Wave_read, and wave.open().""" + + FMT_PCM = struct.pack('<HHLLHH', wave.WAVE_FORMAT_PCM, 1, 11025, 11025, 1, 8) + + @staticmethod + def _wave_file(*chunks): + """Build in-memory WAVE bytes from (name, payload) chunks. + + Each chunk stores its real payload length and is padded to an even + number of bytes, and the RIFF size is computed to match. + """ + body = b'WAVE' + for name, payload in chunks: + body += name + struct.pack('<L', len(payload)) + payload + if len(payload) & 1: + body += b'\x00' + return b'RIFF' + struct.pack('<L', len(body)) + body + + def test_read_unknown_extensible_subformat(self): + # A WAVE_FORMAT_EXTENSIBLE fmt chunk whose SubFormat GUID is not + # KSDATAFORMAT_SUBTYPE_PCM must be rejected. + fmt = struct.pack('<HHLLH', wave.WAVE_FORMAT_EXTENSIBLE, 2, 11025, + 11025 * 2 * 3, 6) + fmt += struct.pack('<H', 24) # bits per sample + fmt += struct.pack('<HHL', 22, 24, 3) # cbSize, valid bits, channel mask + fmt += b'\xff' * 16 # bogus SubFormat GUID + b = self._wave_file((b'fmt ', fmt), (b'data', b'')) + with self.assertRaisesRegex(wave.Error, 'unknown extended format'): + wave.open(io.BytesIO(b)) + + def test_read_truncated_fmt_chunk_header(self): + # fmt chunk too short for the fixed 14-byte header. + fmt = struct.pack('<H', wave.WAVE_FORMAT_PCM) + b'\x00' * 8 + b = self._wave_file((b'fmt ', fmt)) + with self.assertRaises(EOFError): + wave.open(io.BytesIO(b)) + + def test_read_truncated_fmt_chunk_sampwidth(self): + # fmt chunk holds the 14-byte header but is missing the sample width. + fmt = struct.pack('<HHLLH', wave.WAVE_FORMAT_PCM, 1, 11025, 11025, 1) + b = self._wave_file((b'fmt ', fmt)) + with self.assertRaises(EOFError): + wave.open(io.BytesIO(b)) + + def test_read_skips_unknown_chunk(self): + # An unknown, odd-sized chunk between fmt and data must be skipped + # (including its pad byte) so the data chunk is still found. + data = b'\x01\x02\x03\x04' + b = self._wave_file((b'fmt ', self.FMT_PCM), + (b'LIST', b'abc'), # odd size, forces alignment + (b'data', data)) + with wave.open(io.BytesIO(b)) as r: + self.assertEqual(r.getnframes(), 4) + self.assertEqual(r.readframes(4), data) + + +class WaveWriteValidationTest(unittest.TestCase): + """Cover parameter-validation paths of Wave_write.""" + + @staticmethod + def _close(w): + try: + # Make sure that all parameters are set + w.setnchannels(1) + w.setsampwidth(2) + w.setframerate(44100) + except wave.Error: + # Ignore "cannot change parameters after starting to write" error + pass + + w.close() + + def open_writer(self): + w = wave.open(io.BytesIO(), 'wb') + self.addCleanup(self._close, w) + return w + + def test_get(self): + w = self.open_writer() + self.assertEqual(w.getformat(), wave.WAVE_FORMAT_PCM) + self.assertEqual(w.getnframes(), 0) + # getcomptype() and getcompname() raise AttributeError + # until setcomptype() is called + + with self.assertRaisesRegex(wave.Error, 'number of channels not set'): + w.getnchannels() + with self.assertRaisesRegex(wave.Error, 'sample width not set'): + w.getsampwidth() + with self.assertRaisesRegex(wave.Error, 'frame rate not set'): + w.getframerate() + with self.assertRaisesRegex(wave.Error, 'not all parameters set'): + w.getparams() + + def test_set(self): + w = self.open_writer() + + w.setnchannels(1) + self.assertEqual(w.getnchannels(), 1) + with self.assertRaisesRegex(wave.Error, 'bad # of channels'): + w.setnchannels(0) + + w.setsampwidth(2) + self.assertEqual(w.getsampwidth(), 2) + for width in (0, 5): + with self.subTest(width=width): + with self.assertRaisesRegex(wave.Error, 'bad sample width'): + w.setsampwidth(width) + + w.setframerate(44100) + self.assertEqual(w.getframerate(), 44100) + with self.assertRaisesRegex(wave.Error, 'bad frame rate'): + w.setframerate(0) + + w.setnframes(10) + self.assertEqual(w.getnframes(), 0) + + w.setcomptype('NONE', 'not compressed') + self.assertEqual(w.getcomptype(), 'NONE') + self.assertEqual(w.getcompname(), 'not compressed') + with self.assertRaisesRegex(wave.Error, 'unsupported compression type'): + w.setcomptype('ADPCM', 'unsupported') + + w.setformat(wave.WAVE_FORMAT_PCM) + self.assertEqual(w.getformat(), wave.WAVE_FORMAT_PCM) + with self.assertRaisesRegex(wave.Error, 'unsupported wave format'): + w.setformat(0x1234) + + w.setparams((1, 2, 44100, 0, 'NONE', 'not compressed')) + self.assertEqual(w.getparams(), + (1, 2, 44100, 0, 'NONE', 'not compressed')) + with self.assertRaisesRegex(wave.Error, 'bad # of channels'): + w.setparams((0, 2, 44100, 0, 'NONE', 'not compressed')) + + def test_tell(self): + def check_nframes(nframes): + self.assertEqual(w.tell(), nframes) + self.assertEqual(w.getnframes(), nframes) + + w = self.open_writer() + w.setnchannels(1) + w.setsampwidth(2) + w.setframerate(44100) + check_nframes(0) + + frame = b'\x00\x00' + w.writeframes(frame * 5) + check_nframes(5) + + w.writeframes(frame * 3) + check_nframes(8) + + def test_cannot_change_params_after_write(self): + w = self.open_writer() + w.setnchannels(1) + w.setsampwidth(2) + w.setframerate(44100) + w.writeframes(b'\x00\x00') + + setters = ( + ('setnchannels', (1,)), + ('setsampwidth', (2,)), + ('setframerate', (44100,)), + ('setnframes', (10,)), + ('setcomptype', ('NONE', 'not compressed')), + ('setformat', (wave.WAVE_FORMAT_PCM,)), + ('setparams', ((1, 2, 44100, 0, 'NONE', 'not compressed'),)), + ) + for name, args in setters: + with self.subTest(setter=name): + with self.assertRaisesRegex(wave.Error, + 'cannot change parameters'): + getattr(w, name)(*args) if __name__ == '__main__': unittest.main() _______________________________________________ Python-checkins mailing list -- [email protected] To unsubscribe send an email to [email protected] https://mail.python.org/mailman3//lists/python-checkins.python.org Member address: [email protected]