Skip to content

Commit 53bd85f

Browse files
Changes to test glossaries to fix issues with parallelized tests.
- Test glossary deletion is now managed with context managers or finally clauses, rather than deleting all test glossaries at test begin. - Test glossary names include UUIDs.
1 parent 7a6b236 commit 53bd85f

File tree

4 files changed

+402
-271
lines changed

4 files changed

+402
-271
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
### Deprecated
1313
### Removed
1414
### Fixed
15+
* Fix issues with parallelized tests by changing how test glossaries are created and deleted.
1516
### Security
1617

1718

tests/conftest.py

Lines changed: 109 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
import pathlib
88
from pydantic import BaseSettings
99
import pytest
10-
from typing import Optional
10+
from typing import Callable, Optional
11+
from typing_extensions import Protocol
1112
import uuid
1213

1314

@@ -32,9 +33,6 @@ class Config:
3233
env_prefix = "DEEPL_"
3334

3435

35-
glossary_name_prefix = "deepl-python-test-glossary: "
36-
37-
3836
@pytest.fixture
3937
def config():
4038
return Config()
@@ -148,21 +146,118 @@ def translator_with_random_auth_key(server):
148146
return _make_translator(server, auth_key=str(uuid.uuid1()))
149147

150148

151-
def remove_test_glossaries(translator):
152-
glossaries = translator.list_glossaries()
153-
for glossary in glossaries:
154-
if glossary.name.startswith(glossary_name_prefix):
155-
translator.delete_glossary(glossary)
149+
@pytest.fixture
150+
def cleanup_matching_glossaries(translator):
151+
"""
152+
Fixture function to remove all glossaries from the server matching the
153+
given predicate. Can be used, for example, to remove all glossaries with a
154+
matching name.
155+
156+
Usage example:
157+
def test_example(cleanup_matching_glossaries):
158+
...
159+
cleanup_matching_glossaries(
160+
lambda glossary: glossary.name.startswith("test ")
161+
)
162+
"""
163+
164+
def do_cleanup(predicate: Callable[[deepl.GlossaryInfo], bool]):
165+
glossaries = translator.list_glossaries()
166+
for glossary in glossaries:
167+
if predicate(glossary):
168+
try:
169+
translator.delete_glossary(glossary)
170+
except deepl.DeepLException:
171+
pass
172+
173+
return do_cleanup
174+
175+
176+
class ManagedGlossary:
177+
"""
178+
Utility content-manager class to create a test glossary and ensure its
179+
deletion at the end of a test.
180+
"""
181+
182+
def __init__(
183+
self,
184+
translator: deepl.Translator,
185+
glossary_name: str,
186+
source_lang,
187+
target_lang,
188+
entries: dict,
189+
):
190+
self._translator = translator
191+
self._created_glossary = translator.create_glossary(
192+
glossary_name, source_lang, target_lang, entries
193+
)
194+
195+
def __enter__(self) -> deepl.GlossaryInfo:
196+
return self._created_glossary
197+
198+
def __exit__(self, exc_type, exc_val, exc_tb):
199+
try:
200+
self._translator.delete_glossary(
201+
self._created_glossary.glossary_id
202+
)
203+
except deepl.DeepLException:
204+
pass
205+
206+
207+
class CreateManagedGlossaryFunc(Protocol):
208+
"""Helper class for type hints."""
209+
210+
def __call__(
211+
self,
212+
source_lang: str = "EN",
213+
target_lang: str = "DE",
214+
entries: Optional[dict] = None,
215+
glossary_name_suffix: str = "",
216+
) -> ManagedGlossary:
217+
pass
156218

157219

158220
@pytest.fixture
159-
def glossary_name(translator, request) -> str:
160-
"""Returns a suitable glossary name to be used in the test"""
161-
# Remove all test glossaries from the server
162-
remove_test_glossaries(translator)
221+
def glossary_manager(translator, glossary_name) -> CreateManagedGlossaryFunc:
222+
"""
223+
Fixture function that may be used to create context-managed test
224+
glossaries, named using the current test. May be called multiple times in
225+
a test to create multiple glossaries, ideally with a different suffix for
226+
each glossary.
227+
228+
Usage example:
229+
def test_example(glossary_manager):
230+
with glossary_manager(
231+
entries={"a": "b"}, glossary_name_suffix="1"
232+
) as glossary1:
233+
...
234+
"""
235+
236+
def create_managed_glossary(
237+
source_lang: str = "EN",
238+
target_lang: str = "DE",
239+
entries: Optional[dict] = None,
240+
glossary_name_suffix: str = "",
241+
):
242+
if not entries:
243+
entries = {"Hello": "Hallo"}
244+
return ManagedGlossary(
245+
translator,
246+
f"{glossary_name}{glossary_name_suffix}",
247+
source_lang,
248+
target_lang,
249+
entries,
250+
)
251+
252+
return create_managed_glossary
253+
163254

255+
@pytest.fixture
256+
def glossary_name(request) -> str:
257+
"""Returns a suitable glossary name to be used in the test"""
164258
test_name = request.node.name
165-
return f"{glossary_name_prefix}{test_name}"
259+
new_uuid = str(uuid.uuid1())
260+
return f"deepl-python-test-glossary: {test_name} {new_uuid}"
166261

167262

168263
@pytest.fixture
@@ -201,21 +296,6 @@ def output_document_path(tmpdir):
201296
return path
202297

203298

204-
def create_glossary(
205-
translator,
206-
glossary_name,
207-
*,
208-
source_lang="EN",
209-
target_lang="DE",
210-
entries: Optional[dict] = None,
211-
) -> deepl.GlossaryInfo:
212-
if entries is None:
213-
entries = {"Hallo": "Hello"}
214-
return translator.create_glossary(
215-
glossary_name, source_lang, target_lang, entries
216-
)
217-
218-
219299
# Decorate test functions with "@needs_mock_server" to skip them if a real
220300
# server is used
221301
needs_mock_server = pytest.mark.skipif(

tests/test_cli.py

Lines changed: 119 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,9 @@ def test_glossary_no_subcommand(runner):
188188
assert "subcommand is required" in result.output
189189

190190

191-
def test_glossary_create(runner, glossary_name, tmpdir):
191+
def test_glossary_create(
192+
runner, glossary_name, tmpdir, cleanup_matching_glossaries
193+
):
192194
name_cli = f"{glossary_name}-cli"
193195
name_stdin = f"{glossary_name}-stdin"
194196
name_file = f"{glossary_name}-file"
@@ -198,85 +200,120 @@ def test_glossary_create(runner, glossary_name, tmpdir):
198200
file = tmpdir / "glossary_entries"
199201
file.write(entries_tsv)
200202

201-
result = runner.invoke(
202-
deepl.__main__,
203-
f'-vv glossary create --name "{name_cli}" --from DE --to EN '
204-
f"{entries_cli}",
205-
)
206-
assert result.exit_code == 0, f"exit: {result.exit_code}\n {result.output}"
207-
result = runner.invoke(
208-
deepl.__main__,
209-
f'-vv glossary create --name "{name_stdin}" --from DE --to EN -',
210-
input=entries_tsv,
211-
)
212-
assert result.exit_code == 0, f"exit: {result.exit_code}\n {result.output}"
213-
result = runner.invoke(
214-
deepl.__main__,
215-
f'-vv glossary create --name "{name_file}" --from DE --to EN '
216-
f"--file {file}",
217-
)
218-
assert result.exit_code == 0, f"exit: {result.exit_code}\n {result.output}"
219-
220-
result = runner.invoke(deepl.__main__, f"-vv glossary list")
221-
assert result.exit_code == 0, f"exit: {result.exit_code}\n {result.output}"
222-
assert name_cli in result.output
223-
assert name_stdin in result.output
224-
assert name_file in result.output
225-
226-
# Cannot use --file option together with entries
227-
result = runner.invoke(
228-
deepl.__main__,
229-
f'-vv glossary create --name "{name_file}" --from DE --to EN '
230-
f"--file {file} {entries_cli}",
231-
)
232-
assert result.exit_code == 1, f"exit: {result.exit_code}\n {result.output}"
233-
assert "--file argument" in result.output
234-
235-
236-
def test_glossary_get(translator, runner, glossary_name):
237-
created_id = create_glossary(translator, glossary_name).glossary_id
238-
239-
result = runner.invoke(deepl.__main__, f"-vv glossary get {created_id}")
240-
print(result.output)
241-
assert result.exit_code == 0, f"exit: {result.exit_code}\n {result.output}"
242-
assert glossary_name in result.output
243-
244-
245-
def test_glossary_list(translator, runner, glossary_name):
246-
suffix_list = ["1", "2", "3"]
247-
for suffix in suffix_list:
248-
create_glossary(translator, glossary_name + suffix)
249-
250-
result = runner.invoke(deepl.__main__, f"-vv glossary list")
251-
assert result.exit_code == 0, f"exit: {result.exit_code}\n {result.output}"
252-
for suffix in suffix_list:
253-
assert f"{glossary_name}{suffix}" in result.output
254-
255-
256-
def test_glossary_entries(translator, runner, glossary_name):
203+
try:
204+
result = runner.invoke(
205+
deepl.__main__,
206+
f'-vv glossary create --name "{name_cli}" --from DE --to EN '
207+
f"{entries_cli}",
208+
)
209+
assert (
210+
result.exit_code == 0
211+
), f"exit: {result.exit_code}\n {result.output}"
212+
result = runner.invoke(
213+
deepl.__main__,
214+
f'-vv glossary create --name "{name_stdin}" --from DE --to EN -',
215+
input=entries_tsv,
216+
)
217+
assert (
218+
result.exit_code == 0
219+
), f"exit: {result.exit_code}\n {result.output}"
220+
result = runner.invoke(
221+
deepl.__main__,
222+
f'-vv glossary create --name "{name_file}" --from DE --to EN '
223+
f"--file {file}",
224+
)
225+
assert (
226+
result.exit_code == 0
227+
), f"exit: {result.exit_code}\n {result.output}"
228+
229+
result = runner.invoke(deepl.__main__, f"-vv glossary list")
230+
assert (
231+
result.exit_code == 0
232+
), f"exit: {result.exit_code}\n {result.output}"
233+
assert name_cli in result.output
234+
assert name_stdin in result.output
235+
assert name_file in result.output
236+
237+
# Cannot use --file option together with entries
238+
result = runner.invoke(
239+
deepl.__main__,
240+
f'-vv glossary create --name "{name_file}" --from DE --to EN '
241+
f"--file {file} {entries_cli}",
242+
)
243+
assert (
244+
result.exit_code == 1
245+
), f"exit: {result.exit_code}\n {result.output}"
246+
assert "--file argument" in result.output
247+
248+
finally:
249+
cleanup_matching_glossaries(
250+
lambda glossary: glossary.name in [name_file, name_cli, name_stdin]
251+
)
252+
253+
254+
def test_glossary_get(translator, runner, glossary_manager):
255+
with glossary_manager() as created_glossary:
256+
created_id = created_glossary.glossary_id
257+
258+
result = runner.invoke(
259+
deepl.__main__, f"-vv glossary get {created_id}"
260+
)
261+
print(result.output)
262+
assert (
263+
result.exit_code == 0
264+
), f"exit: {result.exit_code}\n {result.output}"
265+
assert created_id in result.output
266+
assert created_glossary.name in result.output
267+
268+
269+
def test_glossary_list(translator, runner, glossary_manager):
270+
with glossary_manager(glossary_name_suffix="1") as g1, glossary_manager(
271+
glossary_name_suffix="2"
272+
) as g2, glossary_manager(glossary_name_suffix="3") as g3:
273+
glossary_list = [g1, g2, g3]
274+
275+
result = runner.invoke(deepl.__main__, f"-vv glossary list")
276+
assert (
277+
result.exit_code == 0
278+
), f"exit: {result.exit_code}\n {result.output}"
279+
for glossary in glossary_list:
280+
assert glossary.name in result.output
281+
282+
283+
def test_glossary_entries(translator, runner, glossary_manager):
257284
entries = {"Hallo": "Hello", "Maler": "Artist"}
258-
created_id = create_glossary(
259-
translator, glossary_name, entries=entries
260-
).glossary_id
261-
262-
result = runner.invoke(
263-
deepl.__main__, f"-vv glossary entries {created_id}"
264-
)
265-
assert result.exit_code == 0, f"exit: {result.exit_code}\n {result.output}"
266-
for source, target in entries.items():
267-
assert f"{source}\t{target}" in result.output
268-
269-
270-
def test_glossary_delete(translator, runner, glossary_name):
271-
created_id = create_glossary(translator, glossary_name).glossary_id
272-
result = runner.invoke(deepl.__main__, f"glossary list")
273-
assert result.exit_code == 0, f"exit: {result.exit_code}\n {result.output}"
274-
assert created_id in result.output
275-
276-
# Remove the created glossary
277-
result = runner.invoke(deepl.__main__, f'glossary delete "{created_id}"')
278-
assert result.exit_code == 0, f"exit: {result.exit_code}\n {result.output}"
279-
280-
result = runner.invoke(deepl.__main__, f"glossary list")
281-
assert result.exit_code == 0, f"exit: {result.exit_code}\n {result.output}"
282-
assert created_id not in result.output
285+
with glossary_manager(entries=entries) as created_glossary:
286+
created_id = created_glossary.glossary_id
287+
288+
result = runner.invoke(
289+
deepl.__main__, f"-vv glossary entries {created_id}"
290+
)
291+
assert (
292+
result.exit_code == 0
293+
), f"exit: {result.exit_code}\n {result.output}"
294+
for source, target in entries.items():
295+
assert f"{source}\t{target}" in result.output
296+
297+
298+
def test_glossary_delete(translator, runner, glossary_manager):
299+
with glossary_manager() as created_glossary:
300+
created_id = created_glossary.glossary_id
301+
result = runner.invoke(deepl.__main__, f"glossary list")
302+
assert (
303+
result.exit_code == 0
304+
), f"exit: {result.exit_code}\n {result.output}"
305+
assert created_id in result.output
306+
307+
# Remove the created glossary
308+
result = runner.invoke(
309+
deepl.__main__, f'glossary delete "{created_id}"'
310+
)
311+
assert (
312+
result.exit_code == 0
313+
), f"exit: {result.exit_code}\n {result.output}"
314+
315+
result = runner.invoke(deepl.__main__, f"glossary list")
316+
assert (
317+
result.exit_code == 0
318+
), f"exit: {result.exit_code}\n {result.output}"
319+
assert created_id not in result.output

0 commit comments

Comments
 (0)