Skip to content

Commit

Permalink
SQ: Move new tests to own module
Browse files Browse the repository at this point in the history
  • Loading branch information
davidfstr committed Feb 10, 2024
1 parent c984e53 commit abd4631
Show file tree
Hide file tree
Showing 3 changed files with 234 additions and 227 deletions.
2 changes: 2 additions & 0 deletions src/crystal/tests/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
test_server,
test_shell,
test_ssd,
test_task_crashes,
test_tasks,
test_tasktree,
test_workflows,
Expand Down Expand Up @@ -85,6 +86,7 @@ def _test_functions_in_module(mod) -> List[Callable]:
_test_functions_in_module(test_server) +
_test_functions_in_module(test_shell) +
_test_functions_in_module(test_ssd) +
_test_functions_in_module(test_task_crashes) +
_test_functions_in_module(test_tasks) +
_test_functions_in_module(test_tasktree) +
_test_functions_in_module(test_workflows) +
Expand Down
229 changes: 229 additions & 0 deletions src/crystal/tests/test_task_crashes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
from crystal.task import (
DownloadResourceGroupTask, DownloadResourceGroupMembersTask, DownloadResourceTask,
UpdateResourceGroupMembersTask
)
from crystal.tests.util.server import served_project
from crystal.tests.util.tasks import (
clear_top_level_tasks_on_exit, scheduler_disabled
)
from crystal.tests.util.wait import wait_for
from crystal.tests.util.windows import OpenOrCreateDialog
from crystal.model import Project, Resource, ResourceGroup
from crystal.util.xfutures import Future
from crystal.util.xthreading import bg_call_later
from typing import Callable, Optional, TypeVar
from unittest.mock import patch


_R = TypeVar('_R')


# ------------------------------------------------------------------------------
# Test: Common Crash Locations in Task
#
# Below, some abbreviations are used in test names:
# - T = Task
# - DRT = DownloadResourceTask
# - DRGMT = DownloadResourceGroupMembersTask
# - DRGT = DownloadResourceGroupTask

async def test_when_T_try_get_next_task_unit_crashes_then_T_displays_as_crashed() -> None:
with scheduler_disabled, served_project('testdata_xkcd.crystalproj.zip') as sp:
# Define URLs
home_url = sp.get_request_url('https://xkcd.com/')

async with (await OpenOrCreateDialog.wait_for()).create() as (mw, _):
project = Project._last_opened_project
assert project is not None

with clear_top_level_tasks_on_exit(project):
# Create DownloadResourceTask
home_r = Resource(project, home_url)
home_r.download()
(download_r_task,) = project.root_task.children
assert isinstance(download_r_task, DownloadResourceTask)

# Force DownloadResourceTask into an illegal state
# (i.e. a container task with empty children)
# which should crash the next call to DRG.try_get_next_task_unit()
assert len(download_r_task.children) > 0
download_r_task._children = []

# Precondition
assert download_r_task.crash_reason is None

unit = project.root_task.try_get_next_task_unit() # step scheduler
assert unit is None

# Postcondition
assert download_r_task.crash_reason is not None


async def test_when_DRT_child_task_did_complete_event_crashes_then_DRT_displays_as_crashed() -> None:
with scheduler_disabled, served_project('testdata_xkcd.crystalproj.zip') as sp:
# Define URLs
home_url = sp.get_request_url('https://xkcd.com/')

async with (await OpenOrCreateDialog.wait_for()).create() as (mw, _):
project = Project._last_opened_project
assert project is not None

with clear_top_level_tasks_on_exit(project):
# Create DownloadResourceTask
home_r = Resource(project, home_url)
home_r.download()
(download_r_task,) = project.root_task.children
assert isinstance(download_r_task, DownloadResourceTask)

# Precondition
assert download_r_task.crash_reason is None

# Download URL
unit = project.root_task.try_get_next_task_unit() # step scheduler
assert unit is not None
await _bg_call_and_wait(unit)

# Parse links
unit = project.root_task.try_get_next_task_unit() # step scheduler
assert unit is not None
# Patch urljoin() to simulate effect of calling urlparse('//[oops'),
# which raises an exception in stock Python:
# https://discuss.python.org/t/urlparse-can-sometimes-raise-an-exception-should-it/44465
#
# NOTE: Overrides the fix in commit 5aaaba57076d537a4872bb3cf7270112ca497a06,
# reintroducing the related bug it fixed.
with patch('crystal.task.urljoin', sideeffect=ValueError('Invalid IPv6 URL')):
await _bg_call_and_wait(unit)

# Postcondition:
# Ensure crashed in DownloadResourceTask.child_task_did_complete(),
# when tried to resolve relative_urls from links parsed by
# ParseResourceRevisionLinks to absolute URLs
assert download_r_task.crash_reason is not None


async def test_when_DRGMT_load_children_crashes_then_DRGT_displays_as_crashed() -> None:
with scheduler_disabled, served_project('testdata_xkcd.crystalproj.zip') as sp:
# Define URLs
atom_feed_url = sp.get_request_url('https://xkcd.com/atom.xml')
rss_feed_url = sp.get_request_url('https://xkcd.com/rss.xml')
feed_pattern = sp.get_request_url('https://xkcd.com/*.xml')

async with (await OpenOrCreateDialog.wait_for()).create() as (mw, _):
project = Project._last_opened_project
assert project is not None

with clear_top_level_tasks_on_exit(project):
atom_feed_r = Resource(project, atom_feed_url)
rss_feed_r = Resource(project, rss_feed_url)

# Create DownloadResourceGroupMembersTask
feed_g = ResourceGroup(project, '', feed_pattern, source=None)
feed_g.download()
(download_rg_task,) = project.root_task.children
assert isinstance(download_rg_task, DownloadResourceGroupTask)
(update_rg_members_task, download_rg_members_task) = download_rg_task.children
assert isinstance(update_rg_members_task, UpdateResourceGroupMembersTask)
assert isinstance(download_rg_members_task, DownloadResourceGroupMembersTask)

# Capture download_rg_members_task.{_pdb, _children_loaded, subtitle}
# after exiting DownloadResourceGroupMembersTask.__init__()
pbc_after_init = download_rg_members_task._pbc
children_loaded_after_init = download_rg_members_task._children_loaded
subtitle_after_init = download_rg_members_task.subtitle
assert None == pbc_after_init
assert False == children_loaded_after_init
assert 'Queued' == subtitle_after_init

super_initialize_children = download_rg_members_task.initialize_children
def initialize_children(self, *args, **kwargs):
# Restore self.{_pdb, _children_loaded, subtitle} to original state
# after exiting DownloadResourceGroupMembersTask.__init__()
self._pbc = pbc_after_init
self._children_loaded = children_loaded_after_init
self.subtitle = subtitle_after_init

return super_initialize_children(*args, **kwargs)

# Precondition
assert download_rg_members_task.crash_reason is None

# Load children of DownloadResourceGroupMembersTask
unit = project.root_task.try_get_next_task_unit() # step scheduler
assert unit is not None
# Patch DownloadResourceGroupMembersTask.initialize_children() to reintroduce
# a bug in DownloadResourceGroupMembersTask._load_children() that was
# fixed in commit 44f5bd429201972d324df1287e673ddef9ffa936
with patch.object(download_rg_members_task, 'initialize_children', initialize_children):
await _bg_call_and_wait(unit)

# Postcondition
assert download_rg_members_task.crash_reason is not None


async def test_when_DRGMT_group_did_add_member_event_crashes_then_DRGT_displays_as_crashed() -> None:
with scheduler_disabled, served_project('testdata_xkcd.crystalproj.zip') as sp:
# Define URLs
atom_feed_url = sp.get_request_url('https://xkcd.com/atom.xml')
rss_feed_url = sp.get_request_url('https://xkcd.com/rss.xml')
feed_pattern = sp.get_request_url('https://xkcd.com/*.xml')

async with (await OpenOrCreateDialog.wait_for()).create() as (mw, _):
project = Project._last_opened_project
assert project is not None

with clear_top_level_tasks_on_exit(project):
atom_feed_r = Resource(project, atom_feed_url)

# Create DownloadResourceGroupMembersTask
feed_g = ResourceGroup(project, '', feed_pattern, source=None)
feed_g.download()
(download_rg_task,) = project.root_task.children
assert isinstance(download_rg_task, DownloadResourceGroupTask)
(update_rg_members_task, download_rg_members_task) = download_rg_task.children
assert isinstance(update_rg_members_task, UpdateResourceGroupMembersTask)
assert isinstance(download_rg_members_task, DownloadResourceGroupMembersTask)

super_notify_did_append_child = download_rg_members_task.notify_did_append_child
def notify_did_append_child(self, *args, **kwargs):
super_notify_did_append_child(*args, **kwargs)

# Simulate failure of `self._pbc.total += 1`
# due to self._pbc being None
raise AttributeError("'NoneType' object has no attribute 'total'")

# Precondition
assert download_rg_members_task.crash_reason is None

with patch.object(download_rg_members_task, 'notify_did_append_child', notify_did_append_child):
rss_feed_r = Resource(project, rss_feed_url)

# Postcondition
assert download_rg_members_task.crash_reason is not None


# ------------------------------------------------------------------------------
# Utility

async def _bg_call_and_wait(callable: Callable[[], _R], *, timeout: Optional[float]=None) -> _R:
"""
Start the specified callable on a background thread and
waits for it to finish running.
The foreground thread IS released while waiting, so the callable can safely
make calls to fg_call_later() and fg_call_and_wait() without deadlocking.
"""
result_cell = Future() # type: Future[_R]
def bg_task() -> None:
result_cell.set_running_or_notify_cancel()
try:
result_cell.set_result(callable())
except BaseException as e:
result_cell.set_exception(e)
bg_call_later(bg_task)
# NOTE: Releases foreground thread while waiting
await wait_for(lambda: result_cell.done() or None, timeout)

Check warning on line 225 in src/crystal/tests/test_task_crashes.py

View workflow job for this annotation

GitHub Actions / build-windows (windows-latest, 3.8)

Soft timeout exceeded (2.1s > 2.0s). <function _bg_call_and_wait.<locals>.<lambda> at 0x0000028FB9D7CEE0>

Check warning on line 225 in src/crystal/tests/test_task_crashes.py

View workflow job for this annotation

GitHub Actions / build-windows (windows-latest, 3.9)

Soft timeout exceeded (2.1s > 2.0s). <function _bg_call_and_wait.<locals>.<lambda> at 0x00000173BA13F430>
return result_cell.result()


# ------------------------------------------------------------------------------
Loading

0 comments on commit abd4631

Please sign in to comment.