From 7e3e055ceba8e7375ac0196ffec02825bc6d6147 Mon Sep 17 00:00:00 2001 From: Eric Joanis Date: Thu, 23 Jul 2026 08:44:20 -0400 Subject: [PATCH 01/10] refactor(tests): remove silence_c_stderr now that we always use pytest --- tests/basic_test_case.py | 30 ------------------------------ tests/test_anchors.py | 4 ++-- tests/test_force_align.py | 4 ++-- 3 files changed, 4 insertions(+), 34 deletions(-) diff --git a/tests/basic_test_case.py b/tests/basic_test_case.py index c48baab3..471a2d5d 100644 --- a/tests/basic_test_case.py +++ b/tests/basic_test_case.py @@ -1,8 +1,6 @@ """Common base class for the ReadAlongs test suites""" import logging -import os -import sys import tempfile from contextlib import contextmanager from pathlib import Path @@ -74,34 +72,6 @@ def tearDown(self): LOGGER.setLevel(logging.INFO) -@contextmanager -def silence_c_stderr(): - """Capture stderr from C output, e.g., from SoundSwallower. - - Note: to capture stderr for both C and Python code, combine this with - redirect_stderr(), but you must use capture_c_stderr() first: - with capture_c_stderr(), redirect_stderr(io.StringIO()): - # code - - Loosely inspired by https://stackoverflow.com/a/24277852, but much simplified to - address our narrow needs, namely to silence stderr in a context manager. - """ - - if "pytest" in sys.modules or os.name == "nt" and sys.version_info < (3, 10): - # Incompatible and pointless with pytest since it captures all output - # Also work around instability for this on Windows with Py 3.8/3.9 - yield - else: - stderr_fileno = sys.stderr.fileno() - stderr_save = os.dup(stderr_fileno) - stderr_fd = os.open(os.devnull, os.O_RDWR) - os.dup2(stderr_fd, stderr_fileno) - yield - os.dup2(stderr_save, stderr_fileno) - os.close(stderr_save) - os.close(stderr_fd) - - @contextmanager def silence_logs(): LOGGER.disabled = True diff --git a/tests/test_anchors.py b/tests/test_anchors.py index b1cc8b7f..d462dbbf 100755 --- a/tests/test_anchors.py +++ b/tests/test_anchors.py @@ -11,7 +11,7 @@ from readalongs.align import align_audio from readalongs.log import LOGGER -from tests.basic_test_case import BasicTestCase, silence_c_stderr +from tests.basic_test_case import BasicTestCase class TestAnchors(BasicTestCase): @@ -88,7 +88,7 @@ def test_anchors_align_modes(self): with open(xml_file, "w", encoding="utf8") as f: print(xml_with_anchors, file=f) with self.assertLogs(LOGGER, level="INFO") as cm: - with silence_c_stderr(), redirect_stderr(StringIO()): + with redirect_stderr(StringIO()): results = align_audio( xml_file, os.path.join(self.data_dir, "noise.mp3"), diff --git a/tests/test_force_align.py b/tests/test_force_align.py index b2a38524..0405fecc 100755 --- a/tests/test_force_align.py +++ b/tests/test_force_align.py @@ -25,7 +25,7 @@ from readalongs.log import LOGGER from readalongs.portable_tempfile import PortableNamedTemporaryFile from readalongs.text.util import load_txt, load_xml, save_xml -from tests.basic_test_case import BasicTestCase, silence_c_stderr +from tests.basic_test_case import BasicTestCase class TestForceAlignment(BasicTestCase): @@ -35,7 +35,7 @@ def test_align(self): """Basic alignment test case with XML input""" xml_path = os.path.join(self.data_dir, "ej-fra.readalong") wav_path = os.path.join(self.data_dir, "ej-fra.m4a") - with silence_c_stderr(), redirect_stderr(StringIO()): + with redirect_stderr(StringIO()): results = align_audio(xml_path, wav_path, unit="w", debug_aligner=True) # Verify that the same IDs are in the output From dc3b1b76d0aebc34834ea13699126588dcd6a470 Mon Sep 17 00:00:00 2001 From: Eric Joanis Date: Thu, 23 Jul 2026 10:30:22 -0400 Subject: [PATCH 02/10] ci: pytest is the way to run tests --- .github/workflows/matrix-tests.yml | 2 +- .github/workflows/tests.yml | 4 ++-- README.md | 4 ---- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/matrix-tests.yml b/.github/workflows/matrix-tests.yml index d9a316d8..4108f361 100644 --- a/.github/workflows/matrix-tests.yml +++ b/.github/workflows/matrix-tests.yml @@ -47,4 +47,4 @@ jobs: run: | pip install -e .[all] - name: Run tests - run: python run_tests.py all + run: pytest diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1bcc69cb..4b7f8cc3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -120,7 +120,7 @@ jobs: pip install -e .[all] - name: Run tests on Windows - run: python run_tests.py all + run: pytest - name: Make sure the CLI outputs utf8 on Windows # Note: we're checking something CLI specific, from a prompt, so we don't want to run @@ -167,4 +167,4 @@ jobs: run: pip install httpx pytest - name: unit test the web API - run: python -m tests.test_web_api + run: pytest tests/test_web_api.py diff --git a/README.md b/README.md index 69966c0b..9aadd564 100644 --- a/README.md +++ b/README.md @@ -160,10 +160,6 @@ or test the installed version (requires installing dev dependencies): pip install 'readalongs[dev]' # if you installed from PyPI, or pip install -e '.[dev]' # if you installed from a local clone - python run_tests.py - -recently, we also made the project compatible with Pytest, so this will run the tests too: - pytest And you can download our [open samples on GitHub](https://github.com/ReadAlongs/OpenSamples) From 06fb420f9efdee5bb96942a6329ab2aee6b946b9 Mon Sep 17 00:00:00 2001 From: Eric Joanis Date: Thu, 23 Jul 2026 11:11:16 -0400 Subject: [PATCH 03/10] refactor(tests): convert many assertions to pytest style - test_align_cli: several conversions by hand - sub self.assertTrue\((.*)\)$ to assert $1 - sub self.assertEqual\(([^,]*), ([^,]*)\)$ by assert $1 == $2 --- tests/test_align_cli.py | 157 +++++++++++++++++-------------------- tests/test_anchors.py | 14 ++-- tests/test_api.py | 42 +++++----- tests/test_audio.py | 8 +- tests/test_config.py | 8 +- tests/test_dna_text.py | 12 +-- tests/test_force_align.py | 16 ++-- tests/test_g2p_cli.py | 78 +++++++++--------- tests/test_make_xml_cli.py | 30 +++---- tests/test_misc.py | 18 ++--- tests/test_package_urls.py | 8 +- tests/test_silence.py | 8 +- tests/test_smil.py | 4 +- tests/test_temp_file.py | 14 ++-- tests/test_tokenize_cli.py | 12 +-- tests/test_tokenize_xml.py | 8 +- tests/test_web_api.py | 96 +++++++++++------------ 17 files changed, 262 insertions(+), 271 deletions(-) diff --git a/tests/test_align_cli.py b/tests/test_align_cli.py index 3654e840..577a6c70 100755 --- a/tests/test_align_cli.py +++ b/tests/test_align_cli.py @@ -5,6 +5,7 @@ """ import os +import re import subprocess import sys import tempfile @@ -57,7 +58,7 @@ def test_invoke_align(self) -> None: ], ) # print(results.output) - self.assertEqual(results.exit_code, 0) + assert results.exit_code == 0 expected_output_files = [ "www/output.readalong", "www/output.m4a", @@ -71,10 +72,9 @@ def test_invoke_align(self) -> None: "www/readme.txt", ] for output_file in expected_output_files: - self.assertTrue( - (output / output_file).exists(), - f"successful alignment should have created {output_file}", - ) + assert ( + output / output_file + ).exists(), f"successful alignment should have created {output_file}" with open(output / "www/index.html", encoding="utf8") as f: contents = f.read() for snippet in ( @@ -82,24 +82,24 @@ def test_invoke_align(self) -> None: 'href="output.readalong"', 'audio="output.m4a"', ): - self.assertIn(snippet, contents) - self.assertTrue( - (output / "tempfiles/output.tokenized.readalong").exists(), - "alignment with -s should have created tempfiles/output.tokenized.readalong", + assert snippet in contents + assert ( + output / "tempfiles/output.tokenized.readalong" + ).exists(), ( + "alignment with -s should have created tempfiles/output.tokenized.readalong" ) with open( output / "tempfiles/output.tokenized.readalong", encoding="utf-8", ) as f: - self.assertNotIn("\ufeff", f.read()) - self.assertTrue( - (output / "www/assets/image-for-page1.jpg").exists(), - "alignment with image files should have copied image-for-page1.jpg to assets", - ) - self.assertIn("image-for-page2.jpg is accessible ", results.output) + assert "\ufeff" not in f.read() + assert ( + output / "www/assets/image-for-page1.jpg" + ).exists(), "alignment with image files should have copied image-for-page1.jpg to assets" + assert "image-for-page2.jpg is accessible " in results.output os.unlink("image-for-page1.jpg") - self.assertFalse(exists("image-for-page1.jpg")) - self.assertRegex(results.output, "Align mode .* succeeded for sequence 0.") + assert not exists("image-for-page1.jpg") + assert "succeeded for sequence 0." in results.output # print(results.output) # Move the alignment output to compare with further down @@ -107,7 +107,7 @@ def test_invoke_align(self) -> None: # changes the contents of the output. output1 = str(output) + "1" os.rename(output, output1) - self.assertFalse(output.exists(), "os.rename() should have moved dir") + assert not output.exists(), "os.rename() should have moved dir" # Run align again, but on an XML input file with various added DNA text results_dna = self.runner.invoke( @@ -125,24 +125,21 @@ def test_invoke_align(self) -> None: str(output), ], ) - self.assertEqual(results_dna.exit_code, 0) + assert results_dna.exit_code == 0 # print(results_dna.output) - self.assertTrue( - (output / "www/output.readalong").exists(), - "successful alignment with DNA should have created output.readalong", - ) - self.assertTrue( - (output / "output.xhtml").exists(), - "successful alignment with -o xhtml should have created output.xhtml", - ) - self.assertIn("Please copy image-for-page1.jpg to ", results_dna.output) - self.assertFalse( - (output / "www/assets/image-for-page1.jpg").exists(), - "image-for-page1.jpg was not on disk, cannot have been copied", - ) - self.assertIn( - "Align mode moderate succeeded for sequence 0.", results_dna.output + assert ( + output / "www/output.readalong" + ).exists(), "successful alignment with DNA should have created output.readalong" + assert ( + output / "output.xhtml" + ).exists(), ( + "successful alignment with -o xhtml should have created output.xhtml" ) + assert "Please copy image-for-page1.jpg to " in results_dna.output + assert not ( + output / "www/assets/image-for-page1.jpg" + ).exists(), "image-for-page1.jpg was not on disk, cannot have been copied" + assert "Align mode moderate succeeded for sequence 0." in results_dna.output # We test error situations in the same test case, since we reuse the same outputs results_output_exists = self.runner.invoke( @@ -153,10 +150,8 @@ def test_invoke_align(self) -> None: str(output), ], ) - self.assertNotEqual(results_output_exists.exit_code, 0) - self.assertIn( - "already exists, use -f to overwrite", results_output_exists.output - ) + assert results_output_exists.exit_code != 0 + assert "already exists, use -f to overwrite" in results_output_exists.output # Output path exists as a regular file results_output_is_regular_file = self.runner.invoke( @@ -167,10 +162,10 @@ def test_invoke_align(self) -> None: str(output / "www/output.readalong"), ], ) - self.assertNotEqual(results_output_is_regular_file, 0) - self.assertIn( - "already exists but is a not a directory", - results_output_is_regular_file.output, + assert results_output_is_regular_file != 0 + assert ( + "already exists but is a not a directory" + in results_output_is_regular_file.output ) def test_align_with_package(self): @@ -191,22 +186,18 @@ def test_align_with_package(self): ], ) # print(results_html.output) - self.assertEqual(results_html.exit_code, 0) - self.assertTrue( - exists(join(output, "Offline-HTML", "html.html")), - "successful html alignment should have created html/Offline-HTML/html.html", - ) + assert results_html.exit_code == 0 + assert exists( + join(output, "Offline-HTML", "html.html") + ), "successful html alignment should have created html/Offline-HTML/html.html" with open(join(output, "Offline-HTML", "html.html"), "rb") as fhtml: path_bytes = fhtml.read() htmldoc = fromstring(path_bytes) b64_pattern = r"data:[\w\/\-\+]*;base64,\w*" - self.assertRegex( - htmldoc.body.xpath("//read-along")[0].attrib["href"], b64_pattern - ) - self.assertRegex( - htmldoc.body.xpath("//read-along")[0].attrib["audio"], b64_pattern - ) + read_along = htmldoc.body.xpath("//read-along")[0] + assert re.search(b64_pattern, read_along.attrib["href"]) + assert re.search(b64_pattern, read_along.attrib["audio"]) def not_test_permission_denied(self): """Non-portable test to make sure denied permission triggers an error -- disabled""" @@ -230,17 +221,17 @@ def not_test_permission_denied(self): dirname, ], ) - self.assertNotEqual(results, 0) - self.assertIn("Cannot write into output folder", results.output) + assert results != 0 + assert "Cannot write into output folder" in results.output def test_langs_cmd(self): """Validates that readalongs langs lists all in-langs that can map to eng-arpabet""" results = self.runner.invoke(langs) - self.assertEqual(results.exit_code, 0) - self.assertIn("crg-tmd", results.output) - self.assertIn("crg-dv ", results.output) - self.assertNotIn("crg ", results.output) - self.assertNotIn("fn-unicode", results.output) + assert results.exit_code == 0 + assert "crg-tmd" in results.output + assert "crg-dv " in results.output + assert "crg " not in results.output + assert "fn-unicode" not in results.output def test_align_english(self): """Validates that the lexicon-based g2p works for English language alignment""" @@ -294,7 +285,7 @@ def test_align_english(self): with open(tokenized_file, encoding="utf8") as f: tok_output = f.read() - self.assertIn(g2p_ref, tok_output) + assert g2p_ref in tok_output def test_invalid_config(self): """unit testing for invalid config specifications""" @@ -310,7 +301,7 @@ def test_invalid_config(self): join(self.tempdir, "out-invalid-config-1"), ], ) - self.assertIn("must be in JSON format", result.output) + assert "must be in JSON format" in result.output # --config parameters needs to contain valid json, test with garbage config_file = join(self.tempdir, "bad-config.json") @@ -326,7 +317,7 @@ def test_invalid_config(self): join(self.tempdir, "out-invalid-config-2"), ], ) - self.assertIn("is not in valid JSON format", result.output) + assert "is not in valid JSON format" in result.output def test_bad_anchors(self): """Make sure invalid anchors yield appropriate errors""" @@ -356,7 +347,7 @@ def test_bad_anchors(self): "Could not parse all anchors", "Aborting.", ]: - self.assertIn(msg, bad_anchors_result.output) + assert msg in bad_anchors_result.output def test_misc_align_errors(self): """Test calling readalongs align with misc CLI errors""" @@ -369,7 +360,7 @@ def test_misc_align_errors(self): ], ) self.assertNotEqual(results.exit_code, 0) - self.assertIn("No input language specified", results.output) + assert "No input language specified" in results.output with SoundSwallowerStub("[NOISE]:0:1"): results = self.runner.invoke( @@ -381,7 +372,7 @@ def test_misc_align_errors(self): ], ) self.assertNotEqual(results.exit_code, 0) - self.assertIn("produced 0 segments", results.output) + assert "produced 0 segments" in results.output with SoundSwallowerStub( "[NOISE]:0:1", "w0:1:1000", ":1000:1100", "w1:1100:2000" @@ -396,7 +387,7 @@ def test_misc_align_errors(self): ) # print(results.output) # We don't check results.exit_code since that's a soft warning, not a hard error - self.assertIn("produced 2 segments", results.output) + assert "produced 2 segments" in results.output self.assertIn( "Alignment produced a different number of segments and tokens than were in the input.", results.output, @@ -418,7 +409,7 @@ def test_infer_plain_text_or_xml(self): ) self.assertNotEqual(results.exit_code, 0) # This error message confirms it's being processed as plain text - self.assertIn("No input language specified for plain text", results.output) + assert "No input language specified for plain text" in results.output # plain text by extension infile2 = write_file(self.tempdir / "infile2.txt", ") not present; tokenizing", log) + assert status == 0 + assert exception is None + assert "Words () not present; tokenizing" in log expected_output_files = ( "www/output.readalong", "www/output.m4a", @@ -69,9 +69,9 @@ def test_call_make_xml(self): self.tempdir / "prepared.readalong", ("fra", "eng"), ) - self.assertEqual(status, 0) - self.assertTrue(exception is None) - self.assertIn("Wrote ", log) + assert status == 0 + assert exception is None + assert "Wrote " in log with open(self.tempdir / "prepared.readalong") as f: xml_text = f.read() self.assertIn('xml:lang="fra" fallback-langs="eng,und"', xml_text) @@ -82,7 +82,7 @@ def test_call_make_xml(self): ("fra", "not-a-lang"), ) self.assertNotEqual(status, 0) - self.assertTrue(isinstance(exception, click.BadParameter)) + assert isinstance(exception, click.BadParameter) (status, exception, log) = api.make_xml( self.data_dir / "file-not-found.txt", @@ -90,12 +90,12 @@ def test_call_make_xml(self): ("fra",), ) self.assertNotEqual(status, 0) - self.assertTrue(isinstance(exception, click.UsageError)) + assert isinstance(exception, click.UsageError) def test_deprecated_prepare(self): with self.assertLogs(LOGGER, level="WARNING") as cm: api.prepare(self.data_dir / "ej-fra.txt", self.tempdir / "foo", ("fra",)) - self.assertIn("deprecated", "\n".join(cm.output)) + assert "deprecated" in "\n".join(cm.output) sentences_to_convert = [ [ @@ -152,7 +152,7 @@ def test_convert_to_readalong(self): ) readalong = re.sub(r"time=\".*?\"", 'time="ttt"', readalong) readalong = re.sub(r"dur=\".*?\"", 'dur="ddd"', readalong) - self.assertEqual(readalong, align_result) + assert readalong == align_result def test_convert_to_offline_html(self): import readalongs.text.make_package as make_package @@ -170,13 +170,13 @@ def test_convert_to_offline_html(self): # with open("test.html", "w", encoding="utf8") as f: # f.write(html) # print(html) - self.assertIn("", html) - self.assertIn("by Jove!", html) + assert "" in html + assert "by Jove!" in html # Make sure the bundles got cached self.assertIsNotNone(make_package.fonts_bundle_contents) @@ -188,7 +188,7 @@ def test_convert_to_offline_html(self): str(self.data_dir / "noise.mp3"), subheader="by Jove!", ) - self.assertEqual(html, html2) + assert html == html2 # Once more, this time pretend we could not fetch the first bundle make_package.fonts_bundle_contents = None @@ -200,7 +200,7 @@ def test_convert_to_offline_html(self): str(self.data_dir / "noise.mp3"), subheader="by Jove!", ) - self.assertEqual(make_package._prev_fonts_status_code, "TIMEOUT") + assert make_package._prev_fonts_status_code == "TIMEOUT" self.assertIsNotNone(make_package.fonts_bundle_contents) self.assertIsNotNone(make_package.js_bundle_contents) @@ -210,13 +210,13 @@ def test_extract_version_from_url(self): # Test that the version is extracted correctly from the URL url = "https://unpkg.com/@readalongs/web-component@1.2.3/dist/bundle.js" version = extract_version_from_url(url) - self.assertEqual(version, "1.2.3") + assert version == "1.2.3" # Test with a URL that doesn't contain a version url = "https://unpkg.com/@readalongs/web-component/dist/bundle.js" with silence_logs(): version = extract_version_from_url(url) - self.assertEqual(version, "unknown") + assert version == "unknown" if __name__ == "__main__": diff --git a/tests/test_audio.py b/tests/test_audio.py index c1d846f9..bbbbc17f 100755 --- a/tests/test_audio.py +++ b/tests/test_audio.py @@ -59,14 +59,14 @@ def test_remove_section(self): """Should remove section of audio""" removed_segment = remove_section(self.audio_segment, 1000, 2000) self.assertNotEqual(len(removed_segment), len(self.audio_segment)) - self.assertEqual(len(removed_segment), len(self.audio_segment) - 1000) + assert len(removed_segment) == len(self.audio_segment) - 1000 def test_rejoin_section(self): """Should rejoin removed/muted sections""" removed_section = self.audio_segment[1000:2000] removed_segment = remove_section(self.audio_segment, 1000, 2000) rejoined_segment = join_section(removed_segment, removed_section, 1000) - self.assertEqual(len(rejoined_segment), len(self.audio_segment)) + assert len(rejoined_segment) == len(self.audio_segment) def test_align_sample(self): """Sanity check that test audio should align""" @@ -115,7 +115,7 @@ def test_align_removed(self): "if dependencies changed.", ) # Make sure ss logs are enabled - self.assertIn("Current configuration", process.stderr) + assert "Current configuration" in process.stderr def test_align_muted(self): """Try aligning section with muted audio""" @@ -156,7 +156,7 @@ def test_write_audio_to_file(self): section = extract_section(self.audio_segment, 1000, 2000) output_path = os.path.join(self.tempdir, "section_output.mp3") write_audio_to_file(section, output_path) - self.assertTrue(os.path.exists(output_path)) + assert os.path.exists(output_path) reloaded_section = read_audio_from_file(output_path) self.assertAlmostEqual( len(section), diff --git a/tests/test_config.py b/tests/test_config.py index 7659cb72..86cf7d4a 100755 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -30,7 +30,7 @@ def test_image(self) -> None: with self.assertRaises(KeyError): new_xml = add_images(self.readalong, {}) new_xml = add_images(self.readalong, {"images": {"0": "test.jpg"}}) - self.assertTrue(len(new_xml.xpath("//graphic")) == 1) + assert len(new_xml.xpath("//graphic")) == 1 with self.assertRaises(TypeError): new_xml = add_images(self.readalong, {"images": [{"0": "test.jpg"}]}) with self.assertRaises(ValueError): @@ -55,7 +55,7 @@ def test_arbitrary_xml(self): ] }, ) - self.assertTrue(len(new_xml.xpath("//test")) == 1) + assert len(new_xml.xpath("//test")) == 1 # bad xml raises lxml.etree.XMLSyntaxError with self.assertRaises(etree.XMLSyntaxError): @@ -76,8 +76,8 @@ def test_arbitrary_xml(self): ] }, ) - self.assertIn("No elements found at //bloop", log_output.getvalue()) - self.assertTrue(len(new_xml.xpath("//shmoop")) == 0) + assert "No elements found at //bloop" in log_output.getvalue() + assert len(new_xml.xpath("//shmoop")) == 0 if __name__ == "__main__": diff --git a/tests/test_dna_text.py b/tests/test_dna_text.py index b7b5c2a7..7d2aef0b 100755 --- a/tests/test_dna_text.py +++ b/tests/test_dna_text.py @@ -37,7 +37,7 @@ def test_tok_all_words(self): """ # print('as_txt="' + as_txt +'"') # print('ref="' + ref +'"') - self.assertEqual(as_txt, ref) + assert as_txt == ref with_ids = add_ids(tokenized) ids_as_txt = etree.tounicode(with_ids) @@ -46,7 +46,7 @@ def test_tok_all_words(self): Bonjour! Comment ça va? Voici une deuxième phrase. """ - self.assertEqual(ids_as_txt, ref_with_ids) + assert ids_as_txt == ref_with_ids def test_tok_some_words(self): """do-not-align text is excluded from tokenization""" @@ -69,7 +69,7 @@ def test_tok_some_words(self): Voici une deuxième phrase. Un mot ou deux à exclure. """ - self.assertEqual(as_txt, ref) + assert as_txt == ref with_ids = add_ids(tokenized) ids_as_txt = etree.tounicode(with_ids) @@ -80,7 +80,7 @@ def test_tok_some_words(self): Voici une deuxième phrase. Un mot ou deux à exclure. """ - self.assertEqual(ids_as_txt, ref_with_ids) + assert ids_as_txt == ref_with_ids def test_tok_div_p_s(self): """Text inside a DNA div, p or s does not get tokenized""" @@ -121,7 +121,7 @@ def test_tok_div_p_s(self):

Trois phrases.

""" - self.assertEqual(as_txt, ref) + assert as_txt == ref with_ids = add_ids(tokenized) ids_as_txt = etree.tounicode(with_ids) @@ -142,7 +142,7 @@ def test_tok_div_p_s(self):

Trois phrases.

""" - self.assertEqual(ids_as_txt, ref_with_ids) + assert ids_as_txt == ref_with_ids def test_dna_word(self): """You can't have a DNA element, that's reserved for tokens to align""" diff --git a/tests/test_force_align.py b/tests/test_force_align.py index 0405fecc..35552ec7 100755 --- a/tests/test_force_align.py +++ b/tests/test_force_align.py @@ -43,9 +43,9 @@ def test_align(self): xml = load_xml(converted_path) words = results["words"] xml_words = xml.xpath(".//w") - self.assertEqual(len(words), len(xml_words)) + assert len(words) == len(xml_words) for w, xw in zip(words, xml_words): - self.assertEqual(xw.attrib["id"], w["id"]) + assert xw.attrib["id"] == w["id"] def test_align_text(self): """Basic alignment test case with plain text input""" @@ -62,17 +62,17 @@ def test_align_text(self): xml = load_xml(converted_path) words = results["words"] xml_words = xml.xpath(".//w") - self.assertEqual(len(words), len(xml_words)) + assert len(words) == len(xml_words) for w, xw in zip(words, xml_words): - self.assertEqual(xw.attrib["id"], w["id"]) + assert xw.attrib["id"] == w["id"] # White-box testing to make sure srt, TextGrid and vtt output will have the # sentences collected correctly. words, sentences = get_word_texts_and_sentences( results["words"], results["tokenized"] ) - self.assertEqual(len(sentences), 7) - self.assertEqual(len(words), 99) + assert len(sentences) == 7 + assert len(words) == 99 def make_element(tag, text="", tail=""): """Convenient Element constructor wrapper""" @@ -157,9 +157,9 @@ def test_align_switch_am(self): xml = load_xml(converted_path) words = results["words"] xml_words = xml.xpath(".//w") - self.assertEqual(len(words), len(xml_words)) + assert len(words) == len(xml_words) for w, xw in zip(words, xml_words): - self.assertEqual(xw.attrib["id"], w["id"]) + assert xw.attrib["id"] == w["id"] def test_align_fail(self): """Alignment test case with bad audio that should fail.""" diff --git a/tests/test_g2p_cli.py b/tests/test_g2p_cli.py index 77f984f8..c125f552 100755 --- a/tests/test_g2p_cli.py +++ b/tests/test_g2p_cli.py @@ -44,27 +44,27 @@ def test_invoke_g2p(self): g2p_file = os.path.join(self.tempdir, "fra-g2p.readalong") results = self.runner.invoke(g2p, [input_file, g2p_file]) # print(f"g2p results.output='{results.output}'") - self.assertEqual(results.exit_code, 0) - self.assertTrue(os.path.exists(os.path.join(self.tempdir, "fra-g2p.readalong"))) + assert results.exit_code == 0 + assert os.path.exists(os.path.join(self.tempdir, "fra-g2p.readalong")) # Testing that it fails when the file already exists has to be in the same test, # otherwise we have a different tempdir and the file won't already exist results = self.runner.invoke(g2p, [input_file, g2p_file]) # print(f"g2p results.output='{results.output}'") self.assertNotEqual(results.exit_code, 0) - self.assertIn("use -f to overwrite", results.output) + assert "use -f to overwrite" in results.output # And add -f to force the overwrite results = self.runner.invoke(g2p, ["-f", input_file, g2p_file]) # print(f"g2p results.output='{results.output}'") - self.assertEqual(results.exit_code, 0) + assert results.exit_code == 0 def test_bad_xml_input(self): """readalongs g2p with invalid XML input""" input_file = os.path.join(self.data_dir, "ej-fra.txt") results = self.runner.invoke(g2p, ["--debug", input_file, "-"]) self.assertNotEqual(results.exit_code, 0) - self.assertIn("Error parsing input file", results.output) + assert "Error parsing input file" in results.output def test_mixed_langs(self): """readalongs g2p with an input containing multiple languages""" @@ -72,8 +72,8 @@ def test_mixed_langs(self): g2p_file = os.path.join(self.tempdir, "mixed-langs.g2p.readalong") results = self.runner.invoke(g2p, [input_file, g2p_file]) # print(f"g2p results.output='{results.output}'") - self.assertEqual(results.exit_code, 0) - self.assertTrue(os.path.exists(g2p_file)) + assert results.exit_code == 0 + assert os.path.exists(g2p_file) ref_file = os.path.join(self.data_dir, "mixed-langs.g2p.readalong") with ( @@ -100,12 +100,12 @@ def test_invoke_with_obsolete_switches(self): g2p, ["--g2p-fallback", "fra:und", input_file, g2p_file] ) self.assertNotEqual(results.exit_code, 0) - self.assertIn("is obsolete", results.output) + assert "is obsolete" in results.output g2p_file = os.path.join(self.tempdir, "obsolete2.readalong") results = self.runner.invoke(g2p, ["--g2p-verbose", input_file, g2p_file]) self.assertNotEqual(results.exit_code, 0) - self.assertIn("is obsolete", results.output) + assert "is obsolete" in results.output # Write text to a temp file, pass it through make-xml -l lang, and then tokenize, # saving the final results into filename. @@ -138,8 +138,8 @@ def test_english_oov(self): f"results.exception={results.exception!r}" ) self.assertNotEqual(results.exit_code, 0) - self.assertIn("could not be g2p", results.output) - # self.assertTrue(isinstance(results.exception, KeyError)) + assert "could not be g2p" in results.output + # assert isinstance(results.exception, KeyError) # with a fall back to und, it works tok_file_with_fallback = os.path.join(self.tempdir, "fallback.readalong") @@ -153,7 +153,7 @@ def test_english_oov(self): f"results.output='{results.output}' " f"results.exception={results.exception!r}" ) - self.assertEqual(results.exit_code, 0) + assert results.exit_code == 0 def test_french_oov(self): """readalongs g2p should handle French OOVs correctly""" @@ -170,7 +170,7 @@ def test_french_oov(self): f"results.exception={results.exception!r}" ) self.assertNotEqual(results.exit_code, 0) - self.assertIn("could not be g2p", results.output) + assert "could not be g2p" in results.output # with a fall back to und, it works tok_file2 = os.path.join(self.tempdir, "tok2.readalong") @@ -185,7 +185,7 @@ def test_french_oov(self): f"results.output='{results.output}' " f"results.exception={results.exception!r}" ) - self.assertEqual(results.exit_code, 0) + assert results.exit_code == 0 def test_three_way_fallback(self): """readalongs g2p --g2p-fallback with multi-step cascades""" @@ -202,8 +202,8 @@ def test_three_way_fallback(self): f"results.output='{results.output}' " f"results.exception={results.exception!r}" ) - self.assertEqual(results.exit_code, 0) - self.assertTrue(os.path.exists(g2p_file)) + assert results.exit_code == 0 + assert os.path.exists(g2p_file) self.assertNotIn("not recognized as IPA", results.output) self.assertNotIn("not fully valid eng-arpabet", results.output) @@ -217,9 +217,9 @@ def test_three_way_fallback(self): f"results.output='{results.output}' " f"results.exception={results.exception!r}" ) - self.assertEqual(results.exit_code, 0) - self.assertIn("not recognized as IPA", results.output) - self.assertIn("not fully valid eng-arpabet", results.output) + assert results.exit_code == 0 + assert "not recognized as IPA" in results.output + assert "not fully valid eng-arpabet" in results.output # this text also works with "und", now that we use unidecode tok_file2 = os.path.join(self.tempdir, "text.tokenized2.readalong") @@ -227,8 +227,8 @@ def test_three_way_fallback(self): "In French été works but Nunavut ᓄᓇᕗᑦ does not.", "eng:und", tok_file2 ) results = self.runner.invoke(g2p, [tok_file2, "-"]) - self.assertEqual(results.exit_code, 0) - self.assertIn("Trying fallback: Und", results.output) + assert results.exit_code == 0 + assert "Trying fallback: Und" in results.output def test_align_with_error(self): """handling g2p errors in readalongs align with --g2p-fallback""" @@ -250,7 +250,7 @@ def test_align_with_error(self): f"results.exception={results.exception!r}" ) self.assertNotEqual(results.exit_code, 0) - self.assertIn("could not be g2p", results.output) + assert "could not be g2p" in results.output self.assertNotIn("Number of aligned segments", results.output) with SoundSwallowerStub("t0b0d0p0s0w0:920:1620", "t0b0d0p0s1w0:1620:1690"): @@ -273,10 +273,10 @@ def test_align_with_error(self): f"results.output='{results.output}' " f"results.exception={results.exception!r}" ) - self.assertIn("Trying fallback: French", results.output) - self.assertIn("Trying fallback: Inuktitut", results.output) + assert "Trying fallback: French" in results.output + assert "Trying fallback: Inuktitut" in results.output self.assertNotIn("could not be g2p", results.output) - self.assertIn("Number of aligned segments", results.output) + assert "Number of aligned segments" in results.output def test_with_stdin(self): """readalongs g2p running with stdin as input""" @@ -284,8 +284,8 @@ def test_with_stdin(self): with open(input_file, encoding="utf8") as f: inputtext = f.read() results = self.runner.invoke(g2p, "-", input=inputtext) - self.assertEqual(results.exit_code, 0) - self.assertIn("S AH S IY", results.output) + assert results.exit_code == 0 + assert "S AH S IY" in results.output def test_align_with_invalid_preg2p(self): """readalongs g2p gracefully handling wrong inputs""" @@ -301,16 +301,16 @@ def test_align_with_invalid_preg2p(self): results = self.runner.invoke(g2p, [input_file, "-"]) self.assertNotEqual(results.exit_code, 0) # print(results.output) - self.assertIn("could not be g2p", results.output) - self.assertIn('word', results.output) - self.assertIn('good', results.output) - self.assertIn('error', results.output) + assert "could not be g2p" in results.output + assert 'word' in results.output + assert 'good' in results.output + assert 'error' in results.output audio_file = os.path.join(self.data_dir, "ej-fra.m4a") with redirect_stderr(StringIO()): with self.assertRaises(RuntimeError) as e: results = align_audio(input_file, audio_file) - self.assertIn("could not be g2p'd", str(e.exception)) + assert "could not be g2p'd" in str(e.exception) def test_align_with_preg2p(self): """readalongs align working on previously g2p'd text""" @@ -387,7 +387,7 @@ def compact_arpabet(xml_string: str) -> str: converted_2 = run_convert_xml( "first part of the wordsecond part of the word" ) - self.assertEqual(compact_arpabet(converted_1), compact_arpabet(converted_2)) + assert compact_arpabet(converted_1) == compact_arpabet(converted_2) def test_convert_xml_subwords(self): """Unit testing for reintroducing subword units""" @@ -418,7 +418,7 @@ def test_convert_xml_subwords(self): moh_eg_with_highlights = "tatiatkèn:sehkwe'" moh_eg_merged = "tatiatkèn:sehkwe'" - self.assertEqual(two_xml_elements(moh_eg_merged), "") + assert two_xml_elements(moh_eg_merged) == "" self.assertEqual( two_xml_elements(run_convert_xml(moh_eg_with_highlights)), two_xml_elements(run_convert_xml(moh_eg_merged)), @@ -442,7 +442,7 @@ def test_convert_xml_subwords(self): """ with self.assertLogs(LOGGER, level="WARNING") as cm: result = run_convert_xml(example_with_fallback_lang) - self.assertIn("S AH S IY N AO T _ZH EH AE L L UW _IY K UW", result) + assert "S AH S IY N AO T _ZH EH AE L L UW _IY K UW" in result logger_output = "\n".join(cm.output) self.assertIn( 'No valid g2p conversion found for "not_really_iku"', logger_output @@ -455,7 +455,7 @@ def test_convert_xml_invalid(self): self.assertEqual( etree.tounicode(c_xml), 'valid' ) - self.assertTrue(valid, "convert_xml with valid pre-g2p'd text") + assert valid, "convert_xml with valid pre-g2p'd text" xml = parse_xml('invalid') with redirect_stderr(StringIO()): @@ -478,9 +478,9 @@ def test_invalid_langs_in_xml(self): c_xml, valid, _ = convert_xml(xml, verbose_warnings=True) self.assertFalse(valid) logger_output = "\n".join(cm.output) - self.assertIn("No lang", logger_output) - self.assertIn("foo", logger_output) - self.assertIn('no path from "crx-syl"', logger_output) + assert "No lang" in logger_output + assert "foo" in logger_output + assert 'no path from "crx-syl"' in logger_output def test_non_convertible_words(self): xml = parse_xml("43:2365:6743:23") diff --git a/tests/test_make_xml_cli.py b/tests/test_make_xml_cli.py index dbdc2996..44e1837d 100755 --- a/tests/test_make_xml_cli.py +++ b/tests/test_make_xml_cli.py @@ -40,7 +40,7 @@ def test_invoke_prepare(self): make_xml, ["-l", "atj", "-d", self.empty_file, os.path.join(self.tempdir, "delme")], ) - self.assertEqual(results.exit_code, 0) + assert results.exit_code == 0 def test_invoke_make_xml(self): """Basic usage of readalongs make-xml""" @@ -48,7 +48,7 @@ def test_invoke_make_xml(self): make_xml, ["-l", "atj", "-d", self.empty_file, os.path.join(self.tempdir, "delme")], ) - self.assertEqual(results.exit_code, 0) + assert results.exit_code == 0 self.assertRegex(results.output, "Running readalongs make-xml") def test_no_lang(self): @@ -84,15 +84,15 @@ def test_output_exists(self): results = self.runner.invoke( make_xml, ["-l", "fra", os.path.join(self.data_dir, "fra.txt"), xmlfile] ) - self.assertEqual(results.exit_code, 0) - self.assertTrue(os.path.exists(xmlfile), "output xmlfile did not get created") + assert results.exit_code == 0 + assert os.path.exists(xmlfile), "output xmlfile did not get created" def test_output_correct(self): """Make sure the contents of readalongs make-xml's output file is correct.""" input_file = os.path.join(self.data_dir, "fra.txt") xml_file = os.path.join(self.tempdir, "fra.readalong") results = self.runner.invoke(make_xml, ["-l", "fra", input_file, xml_file]) - self.assertEqual(results.exit_code, 0) + assert results.exit_code == 0 ref_file = os.path.join(self.data_dir, "fra-prepared.readalong") with ( @@ -115,9 +115,9 @@ def test_input_is_stdin(self): results = self.runner.invoke(make_xml, "-l fra -", input="Ceci est un test.") # LOGGER.warning("Output: {}".format(results.output)) # LOGGER.warning("Exception: {}".format(results.exception)) - self.assertEqual(results.exit_code, 0) - self.assertIn("Ceci est un test", results.stdout) - self.assertIn('Ceci est un test" in results.stdout + assert '', results.output) results = self.runner.invoke(make_xml, ["-l", "fra,iku:und", input_file, "-"]) - self.assertEqual(results.exit_code, 0) + assert results.exit_code == 0 self.assertIn('', results.output) results = self.runner.invoke( make_xml, ["-l", "fra:iku", "-l", "und", input_file, "-"] ) - self.assertEqual(results.exit_code, 0) + assert results.exit_code == 0 self.assertIn('', results.output) def test_make_xml_invalid_lang(self): @@ -232,7 +232,7 @@ def test_make_xml_invalid_utf8_input(self): # Read noise.mp3 as if it was utf8 text, via create_input_ras(input_file_handle) results = self.runner.invoke(make_xml, ["-l", "fra", noise_file, "-"]) self.assertNotEqual(results.exit_code, 0) - self.assertIn("provide a correctly encoded utf-8", results.output) + assert "provide a correctly encoded utf-8" in results.output # Read noise.mp3 as if it was utf8 text, via create_input_ras(input_file_name) results = self.runner.invoke( @@ -240,7 +240,7 @@ def test_make_xml_invalid_utf8_input(self): ["-l", "fra", noise_file, os.path.join(self.tempdir, "noise.readalong")], ) self.assertNotEqual(results.exit_code, 0) - self.assertIn("provide a correctly encoded utf-8", results.output) + assert "provide a correctly encoded utf-8" in results.output # align also calls create_input_ras(input_file_name) results = self.runner.invoke( @@ -254,7 +254,7 @@ def test_make_xml_invalid_utf8_input(self): ], ) self.assertNotEqual(results.exit_code, 0) - self.assertIn("provide a correctly encoded utf-8", results.output) + assert "provide a correctly encoded utf-8" in results.output def test_blank_lines_stripped(self): """Blank lines for paragraph and page breaks are allowed to have whitespace""" diff --git a/tests/test_misc.py b/tests/test_misc.py index 947b3000..3731bfec 100755 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -87,7 +87,7 @@ def test_split_silences(self): ("5", 5.000, 6.050), ) ] - self.assertEqual(words, ref) + assert words == ref def test_get_attrib_recursive(self): raw_xml = """ @@ -253,7 +253,7 @@ def test_parse_xml(self): xml_text = 'text' xml = parse_xml(xml_text) xml2 = parse_xml(bytes(xml_text, encoding="latin1")) - self.assertEqual(etree.tostring(xml), etree.tostring(xml2)) + assert etree.tostring(xml) == etree.tostring(xml2) malformed_xml_text = "Ceci est une phrase", results.output ) @@ -56,13 +56,13 @@ def test_file_already_exists(self): """Test that readalongs tokenize does not overwrite existing files by default""" results = self.runner.invoke(tokenize, [self.rasfile, self.rasfile]) self.assertNotEqual(results.exit_code, 0) - self.assertIn("use -f to overwrite", results.output) + assert "use -f to overwrite" in results.output def test_bad_input(self): """Test readalongs tokenize with invalid XML as input""" results = self.runner.invoke(tokenize, "- -", input="this is not XML!") self.assertNotEqual(results.exit_code, 0) - self.assertIn("Error parsing", results.output) + assert "Error parsing" in results.output # LOGGER.warning("Output: {}".format(results.output)) # LOGGER.warning("Exception: {}".format(results.exception)) diff --git a/tests/test_tokenize_xml.py b/tests/test_tokenize_xml.py index e9378edb..6baf15f4 100755 --- a/tests/test_tokenize_xml.py +++ b/tests/test_tokenize_xml.py @@ -30,7 +30,7 @@ def test_simple(self): with redirect_stderr(StringIO()): tokenized = tokenize_xml.tokenize_xml(xml) # print(etree.tounicode(tokenized)) - self.assertEqual(etree.tounicode(tokenized), ref) + assert etree.tounicode(tokenized) == ref def test_mixed_lang(self): """Tokenization test case with mixed languages""" @@ -47,7 +47,7 @@ def test_mixed_lang(self): with redirect_stderr(StringIO()): tokenized = tokenize_xml.tokenize_xml(xml) # print(etree.tounicode(tokenized)) - self.assertEqual(etree.tounicode(tokenized), ref) + assert etree.tounicode(tokenized) == ref def test_mixed_words(self): """Tokenization should be bypassed when elements are already found in the input""" @@ -64,7 +64,7 @@ def test_mixed_words(self): with redirect_stderr(StringIO()): tokenized = tokenize_xml.tokenize_xml(xml) # print(etree.tounicode(tokenized)) - self.assertEqual(etree.tounicode(tokenized), ref) + assert etree.tounicode(tokenized) == ref def test_comments(self): """Make sure tokenize_xml ignores stuff inside comments""" @@ -83,7 +83,7 @@ def test_comments(self): with redirect_stderr(StringIO()): tokenized = tokenize_xml.tokenize_xml(xml) # print(etree.tounicode(tokenized)) - self.assertEqual(etree.tounicode(tokenized), ref) + assert etree.tounicode(tokenized) == ref if __name__ == "__main__": diff --git a/tests/test_web_api.py b/tests/test_web_api.py index 3553cdd0..dab7d8ce 100755 --- a/tests/test_web_api.py +++ b/tests/test_web_api.py @@ -55,20 +55,20 @@ def test_assemble_from_plain_text(self): response = self.API_CLIENT.post("/api/v1/assemble", json=request) resp_dict = json.loads(response.content.decode("utf-8")) - self.assertEqual(response.status_code, 200) - self.assertTrue(resp_dict["processed_ras"].find("= 0) + assert response.status_code == 200 + assert resp_dict["processed_ras"].find("= 0 def test_bad_path(self): # Test a request to a path that doesn't exist with redirect_stderr(StringIO()): response = self.API_CLIENT.get("/pathdoesntexist") - self.assertEqual(response.status_code, 404) + assert response.status_code == 404 def test_bad_method(self): # Test a request to a valid path with a bad method with redirect_stderr(StringIO()): response = self.API_CLIENT.get("/api/v1/assemble") - self.assertEqual(response.status_code, 405) + assert response.status_code == 405 def test_assemble_from_xml(self): # Test the assemble endpoint with XML @@ -80,7 +80,7 @@ def test_assemble_from_xml(self): } with redirect_stderr(StringIO()): response = self.API_CLIENT.post("/api/v1/assemble", json=request) - self.assertEqual(response.status_code, 200) + assert response.status_code == 200 def test_illformed_xml(self): # Test the assemble endpoint with ill-formed XML @@ -91,7 +91,7 @@ def test_illformed_xml(self): } with redirect_stderr(StringIO()): response = self.API_CLIENT.post("/api/v1/assemble", json=request) - self.assertEqual(response.status_code, 422) + assert response.status_code == 422 def test_invalid_ras(self): # Test the assemble endpoint with invalid RAS XML @@ -102,7 +102,7 @@ def test_invalid_ras(self): } with redirect_stderr(StringIO()): response = self.API_CLIENT.post("/api/v1/assemble", json=request) - self.assertEqual(response.status_code, 422) + assert response.status_code == 422 def test_create_grammar(self): # Test the create grammar function @@ -113,9 +113,9 @@ def test_create_grammar(self): g2ped, valid, _ = convert_xml(ids_added) word_dict, text = create_grammar(g2ped) - self.assertTrue(valid) - self.assertEqual(len(word_dict), len(text.split())) - self.assertEqual(len(word_dict), 99) + assert valid + assert len(word_dict) == len(text.split()) + assert len(word_dict) == 99 def test_g2p_exceeds_time_limit(self): # preprocessing takes about 5 ms, g2p about 200 ms, use 50 ms is nicely between @@ -129,10 +129,10 @@ def test_g2p_exceeds_time_limit(self): } with redirect_stderr(StringIO()): response = self.API_CLIENT.post("/api/v1/assemble", json=request) - self.assertEqual(response.status_code, 422) + assert response.status_code == 422 # But still let the test pass if it's the preprocessing that fails by # asserting the common substring of the two possible failure messages. - self.assertIn("exceeded time limit", response.json()["detail"]) + assert "exceeded time limit" in response.json()["detail"] def test_prepro_exceeds_time_limit(self): # preprocessing takes about 5 ms, so 1 micros is guaranteed to be too short on any hardware. @@ -145,7 +145,7 @@ def test_prepro_exceeds_time_limit(self): } with redirect_stderr(StringIO()): response = self.API_CLIENT.post("/api/v1/assemble", json=request) - self.assertEqual(response.status_code, 422) + assert response.status_code == 422 self.assertIn( "Preprocessing the input exceeded time limit", response.json()["detail"] ) @@ -167,9 +167,9 @@ def test_convert_time_limit(self): _, valid, _ = convert_xml( ids_added, time_limit=100, start_time=perf_counter() - 1.0 ) - self.assertTrue(valid) + assert valid _, valid, _ = convert_xml(ids_added, time_limit=100) - self.assertTrue(valid) + assert valid def test_bad_g2p(self): # Test the assemble endpoint with invalid g2p languages @@ -180,8 +180,8 @@ def test_bad_g2p(self): } with redirect_stderr(StringIO()): response = self.API_CLIENT.post("/api/v1/assemble", json=request) - self.assertIn("No language called", response.json()["detail"]) - self.assertEqual(response.status_code, 422) + assert "No language called" in response.json()["detail"] + assert response.status_code == 422 def test_g2p_faiture(self): # Test the assemble endpoint where g2p actually fails @@ -192,9 +192,9 @@ def test_g2p_faiture(self): } with redirect_stderr(StringIO()): response = self.API_CLIENT.post("/api/v1/assemble", json=request) - self.assertEqual(response.status_code, 422) + assert response.status_code == 422 content = response.json() - self.assertIn("These words could not", content["detail"]) + assert "These words could not" in content["detail"] def test_no_words(self): # Test the assemble endpoint with no actual words in the text @@ -205,9 +205,9 @@ def test_no_words(self): } with redirect_stderr(StringIO()): response = self.API_CLIENT.post("/api/v1/assemble", json=request) - self.assertEqual(response.status_code, 422) + assert response.status_code == 422 content = response.json() - self.assertIn("Could not find any words", content["detail"]) + assert "Could not find any words" in content["detail"] def test_empty_g2p(self): # When the input has numbers of non-g2p-able stuff, let's give the user @@ -219,22 +219,22 @@ def test_empty_g2p(self): } with redirect_stderr(StringIO()): response = self.API_CLIENT.post("/api/v1/assemble", json=request) - self.assertEqual(response.status_code, 422) + assert response.status_code == 422 content = response.json() content_log = content["detail"] for message_part in ["These words could not", "24", "23"]: - self.assertIn(message_part, content_log) + assert message_part in content_log self.assertEqual(content["g2p_error_words"], ["24", "23", "99", "1234"]) - self.assertIn("partial_ras", content) + assert "partial_ras" in content def test_langs(self): # Test the langs endpoint with redirect_stderr(StringIO()): response = self.API_CLIENT.get("/api/v1/langs") codes = [x["code"] for x in response.json()] - self.assertEqual(set(codes), set(get_langs()[0])) - self.assertEqual(codes, list(sorted(codes))) + assert set(codes) == set(get_langs()[0]) + assert codes == list(sorted(codes)) self.assertEqual( dict((x["code"], x["names"]["_"]) for x in response.json()), get_langs()[1] ) @@ -251,7 +251,7 @@ def test_logs(self): response = self.API_CLIENT.post("/api/v1/assemble", json=request) content = response.json() # print("Content", content) - self.assertIn('Could not g2p "ña" as French', content["log"]) + assert 'Could not g2p "ña" as French' in content["log"] def test_debug(self): # Test the assemble endpoint with debug mode on @@ -264,7 +264,7 @@ def test_debug(self): with redirect_stderr(StringIO()): response = self.API_CLIENT.post("/api/v1/assemble", json=request) content = response.json() - self.assertEqual(content["input"], request) + assert content["input"] == request self.assertGreater(len(content["tokenized"]), 10) self.assertGreater(len(content["parsed"]), 10) self.assertGreater(len(content["g2ped"]), 10) @@ -313,8 +313,8 @@ def test_convert_to_TextGrid(self): response = self.API_CLIENT.post( "/api/v1/convert_alignment/textgrid", json=request ) - self.assertEqual(response.status_code, 200) - self.assertIn("aligned.TextGrid", response.headers["content-disposition"]) + assert response.status_code == 200 + assert "aligned.TextGrid" in response.headers["content-disposition"] self.assertEqual( response.text, dedent( @@ -378,8 +378,8 @@ class = "IntervalTier" response = self.API_CLIENT.post( "/api/v1/convert_alignment/textgrid", json=request ) - self.assertEqual(response.status_code, 200) - self.assertIn("aligned.TextGrid", response.headers["content-disposition"]) + assert response.status_code == 200 + assert "aligned.TextGrid" in response.headers["content-disposition"] self.assertNotIn("xmax = 83.100000", response.text) def test_convert_to_eaf(self): @@ -391,9 +391,9 @@ def test_convert_to_eaf(self): response = self.API_CLIENT.post( "/api/v1/convert_alignment/eaf", json=request ) - self.assertEqual(response.status_code, 200) - self.assertIn("LF, for Windows. dedent( @@ -421,8 +421,8 @@ def test_convert_to_srt(self): response = self.API_CLIENT.post( "/api/v1/convert_alignment/srt?tier=word", json=request ) - self.assertEqual(response.status_code, 200) - self.assertIn("aligned_words.srt", response.headers["content-disposition"]) + assert response.status_code == 200 + assert "aligned_words.srt" in response.headers["content-disposition"] self.assertEqual( response.text.replace("\r", "").strip(), # CRLF->LF, for Windows dedent( @@ -448,8 +448,8 @@ def test_convert_to_vtt(self): response = self.API_CLIENT.post( "/api/v1/convert_alignment/vtt?tier=sentence", json=request ) - self.assertEqual(response.status_code, 200) - self.assertIn("aligned_sentences.vtt", response.headers["content-disposition"]) + assert response.status_code == 200 + assert "aligned_sentences.vtt" in response.headers["content-disposition"] self.assertEqual( response.text.replace("\r", ""), # CRLF->LF, in case we're on Windows. dedent( @@ -466,8 +466,8 @@ def test_convert_to_vtt(self): response = self.API_CLIENT.post( "/api/v1/convert_alignment/vtt?tier=word", json=request ) - self.assertEqual(response.status_code, 200) - self.assertIn("aligned_words.vtt", response.headers["content-disposition"]) + assert response.status_code == 200 + assert "aligned_words.vtt" in response.headers["content-disposition"] self.assertEqual( response.text.replace("\r", ""), # CRLF->LF, in case we're on Windows. dedent( @@ -514,7 +514,7 @@ def test_cleanup_temp_dir(self): response = self.API_CLIENT.post( "/api/v1/convert_alignment/textgrid", json=request ) - self.assertEqual(response.status_code, 200) + assert response.status_code == 200 # print(log_cm.output) match = re.search( "Temporary directory: (.*)($|\r|\n)", "\n".join(log_cm.output) @@ -559,7 +559,7 @@ def test_cleanup_even_if_error(self): response = self.API_CLIENT.post( f"/api/v1/convert_alignment/{format_name.value}", json=request ) - self.assertEqual(response.status_code, 422) + assert response.status_code == 422 # print(log_cm.output) match = re.search( "Temporary directory: (.*)($|\r|\n)", "\n".join(log_cm.output) @@ -577,7 +577,7 @@ def test_convert_to_bad_format(self): response = self.API_CLIENT.post( "/api/v1/convert_alignment/badformat", json=request ) - self.assertEqual(response.status_code, 422) + assert response.status_code == 422 request = { "dur": 83.1, @@ -585,13 +585,13 @@ def test_convert_to_bad_format(self): } with redirect_stderr(StringIO()): response = self.API_CLIENT.post("/api/v1/convert_alignment", json=request) - self.assertEqual(response.status_code, 404) + assert response.status_code == 404 with redirect_stderr(StringIO()): response = self.API_CLIENT.post( "/api/v1/convert_alignment/vtt?tier=badtier", json=request ) - self.assertEqual(response.status_code, 422) + assert response.status_code == 422 if __name__ == "__main__": From 4bfc6d6c1b16dc5f4226aebbc32c9c56ca3877e6 Mon Sep 17 00:00:00 2001 From: Eric Joanis Date: Thu, 23 Jul 2026 11:28:37 -0400 Subject: [PATCH 04/10] refactor(tests): more conversions to pytest assertions sub self.assertFalse\((.*)\)$ by assert not $1 sub self.assertNotEqual\(([^,]*), ([^,]*)\)$ by assert $1 != $2 sub self.assertNotIn\(([^,]*), ([^,]*)\)$ by assert $1 not in $2 sub self.assertIsNotNone\(([^,]*)\)$ by assert $1 is not None sub with self.assertRaises\(([^,]*)\): by with raises($1): --- tests/test_align_cli.py | 26 +++++++++++++------------- tests/test_anchors.py | 12 ++++++------ tests/test_api.py | 16 ++++++++-------- tests/test_audio.py | 4 ++-- tests/test_config.py | 14 +++++++------- tests/test_g2p_cli.py | 30 +++++++++++++++--------------- tests/test_make_xml_cli.py | 20 ++++++++++---------- tests/test_misc.py | 14 +++++++------- tests/test_package_urls.py | 2 +- tests/test_silence.py | 2 +- tests/test_smil.py | 10 +++++----- tests/test_temp_file.py | 4 ++-- tests/test_tokenize_cli.py | 4 ++-- tests/test_web_api.py | 16 ++++++++-------- 14 files changed, 87 insertions(+), 87 deletions(-) diff --git a/tests/test_align_cli.py b/tests/test_align_cli.py index 577a6c70..f017224a 100755 --- a/tests/test_align_cli.py +++ b/tests/test_align_cli.py @@ -359,7 +359,7 @@ def test_misc_align_errors(self): join(self.tempdir, "out-missing-l"), ], ) - self.assertNotEqual(results.exit_code, 0) + assert results.exit_code != 0 assert "No input language specified" in results.output with SoundSwallowerStub("[NOISE]:0:1"): @@ -371,7 +371,7 @@ def test_misc_align_errors(self): join(self.tempdir, "noise-only"), ], ) - self.assertNotEqual(results.exit_code, 0) + assert results.exit_code != 0 assert "produced 0 segments" in results.output with SoundSwallowerStub( @@ -407,7 +407,7 @@ def test_infer_plain_text_or_xml(self): join(self.tempdir, "outdir1"), ], ) - self.assertNotEqual(results.exit_code, 0) + assert results.exit_code != 0 # This error message confirms it's being processed as plain text assert "No input language specified for plain text" in results.output @@ -422,7 +422,7 @@ def test_infer_plain_text_or_xml(self): join(self.tempdir, "outdir2"), ], ) - self.assertNotEqual(results.exit_code, 0) + assert results.exit_code != 0 # This error message confirms it's being processed as plain text assert "No input language specified for plain text" in results.output @@ -458,7 +458,7 @@ def test_infer_plain_text_or_xml(self): join(self.tempdir, "outdir4"), ], ) - self.assertNotEqual(results.exit_code, 0) + assert results.exit_code != 0 assert "Error parsing XML" in results.output # XML by file extension @@ -472,7 +472,7 @@ def test_infer_plain_text_or_xml(self): join(self.tempdir, "outdir5"), ], ) - self.assertNotEqual(results.exit_code, 0) + assert results.exit_code != 0 assert "Error parsing XML" in results.output def test_obsolete_switches(self): @@ -487,7 +487,7 @@ def test_obsolete_switches(self): join(self.tempdir, "outdir6"), ], ) - self.assertNotEqual(results.exit_code, 0) + assert results.exit_code != 0 assert "is obsolete." in results.output # Giving --g2p-verbose switch generates an obsolete-switch error message @@ -501,7 +501,7 @@ def test_obsolete_switches(self): join(self.tempdir, "outdir7"), ], ) - self.assertNotEqual(results.exit_code, 0) + assert results.exit_code != 0 assert "is obsolete." in results.output # Giving --g2p-fallback switch generates an obsolete-switch error message @@ -516,7 +516,7 @@ def test_obsolete_switches(self): join(self.tempdir, "outdir8"), ], ) - self.assertNotEqual(results.exit_code, 0) + assert results.exit_code != 0 assert "is obsolete." in results.output def test_oo_option(self): @@ -545,7 +545,7 @@ def test_oo_option(self): join(self.tempdir, "outdir10"), ], ) - self.assertNotEqual(results.exit_code, 0) + assert results.exit_code != 0 assert "Could not g2p" in results.output assert "not-an-alphabet" in results.output @@ -560,7 +560,7 @@ def test_oo_option(self): join(self.tempdir, "outdir11"), ], ) - self.assertNotEqual(results.exit_code, 0) + assert results.exit_code != 0 assert "Could not g2p" in results.output assert "no path" in results.output @@ -577,7 +577,7 @@ def test_oo_option(self): join(self.tempdir, "outdir12"), ], ) - self.assertNotEqual(results.exit_code, 0) + assert results.exit_code != 0 assert "Could not g2p" in results.output assert 'no path from "eng" to ' in results.output @@ -617,7 +617,7 @@ def slurp_text(filename, encoding): self.assertEqual( slurp_text(bom_file, "utf-8"), "\ufeff" + slurp_text(base_file, "utf-8") ) - self.assertNotEqual(slurp_bin(base_file), slurp_bin(bom_file)) + assert slurp_bin(base_file) != slurp_bin(bom_file) assert b"\xef\xbb\xbf" + slurp_bin(base_file) == slurp_bin(bom_file) bom_file_pathlib = self.add_bom(Path(base_file)) diff --git a/tests/test_anchors.py b/tests/test_anchors.py index 8d21eb06..04d7adba 100755 --- a/tests/test_anchors.py +++ b/tests/test_anchors.py @@ -31,12 +31,12 @@ def test_anchors_inner_only(self): assert len(words) == 99 # Make sure the aligned segments stay on the right side of their anchors - self.assertLessEqual(words[0]["end"], 1.62) - self.assertGreaterEqual(words[1]["start"], 1.62) - self.assertLessEqual(words[8]["end"], 3.81) - self.assertGreaterEqual(words[9]["start"], 3.82) - self.assertLessEqual(words[21]["end"], 6.74) - self.assertGreaterEqual(words[22]["start"], 6.74) + assert words[0]["end"] <= 1.62 + assert words[1]["start"] >= 1.62 + assert words[8]["end"] <= 3.81 + assert words[9]["start"] >= 3.81 + assert words[21]["end"] <= 6.74 + assert words[22]["start"] >= 6.74 def test_anchors_outer_too(self): """Test aligning with anchors defining DNA segments at start and end too""" diff --git a/tests/test_api.py b/tests/test_api.py index bf4c3377..61dde3c7 100755 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -59,8 +59,8 @@ def test_call_align(self): with redirect_stderr(StringIO()): (status, exception, log) = api.align("", "", self.tempdir / "errors") - self.assertNotEqual(status, 0) - self.assertFalse(exception is None) + assert status != 0 + assert exception is not None def test_call_make_xml(self): with redirect_stderr(StringIO()): @@ -81,7 +81,7 @@ def test_call_make_xml(self): self.tempdir / "bad.readalong", ("fra", "not-a-lang"), ) - self.assertNotEqual(status, 0) + assert status != 0 assert isinstance(exception, click.BadParameter) (status, exception, log) = api.make_xml( @@ -89,7 +89,7 @@ def test_call_make_xml(self): self.tempdir / "none.readalong", ("fra",), ) - self.assertNotEqual(status, 0) + assert status != 0 assert isinstance(exception, click.UsageError) def test_deprecated_prepare(self): @@ -179,8 +179,8 @@ def test_convert_to_offline_html(self): assert "by Jove!" in html # Make sure the bundles got cached - self.assertIsNotNone(make_package.fonts_bundle_contents) - self.assertIsNotNone(make_package.js_bundle_contents) + assert make_package.fonts_bundle_contents is not None + assert make_package.js_bundle_contents is not None # And convert again, this time it's going to use the cached bundles. html2, _ = api.convert_prealigned_text_to_offline_html( @@ -201,8 +201,8 @@ def test_convert_to_offline_html(self): subheader="by Jove!", ) assert make_package._prev_fonts_status_code == "TIMEOUT" - self.assertIsNotNone(make_package.fonts_bundle_contents) - self.assertIsNotNone(make_package.js_bundle_contents) + assert make_package.fonts_bundle_contents is not None + assert make_package.js_bundle_contents is not None def test_extract_version_from_url(self): from readalongs.text.make_package import extract_version_from_url diff --git a/tests/test_audio.py b/tests/test_audio.py index bbbbc17f..9954a20c 100755 --- a/tests/test_audio.py +++ b/tests/test_audio.py @@ -58,7 +58,7 @@ def test_mute_section(self): def test_remove_section(self): """Should remove section of audio""" removed_segment = remove_section(self.audio_segment, 1000, 2000) - self.assertNotEqual(len(removed_segment), len(self.audio_segment)) + assert len(removed_segment) != len(self.audio_segment) assert len(removed_segment) == len(self.audio_segment) - 1000 def test_rejoin_section(self): @@ -88,7 +88,7 @@ def test_align_sample(self): "if dependencies changed.", ) # Make sure ss logs are disabled - self.assertNotIn("Current configuration", process.stderr) + assert "Current configuration" not in process.stderr def test_align_removed(self): """Try aligning section with removed audio""" diff --git a/tests/test_config.py b/tests/test_config.py index 86cf7d4a..c8e5742d 100755 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -9,7 +9,7 @@ from unittest import TestCase from lxml import etree -from pytest import main +from pytest import main, raises from readalongs.text.add_elements_to_xml import add_images, add_supplementary_xml from readalongs.text.util import load_xml @@ -27,22 +27,22 @@ def setUpClass(cls) -> None: def test_image(self) -> None: """Test images are added correctly""" - with self.assertRaises(KeyError): + with raises(KeyError): new_xml = add_images(self.readalong, {}) new_xml = add_images(self.readalong, {"images": {"0": "test.jpg"}}) assert len(new_xml.xpath("//graphic")) == 1 - with self.assertRaises(TypeError): + with raises(TypeError): new_xml = add_images(self.readalong, {"images": [{"0": "test.jpg"}]}) - with self.assertRaises(ValueError): + with raises(ValueError): new_xml = add_images(self.readalong, {"images": {"a": "test.jpg"}}) - with self.assertRaises(IndexError): + with raises(IndexError): new_xml = add_images( self.readalong, {"images": {"0": "test.jpg", "999": "out_of_range.jpg"}} ) def test_arbitrary_xml(self): """Test arbitrary xml is added correctly""" - with self.assertRaises(KeyError): + with raises(KeyError): new_xml = add_supplementary_xml(self.readalong, {}) new_xml = add_supplementary_xml( self.readalong, @@ -58,7 +58,7 @@ def test_arbitrary_xml(self): assert len(new_xml.xpath("//test")) == 1 # bad xml raises lxml.etree.XMLSyntaxError - with self.assertRaises(etree.XMLSyntaxError): + with raises(etree.XMLSyntaxError): new_xml = add_supplementary_xml( self.readalong, {"xml": [{"xpath": "//div[1]", "value": "bloop"}]} ) diff --git a/tests/test_g2p_cli.py b/tests/test_g2p_cli.py index c125f552..f4f43a82 100755 --- a/tests/test_g2p_cli.py +++ b/tests/test_g2p_cli.py @@ -51,7 +51,7 @@ def test_invoke_g2p(self): # otherwise we have a different tempdir and the file won't already exist results = self.runner.invoke(g2p, [input_file, g2p_file]) # print(f"g2p results.output='{results.output}'") - self.assertNotEqual(results.exit_code, 0) + assert results.exit_code != 0 assert "use -f to overwrite" in results.output # And add -f to force the overwrite @@ -63,7 +63,7 @@ def test_bad_xml_input(self): """readalongs g2p with invalid XML input""" input_file = os.path.join(self.data_dir, "ej-fra.txt") results = self.runner.invoke(g2p, ["--debug", input_file, "-"]) - self.assertNotEqual(results.exit_code, 0) + assert results.exit_code != 0 assert "Error parsing input file" in results.output def test_mixed_langs(self): @@ -99,12 +99,12 @@ def test_invoke_with_obsolete_switches(self): results = self.runner.invoke( g2p, ["--g2p-fallback", "fra:und", input_file, g2p_file] ) - self.assertNotEqual(results.exit_code, 0) + assert results.exit_code != 0 assert "is obsolete" in results.output g2p_file = os.path.join(self.tempdir, "obsolete2.readalong") results = self.runner.invoke(g2p, ["--g2p-verbose", input_file, g2p_file]) - self.assertNotEqual(results.exit_code, 0) + assert results.exit_code != 0 assert "is obsolete" in results.output # Write text to a temp file, pass it through make-xml -l lang, and then tokenize, @@ -137,7 +137,7 @@ def test_english_oov(self): f"results.output='{results.output}' " f"results.exception={results.exception!r}" ) - self.assertNotEqual(results.exit_code, 0) + assert results.exit_code != 0 assert "could not be g2p" in results.output # assert isinstance(results.exception, KeyError) @@ -169,7 +169,7 @@ def test_french_oov(self): f"results.output='{results.output}' " f"results.exception={results.exception!r}" ) - self.assertNotEqual(results.exit_code, 0) + assert results.exit_code != 0 assert "could not be g2p" in results.output # with a fall back to und, it works @@ -204,8 +204,8 @@ def test_three_way_fallback(self): ) assert results.exit_code == 0 assert os.path.exists(g2p_file) - self.assertNotIn("not recognized as IPA", results.output) - self.assertNotIn("not fully valid eng-arpabet", results.output) + assert "not recognized as IPA" not in results.output + assert "not fully valid eng-arpabet" not in results.output # Run with verbose output and look for the warning messages results = self.runner.invoke( @@ -249,9 +249,9 @@ def test_align_with_error(self): f"results.output='{results.output}' " f"results.exception={results.exception!r}" ) - self.assertNotEqual(results.exit_code, 0) + assert results.exit_code != 0 assert "could not be g2p" in results.output - self.assertNotIn("Number of aligned segments", results.output) + assert "Number of aligned segments" not in results.output with SoundSwallowerStub("t0b0d0p0s0w0:920:1620", "t0b0d0p0s1w0:1620:1690"): results = self.runner.invoke( @@ -275,7 +275,7 @@ def test_align_with_error(self): ) assert "Trying fallback: French" in results.output assert "Trying fallback: Inuktitut" in results.output - self.assertNotIn("could not be g2p", results.output) + assert "could not be g2p" not in results.output assert "Number of aligned segments" in results.output def test_with_stdin(self): @@ -299,7 +299,7 @@ def test_align_with_invalid_preg2p(self): print(txt, file=f) results = self.runner.invoke(g2p, [input_file, "-"]) - self.assertNotEqual(results.exit_code, 0) + assert results.exit_code != 0 # print(results.output) assert "could not be g2p" in results.output assert 'word' in results.output @@ -463,7 +463,7 @@ def test_convert_xml_invalid(self): self.assertEqual( etree.tounicode(c_xml), 'invalid' ) - self.assertFalse(valid, "convert_xml with invalid pre-g2p'd text") + assert not valid, "convert_xml with invalid pre-g2p'd text" def test_invalid_langs_in_xml(self): xml = parse_xml( @@ -476,7 +476,7 @@ def test_invalid_langs_in_xml(self): ) with self.assertLogs(LOGGER, level="WARNING") as cm: c_xml, valid, _ = convert_xml(xml, verbose_warnings=True) - self.assertFalse(valid) + assert not valid logger_output = "\n".join(cm.output) assert "No lang" in logger_output assert "foo" in logger_output @@ -486,7 +486,7 @@ def test_non_convertible_words(self): xml = parse_xml("43:2365:6743:23") with self.assertLogs(LOGGER, level="WARNING"): g2p_xml, valid, non_convertible_words = convert_xml(xml) - self.assertFalse(valid) + assert not valid self.assertEqual(non_convertible_words, ["43:23", "65:67"]) diff --git a/tests/test_make_xml_cli.py b/tests/test_make_xml_cli.py index 44e1837d..6d247008 100755 --- a/tests/test_make_xml_cli.py +++ b/tests/test_make_xml_cli.py @@ -8,7 +8,7 @@ import sys from shutil import copyfile -from pytest import main +from pytest import main, raises # from readalongs.log import LOGGER from readalongs._version import READALONG_FILE_FORMAT_VERSION, VERSION @@ -56,13 +56,13 @@ def test_no_lang(self): results = self.runner.invoke( make_xml, [self.empty_file, self.empty_file + ".readalong"] ) - self.assertNotEqual(results.exit_code, 0) + assert results.exit_code != 0 self.assertRegex(results.output, "Missing.*language") def test_inputfile_not_exist(self): """Error case: input file does not exist""" results = self.runner.invoke(make_xml, "-l atj /file/does/not/exist delme") - self.assertNotEqual(results.exit_code, 0) + assert results.exit_code != 0 self.assertRegex(results.output, "No such file or directory") def test_outputfile_exists(self): @@ -75,7 +75,7 @@ def test_outputfile_exists(self): make_xml, ["-l", "atj", self.empty_file, os.path.join(self.tempdir, "exists")], ) - self.assertNotEqual(results.exit_code, 0) + assert results.exit_code != 0 self.assertRegex(results.output, "exists.*overwrite") def test_output_exists(self): @@ -191,11 +191,11 @@ def test_create_input_ras_errors(self): # These used to be RuntimeError, but that was not right: *programmer* # errors can and should dump stack traces, unlike *user* errors, which # warrant nice friendly messages. - with self.assertRaises(AssertionError): + with raises(AssertionError): # missing input_file_name or input_file_handle _, _ = create_input_ras() - with self.assertRaises(AssertionError): + with raises(AssertionError): # missing text_languages _, _ = create_input_ras( input_file_name=os.path.join(self.data_dir, "fra.txt") @@ -223,7 +223,7 @@ def test_make_xml_invalid_lang(self): results = self.runner.invoke( make_xml, ["-l", "fra:notalang:und", input_file, "-"] ) - self.assertNotEqual(results.exit_code, 0) + assert results.exit_code != 0 self.assertRegex(results.output, r"Invalid value.*'notalang'") def test_make_xml_invalid_utf8_input(self): @@ -231,7 +231,7 @@ def test_make_xml_invalid_utf8_input(self): # Read noise.mp3 as if it was utf8 text, via create_input_ras(input_file_handle) results = self.runner.invoke(make_xml, ["-l", "fra", noise_file, "-"]) - self.assertNotEqual(results.exit_code, 0) + assert results.exit_code != 0 assert "provide a correctly encoded utf-8" in results.output # Read noise.mp3 as if it was utf8 text, via create_input_ras(input_file_name) @@ -239,7 +239,7 @@ def test_make_xml_invalid_utf8_input(self): make_xml, ["-l", "fra", noise_file, os.path.join(self.tempdir, "noise.readalong")], ) - self.assertNotEqual(results.exit_code, 0) + assert results.exit_code != 0 assert "provide a correctly encoded utf-8" in results.output # align also calls create_input_ras(input_file_name) @@ -253,7 +253,7 @@ def test_make_xml_invalid_utf8_input(self): os.path.join(self.tempdir, "noise-out"), ], ) - self.assertNotEqual(results.exit_code, 0) + assert results.exit_code != 0 assert "provide a correctly encoded utf-8" in results.output def test_blank_lines_stripped(self): diff --git a/tests/test_misc.py b/tests/test_misc.py index 3731bfec..9c5ed679 100755 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -9,7 +9,7 @@ import click from lxml import etree from pep440 import is_canonical -from pytest import main +from pytest import main, raises from readalongs._version import READALONG_FILE_FORMAT_VERSION, VERSION from readalongs.align import split_silences @@ -177,7 +177,7 @@ def test_joiner_callback(self): cb = JoinerCallbackForClick(iter("qwer")) # iterable over four characters self.assertEqual(cb(None, None, ["e:r"]), ["e", "r"]) self.assertEqual(cb(None, None, ["q,w"]), ["q", "w"]) - with self.assertRaises(click.BadParameter): + with raises(click.BadParameter): cb(None, None, ["q:e", "a,w"]) self.assertEqual(cb(None, None, ["r:q", "w"]), ["r", "q", "w"]) @@ -215,18 +215,18 @@ def test_load_xml(self): def test_load_xml_errors(self): # non-existent file - with self.assertRaises(OSError): + with raises(OSError): load_xml("file-does-not-exist.readalong") # invalid XML file bad_file = self.tempdir / "bad.readalong" with open(bad_file, "w") as f: print("This is not XML", file=f) - with self.assertRaises(etree.ParseError): + with raises(etree.ParseError): load_xml(bad_file) # empty file is also invalid - with self.assertRaises(etree.ParseError): + with raises(etree.ParseError): load_xml(os.devnull) # make sure we're not vulnerable to XML bombs @@ -256,7 +256,7 @@ def test_parse_xml(self): assert etree.tostring(xml) == etree.tostring(xml2) malformed_xml_text = " """ ) - with self.assertRaises(ValueError): + with raises(ValueError): _ = parse_smil(missing_id) missing_clip_end = dedent( @@ -86,7 +86,7 @@ def test_parse_bad_smil(self): """ ) - with self.assertRaises(ValueError): + with raises(ValueError): _ = parse_smil(missing_clip_end) bad_float = dedent( @@ -101,7 +101,7 @@ def test_parse_bad_smil(self): """ ) - with self.assertRaises(ValueError): + with raises(ValueError): _ = parse_smil(bad_float) diff --git a/tests/test_temp_file.py b/tests/test_temp_file.py index a5d82e55..5881a08f 100755 --- a/tests/test_temp_file.py +++ b/tests/test_temp_file.py @@ -23,7 +23,7 @@ def test_basic_file(self): f.close() assert os.path.exists("delme_test_temp_file") os.unlink("delme_test_temp_file") - self.assertFalse(os.path.exists("delme_test_temp_file")) + assert not os.path.exists("delme_test_temp_file") def test_ntf(self): """Regular usage of tempfile.NamedTemporaryFile from the standard library""" @@ -84,7 +84,7 @@ def test_using_with(self): readf.close() assert text == "Some text" assert os.path.exists(filename) - self.assertFalse(os.path.exists(filename)) + assert not os.path.exists(filename) def test_seek(self): """read/write operations should work on a PortableNamedTemporaryFile""" diff --git a/tests/test_tokenize_cli.py b/tests/test_tokenize_cli.py index 7a56e596..c8ef15e8 100755 --- a/tests/test_tokenize_cli.py +++ b/tests/test_tokenize_cli.py @@ -55,13 +55,13 @@ def test_with_stdin(self): def test_file_already_exists(self): """Test that readalongs tokenize does not overwrite existing files by default""" results = self.runner.invoke(tokenize, [self.rasfile, self.rasfile]) - self.assertNotEqual(results.exit_code, 0) + assert results.exit_code != 0 assert "use -f to overwrite" in results.output def test_bad_input(self): """Test readalongs tokenize with invalid XML as input""" results = self.runner.invoke(tokenize, "- -", input="this is not XML!") - self.assertNotEqual(results.exit_code, 0) + assert results.exit_code != 0 assert "Error parsing" in results.output # LOGGER.warning("Output: {}".format(results.output)) # LOGGER.warning("Exception: {}".format(results.exception)) diff --git a/tests/test_web_api.py b/tests/test_web_api.py index dab7d8ce..a66a0afb 100755 --- a/tests/test_web_api.py +++ b/tests/test_web_api.py @@ -10,7 +10,7 @@ from time import perf_counter from unittest.mock import patch -from pytest import main +from pytest import main, raises from readalongs._version import READALONG_FILE_FORMAT_VERSION, VERSION from readalongs.log import LOGGER @@ -157,9 +157,9 @@ def test_convert_time_limit(self): ids_added = add_ids(tokenized) # This convert_xml call takes about 0.2s on my machine, so 1ms is guaranteed to be # too short on any hardware. - with self.assertRaises(TimeLimitException): + with raises(TimeLimitException): _ = convert_xml(ids_added, time_limit=0.001) - with self.assertRaises(TimeLimitException): + with raises(TimeLimitException): _ = convert_xml( ids_added, time_limit=1.001, start_time=perf_counter() - 1.0 ) @@ -380,7 +380,7 @@ class = "IntervalTier" ) assert response.status_code == 200 assert "aligned.TextGrid" in response.headers["content-disposition"] - self.assertNotIn("xmax = 83.100000", response.text) + assert "xmax = 83.100000" not in response.text def test_convert_to_eaf(self): request = { @@ -519,9 +519,9 @@ def test_cleanup_temp_dir(self): match = re.search( "Temporary directory: (.*)($|\r|\n)", "\n".join(log_cm.output) ) - self.assertIsNotNone(match) assert match is not None - self.assertFalse(os.path.isdir(match[1])) + assert match is not None + assert not os.path.isdir(match[1]) def test_cleanup_even_if_error(self): # This is seriously white-box testing... overlapping words @@ -564,9 +564,9 @@ def test_cleanup_even_if_error(self): match = re.search( "Temporary directory: (.*)($|\r|\n)", "\n".join(log_cm.output) ) - self.assertIsNotNone(match) assert match is not None - self.assertFalse(os.path.isdir(match[1])) + assert match is not None + assert not os.path.isdir(match[1]) def test_convert_to_bad_format(self): request = { From fae5a99cb31ee0e20176e62d2500020093759696 Mon Sep 17 00:00:00 2001 From: Eric Joanis Date: Thu, 23 Jul 2026 12:25:46 -0400 Subject: [PATCH 05/10] refactor(tests): a bunch more pytest assertion conversions including sub self.assertGreater\(([^,]*), ([^,]*)\)$ by assert $1 > $2 sub self.assertIsNone\(([^,]*)\)$ by assert $1 is None and a bunch more --- tests/test_align_cli.py | 22 +++-- tests/test_anchors.py | 28 +++--- tests/test_api.py | 9 +- tests/test_audio.py | 34 +++----- tests/test_dna_text.py | 10 ++- tests/test_dna_utils.py | 172 +++++++++++++++---------------------- tests/test_dtd.py | 21 ++--- tests/test_force_align.py | 31 +++---- tests/test_g2p_cli.py | 83 ++++++++---------- tests/test_make_xml_cli.py | 6 +- tests/test_web_api.py | 14 +-- 11 files changed, 185 insertions(+), 245 deletions(-) diff --git a/tests/test_align_cli.py b/tests/test_align_cli.py index f017224a..3e0a4a2e 100755 --- a/tests/test_align_cli.py +++ b/tests/test_align_cli.py @@ -388,9 +388,9 @@ def test_misc_align_errors(self): # print(results.output) # We don't check results.exit_code since that's a soft warning, not a hard error assert "produced 2 segments" in results.output - self.assertIn( - "Alignment produced a different number of segments and tokens than were in the input.", - results.output, + assert ( + "Alignment produced a different number of segments and tokens than were in the input." + in results.output ) def test_infer_plain_text_or_xml(self): @@ -611,18 +611,16 @@ def slurp_text(filename, encoding): base_file = write_file(self.tempdir / "add-bom-input.txt", "Random Text été") bom_file = self.add_bom(base_file) - self.assertEqual( - slurp_text(base_file, "utf-8"), slurp_text(bom_file, "utf-8-sig") - ) - self.assertEqual( - slurp_text(bom_file, "utf-8"), "\ufeff" + slurp_text(base_file, "utf-8") + assert slurp_text(base_file, "utf-8") == slurp_text(bom_file, "utf-8-sig") + assert slurp_text(bom_file, "utf-8") == "\ufeff" + slurp_text( + base_file, "utf-8" ) assert slurp_bin(base_file) != slurp_bin(bom_file) assert b"\xef\xbb\xbf" + slurp_bin(base_file) == slurp_bin(bom_file) bom_file_pathlib = self.add_bom(Path(base_file)) - self.assertEqual( - slurp_text(base_file, "utf-8"), slurp_text(bom_file_pathlib, "utf-8-sig") + assert slurp_text(base_file, "utf-8") == slurp_text( + bom_file_pathlib, "utf-8-sig" ) def test_ffmpeg_is_present(self): @@ -636,9 +634,9 @@ def test_ffmpeg_is_present(self): result = subprocess.run( [ff_program, "-version"], capture_output=True, check=True ) - self.assertEqual(result.returncode, 0, failure_message) + assert result.returncode == 0, failure_message except Exception: - self.fail(failure_message) + assert False, failure_message if __name__ == "__main__": diff --git a/tests/test_anchors.py b/tests/test_anchors.py index 04d7adba..68439368 100755 --- a/tests/test_anchors.py +++ b/tests/test_anchors.py @@ -55,26 +55,22 @@ def test_anchors_outer_too(self): # Make sure the aligned segments stay on the right side of their anchors, # including the initial and final ones inserted into anchors2.readalong - self.assertGreaterEqual(words[0]["start"], 0.5) - self.assertLessEqual(words[0]["end"], 1.2) - self.assertGreaterEqual(words[1]["start"], 1.2) - self.assertLessEqual(words[8]["end"], 3.6) - self.assertGreaterEqual(words[9]["start"], 3.9) - self.assertLessEqual(words[21]["end"], 7.0) - self.assertGreaterEqual(words[22]["start"], 7.0) - self.assertLessEqual(words[-1]["end"], 33.2) + assert words[0]["start"] >= 0.5 + assert words[0]["end"] <= 1.2 + assert words[1]["start"] >= 1.2 + assert words[8]["end"] <= 3.6 + assert words[9]["start"] >= 3.9 + assert words[21]["end"] <= 7.0 + assert words[22]["start"] >= 7.0 + assert words[-1]["end"] <= 33.2 # Make sure the audio segment temp files were written and are not empty for suff in ("", ".2", ".3", ".4"): partial_wav_file = save_temps_prefix + ".wav" + suff - self.assertTrue( - os.path.exists(partial_wav_file), f"{partial_wav_file} should exist" - ) - self.assertGreater( - os.path.getsize(partial_wav_file), - 0, - f"{partial_wav_file} should not be empty", - ) + assert os.path.exists(partial_wav_file), f"{partial_wav_file} should exist" + assert ( + os.path.getsize(partial_wav_file) > 0 + ), f"{partial_wav_file} should not be empty" def test_anchors_align_modes(self): xml_with_anchors = """ diff --git a/tests/test_api.py b/tests/test_api.py index 61dde3c7..3008f450 100755 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -47,13 +47,10 @@ def test_call_align(self): "Offline-HTML/output.html", ) for f in expected_output_files: - self.assertTrue( - (self.tempdir / "output" / f).exists(), + assert (self.tempdir / "output" / f).exists(), ( f"successful alignment should have created {f}", ) - self.assertEqual( - list(langs), - ["fra"], + assert list(langs) == ["fra"], ( "Make sure the API call doesn't not modify my variables", ) @@ -74,7 +71,7 @@ def test_call_make_xml(self): assert "Wrote " in log with open(self.tempdir / "prepared.readalong") as f: xml_text = f.read() - self.assertIn('xml:lang="fra" fallback-langs="eng,und"', xml_text) + assert 'xml:lang="fra" fallback-langs="eng,und"' in xml_text (status, exception, log) = api.make_xml( self.data_dir / "ej-fra.txt", diff --git a/tests/test_audio.py b/tests/test_audio.py index 9954a20c..6a294106 100755 --- a/tests/test_audio.py +++ b/tests/test_audio.py @@ -7,7 +7,7 @@ from pathlib import Path from subprocess import run -from pytest import main +from pytest import approx, main from readalongs.audio_utils import ( extract_section, @@ -50,10 +50,10 @@ def test_mute_section(self): muted_segment = mute_section(self.audio_segment, 1000, 2000) muted_section = muted_segment[1000:2000] # This worked with pydub 0.23.1, but it does not work with 0.25.1 - # self.assertLessEqual(muted_section.max, 1) + # assert muted_sectino.max <= 1 # Muting applies a gain of -120, so the results is not necessarily 0, # it's just much smaller. - self.assertLessEqual(muted_section.max, max_before / 1000) + assert muted_section.max <= max_before / 1000 def test_remove_section(self): """Should remove section of audio""" @@ -81,8 +81,7 @@ def test_align_sample(self): # Check Result raspath = Path(output_path) / "www" ras_files = raspath.glob("*.readalong") - self.assertTrue( - next(ras_files, False), + assert next(ras_files, False), ( "No *.readalong files found; " "pip install --force-reinstall --upgrade might be required " "if dependencies changed.", @@ -108,8 +107,7 @@ def test_align_removed(self): # Check Result raspath = Path(output_path) / "www" ras_files = raspath.glob("*.readalong") - self.assertTrue( - next(ras_files, False), + assert next(ras_files, False), ( "No *.readalong files found; " "pip install --force-reinstall --upgrade might be required " "if dependencies changed.", @@ -135,8 +133,7 @@ def test_align_muted(self): # Check Result raspath = Path(output_path) / "www" ras_files = raspath.glob("*.readalong") - self.assertTrue( - next(ras_files, False), + assert next(ras_files, False), ( "No *.readalong files found; " "pip install --force-reinstall --upgrade might be required " "if dependencies changed.", @@ -144,11 +141,11 @@ def test_align_muted(self): def test_extract_section(self): """Unit test extract_section()""" - self.assertEqual(len(extract_section(self.audio_segment, 1000, 2000)), 1000) - self.assertEqual(len(extract_section(self.audio_segment, None, 500)), 500) - self.assertEqual( - len(extract_section(self.audio_segment, 1000, None)), - len(self.audio_segment) - 1000, + assert len(extract_section(self.audio_segment, 1000, 2000)) == 1000 + assert len(extract_section(self.audio_segment, None, 500)) == 500 + assert ( + len(extract_section(self.audio_segment, 1000, None)) + == len(self.audio_segment) - 1000 ) def test_write_audio_to_file(self): @@ -158,12 +155,9 @@ def test_write_audio_to_file(self): write_audio_to_file(section, output_path) assert os.path.exists(output_path) reloaded_section = read_audio_from_file(output_path) - self.assertAlmostEqual( - len(section), - len(reloaded_section), - msg="reloaded audio file is more than 50ms shorter or longer", - delta=50, - ) + assert len(section) == approx( + len(reloaded_section), abs=50 + ), "reloaded audio file is more than 50ms shorter or longer" if __name__ == "__main__": diff --git a/tests/test_dna_text.py b/tests/test_dna_text.py index 7d2aef0b..a2a7f924 100755 --- a/tests/test_dna_text.py +++ b/tests/test_dna_text.py @@ -7,7 +7,7 @@ from io import StringIO from lxml import etree -from pytest import main +from pytest import main, raises from readalongs.text import tokenize_xml from readalongs.text.add_ids_to_xml import add_ids @@ -151,7 +151,8 @@ def test_dna_word(self): xml = parse_xml(txt) with redirect_stderr(StringIO()): tokenized = tokenize_xml.tokenize_xml(xml) - self.assertRaises(RuntimeError, add_ids, tokenized) + with raises(RuntimeError, match=r"Found element with.*This is not allowed"): + add_ids(tokenized) def test_dna_word_nested(self): """You also can't have a element inside a DNA element""" @@ -160,7 +161,10 @@ def test_dna_word_nested(self): xml = parse_xml(txt) with redirect_stderr(StringIO()): tokenized = tokenize_xml.tokenize_xml(xml) - self.assertRaises(RuntimeError, add_ids, tokenized) + with raises( + RuntimeError, match=r"Found nested inside.*This is not allowed" + ): + add_ids(tokenized) if __name__ == "__main__": diff --git a/tests/test_dna_utils.py b/tests/test_dna_utils.py index 0ddf18d1..cd8488c0 100755 --- a/tests/test_dna_utils.py +++ b/tests/test_dna_utils.py @@ -31,48 +31,35 @@ class TestDNAUtils(TestCase): def test_sort_and_join_dna_segments(self): """Make sure sorting and joining DNA segments works.""" - self.assertEqual( - sort_and_join_dna_segments(segments_from_pairs((1000, 1100), (1500, 2100))), - segments_from_pairs((1000, 1100), (1500, 2100)), - ) - self.assertEqual( - sort_and_join_dna_segments(segments_from_pairs((1500, 2100), (1000, 1100))), - segments_from_pairs((1000, 1100), (1500, 2100)), - ) - self.assertEqual( - sort_and_join_dna_segments( - segments_from_pairs( - (2, 3), (11, 14), (23, 25), (1, 4), (12, 13), (24, 26) - ) - ), - segments_from_pairs((1, 4), (11, 14), (23, 26)), - ) + assert sort_and_join_dna_segments( + segments_from_pairs((1000, 1100), (1500, 2100)) + ) == segments_from_pairs((1000, 1100), (1500, 2100)) + assert sort_and_join_dna_segments( + segments_from_pairs((1500, 2100), (1000, 1100)) + ) == segments_from_pairs((1000, 1100), (1500, 2100)) + assert sort_and_join_dna_segments( + segments_from_pairs((2, 3), (11, 14), (23, 25), (1, 4), (12, 13), (24, 26)) + ) == segments_from_pairs((1, 4), (11, 14), (23, 26)) def test_adjustment_calculation(self): """Try adjusting alignments of re-built audio""" - self.assertEqual( - calculate_adjustment(1000, [{"begin": 1000, "end": 1100}]), 100 - ) - self.assertEqual(calculate_adjustment(900, [{"begin": 1000, "end": 1100}]), 0) - self.assertEqual( - calculate_adjustment(1000, [{"begin": 1000, "end": 1100}]), 100 - ) - self.assertEqual(calculate_adjustment(900, [{"begin": 1000, "end": 1100}]), 0) - self.assertEqual( - calculate_adjustment(2000, [{"begin": 1000, "end": 1100}]), 100 - ) + assert calculate_adjustment(1000, [{"begin": 1000, "end": 1100}]) == 100 + assert calculate_adjustment(900, [{"begin": 1000, "end": 1100}]) == 0 + assert calculate_adjustment(1000, [{"begin": 1000, "end": 1100}]) == 100 + assert calculate_adjustment(900, [{"begin": 1000, "end": 1100}]) == 0 + assert calculate_adjustment(2000, [{"begin": 1000, "end": 1100}]) == 100 # Function only accepts args in ms, so 1.0 and 2.0 are the same as 1 and 2 ms - self.assertEqual( + assert ( calculate_adjustment( 1, [{"begin": 1000, "end": 1100}, {"begin": 1500, "end": 1700}] - ), - 0, + ) + == 0 ) - self.assertEqual( + assert ( calculate_adjustment( 2, [{"begin": 1000, "end": 1100}, {"begin": 1500, "end": 2100}] - ), - 0, + ) + == 0 ) # When there are multiple dna segments, the timestamp to adjust has to # shift with the adjustment, e.g.: @@ -86,95 +73,78 @@ def test_adjustment_calculation(self): (3000, 2000), (4000, 2000), ]: - self.assertEqual( + assert ( calculate_adjustment( value, [{"begin": 1000, "end": 2000}, {"begin": 4000, "end": 5000}] - ), - adjustment, - f"DNA removal adjustment for t={value}ms should have been {adjustment}ms", - ) + ) + == adjustment + ), f"DNA removal adjustment for t={value}ms should have been {adjustment}ms" def test_adjustment_correction(self): """Try correcting adjusted alignments of re-built audio""" - self.assertEqual( - correct_adjustments(950, 1125, [{"begin": 1000, "end": 1100}]), (950, 1000) + assert correct_adjustments(950, 1125, [{"begin": 1000, "end": 1100}]) == ( + 950, + 1000, ) - self.assertEqual( - correct_adjustments(975, 1150, [{"begin": 1000, "end": 1100}]), (1100, 1150) + assert correct_adjustments(975, 1150, [{"begin": 1000, "end": 1100}]) == ( + 1100, + 1150, ) # Function only accepts args in ms - self.assertNotEqual( - correct_adjustments(0.950, 1.125, [{"begin": 1000, "end": 1100}]), # type: ignore - (950, 1000), + assert correct_adjustments(0.950, 1.125, [{"begin": 1000, "end": 1100}]) != ( # type: ignore[arg-type] + 950, + 1000, ) - self.assertNotEqual( - correct_adjustments(0.975, 1.150, [{"begin": 1000, "end": 1100}]), # type: ignore - (1100, 1150), + assert correct_adjustments(0.975, 1.150, [{"begin": 1000, "end": 1100}]) != ( # type: ignore[arg-type] + 1100, + 1150, ) def test_segment_intersection(self): """Unit testing of segment_intersection()""" - self.assertEqual(segment_intersection([], []), []) - self.assertEqual(segment_intersection(segments_from_pairs((1, 3)), []), []) - self.assertEqual(segment_intersection([], segments_from_pairs((1, 3))), []) - self.assertEqual( - segment_intersection( - segments_from_pairs( - (15, 16), (30, 40), (42, 48), (50, 60), (65, 66), (84, 89), (90, 91) - ), - segments_from_pairs( - (20, 21), (23, 24), (45, 50), (55, 57), (60, 70), (80, 85), (93, 94) - ), - ), + assert segment_intersection([], []) == [] + assert segment_intersection(segments_from_pairs((1, 3)), []) == [] + assert segment_intersection([], segments_from_pairs((1, 3))) == [] + assert segment_intersection( segments_from_pairs( - (45, 48), (50, 50), (55, 57), (60, 60), (65, 66), (84, 85) - ), - ) - self.assertEqual( - segment_intersection( - segments_from_pairs((10, 30)), segments_from_pairs((19, 19)) + (15, 16), (30, 40), (42, 48), (50, 60), (65, 66), (84, 89), (90, 91) ), - segments_from_pairs((19, 19)), - ) - self.assertEqual( - segment_intersection( - segments_from_pairs((1610, 1620)), segments_from_pairs((1620, 1620)) - ), - segments_from_pairs((1620, 1620)), - ) - self.assertEqual( - segment_intersection( - segments_from_pairs((1610, 1620)), - segments_from_pairs((1610, 1610), (1620, 1620)), + segments_from_pairs( + (20, 21), (23, 24), (45, 50), (55, 57), (60, 70), (80, 85), (93, 94) ), + ) == segments_from_pairs( + (45, 48), (50, 50), (55, 57), (60, 60), (65, 66), (84, 85) + ) + assert segment_intersection( + segments_from_pairs((10, 30)), segments_from_pairs((19, 19)) + ) == segments_from_pairs((19, 19)) + assert segment_intersection( + segments_from_pairs((1610, 1620)), segments_from_pairs((1620, 1620)) + ) == segments_from_pairs((1620, 1620)) + assert segment_intersection( + segments_from_pairs((1610, 1620)), segments_from_pairs((1610, 1610), (1620, 1620)), - ) + ) == segments_from_pairs((1610, 1610), (1620, 1620)) def test_dna_union(self): """Unit testing of dna_union()""" - self.assertEqual( - dna_union(1000, 2000, 3000, [{"begin": 1100, "end": 1200}]), - segments_from_pairs((0, 1000), (1100, 1200), (2000, 3000)), - ) - self.assertEqual( - dna_union(None, 2000, 3000, [{"begin": 1100, "end": 1200}]), - segments_from_pairs((1100, 1200), (2000, 3000)), - ) - self.assertEqual( - dna_union(1000, None, 3000, [{"begin": 1100, "end": 1200}]), - segments_from_pairs((0, 1000), (1100, 1200)), - ) - self.assertEqual( - dna_union( - 1000, - 2000, - 3000, - segments_from_pairs( - (400, 500), (900, 1100), (1200, 1300), (1900, 2100), (2500, 2600) - ), + assert dna_union( + 1000, 2000, 3000, [{"begin": 1100, "end": 1200}] + ) == segments_from_pairs((0, 1000), (1100, 1200), (2000, 3000)) + assert dna_union( + None, 2000, 3000, [{"begin": 1100, "end": 1200}] + ) == segments_from_pairs((1100, 1200), (2000, 3000)) + assert dna_union( + 1000, None, 3000, [{"begin": 1100, "end": 1200}] + ) == segments_from_pairs((0, 1000), (1100, 1200)) + assert dna_union( + 1000, + 2000, + 3000, + segments_from_pairs( + (400, 500), (900, 1100), (1200, 1300), (1900, 2100), (2500, 2600) ), - segments_from_pairs((0, 1100), (1200, 1300), (1900, 3000)), - ) + ) == segments_from_pairs((0, 1100), (1200, 1300), (1900, 3000)) if __name__ == "__main__": diff --git a/tests/test_dtd.py b/tests/test_dtd.py index 2872a486..6f9eb8e1 100644 --- a/tests/test_dtd.py +++ b/tests/test_dtd.py @@ -53,9 +53,7 @@ def test_valid_inputs(self): with open(path, "rb") as infh: try: parsed = load_xml(infh) - self.assertTrue( - self.dtd.validate(parsed), f"{name} does not validate" - ) + assert self.dtd.validate(parsed), f"{name} does not validate" except etree.ParseError as e: self.fail("Error parsing XML input file %s: %s." % (path, e)) @@ -65,9 +63,9 @@ def test_invalid_inputs(self): with open(path, "rb") as infh: try: parsed = load_xml(infh) - self.assertFalse( - self.dtd.validate(parsed), f"{name} validates but shouldn't" - ) + assert not self.dtd.validate( + parsed + ), f"{name} validates but shouldn't" except etree.ParseError as e: self.fail("Error parsing XML input file %s: %s." % (path, e)) @@ -84,9 +82,7 @@ def test_backwards_compatibility(self): with open(path, "rb") as infh: try: parsed = load_xml(infh) - self.assertTrue( - self.dtd.validate(parsed), f"{name} does not validate" - ) + assert self.dtd.validate(parsed), f"{name} does not validate" except etree.ParseError as e: self.fail("Error parsing XML input file %s: %s." % (path, e)) @@ -103,10 +99,9 @@ def test_backwards_compatibility(self): ) as rasFile: try: parsed = load_xml(rasFile) - self.assertFalse( - dtd.validate(parsed), - f"{versions[1]} validates with 1.0 but shouldn't", - ) + assert not dtd.validate( + parsed + ), f"{versions[1]} validates with 1.0 but shouldn't" except etree.ParseError as e: self.fail("Error parsing XML input file %s: %s." % (rasFile, e)) diff --git a/tests/test_force_align.py b/tests/test_force_align.py index 35552ec7..4b9d32eb 100755 --- a/tests/test_force_align.py +++ b/tests/test_force_align.py @@ -13,7 +13,7 @@ from tempfile import TemporaryDirectory from lxml import etree -from pytest import main +from pytest import main, raises from soundswallower import get_model_path from readalongs.align import align_audio @@ -113,16 +113,13 @@ def make_element(tag, text="", tail=""): _, sentences = get_word_texts_and_sentences( results["words"], results["tokenized"] ) - self.assertEqual( - [w["text"] for w in sentences[1]], - [ - "Je stuff", - "subwordtext", - "syl;syl;syl;", - "head text;syllable text;syl tail;", - "Joanissyl;sub;tail;another syl;", - ], - ) + assert [w["text"] for w in sentences[1]] == [ + "Je stuff", + "subwordtext", + "syl;syl;syl;", + "head text;syllable text;syl tail;", + "Joanissyl;sub;tail;another syl;", + ] def test_align_switch_am(self): """Alignment test case with an alternate acoustic model and custom @@ -170,11 +167,11 @@ def test_align_fail(self): writer.setsampwidth(2) writer.setframerate(16000) writer.writeframes(b"\x00\x00") - with self.assertRaises(RuntimeError), redirect_stderr(StringIO()): + with raises(RuntimeError), redirect_stderr(StringIO()): _ = align_audio(xml_path, tf.name, unit="w") def test_bad_align_mode(self): - with self.assertRaises(AssertionError), redirect_stderr(StringIO()): + with raises(AssertionError), redirect_stderr(StringIO()): _ = align_audio( os.path.join(self.data_dir, "ej-fra.readalong"), os.path.join(self.data_dir, "noise.mp3"), @@ -194,8 +191,8 @@ def test_convert(self): save_xml(tf.name, xml) txt = load_txt(tf.name) self.maxDiff = None - self.assertEqual( - txt, load_txt(os.path.join(self.data_dir, "ej-fra-converted.xhtml")) + assert txt == load_txt( + os.path.join(self.data_dir, "ej-fra-converted.xhtml") ) def test_convert_no_version(self): @@ -207,8 +204,8 @@ def test_convert_no_version(self): save_xml(tf.name, xml) txt = load_txt(tf.name) self.maxDiff = None - self.assertEqual( - txt, load_txt(os.path.join(self.data_dir, "ej-fra-converted.xhtml")) + assert txt == load_txt( + os.path.join(self.data_dir, "ej-fra-converted.xhtml") ) diff --git a/tests/test_g2p_cli.py b/tests/test_g2p_cli.py index f4f43a82..399ba563 100755 --- a/tests/test_g2p_cli.py +++ b/tests/test_g2p_cli.py @@ -9,7 +9,7 @@ from io import StringIO from lxml import etree -from pytest import main +from pytest import main, raises from readalongs.align import align_audio from readalongs.cli import align, g2p, make_xml, tokenize @@ -85,11 +85,9 @@ def test_mixed_langs(self): ref_list = list(ref_f) ref_list[1] = updateFormatVersion(ref_list[1]) ref_list[2] = updateStudioVersion(ref_list[2]) - self.assertListEqual( - list(output_f), - ref_list, - f"output {g2p_file} and reference {ref_file} differ.", - ) + assert ( + list(output_f) == ref_list + ), f"output {g2p_file} and reference {ref_file} differ." def test_invoke_with_obsolete_switches(self): """Using obsolete options should yield a helpful error message""" @@ -308,9 +306,8 @@ def test_align_with_invalid_preg2p(self): audio_file = os.path.join(self.data_dir, "ej-fra.m4a") with redirect_stderr(StringIO()): - with self.assertRaises(RuntimeError) as e: + with raises(RuntimeError, match="could not be g2p'd"): results = align_audio(input_file, audio_file) - assert "could not be g2p'd" in str(e.exception) def test_align_with_preg2p(self): """readalongs align working on previously g2p'd text""" @@ -341,10 +338,10 @@ def test_align_with_preg2p(self): ) with open(os.path.join(self.tempdir, "foo.dict"), encoding="utf8") as f: dict_file = f.read() - self.assertIn("S AH S IY", dict_file) # "ceci" in fra - self.assertIn("DH IH S", dict_file) # "this" in eng - self.assertIn("HH EH Y", dict_file) # "Hej" in dan - self.assertIn("D G IY T UW P IY D", dict_file) # pre-g2p'd OOV + assert "S AH S IY" in dict_file # "ceci" in fra + assert "DH IH S" in dict_file # "this" in eng + assert "HH EH Y" in dict_file # "Hej" in dan + assert "D G IY T UW P IY D" in dict_file # pre-g2p'd OOV def test_convert_xml(self): """unit testing for readalongs.text.convert_xml.convert_xml() @@ -352,22 +349,22 @@ def test_convert_xml(self): convert_xml() is the inner method in readalongs that calls g2p. It's not very well named, but it still needs unit testing. :) """ - self.assertEqual( - run_convert_xml("wordnot word"), - 'wordnot word', + assert ( + run_convert_xml("wordnot word") + == 'wordnot word' ) - self.assertEqual( + assert ( run_convert_xml( 'Patrickxtła̱n' - ), - 'Patrick' - 'xtła̱n', + ) + == 'Patrick' + 'xtła̱n' ) - self.assertEqual( - run_convert_xml('Patrickxtła̱n'), - 'Patrickxtła̱n', + assert ( + run_convert_xml('Patrickxtła̱n') + == 'Patrickxtła̱n' ) def test_convert_xml_with_newlines(self): @@ -391,38 +388,36 @@ def compact_arpabet(xml_string: str) -> str: def test_convert_xml_subwords(self): """Unit testing for reintroducing subword units""" - self.assertEqual( + assert ( run_convert_xml( 'Patrickxtła̱n' - ), - 'Patrick' - 'xtła̱n', + ) + == 'Patrick' + 'xtła̱n' ) - self.assertEqual( + assert ( run_convert_xml( 'fooPatrickbarxtła̱nbaz' - ), - '' - 'fooPatrickbarxtła̱nbaz', + ) + == '' + 'fooPatrickbarxtła̱nbaz' ) converted_by_syllable = run_convert_xml( 'abcdefghi' ) converted_as_a_whole = run_convert_xml('abcdefghi') - self.assertEqual( - two_xml_elements(converted_by_syllable), - two_xml_elements(converted_as_a_whole), + assert two_xml_elements(converted_by_syllable) == two_xml_elements( + converted_as_a_whole ) moh_eg_with_highlights = "tatiatkèn:sehkwe'" moh_eg_merged = "tatiatkèn:sehkwe'" assert two_xml_elements(moh_eg_merged) == "" - self.assertEqual( - two_xml_elements(run_convert_xml(moh_eg_with_highlights)), - two_xml_elements(run_convert_xml(moh_eg_merged)), - ) + assert two_xml_elements( + run_convert_xml(moh_eg_with_highlights) + ) == two_xml_elements(run_convert_xml(moh_eg_merged)) moh_example_input_full = """ @@ -444,25 +439,19 @@ def test_convert_xml_subwords(self): result = run_convert_xml(example_with_fallback_lang) assert "S AH S IY N AO T _ZH EH AE L L UW _IY K UW" in result logger_output = "\n".join(cm.output) - self.assertIn( - 'No valid g2p conversion found for "not_really_iku"', logger_output - ) + assert 'No valid g2p conversion found for "not_really_iku"' in logger_output def test_convert_xml_invalid(self): """test readalongs.text.convert_xml.convert_xml() with invalid input""" xml = parse_xml('valid') c_xml, valid, _ = convert_xml(xml) - self.assertEqual( - etree.tounicode(c_xml), 'valid' - ) + assert etree.tounicode(c_xml) == 'valid' assert valid, "convert_xml with valid pre-g2p'd text" xml = parse_xml('invalid') with redirect_stderr(StringIO()): c_xml, valid, _ = convert_xml(xml) - self.assertEqual( - etree.tounicode(c_xml), 'invalid' - ) + assert etree.tounicode(c_xml) == 'invalid' assert not valid, "convert_xml with invalid pre-g2p'd text" def test_invalid_langs_in_xml(self): @@ -487,7 +476,7 @@ def test_non_convertible_words(self): with self.assertLogs(LOGGER, level="WARNING"): g2p_xml, valid, non_convertible_words = convert_xml(xml) assert not valid - self.assertEqual(non_convertible_words, ["43:23", "65:67"]) + assert non_convertible_words == ["43:23", "65:67"] if __name__ == "__main__": diff --git a/tests/test_make_xml_cli.py b/tests/test_make_xml_cli.py index 6d247008..911eefba 100755 --- a/tests/test_make_xml_cli.py +++ b/tests/test_make_xml_cli.py @@ -49,7 +49,7 @@ def test_invoke_make_xml(self): ["-l", "atj", "-d", self.empty_file, os.path.join(self.tempdir, "delme")], ) assert results.exit_code == 0 - self.assertRegex(results.output, "Running readalongs make-xml") + assert "Running readalongs make-xml" in results.output def test_no_lang(self): """Error case: readalongs make-xml without the mandatory -l switch""" @@ -57,13 +57,13 @@ def test_no_lang(self): make_xml, [self.empty_file, self.empty_file + ".readalong"] ) assert results.exit_code != 0 - self.assertRegex(results.output, "Missing.*language") + assert re.search("Missing.*language", results.output) def test_inputfile_not_exist(self): """Error case: input file does not exist""" results = self.runner.invoke(make_xml, "-l atj /file/does/not/exist delme") assert results.exit_code != 0 - self.assertRegex(results.output, "No such file or directory") + assert "No such file or directory" in results.output def test_outputfile_exists(self): """Existing output file should not be overwritten by readalongs make-xml by default""" diff --git a/tests/test_web_api.py b/tests/test_web_api.py index a66a0afb..a92a9a19 100755 --- a/tests/test_web_api.py +++ b/tests/test_web_api.py @@ -265,9 +265,9 @@ def test_debug(self): response = self.API_CLIENT.post("/api/v1/assemble", json=request) content = response.json() assert content["input"] == request - self.assertGreater(len(content["tokenized"]), 10) - self.assertGreater(len(content["parsed"]), 10) - self.assertGreater(len(content["g2ped"]), 10) + assert len(content["tokenized"]) > 10 + assert len(content["parsed"]) > 10 + assert len(content["g2ped"]) > 10 # Test that debug mode is off by default request = { @@ -278,10 +278,10 @@ def test_debug(self): with redirect_stderr(StringIO()): response = self.API_CLIENT.post("/api/v1/assemble", json=request) content = response.json() - self.assertIsNone(content["input"]) - self.assertIsNone(content["tokenized"]) - self.assertIsNone(content["parsed"]) - self.assertIsNone(content["g2ped"]) + assert content["input"] is None + assert content["tokenized"] is None + assert content["parsed"] is None + assert content["g2ped"] is None hej_verden_xml = dedent( """ From 3861574e2f224090613c6b0cc8ce2256ca8f803f Mon Sep 17 00:00:00 2001 From: Eric Joanis Date: Fri, 24 Jul 2026 10:21:30 -0400 Subject: [PATCH 06/10] refactor(tests): more assertion conversions --- tests/test_make_xml_cli.py | 78 +++++++++++-------------- tests/test_misc.py | 75 ++++++++---------------- tests/test_package_urls.py | 2 +- tests/test_silence.py | 11 ++-- tests/test_tokenize_cli.py | 8 +-- tests/test_web_api.py | 113 ++++++++++++++++++------------------- 6 files changed, 118 insertions(+), 169 deletions(-) diff --git a/tests/test_make_xml_cli.py b/tests/test_make_xml_cli.py index 911eefba..6617d72e 100755 --- a/tests/test_make_xml_cli.py +++ b/tests/test_make_xml_cli.py @@ -76,7 +76,7 @@ def test_outputfile_exists(self): ["-l", "atj", self.empty_file, os.path.join(self.tempdir, "exists")], ) assert results.exit_code != 0 - self.assertRegex(results.output, "exists.*overwrite") + assert re.search("exists.*overwrite", results.output) def test_output_exists(self): """Make sure readalongs make-xml create the expected output file""" @@ -104,11 +104,9 @@ def test_output_correct(self): ref_list = list(ref_f) ref_list[1] = updateFormatVersion(ref_list[1]) ref_list[2] = updateStudioVersion(ref_list[2]) - self.assertListEqual( - list(output_f), - ref_list, - f"output {xml_file} and reference {ref_file} differ.", - ) + assert ( + list(output_f) == ref_list + ), f"output {xml_file} and reference {ref_file} differ." def test_input_is_stdin(self): """Validate that readalongs make-xml can use stdin as input""" @@ -127,10 +125,8 @@ def test_generate_output_name(self): # LOGGER.warning("Output: {}".format(results.output)) # LOGGER.warning("Exception: {}".format(results.exception)) assert results.exit_code == 0 - self.assertRegex(results.output, "Wrote.*someinput[.]readalong") - self.assertTrue( - os.path.exists(os.path.join(self.tempdir, "someinput.readalong")) - ) + assert re.search(r"Wrote.*someinput\.readalong", results.output) + assert os.path.exists(os.path.join(self.tempdir, "someinput.readalong")) def test_make_xml_with_different_newlines(self): """readalongs make-xml handling single and double blank lines for paragraphs and pages""" @@ -143,11 +139,11 @@ def test_make_xml_with_different_newlines(self): linux_output = linux_results.output # The "Linux" output is the reference output, but we validate it a bit # too, with a regex: it has to have 2 pages and 3 paragraphs - self.assertRegex( - linux_output, + assert re.search( re.compile( '
.*

.*

.*

.*

', re.DOTALL ), + linux_output, ) no_eol_file = os.path.join(self.tempdir, "no_eol") @@ -156,11 +152,9 @@ def test_make_xml_with_different_newlines(self): f.write(file_contents.encode("ascii")) no_eol_results = self.runner.invoke(make_xml, ["-l", "fra", no_eol_file, "-"]) no_eol_output = no_eol_results.output - self.assertEqual( - linux_output, - no_eol_output, - "An absent final newline should not affect make-xml", - ) + assert ( + linux_output == no_eol_output + ), "An absent final newline should not affect make-xml" dos_file = os.path.join(self.tempdir, "dos_file") with open(dos_file, mode="wb") as f: @@ -168,11 +162,9 @@ def test_make_xml_with_different_newlines(self): f.write(file_contents.encode("ascii")) dos_results = self.runner.invoke(make_xml, ["-l", "fra", dos_file, "-"]) dos_output = dos_results.output - self.assertEqual( - linux_output, - dos_output, - "Using DOS-style newlines should not affect make-xml", - ) + assert ( + linux_output == dos_output + ), "Using DOS-style newlines should not affect make-xml" mac_file = os.path.join(self.tempdir, "mac_file") with open(mac_file, mode="wb") as f: @@ -180,11 +172,9 @@ def test_make_xml_with_different_newlines(self): f.write(file_contents.encode("ascii")) mac_results = self.runner.invoke(make_xml, ["-l", "fra", mac_file, "-"]) mac_output = mac_results.output - self.assertEqual( - linux_output, - mac_output, - "Using old Mac-style newlines should not affect make-xml", - ) + assert ( + linux_output == mac_output + ), "Using old Mac-style newlines should not affect make-xml" def test_create_input_ras_errors(self): """create_input_ras should raise a AssertionError when parameters are missing.""" @@ -208,15 +198,15 @@ def test_make_xml_multiple_langs(self): make_xml, ["-l", "fra", "-l", "iku:und", input_file, "-"] ) assert results.exit_code == 0 - self.assertIn('', results.output) + assert '' in results.output results = self.runner.invoke(make_xml, ["-l", "fra,iku:und", input_file, "-"]) assert results.exit_code == 0 - self.assertIn('', results.output) + assert '' in results.output results = self.runner.invoke( make_xml, ["-l", "fra:iku", "-l", "und", input_file, "-"] ) assert results.exit_code == 0 - self.assertIn('', results.output) + assert '' in results.output def test_make_xml_invalid_lang(self): input_file = os.path.join(self.data_dir, "fra.txt") @@ -224,7 +214,7 @@ def test_make_xml_invalid_lang(self): make_xml, ["-l", "fra:notalang:und", input_file, "-"] ) assert results.exit_code != 0 - self.assertRegex(results.output, r"Invalid value.*'notalang'") + assert re.search(r"Invalid value.*'notalang'", results.output) def test_make_xml_invalid_utf8_input(self): noise_file = os.path.join(self.data_dir, "noise.mp3") @@ -261,10 +251,9 @@ def test_blank_lines_stripped(self): input_text_with_spaces = "Ceci est un test\n \nParagraphe\n\t \n \nPage\n" input_text_stripped = "Ceci est un test\n\nParagraphe\n\n\nPage\n" - self.assertEqual( - create_ras_from_text(text2lines(input_text_with_spaces), ["fra"]), - create_ras_from_text(text2lines(input_text_stripped), ["fra"]), - ) + assert create_ras_from_text( + text2lines(input_text_with_spaces), ["fra"] + ) == create_ras_from_text(text2lines(input_text_stripped), ["fra"]) def test_ignore_superfluous_blank_lines(self): """Don't insert blank pages when there are more blank lines than required.""" @@ -274,25 +263,22 @@ def test_ignore_superfluous_blank_lines(self): input_text_stripped = "Page1\n\n\nPage2\n\n\nPage3" self.maxDiff = None - self.assertEqual( - create_ras_from_text(text2lines(input_text_with_extra_nls), ["eng"]), - create_ras_from_text(text2lines(input_text_stripped), ["eng"]), - ) + assert create_ras_from_text( + text2lines(input_text_with_extra_nls), ["eng"] + ) == create_ras_from_text(text2lines(input_text_stripped), ["eng"]) for n in range(3, 10): - self.assertEqual( - create_ras_from_text(text2lines("Page1" + "\n" * n + "Page2"), ["eng"]), - create_ras_from_text(text2lines("Page1" + "\n" * 3 + "Page2"), ["eng"]), - ) + assert create_ras_from_text( + text2lines("Page1" + "\n" * n + "Page2"), ["eng"] + ) == create_ras_from_text(text2lines("Page1" + "\n" * 3 + "Page2"), ["eng"]) def test_split_vs_readlines(self): """Calling create_ras_from_text should work any way we split the lines""" # string.split("\n") strips newlines and might not be identical to readlines(), # but that should make no difference to create_tei_from_text text = "Blah\nBlah\n\nFoo \n\n \nBar" - self.assertEqual( - create_ras_from_text(text.split("\n"), ["eng"]), - create_ras_from_text(text2lines(text), ["eng"]), + assert create_ras_from_text(text.split("\n"), ["eng"]) == create_ras_from_text( + text2lines(text), ["eng"] ) diff --git a/tests/test_misc.py b/tests/test_misc.py index 9c5ed679..f81fcad2 100755 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -50,17 +50,12 @@ def test_parse_time(self): ("2h3", 7203000), ("2h3ms", 7200003), ): - self.assertEqual( - parse_time(time_str), time_in_ms, f'error parsing "{time_str}"' - ) + assert parse_time(time_str) == time_in_ms, f'error parsing "{time_str}"' def test_parse_time_errors(self): """Test readalongs.text.util.parse_time() with invalid inputs""" for err_time_str in ("3.4.5 ms", ".", "", "asdf", " 0 h z ", "nm"): - with self.assertRaises( - ValueError, - msg=f'parsing "{err_time_str}" should have raised ValueError', - ): + with raises(ValueError): _ = parse_time(err_time_str) def test_split_silences(self): @@ -125,11 +120,9 @@ def test_get_attrib_recursive(self): None, ), ): - self.assertEqual( - get_attrib_recursive(s, "lang"), - lang, - f"expected lang={lang} for {etree.tostring(s)} (i={i})", - ) + assert ( + get_attrib_recursive(s, "lang") == lang + ), f"expected lang={lang} for {etree.tostring(s)} (i={i})" for i, s, get_lang in zip( itertools.count(), @@ -147,22 +140,18 @@ def test_get_attrib_recursive(self): "p4p2c", ), ): - self.assertEqual( - get_lang_attrib(s), - get_lang, - f"expected get_lang={get_lang} for {etree.tostring(s)} (i={i})", - ) + assert ( + get_lang_attrib(s) == get_lang + ), f"expected get_lang={get_lang} for {etree.tostring(s)} (i={i})" for i, s, xml_lang in zip( itertools.count(), xml.xpath(".//s"), (None, None, None, None, "p3", "p3", "p3", "p4s1", None, "p4p2c"), ): - self.assertEqual( - get_attrib_recursive(s, "xml:lang"), - xml_lang, - f"expected xml:lang={xml_lang} for {etree.tostring(s)} (i={i})", - ) + assert ( + get_attrib_recursive(s, "xml:lang") == xml_lang + ), f"expected xml:lang={xml_lang} for {etree.tostring(s)} (i={i})" # Show what xml:lang actually looks like in element.attrib: # for p in xml.xpath(".//p"): @@ -175,43 +164,30 @@ def test_get_attrib_recursive(self): def test_joiner_callback(self): cb = JoinerCallbackForClick(iter("qwer")) # iterable over four characters - self.assertEqual(cb(None, None, ["e:r"]), ["e", "r"]) - self.assertEqual(cb(None, None, ["q,w"]), ["q", "w"]) + assert cb(None, None, ["e:r"]) == ["e", "r"] + assert cb(None, None, ["q,w"]) == ["q", "w"] with raises(click.BadParameter): cb(None, None, ["q:e", "a,w"]) - self.assertEqual(cb(None, None, ["r:q", "w"]), ["r", "q", "w"]) + assert cb(None, None, ["r:q", "w"]) == ["r", "q", "w"] def test_get_word_text(self): - self.assertEqual( - get_word_text(parse_xml("basicword")), - "basicword", - ) - self.assertEqual( - get_word_text(parse_xml("subwcase")), - "subwcase", + assert get_word_text(parse_xml("basicword")) == "basicword" + assert get_word_text(parse_xml("subwcase")) == "subwcase" + assert ( + get_word_text(parse_xml("syl1syl2")) + == "syl1syl2" ) - self.assertEqual( - get_word_text(parse_xml("syl1syl2")), - "syl1syl2", - ) - self.assertEqual( - get_word_text(parse_xml("textsubtail")), - "textsubtail", - ) - self.assertEqual( - get_word_text(parse_xml("abcd")), - "abcd", + assert ( + get_word_text(parse_xml("textsubtail")) == "textsubtail" ) + assert get_word_text(parse_xml("abcd")) == "abcd" def test_load_xml(self): xml_text = 'text' foo_file = self.tempdir / "foo.readalong" with open(foo_file, "w") as f: print(xml_text, file=f) - self.assertEqual( - xml_text.encode(encoding="ascii"), - etree.tostring(load_xml(foo_file)), - ) + assert xml_text.encode(encoding="ascii") == etree.tostring(load_xml(foo_file)) def test_load_xml_errors(self): # non-existent file @@ -241,10 +217,7 @@ def test_load_xml_errors(self): explode_file = self.tempdir / "explode.readalong" with open(explode_file, "w") as f: f.write(xml_bomb) - self.assertEqual( - etree.tostring(load_xml(explode_file)), - b"&c;&c;", - ) + assert etree.tostring(load_xml(explode_file)) == b"&c;&c;" # Would be this if we allowed entity expansion: # b'AAAAAAAAAAAAAAAA' # See https://en.wikipedia.org/wiki/Billion_laughs_attack diff --git a/tests/test_package_urls.py b/tests/test_package_urls.py index 2575f030..f86e1f17 100755 --- a/tests/test_package_urls.py +++ b/tests/test_package_urls.py @@ -42,7 +42,7 @@ def test_fetch_bundles_fallback(self): status, contents, _ = fetch_bundle_file(bad_url, "bundle.js", None) # print(status, len(contents)) assert status != 200 - self.assertIsInstance(status, str) + assert isinstance(status, str) assert ref_length == len(contents) diff --git a/tests/test_silence.py b/tests/test_silence.py index 847fa01b..a3dd2704 100755 --- a/tests/test_silence.py +++ b/tests/test_silence.py @@ -6,7 +6,7 @@ import sys from pydub import AudioSegment -from pytest import main +from pytest import approx, main from readalongs.cli import align from readalongs.text.util import load_xml @@ -50,12 +50,9 @@ def test_basic_silence_insertion(self): os.path.join(self.data_dir, "ej-fra.m4a") ) new_audio = AudioSegment.from_file(output / "www/silence.m4a", format="m4a") - self.assertAlmostEqual( - len(new_audio) - len(original_audio), - 2882, - msg="silence-added audio file is more than 50ms shorter or longer", - delta=50, - ) + assert len(new_audio) - len(original_audio) == approx( + 2882, abs=50 + ), "silence-added audio file is more than 50ms shorter or longer" def test_bad_silence(self): output = self.tempdir / "bad_silence" diff --git a/tests/test_tokenize_cli.py b/tests/test_tokenize_cli.py index c8ef15e8..7160d2dc 100755 --- a/tests/test_tokenize_cli.py +++ b/tests/test_tokenize_cli.py @@ -38,9 +38,7 @@ def test_generate_output_name(self): """Test letting readalongs tokenize generate the output filename""" results = self.runner.invoke(tokenize, ["--debug", self.rasfile]) assert results.exit_code == 0 - self.assertTrue( - os.path.exists(os.path.join(self.tempdir, "fra.tokenized.readalong")) - ) + assert os.path.exists(os.path.join(self.tempdir, "fra.tokenized.readalong")) def test_with_stdin(self): """Test readalongs reading from stdin and writing to stdout""" @@ -48,9 +46,7 @@ def test_with_stdin(self): inputtext = f.read() results = self.runner.invoke(tokenize, "-", input=inputtext) assert results.exit_code == 0 - self.assertIn( - "Ceci est une phrase", results.output - ) + assert "Ceci est une phrase" in results.output def test_file_already_exists(self): """Test that readalongs tokenize does not overwrite existing files by default""" diff --git a/tests/test_web_api.py b/tests/test_web_api.py index a92a9a19..2fd2a29a 100755 --- a/tests/test_web_api.py +++ b/tests/test_web_api.py @@ -146,8 +146,9 @@ def test_prepro_exceeds_time_limit(self): with redirect_stderr(StringIO()): response = self.API_CLIENT.post("/api/v1/assemble", json=request) assert response.status_code == 422 - self.assertIn( - "Preprocessing the input exceeded time limit", response.json()["detail"] + assert ( + "Preprocessing the input exceeded time limit" + in response.json()["detail"] ) def test_convert_time_limit(self): @@ -225,7 +226,7 @@ def test_empty_g2p(self): for message_part in ["These words could not", "24", "23"]: assert message_part in content_log - self.assertEqual(content["g2p_error_words"], ["24", "23", "99", "1234"]) + assert content["g2p_error_words"] == ["24", "23", "99", "1234"] assert "partial_ras" in content def test_langs(self): @@ -235,8 +236,9 @@ def test_langs(self): codes = [x["code"] for x in response.json()] assert set(codes) == set(get_langs()[0]) assert codes == list(sorted(codes)) - self.assertEqual( - dict((x["code"], x["names"]["_"]) for x in response.json()), get_langs()[1] + assert ( + dict((x["code"], x["names"]["_"]) for x in response.json()) + == get_langs()[1] ) def test_logs(self): @@ -284,9 +286,10 @@ def test_debug(self): assert content["g2ped"] is None hej_verden_xml = dedent( - """ + """\ + - +

@@ -315,10 +318,8 @@ def test_convert_to_TextGrid(self): ) assert response.status_code == 200 assert "aligned.TextGrid" in response.headers["content-disposition"] - self.assertEqual( - response.text, - dedent( - """\ + assert response.text == dedent( + """\ File type = "ooTextFile" Object class = "TextGrid" @@ -368,7 +369,6 @@ class = "IntervalTier" xmax = 83.100000 text = "" """ - ), ) # Test default duration request = { @@ -406,15 +406,17 @@ def test_convert_to_srt(self): ) assert response.status_code == 200 assert "aligned_sentences.srt" in response.headers["content-disposition"] - self.assertEqual( - response.text.replace("\r", "").strip(), # CRLF->LF, for Windows. - dedent( + # The tests in this file use replace("\r", "") to convert CRLF to LF, + # for when we are testing on Windows. + assert ( + response.text.replace("\r", "").strip() + == dedent( """\ 1 00:00:17,745 --> 00:01:22,190 hej é verden à """ - ).strip(), + ).strip() ) with redirect_stderr(StringIO()): @@ -423,9 +425,9 @@ def test_convert_to_srt(self): ) assert response.status_code == 200 assert "aligned_words.srt" in response.headers["content-disposition"] - self.assertEqual( - response.text.replace("\r", "").strip(), # CRLF->LF, for Windows - dedent( + assert ( + response.text.replace("\r", "").strip() + == dedent( """\ 1 00:00:17,745 --> 00:00:58,600 @@ -435,7 +437,7 @@ def test_convert_to_srt(self): 00:00:58,600 --> 00:01:22,190 verden à """ - ).strip(), + ).strip() ) def test_convert_to_vtt(self): @@ -450,16 +452,13 @@ def test_convert_to_vtt(self): ) assert response.status_code == 200 assert "aligned_sentences.vtt" in response.headers["content-disposition"] - self.assertEqual( - response.text.replace("\r", ""), # CRLF->LF, in case we're on Windows. - dedent( - """\ - WEBVTT + assert response.text.replace("\r", "") == dedent( + """\ + WEBVTT - 00:00:17.745 --> 00:01:22.190 - hej é verden à - """ - ), + 00:00:17.745 --> 00:01:22.190 + hej é verden à + """ ) with redirect_stderr(StringIO()): @@ -468,19 +467,16 @@ def test_convert_to_vtt(self): ) assert response.status_code == 200 assert "aligned_words.vtt" in response.headers["content-disposition"] - self.assertEqual( - response.text.replace("\r", ""), # CRLF->LF, in case we're on Windows. - dedent( - """\ - WEBVTT + assert response.text.replace("\r", "") == dedent( + """\ + WEBVTT - 00:00:17.745 --> 00:00:58.600 - hej é + 00:00:17.745 --> 00:00:58.600 + hej é - 00:00:58.600 --> 00:01:22.190 - verden à - """ - ), + 00:00:58.600 --> 00:01:22.190 + verden à + """ ) def test_convert_to_TextGrid_errors(self): @@ -492,7 +488,7 @@ def test_convert_to_TextGrid_errors(self): response = self.API_CLIENT.post( "/api/v1/convert_alignment/textgrid", json=request ) - self.assertEqual(response.status_code, 422, "Invalid XML should fail.") + assert response.status_code == 422, "Invalid XML should fail." request = { "dur": -10.0, @@ -502,7 +498,7 @@ def test_convert_to_TextGrid_errors(self): response = self.API_CLIENT.post( "/api/v1/convert_alignment/textgrid", json=request ) - self.assertEqual(response.status_code, 422, "Negative duration should fail.") + assert response.status_code == 422, "Negative duration should fail." def test_cleanup_temp_dir(self): """Make sure convert's temporary directory actually gets deleted.""" @@ -530,22 +526,23 @@ def test_cleanup_even_if_error(self): # that exception in a sane way, with a 422 status code, while # also making sure the temporary directory gets deleted. overlap_xml = dedent( - """ - - - - -
-

- - hej é - verden à - -

-
- -
-
+ """\ + + + + + +
+

+ + hej é + verden à + +

+
+ +
+
""" % (READALONG_FILE_FORMAT_VERSION, VERSION) ) From 9116076b487d019396081d75ba2f07c8769033b5 Mon Sep 17 00:00:00 2001 From: Eric Joanis Date: Tue, 8 Sep 2026 17:29:06 -0400 Subject: [PATCH 07/10] refactor(tests): finish converting tests to pytest Done with the help of GPT 5.6 Luna, with proposed changes reviewed line-by-line by Eric Joanis, and code adjusted where needed. --- tests/basic_test_case.py | 37 +++++++++++--------------------- tests/test_anchors.py | 16 +++++++------- tests/test_api.py | 13 ++++++------ tests/test_audio.py | 3 +-- tests/test_config.py | 8 +++---- tests/test_dna_utils.py | 3 +-- tests/test_dtd.py | 18 ++++++++-------- tests/test_g2p_cli.py | 20 ++++++++---------- tests/test_make_xml_cli.py | 3 +-- tests/test_misc.py | 19 ++++++++--------- tests/test_package_urls.py | 5 ++--- tests/test_smil.py | 3 +-- tests/test_temp_file.py | 3 +-- tests/test_tokenize_cli.py | 5 ++--- tests/test_tokenize_xml.py | 3 +-- tests/test_web_api.py | 43 ++++++++++++++++++++------------------ 16 files changed, 90 insertions(+), 112 deletions(-) diff --git a/tests/basic_test_case.py b/tests/basic_test_case.py index 471a2d5d..c8d2c570 100644 --- a/tests/basic_test_case.py +++ b/tests/basic_test_case.py @@ -1,11 +1,10 @@ -"""Common base class for the ReadAlongs test suites""" +"""Common base class for the ReadAlongs test suites""" import logging import tempfile -from contextlib import contextmanager from pathlib import Path -from unittest import TestCase +import pytest from click.testing import CliRunner import readalongs.text.make_package as make_package @@ -14,8 +13,8 @@ make_package.FETCH_BUNDLE_TIMEOUT_SECONDS = 5 # shorter timeout for testing -class BasicTestCase(TestCase): - """A Basic Unittest build block class that comes bundled with +class BasicTestCase: + """A pytest building block class that comes bundled with a temporary directory (self.tempdir), the path to the test data (self.data_dir) For convenience, self.tempdir and self.data_dir are pathlib.Path objects @@ -35,13 +34,9 @@ class BasicTestCase(TestCase): # self.keep_temp_dir_after_running = True keep_temp_dir_after_running = False - def setUp(self): - """Create a temporary directory, self.tempdir, and a test runner, self.runner - - If a subclass needs its own setUp() function, make sure to call - super().setUp() - at the beginning of it. - """ + @pytest.fixture(autouse=True) + def _pytest_setup(self): + """Create per-test temporary state and run an optional subclass hook.""" self.runner = CliRunner() tempdir_prefix = f"tmpdir_{type(self).__name__}_" if not self.keep_temp_dir_after_running: @@ -55,13 +50,12 @@ def setUp(self): print(f"tmpdir={tempdir_name}") self.tempdir = Path(tempdir_name) - def tearDown(self): - """Clean up the temporary directory + setup = getattr(self, "_setUp", None) + if setup is not None: + setup() + + yield - If a subclass needs its own tearDown() function, make sure to call - super().tearDown() - at the end of it. - """ if not self.keep_temp_dir_after_running: self.tempdirobj.cleanup() @@ -70,10 +64,3 @@ def tearDown(self): # Some test cases can set the logging level to DEBUG when they pass # --debug to a CLI command, but don't let that affect subsequent tests. LOGGER.setLevel(logging.INFO) - - -@contextmanager -def silence_logs(): - LOGGER.disabled = True - yield - LOGGER.disabled = False diff --git a/tests/test_anchors.py b/tests/test_anchors.py index 68439368..d18c3954 100755 --- a/tests/test_anchors.py +++ b/tests/test_anchors.py @@ -72,7 +72,7 @@ def test_anchors_outer_too(self): os.path.getsize(partial_wav_file) > 0 ), f"{partial_wav_file} should not be empty" - def test_anchors_align_modes(self): + def test_anchors_align_modes(self, caplog): xml_with_anchors = """ Bonjour. @@ -83,15 +83,15 @@ def test_anchors_align_modes(self): xml_file = os.path.join(self.tempdir, "text-with-anchors.readalong") with open(xml_file, "w", encoding="utf8") as f: print(xml_with_anchors, file=f) - with self.assertLogs(LOGGER, level="INFO") as cm: - with redirect_stderr(StringIO()): - results = align_audio( - xml_file, - os.path.join(self.data_dir, "noise.mp3"), - ) + caplog.set_level("INFO", logger=LOGGER.name) + with redirect_stderr(StringIO()): + results = align_audio( + xml_file, + os.path.join(self.data_dir, "noise.mp3"), + ) words = results["words"] assert len(words) == 10 - logger_output = "\n".join(cm.output) + logger_output = caplog.text assert "Align mode strict succeeded for sequence 0." in logger_output assert "Align mode strict failed for sequence 1." in logger_output assert "Align mode moderate failed for sequence 1." in logger_output diff --git a/tests/test_api.py b/tests/test_api.py index 3008f450..b7176d55 100755 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -14,7 +14,7 @@ from readalongs import api from readalongs.log import LOGGER -from tests.basic_test_case import BasicTestCase, silence_logs +from tests.basic_test_case import BasicTestCase from tests.sound_swallower_stub import SoundSwallowerStub @@ -89,10 +89,10 @@ def test_call_make_xml(self): assert status != 0 assert isinstance(exception, click.UsageError) - def test_deprecated_prepare(self): - with self.assertLogs(LOGGER, level="WARNING") as cm: - api.prepare(self.data_dir / "ej-fra.txt", self.tempdir / "foo", ("fra",)) - assert "deprecated" in "\n".join(cm.output) + def test_deprecated_prepare(self, caplog): + caplog.set_level("WARNING", logger=LOGGER.name) + api.prepare(self.data_dir / "ej-fra.txt", self.tempdir / "foo", ("fra",)) + assert "deprecated" in caplog.text sentences_to_convert = [ [ @@ -211,8 +211,7 @@ def test_extract_version_from_url(self): # Test with a URL that doesn't contain a version url = "https://unpkg.com/@readalongs/web-component/dist/bundle.js" - with silence_logs(): - version = extract_version_from_url(url) + version = extract_version_from_url(url) assert version == "unknown" diff --git a/tests/test_audio.py b/tests/test_audio.py index 6a294106..8bc54fea 100755 --- a/tests/test_audio.py +++ b/tests/test_audio.py @@ -24,8 +24,7 @@ class TestAudio(BasicTestCase): """Test suite for various audio contents handling methods""" - def setUp(self): - super().setUp() + def _setUp(self): self.audio_segment = read_audio_from_file( os.path.join(self.data_dir, "audio_sample.ogg") ) diff --git a/tests/test_config.py b/tests/test_config.py index c8e5742d..762d56e6 100755 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -6,22 +6,22 @@ import os import sys from contextlib import redirect_stderr -from unittest import TestCase from lxml import etree -from pytest import main, raises +from pytest import fixture, main, raises from readalongs.text.add_elements_to_xml import add_images, add_supplementary_xml from readalongs.text.util import load_xml -class TestConfig(TestCase): +class TestConfig: """Test suite for loading the config.json configuration file for readalongs align""" readalong: etree + @fixture(autouse=True, scope="class") @classmethod - def setUpClass(cls) -> None: + def _pytest_setup(cls) -> None: data_dir = os.path.join(os.path.dirname(__file__), "data") cls.readalong = load_xml(os.path.join(data_dir, "ej-fra.readalong")) diff --git a/tests/test_dna_utils.py b/tests/test_dna_utils.py index cd8488c0..d565b577 100755 --- a/tests/test_dna_utils.py +++ b/tests/test_dna_utils.py @@ -3,7 +3,6 @@ """Test suite for DNA segment manupulation methods""" import sys -from unittest import TestCase from pytest import main @@ -26,7 +25,7 @@ def segments_from_pairs(*pairs): return list({"begin": b, "end": e} for b, e in pairs) -class TestDNAUtils(TestCase): +class TestDNAUtils: """Test suite for dna segment manipulation methods in dna_utils.py""" def test_sort_and_join_dna_segments(self): diff --git a/tests/test_dtd.py b/tests/test_dtd.py index 6f9eb8e1..b424203d 100644 --- a/tests/test_dtd.py +++ b/tests/test_dtd.py @@ -5,10 +5,9 @@ import os import sys from os.path import dirname -from unittest import TestCase +import pytest from lxml import etree -from pytest import main from readalongs.text.util import load_xml @@ -39,10 +38,11 @@ """.strip().split() -class TestDTD(TestCase): +class TestDTD: """Test the XML DTD""" - def setUp(self): + @pytest.fixture(autouse=True) + def _pytest_setup(self): with open(DTDPATH) as infh: self.dtd = etree.DTD(infh) @@ -55,7 +55,7 @@ def test_valid_inputs(self): parsed = load_xml(infh) assert self.dtd.validate(parsed), f"{name} does not validate" except etree.ParseError as e: - self.fail("Error parsing XML input file %s: %s." % (path, e)) + pytest.fail("Error parsing XML input file %s: %s." % (path, e)) def test_invalid_inputs(self): for name in INVALID_RAS: @@ -67,7 +67,7 @@ def test_invalid_inputs(self): parsed ), f"{name} validates but shouldn't" except etree.ParseError as e: - self.fail("Error parsing XML input file %s: %s." % (path, e)) + pytest.fail("Error parsing XML input file %s: %s." % (path, e)) def test_backwards_compatibility(self): # the DTD needs to be backwards compatible as long as the major version does not change @@ -84,7 +84,7 @@ def test_backwards_compatibility(self): parsed = load_xml(infh) assert self.dtd.validate(parsed), f"{name} does not validate" except etree.ParseError as e: - self.fail("Error parsing XML input file %s: %s." % (path, e)) + pytest.fail("Error parsing XML input file %s: %s." % (path, e)) # test that previous DTD fails current version # test DTD 1.0 with format 1.1 @@ -103,8 +103,8 @@ def test_backwards_compatibility(self): parsed ), f"{versions[1]} validates with 1.0 but shouldn't" except etree.ParseError as e: - self.fail("Error parsing XML input file %s: %s." % (rasFile, e)) + pytest.fail("Error parsing XML input file %s: %s." % (rasFile, e)) if __name__ == "__main__": - main(sys.argv) + pytest.main(sys.argv) diff --git a/tests/test_g2p_cli.py b/tests/test_g2p_cli.py index 399ba563..0730f7ef 100755 --- a/tests/test_g2p_cli.py +++ b/tests/test_g2p_cli.py @@ -386,7 +386,7 @@ def compact_arpabet(xml_string: str) -> str: ) assert compact_arpabet(converted_1) == compact_arpabet(converted_2) - def test_convert_xml_subwords(self): + def test_convert_xml_subwords(self, caplog): """Unit testing for reintroducing subword units""" assert ( run_convert_xml( @@ -435,11 +435,10 @@ def test_convert_xml_subwords(self): cecinot_really_iku """ - with self.assertLogs(LOGGER, level="WARNING") as cm: - result = run_convert_xml(example_with_fallback_lang) + caplog.set_level("WARNING", logger=LOGGER.name) + result = run_convert_xml(example_with_fallback_lang) assert "S AH S IY N AO T _ZH EH AE L L UW _IY K UW" in result - logger_output = "\n".join(cm.output) - assert 'No valid g2p conversion found for "not_really_iku"' in logger_output + assert 'No valid g2p conversion found for "not_really_iku"' in caplog.text def test_convert_xml_invalid(self): """test readalongs.text.convert_xml.convert_xml() with invalid input""" @@ -454,7 +453,7 @@ def test_convert_xml_invalid(self): assert etree.tounicode(c_xml) == 'invalid' assert not valid, "convert_xml with invalid pre-g2p'd text" - def test_invalid_langs_in_xml(self): + def test_invalid_langs_in_xml(self, caplog): xml = parse_xml( """ @@ -463,18 +462,17 @@ def test_invalid_langs_in_xml(self): """ ) - with self.assertLogs(LOGGER, level="WARNING") as cm: - c_xml, valid, _ = convert_xml(xml, verbose_warnings=True) + caplog.set_level("WARNING", logger=LOGGER.name) + c_xml, valid, _ = convert_xml(xml, verbose_warnings=True) assert not valid - logger_output = "\n".join(cm.output) + logger_output = caplog.text assert "No lang" in logger_output assert "foo" in logger_output assert 'no path from "crx-syl"' in logger_output def test_non_convertible_words(self): xml = parse_xml("43:2365:6743:23") - with self.assertLogs(LOGGER, level="WARNING"): - g2p_xml, valid, non_convertible_words = convert_xml(xml) + g2p_xml, valid, non_convertible_words = convert_xml(xml) assert not valid assert non_convertible_words == ["43:23", "65:67"] diff --git a/tests/test_make_xml_cli.py b/tests/test_make_xml_cli.py index 6617d72e..0b332334 100755 --- a/tests/test_make_xml_cli.py +++ b/tests/test_make_xml_cli.py @@ -28,8 +28,7 @@ def updateStudioVersion(input): class TestMakeXMLCli(BasicTestCase): """Test suite for the readalongs make-xml CLI command""" - def setUp(self): - super().setUp() + def _setUp(self): self.empty_file = os.path.join(self.tempdir, "empty.txt") with open(self.empty_file, "wb"): pass diff --git a/tests/test_misc.py b/tests/test_misc.py index f81fcad2..34da85c6 100755 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -252,19 +252,18 @@ def test_capture_logs(self): LOGGER.info("foo bar baz") assert "foo bar baz" in captured_logs.getvalue() - def test_capture_logs_some_more(self): + def test_capture_logs_some_more(self, caplog): with capture_logs() as captured_logs: LOGGER.info("this will be captured") assert "this will be captured" in captured_logs.getvalue() - with self.assertLogs(LOGGER): - LOGGER.info("blah") - with self.assertLogs(LOGGER) as cm: - with capture_logs() as captured_logs: - LOGGER.info("This text does not propagate to root") - LOGGER.info("This text is included in root") - assert "propagate" in captured_logs.getvalue() - assert "included" in "".join(cm.output) - assert "propagate" not in "".join(cm.output) + caplog.set_level("INFO", logger=LOGGER.name) + LOGGER.info("blah") + with capture_logs() as captured_logs: + LOGGER.info("This text does not propagate to root") + LOGGER.info("This text is included in root") + assert "propagate" in captured_logs.getvalue() + assert "included" in caplog.text + assert "propagate" not in caplog.text def test_version_is_pep440_compliant(self): assert is_canonical(VERSION) diff --git a/tests/test_package_urls.py b/tests/test_package_urls.py index f86e1f17..0d5fa4f5 100755 --- a/tests/test_package_urls.py +++ b/tests/test_package_urls.py @@ -10,7 +10,7 @@ JS_BUNDLE_URL, fetch_bundle_file, ) -from tests.basic_test_case import BasicTestCase, silence_logs +from tests.basic_test_case import BasicTestCase class TestPackageURLs(BasicTestCase): @@ -38,8 +38,7 @@ def test_fetch_bundles_fallback(self): # Try with a bad URL bad_url = JS_BUNDLE_URL.replace("unpkg.com", "not-a-server.zzz") # print(bad_url) - with silence_logs(): - status, contents, _ = fetch_bundle_file(bad_url, "bundle.js", None) + status, contents, _ = fetch_bundle_file(bad_url, "bundle.js", None) # print(status, len(contents)) assert status != 200 assert isinstance(status, str) diff --git a/tests/test_smil.py b/tests/test_smil.py index d4ae9676..d26f994d 100644 --- a/tests/test_smil.py +++ b/tests/test_smil.py @@ -16,8 +16,7 @@ class TestSmilUtilities(BasicTestCase): """Unit test suite for the smil writing and parsing utilities""" - def setUp(self): - super().setUp() + def _setUp(self): self.words = [ {"id": "w1", "start": 0.01, "end": 0.75}, {"id": "w2", "start": 0.8, "end": 1.04}, diff --git a/tests/test_temp_file.py b/tests/test_temp_file.py index 5881a08f..cc83806f 100755 --- a/tests/test_temp_file.py +++ b/tests/test_temp_file.py @@ -5,7 +5,6 @@ import os import sys from tempfile import NamedTemporaryFile -from unittest import TestCase from pytest import main @@ -13,7 +12,7 @@ from readalongs.portable_tempfile import PortableNamedTemporaryFile -class TestTempFile(TestCase): +class TestTempFile: """Test PortableNamedTemporaryFile class""" def test_basic_file(self): diff --git a/tests/test_tokenize_cli.py b/tests/test_tokenize_cli.py index 7160d2dc..b8ae359b 100755 --- a/tests/test_tokenize_cli.py +++ b/tests/test_tokenize_cli.py @@ -16,9 +16,8 @@ class TestTokenizeCli(BasicTestCase): """Test suite for the readalongs tokenize CLI command""" - def setUp(self): - """setUp() creates self.tempdir and makes an XML file for use in other tests""" - super().setUp() + def _setUp(self): + """Create the XML file used by the tests.""" self.rasfile = os.path.join(self.tempdir, "fra.readalong") _ = self.runner.invoke( diff --git a/tests/test_tokenize_xml.py b/tests/test_tokenize_xml.py index 6baf15f4..8b31419e 100755 --- a/tests/test_tokenize_xml.py +++ b/tests/test_tokenize_xml.py @@ -5,7 +5,6 @@ import sys from contextlib import redirect_stderr from io import StringIO -from unittest import TestCase from lxml import etree from pytest import main @@ -14,7 +13,7 @@ from readalongs.text.util import parse_xml -class TestTokenizer(TestCase): +class TestTokenizer: """Test the tokenize_xml function""" def test_simple(self): diff --git a/tests/test_web_api.py b/tests/test_web_api.py index 2fd2a29a..052c22ed 100755 --- a/tests/test_web_api.py +++ b/tests/test_web_api.py @@ -8,8 +8,8 @@ from io import StringIO from textwrap import dedent from time import perf_counter -from unittest.mock import patch +import pytest from pytest import main, raises from readalongs._version import READALONG_FILE_FORMAT_VERSION, VERSION @@ -121,7 +121,10 @@ def test_g2p_exceeds_time_limit(self): # preprocessing takes about 5 ms, g2p about 200 ms, use 50 ms is nicely between # so we know it'll fail in g2p. text = self.slurp_data_file("ej-fra.txt") - with patch("readalongs.web_api.ASSEMBLE_TIME_LIMIT_IN_SECONDS", 0.05): + with pytest.MonkeyPatch.context() as monkeypatch: + monkeypatch.setattr( + "readalongs.web_api.ASSEMBLE_TIME_LIMIT_IN_SECONDS", 0.05 + ) request = { "input": text, "type": "text/plain", @@ -137,7 +140,10 @@ def test_g2p_exceeds_time_limit(self): def test_prepro_exceeds_time_limit(self): # preprocessing takes about 5 ms, so 1 micros is guaranteed to be too short on any hardware. text = self.slurp_data_file("ej-fra.txt") - with patch("readalongs.web_api.ASSEMBLE_TIME_LIMIT_IN_SECONDS", 0.000001): + with pytest.MonkeyPatch.context() as monkeypatch: + monkeypatch.setattr( + "readalongs.web_api.ASSEMBLE_TIME_LIMIT_IN_SECONDS", 0.000001 + ) request = { "input": text, "type": "text/plain", @@ -500,26 +506,24 @@ def test_convert_to_TextGrid_errors(self): ) assert response.status_code == 422, "Negative duration should fail." - def test_cleanup_temp_dir(self): + def test_cleanup_temp_dir(self, caplog): """Make sure convert's temporary directory actually gets deleted.""" request = { "dur": 83.1, "ras": self.hej_verden_xml, } - with self.assertLogs(LOGGER, "INFO") as log_cm: - response = self.API_CLIENT.post( - "/api/v1/convert_alignment/textgrid", json=request - ) + caplog.set_level("INFO", logger=LOGGER.name) + response = self.API_CLIENT.post( + "/api/v1/convert_alignment/textgrid", json=request + ) assert response.status_code == 200 # print(log_cm.output) - match = re.search( - "Temporary directory: (.*)($|\r|\n)", "\n".join(log_cm.output) - ) - assert match is not None + match = re.search("Temporary directory: (.*)($|\r|\n)", caplog.text) assert match is not None + assert match[1] is not None assert not os.path.isdir(match[1]) - def test_cleanup_even_if_error(self): + def test_cleanup_even_if_error(self, caplog): # This is seriously white-box testing... overlapping words # will cause an exception deeper in the code after the # temporary directory is created. We exercise here catching @@ -552,15 +556,14 @@ def test_cleanup_even_if_error(self): } for format_name in OutputFormat: - with self.assertLogs(LOGGER, "INFO") as log_cm: - response = self.API_CLIENT.post( - f"/api/v1/convert_alignment/{format_name.value}", json=request - ) + caplog.clear() + caplog.set_level("INFO", logger=LOGGER.name) + response = self.API_CLIENT.post( + f"/api/v1/convert_alignment/{format_name.value}", json=request + ) assert response.status_code == 422 # print(log_cm.output) - match = re.search( - "Temporary directory: (.*)($|\r|\n)", "\n".join(log_cm.output) - ) + match = re.search("Temporary directory: (.*)($|\r|\n)", caplog.text) assert match is not None assert match is not None assert not os.path.isdir(match[1]) From 8146967f5062f69c94c7a726f65744d4044abfe5 Mon Sep 17 00:00:00 2001 From: Eric Joanis Date: Tue, 8 Sep 2026 17:39:06 -0400 Subject: [PATCH 08/10] refactor(tests): normalize how we import pytest --- tests/test_audio.py | 6 +++--- tests/test_config.py | 18 +++++++++--------- tests/test_dna_text.py | 10 ++++++---- tests/test_force_align.py | 8 ++++---- tests/test_g2p_cli.py | 6 +++--- tests/test_make_xml_cli.py | 8 ++++---- tests/test_misc.py | 16 ++++++++-------- tests/test_silence.py | 6 +++--- tests/test_smil.py | 12 ++++++------ tests/test_web_api.py | 7 +++---- 10 files changed, 49 insertions(+), 48 deletions(-) diff --git a/tests/test_audio.py b/tests/test_audio.py index 8bc54fea..15882fac 100755 --- a/tests/test_audio.py +++ b/tests/test_audio.py @@ -7,7 +7,7 @@ from pathlib import Path from subprocess import run -from pytest import approx, main +import pytest from readalongs.audio_utils import ( extract_section, @@ -154,10 +154,10 @@ def test_write_audio_to_file(self): write_audio_to_file(section, output_path) assert os.path.exists(output_path) reloaded_section = read_audio_from_file(output_path) - assert len(section) == approx( + assert len(section) == pytest.approx( len(reloaded_section), abs=50 ), "reloaded audio file is more than 50ms shorter or longer" if __name__ == "__main__": - main(sys.argv) + pytest.main(sys.argv) diff --git a/tests/test_config.py b/tests/test_config.py index 762d56e6..e365b8fc 100755 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -7,8 +7,8 @@ import sys from contextlib import redirect_stderr +import pytest from lxml import etree -from pytest import fixture, main, raises from readalongs.text.add_elements_to_xml import add_images, add_supplementary_xml from readalongs.text.util import load_xml @@ -19,7 +19,7 @@ class TestConfig: readalong: etree - @fixture(autouse=True, scope="class") + @pytest.fixture(autouse=True, scope="class") @classmethod def _pytest_setup(cls) -> None: data_dir = os.path.join(os.path.dirname(__file__), "data") @@ -27,22 +27,22 @@ def _pytest_setup(cls) -> None: def test_image(self) -> None: """Test images are added correctly""" - with raises(KeyError): + with pytest.raises(KeyError): new_xml = add_images(self.readalong, {}) new_xml = add_images(self.readalong, {"images": {"0": "test.jpg"}}) assert len(new_xml.xpath("//graphic")) == 1 - with raises(TypeError): + with pytest.raises(TypeError): new_xml = add_images(self.readalong, {"images": [{"0": "test.jpg"}]}) - with raises(ValueError): + with pytest.raises(ValueError): new_xml = add_images(self.readalong, {"images": {"a": "test.jpg"}}) - with raises(IndexError): + with pytest.raises(IndexError): new_xml = add_images( self.readalong, {"images": {"0": "test.jpg", "999": "out_of_range.jpg"}} ) def test_arbitrary_xml(self): """Test arbitrary xml is added correctly""" - with raises(KeyError): + with pytest.raises(KeyError): new_xml = add_supplementary_xml(self.readalong, {}) new_xml = add_supplementary_xml( self.readalong, @@ -58,7 +58,7 @@ def test_arbitrary_xml(self): assert len(new_xml.xpath("//test")) == 1 # bad xml raises lxml.etree.XMLSyntaxError - with raises(etree.XMLSyntaxError): + with pytest.raises(etree.XMLSyntaxError): new_xml = add_supplementary_xml( self.readalong, {"xml": [{"xpath": "//div[1]", "value": "bloop"}]} ) @@ -81,4 +81,4 @@ def test_arbitrary_xml(self): if __name__ == "__main__": - main(sys.argv) + pytest.main(sys.argv) diff --git a/tests/test_dna_text.py b/tests/test_dna_text.py index a2a7f924..155a7629 100755 --- a/tests/test_dna_text.py +++ b/tests/test_dna_text.py @@ -6,8 +6,8 @@ from contextlib import redirect_stderr from io import StringIO +import pytest from lxml import etree -from pytest import main, raises from readalongs.text import tokenize_xml from readalongs.text.add_ids_to_xml import add_ids @@ -151,7 +151,9 @@ def test_dna_word(self): xml = parse_xml(txt) with redirect_stderr(StringIO()): tokenized = tokenize_xml.tokenize_xml(xml) - with raises(RuntimeError, match=r"Found element with.*This is not allowed"): + with pytest.raises( + RuntimeError, match=r"Found element with.*This is not allowed" + ): add_ids(tokenized) def test_dna_word_nested(self): @@ -161,11 +163,11 @@ def test_dna_word_nested(self): xml = parse_xml(txt) with redirect_stderr(StringIO()): tokenized = tokenize_xml.tokenize_xml(xml) - with raises( + with pytest.raises( RuntimeError, match=r"Found nested inside.*This is not allowed" ): add_ids(tokenized) if __name__ == "__main__": - main(sys.argv) + pytest.main(sys.argv) diff --git a/tests/test_force_align.py b/tests/test_force_align.py index 4b9d32eb..a1af4fcf 100755 --- a/tests/test_force_align.py +++ b/tests/test_force_align.py @@ -12,8 +12,8 @@ from io import StringIO from tempfile import TemporaryDirectory +import pytest from lxml import etree -from pytest import main, raises from soundswallower import get_model_path from readalongs.align import align_audio @@ -167,11 +167,11 @@ def test_align_fail(self): writer.setsampwidth(2) writer.setframerate(16000) writer.writeframes(b"\x00\x00") - with raises(RuntimeError), redirect_stderr(StringIO()): + with pytest.raises(RuntimeError), redirect_stderr(StringIO()): _ = align_audio(xml_path, tf.name, unit="w") def test_bad_align_mode(self): - with raises(AssertionError), redirect_stderr(StringIO()): + with pytest.raises(AssertionError), redirect_stderr(StringIO()): _ = align_audio( os.path.join(self.data_dir, "ej-fra.readalong"), os.path.join(self.data_dir, "noise.mp3"), @@ -211,4 +211,4 @@ def test_convert_no_version(self): if __name__ == "__main__": LOGGER.setLevel("DEBUG") - main(sys.argv) + pytest.main(sys.argv) diff --git a/tests/test_g2p_cli.py b/tests/test_g2p_cli.py index 0730f7ef..6691174a 100755 --- a/tests/test_g2p_cli.py +++ b/tests/test_g2p_cli.py @@ -8,8 +8,8 @@ from contextlib import redirect_stderr from io import StringIO +import pytest from lxml import etree -from pytest import main, raises from readalongs.align import align_audio from readalongs.cli import align, g2p, make_xml, tokenize @@ -306,7 +306,7 @@ def test_align_with_invalid_preg2p(self): audio_file = os.path.join(self.data_dir, "ej-fra.m4a") with redirect_stderr(StringIO()): - with raises(RuntimeError, match="could not be g2p'd"): + with pytest.raises(RuntimeError, match="could not be g2p'd"): results = align_audio(input_file, audio_file) def test_align_with_preg2p(self): @@ -478,4 +478,4 @@ def test_non_convertible_words(self): if __name__ == "__main__": - main(sys.argv) + pytest.main(sys.argv) diff --git a/tests/test_make_xml_cli.py b/tests/test_make_xml_cli.py index 0b332334..ccddc9da 100755 --- a/tests/test_make_xml_cli.py +++ b/tests/test_make_xml_cli.py @@ -8,7 +8,7 @@ import sys from shutil import copyfile -from pytest import main, raises +import pytest # from readalongs.log import LOGGER from readalongs._version import READALONG_FILE_FORMAT_VERSION, VERSION @@ -180,11 +180,11 @@ def test_create_input_ras_errors(self): # These used to be RuntimeError, but that was not right: *programmer* # errors can and should dump stack traces, unlike *user* errors, which # warrant nice friendly messages. - with raises(AssertionError): + with pytest.raises(AssertionError): # missing input_file_name or input_file_handle _, _ = create_input_ras() - with raises(AssertionError): + with pytest.raises(AssertionError): # missing text_languages _, _ = create_input_ras( input_file_name=os.path.join(self.data_dir, "fra.txt") @@ -287,4 +287,4 @@ def text2lines(text: str): if __name__ == "__main__": - main(sys.argv) + pytest.main(sys.argv) diff --git a/tests/test_misc.py b/tests/test_misc.py index 34da85c6..38095269 100755 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -7,9 +7,9 @@ import sys import click +import pytest from lxml import etree from pep440 import is_canonical -from pytest import main, raises from readalongs._version import READALONG_FILE_FORMAT_VERSION, VERSION from readalongs.align import split_silences @@ -55,7 +55,7 @@ def test_parse_time(self): def test_parse_time_errors(self): """Test readalongs.text.util.parse_time() with invalid inputs""" for err_time_str in ("3.4.5 ms", ".", "", "asdf", " 0 h z ", "nm"): - with raises(ValueError): + with pytest.raises(ValueError): _ = parse_time(err_time_str) def test_split_silences(self): @@ -166,7 +166,7 @@ def test_joiner_callback(self): cb = JoinerCallbackForClick(iter("qwer")) # iterable over four characters assert cb(None, None, ["e:r"]) == ["e", "r"] assert cb(None, None, ["q,w"]) == ["q", "w"] - with raises(click.BadParameter): + with pytest.raises(click.BadParameter): cb(None, None, ["q:e", "a,w"]) assert cb(None, None, ["r:q", "w"]) == ["r", "q", "w"] @@ -191,18 +191,18 @@ def test_load_xml(self): def test_load_xml_errors(self): # non-existent file - with raises(OSError): + with pytest.raises(OSError): load_xml("file-does-not-exist.readalong") # invalid XML file bad_file = self.tempdir / "bad.readalong" with open(bad_file, "w") as f: print("This is not XML", file=f) - with raises(etree.ParseError): + with pytest.raises(etree.ParseError): load_xml(bad_file) # empty file is also invalid - with raises(etree.ParseError): + with pytest.raises(etree.ParseError): load_xml(os.devnull) # make sure we're not vulnerable to XML bombs @@ -229,7 +229,7 @@ def test_parse_xml(self): assert etree.tostring(xml) == etree.tostring(xml2) malformed_xml_text = " """ ) - with raises(ValueError): + with pytest.raises(ValueError): _ = parse_smil(missing_id) missing_clip_end = dedent( @@ -85,7 +85,7 @@ def test_parse_bad_smil(self): """ ) - with raises(ValueError): + with pytest.raises(ValueError): _ = parse_smil(missing_clip_end) bad_float = dedent( @@ -100,9 +100,9 @@ def test_parse_bad_smil(self): """ ) - with raises(ValueError): + with pytest.raises(ValueError): _ = parse_smil(bad_float) if __name__ == "__main__": - main(sys.argv) + pytest.main(sys.argv) diff --git a/tests/test_web_api.py b/tests/test_web_api.py index 052c22ed..64d96907 100755 --- a/tests/test_web_api.py +++ b/tests/test_web_api.py @@ -10,7 +10,6 @@ from time import perf_counter import pytest -from pytest import main, raises from readalongs._version import READALONG_FILE_FORMAT_VERSION, VERSION from readalongs.log import LOGGER @@ -164,9 +163,9 @@ def test_convert_time_limit(self): ids_added = add_ids(tokenized) # This convert_xml call takes about 0.2s on my machine, so 1ms is guaranteed to be # too short on any hardware. - with raises(TimeLimitException): + with pytest.raises(TimeLimitException): _ = convert_xml(ids_added, time_limit=0.001) - with raises(TimeLimitException): + with pytest.raises(TimeLimitException): _ = convert_xml( ids_added, time_limit=1.001, start_time=perf_counter() - 1.0 ) @@ -595,4 +594,4 @@ def test_convert_to_bad_format(self): if __name__ == "__main__": - main(sys.argv) + pytest.main(sys.argv) From 4ae63b2dcce79e34109a493bc3819ae870188b68 Mon Sep 17 00:00:00 2001 From: Eric Joanis Date: Wed, 9 Sep 2026 09:14:45 -0400 Subject: [PATCH 09/10] fix(tests): use pytest std setup_class and setup_method where possible --- tests/test_config.py | 3 +-- tests/test_dtd.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/test_config.py b/tests/test_config.py index e365b8fc..98dfda9b 100755 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -19,9 +19,8 @@ class TestConfig: readalong: etree - @pytest.fixture(autouse=True, scope="class") @classmethod - def _pytest_setup(cls) -> None: + def setup_class(cls) -> None: data_dir = os.path.join(os.path.dirname(__file__), "data") cls.readalong = load_xml(os.path.join(data_dir, "ej-fra.readalong")) diff --git a/tests/test_dtd.py b/tests/test_dtd.py index b424203d..6fe9489e 100644 --- a/tests/test_dtd.py +++ b/tests/test_dtd.py @@ -41,8 +41,7 @@ class TestDTD: """Test the XML DTD""" - @pytest.fixture(autouse=True) - def _pytest_setup(self): + def setup_method(self) -> None: with open(DTDPATH) as infh: self.dtd = etree.DTD(infh) From 1e2f559e3623c97ea4e76003f01433d852e60b34 Mon Sep 17 00:00:00 2001 From: Eric Joanis Date: Thu, 10 Sep 2026 13:51:29 -0400 Subject: [PATCH 10/10] fix(tests): correct test error flagged by AP in review --- tests/test_anchors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_anchors.py b/tests/test_anchors.py index d18c3954..3d2ccc29 100755 --- a/tests/test_anchors.py +++ b/tests/test_anchors.py @@ -34,7 +34,7 @@ def test_anchors_inner_only(self): assert words[0]["end"] <= 1.62 assert words[1]["start"] >= 1.62 assert words[8]["end"] <= 3.81 - assert words[9]["start"] >= 3.81 + assert words[9]["start"] >= 3.82 assert words[21]["end"] <= 6.74 assert words[22]["start"] >= 6.74