Skip to content

Commit 936f03c

Browse files
committed
Tests: Increase determinism and speed of tests that perform downloads
2 parents 1164301 + 862f75c commit 936f03c

16 files changed

+254
-350
lines changed

RELEASE_NOTES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ Release Notes ⋮
1616
* macOS 14+ is now the minimum macOS version.
1717
Drop support for macOS 13.
1818

19+
* Testing improvements
20+
* Increased determinism and speed of tests that perform downloads.
21+
1922
### v2.0.0 (September 26, 2025)
2023

2124
Crystal 2.0 is a huge release with many new features and all-new tutorials!

src/crystal/browser/__init__.py

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1242,28 +1242,33 @@ def fg_task() -> bool:
12421242
else:
12431243
progress_dialog = None
12441244

1245-
# Run download() on a background thread because it can take a long time
1246-
# to instantiate the tree of related download tasks (when _LAZY_LOAD_CHILDREN == False)
1247-
#
1248-
# NOTE: Loudly crashes the entire scheduler thread upon failure.
1249-
# If this failure mode ends up happening commonly,
1250-
# suggest implementing a less drastic failure mode.
1251-
@capture_crashes_to(self.project.root_task)
1252-
def bg_task() -> None:
1253-
assert selected_entity is not None
1254-
1245+
if DownloadResourceGroupMembersTask._LAZY_LOAD_CHILDREN:
12551246
# Start download
12561247
selected_entity.download(needs_result=False)
1257-
1258-
# Close progress dialog, if applicable
1259-
if progress_dialog is not None:
1260-
def fg_task() -> None:
1261-
nonlocal progress_dialog
1262-
assert progress_dialog is not None
1263-
progress_dialog.Destroy()
1264-
progress_dialog = None # unexport
1265-
fg_call_and_wait(fg_task)
1266-
bg_call_later(bg_task)
1248+
else:
1249+
# Run download() on a background thread because it can take a long time
1250+
# to instantiate the tree of related download tasks
1251+
# (when _LAZY_LOAD_CHILDREN == False)
1252+
#
1253+
# NOTE: Loudly crashes the entire scheduler thread upon failure.
1254+
# If this failure mode ends up happening commonly,
1255+
# suggest implementing a less drastic failure mode.
1256+
@capture_crashes_to(self.project.root_task)
1257+
def bg_task() -> None:
1258+
assert selected_entity is not None
1259+
1260+
# Start download
1261+
selected_entity.download(needs_result=False)
1262+
1263+
# Close progress dialog, if applicable
1264+
if progress_dialog is not None:
1265+
def fg_task() -> None:
1266+
nonlocal progress_dialog
1267+
assert progress_dialog is not None
1268+
progress_dialog.Destroy()
1269+
progress_dialog = None # unexport
1270+
fg_call_and_wait(fg_task)
1271+
bg_call_later(bg_task)
12671272

12681273
def _on_update_group_members(self, event):
12691274
selected_entity = self.entity_tree.selected_entity

src/crystal/tests/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -324,13 +324,13 @@ download.
324324
It's not recommended to manually wait for a download to complete using a
325325
fixed timeout because they can take a highly variable amount of time to complete,
326326
depending on how many embedded subresources there are, among other factors.
327-
Instead use the `wait_for_download_to_start_and_finish` utility method,
327+
Instead use the `wait_for_download_task_to_start_and_finish` utility method,
328328
which looks for continued *progress* in a download, regardless of how long the
329329
total download task takes to finish:
330330

331331
```
332-
home_ti.Expand()
333-
await wait_for_download_to_start_and_finish(mw.task_tree)
332+
async with wait_for_download_task_to_start_and_finish(project):
333+
home_ti.Expand()
334334
```
335335

336336
## Waiting for other things to happen

src/crystal/tests/test_do_not_download_groups.py

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
assert_does_open_webbrowser_to, extracted_project,
1212
served_project_from_filepath,
1313
)
14-
from crystal.tests.util.tasks import wait_for_download_to_start_and_finish
14+
from crystal.tests.util.tasks import wait_for_download_task_to_start_and_finish
1515
from crystal.tests.util.wait import (
1616
first_child_of_tree_item_is_not_loading_condition, wait_for,
1717
)
@@ -34,8 +34,8 @@ async def test_when_download_html_page_then_does_not_download_embedded_resource_
3434
root_ti = TreeItem.GetRootItem(mw.entity_tree.window)
3535
home_ti = root_ti.GetFirstChild()
3636
assert home_ti is not None
37-
home_ti.Expand()
38-
await wait_for_download_to_start_and_finish(mw.task_tree)
37+
async with wait_for_download_task_to_start_and_finish(project):
38+
home_ti.Expand()
3939
assert first_child_of_tree_item_is_not_loading_condition(home_ti)()
4040

4141
# Expand HTML page children in entity tree
@@ -60,8 +60,8 @@ async def test_then_embedded_resource_does_not_appear_in_a_hidden_embedded_clust
6060
root_ti = TreeItem.GetRootItem(mw.entity_tree.window)
6161
home_ti = root_ti.GetFirstChild()
6262
assert home_ti is not None
63-
home_ti.Expand()
64-
await wait_for_download_to_start_and_finish(mw.task_tree)
63+
async with wait_for_download_task_to_start_and_finish(project):
64+
home_ti.Expand()
6565
assert first_child_of_tree_item_is_not_loading_condition(home_ti)()
6666

6767
# Expand HTML page children in entity tree
@@ -83,8 +83,8 @@ async def test_when_browse_to_html_page_and_browser_requests_embedded_resource_t
8383
home_ti = root_ti.GetFirstChild()
8484
assert home_ti is not None
8585
home_ti.SelectItem()
86-
await mw.click_download_button()
87-
await wait_for_download_to_start_and_finish(mw.task_tree)
86+
async with wait_for_download_task_to_start_and_finish(project):
87+
click_button(mw.download_button)
8888

8989
# Start server
9090
home_ti.SelectItem()
@@ -104,8 +104,8 @@ async def test_given_embedded_resource_selected_in_entity_tree_when_press_downlo
104104
root_ti = TreeItem.GetRootItem(mw.entity_tree.window)
105105
home_ti = root_ti.GetFirstChild()
106106
assert home_ti is not None
107-
home_ti.Expand()
108-
await wait_for_download_to_start_and_finish(mw.task_tree)
107+
async with wait_for_download_task_to_start_and_finish(project):
108+
home_ti.Expand()
109109
assert first_child_of_tree_item_is_not_loading_condition(home_ti)()
110110

111111
# Expand HTML page children in entity tree
@@ -119,13 +119,8 @@ async def test_given_embedded_resource_selected_in_entity_tree_when_press_downlo
119119
assert not comic_image_r.has_any_revisions()
120120

121121
comic_image_r_ti.SelectItem()
122-
await mw.click_download_button(
123-
# NOTE: May "finish immediately" because has no embedded subresources
124-
immediate_finish_ok=True)
125-
await wait_for_download_to_start_and_finish(
126-
mw.task_tree,
127-
# NOTE: May "finish immediately" because has no embedded subresources
128-
immediate_finish_ok=True)
122+
async with wait_for_download_task_to_start_and_finish(project):
123+
click_button(mw.download_button)
129124
assert comic_image_r.has_any_revisions()
130125

131126

@@ -137,8 +132,8 @@ async def test_given_do_not_download_group_selected_in_entity_tree_when_press_do
137132
home_ti = root_ti.GetFirstChild()
138133
assert home_ti is not None
139134
home_ti.SelectItem()
140-
await mw.click_download_button()
141-
await wait_for_download_to_start_and_finish(mw.task_tree)
135+
async with wait_for_download_task_to_start_and_finish(project):
136+
click_button(mw.download_button)
142137

143138
# Expand group children in entity tree
144139
comic_image_rg_ti = root_ti.find_child(comic_image_rg_pattern)
@@ -151,8 +146,8 @@ async def test_given_do_not_download_group_selected_in_entity_tree_when_press_do
151146
assert not comic_image_r.has_any_revisions()
152147

153148
comic_image_rg_ti.SelectItem()
154-
await mw.click_download_button()
155-
await wait_for_download_to_start_and_finish(mw.task_tree)
149+
async with wait_for_download_task_to_start_and_finish(project):
150+
click_button(mw.download_button)
156151
assert comic_image_r.has_any_revisions()
157152

158153

@@ -163,8 +158,8 @@ async def test_then_embedded_resource_in_entity_tree_appears_with_do_not_downloa
163158
root_ti = TreeItem.GetRootItem(mw.entity_tree.window)
164159
home_ti = root_ti.GetFirstChild()
165160
assert home_ti is not None
166-
home_ti.Expand()
167-
await wait_for_download_to_start_and_finish(mw.task_tree)
161+
async with wait_for_download_task_to_start_and_finish(project):
162+
home_ti.Expand()
168163
assert first_child_of_tree_item_is_not_loading_condition(home_ti)()
169164

170165
# Expand HTML page children in entity tree

src/crystal/tests/test_download.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from crystal.tests.util.controls import click_button, TreeItem
88
from crystal.tests.util.runner import bg_sleep
99
from crystal.tests.util.server import MockHttpServer, served_project
10-
from crystal.tests.util.tasks import wait_for_download_to_start_and_finish
10+
from crystal.tests.util.tasks import wait_for_download_task_to_start_and_finish
1111
from crystal.tests.util.wait import DEFAULT_WAIT_PERIOD, wait_for
1212
from crystal.tests.util.windows import NewGroupDialog, OpenOrCreateDialog
1313
import crystal.tests.util.xtempfile as xtempfile
@@ -37,12 +37,11 @@ async def test_given_downloading_resource_when_start_download_resource_then_exis
3737
async with (await OpenOrCreateDialog.wait_for()).create() as (mw, project):
3838
r = Resource(project, home_url)
3939

40-
rr_future = r.download()
41-
42-
rr_future2 = r.download()
43-
assert rr_future2 is rr_future
44-
45-
await wait_for_download_to_start_and_finish(mw.task_tree)
40+
async with wait_for_download_task_to_start_and_finish(project):
41+
rr_future = r.download()
42+
43+
rr_future2 = r.download()
44+
assert rr_future2 is rr_future
4645

4746

4847
# ------------------------------------------------------------------------------

src/crystal/tests/test_entitytree.py

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from crystal.tests.util.downloads import network_down
88
from crystal.tests.util.runner import bg_sleep
99
from crystal.tests.util.server import extracted_project, served_project
10-
from crystal.tests.util.tasks import wait_for_download_to_start_and_finish
10+
from crystal.tests.util.tasks import wait_for_download_task_to_start_and_finish
1111
from crystal.tests.util.wait import (
1212
DEFAULT_WAIT_PERIOD, first_child_of_tree_item_is_not_loading_condition,
1313
wait_for,
@@ -725,11 +725,11 @@ async def test_given_rr_is_downloaded_and_is_error_when_expand_rrn_then_shows_er
725725
with network_down():
726726
r = Resource(project, home_url)
727727
home_rr = RootResource(project, 'Home', r)
728-
revision_future = home_rr.download()
729-
while not revision_future.done():
730-
await bg_sleep(DEFAULT_WAIT_PERIOD)
731-
# Wait for download to complete, including the trailing wait
732-
await wait_for_download_to_start_and_finish(mw.task_tree, immediate_finish_ok=True)
728+
async with wait_for_download_task_to_start_and_finish(project):
729+
revision_future = home_rr.download()
730+
while not revision_future.done():
731+
await bg_sleep(DEFAULT_WAIT_PERIOD)
732+
# (Wait for download to complete, including the trailing wait)
733733

734734
rr = revision_future.result()
735735
assert DownloadErrorDict(
@@ -780,16 +780,15 @@ async def test_given_rr_is_downloaded_but_revision_body_missing_when_expand_rrn_
780780
# TODO: In the future, block on the redownload finishing
781781
# and list the links in the redownloaded revision,
782782
# WITHOUT needing to reopen the project later
783-
home_ti.Expand()
784-
await wait_for(first_child_of_tree_item_is_not_loading_condition(home_ti))
785-
(error_ti,) = home_ti.Children
786-
assert (
787-
'Cannot list links: URL revision body is missing. Recommend delete and redownload.' ==
788-
error_ti.Text
789-
)
790-
791-
# Wait for redownload to complete
792-
await wait_for_download_to_start_and_finish(mw.task_tree, immediate_finish_ok=True)
783+
async with wait_for_download_task_to_start_and_finish(project):
784+
home_ti.Expand()
785+
await wait_for(first_child_of_tree_item_is_not_loading_condition(home_ti))
786+
(error_ti,) = home_ti.Children
787+
assert (
788+
'Cannot list links: URL revision body is missing. Recommend delete and redownload.' ==
789+
error_ti.Text
790+
)
791+
# (Wait for redownload to complete)
793792

794793
# Reexpand RootResourceNode and ensure the children are the same
795794
home_ti.Collapse()

src/crystal/tests/test_load_urls.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from crystal.tests.util.server import extracted_project, served_project
77
from crystal.tests.util.ssd import database_on_ssd
88
from crystal.tests.util.subtests import awith_subtests, SubtestsContext
9-
from crystal.tests.util.tasks import wait_for_download_to_start_and_finish
9+
from crystal.tests.util.tasks import wait_for_download_task_to_start_and_finish
1010
from crystal.tests.util.wait import (
1111
first_child_of_tree_item_is_not_loading_condition,
1212
tree_has_no_children_condition, wait_for,
@@ -181,8 +181,8 @@ async def test_given_project_database_on_ssd_given_resource_group_node_selected_
181181
feed_group_ti.SelectItem()
182182

183183
# Wait for download to start and complete
184-
await mw.click_download_button()
185-
await wait_for_download_to_start_and_finish(mw.task_tree)
184+
async with wait_for_download_task_to_start_and_finish(project):
185+
click_button(mw.download_button)
186186

187187
# Ensure did not show LoadUrlsProgressDialog
188188
assert 0 == progress_listener_method.call_count

0 commit comments

Comments
 (0)