diff --git a/src/crystal/tests/test_addrooturl.py b/src/crystal/tests/test_addrooturl.py index d4dd264e..8d032593 100644 --- a/src/crystal/tests/test_addrooturl.py +++ b/src/crystal/tests/test_addrooturl.py @@ -801,16 +801,15 @@ def _EXPAND_enabled() -> Iterator[None]: async def _add_url_dialog_open(*, autoclose: bool=True) -> AsyncIterator[AddUrlDialog]: # Never allow automated tests to make real internet requests with _urlopen_responding_with(_UrlOpenHttpResponse(code=590, url=ANY)): - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath) as (mw, _): - click_button(mw.add_url_button) - aud = await AddUrlDialog.wait_for() - - try: - yield aud - finally: - if autoclose and aud.shown: - await aud.cancel() + async with (await OpenOrCreateDialog.wait_for()).create() as (mw, _): + click_button(mw.add_url_button) + aud = await AddUrlDialog.wait_for() + + try: + yield aud + finally: + if autoclose and aud.shown: + await aud.cancel() @contextmanager diff --git a/src/crystal/tests/test_disk_io_errors.py b/src/crystal/tests/test_disk_io_errors.py index 502ea986..aa4a203b 100644 --- a/src/crystal/tests/test_disk_io_errors.py +++ b/src/crystal/tests/test_disk_io_errors.py @@ -26,31 +26,30 @@ async def test_given_default_revision_with_missing_body_when_download_related_re with served_project('testdata_xkcd.crystalproj.zip') as sp: home_url = sp.get_request_url('https://xkcd.com/') - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath) as (mw, _): - project = Project._last_opened_project - assert project is not None - - # Download revision - r = Resource(project, home_url) - revision_future = r.download_body() + async with (await OpenOrCreateDialog.wait_for()).create() as (mw, _): + project = Project._last_opened_project + assert project is not None + + # Download revision + r = Resource(project, home_url) + revision_future = r.download_body() + while not revision_future.done(): + await bg_sleep(DEFAULT_WAIT_PERIOD) + + # Simulate loss of revision body file + revision = revision_future.result() + os.remove(revision._body_filepath) + + # Download related resource + with redirect_stderr(io.StringIO()) as captured_stderr: + revision_future = r.download(wait_for_embedded=True, needs_result=True) while not revision_future.done(): await bg_sleep(DEFAULT_WAIT_PERIOD) - - # Simulate loss of revision body file - revision = revision_future.result() - os.remove(revision._body_filepath) - - # Download related resource - with redirect_stderr(io.StringIO()) as captured_stderr: - revision_future = r.download(wait_for_embedded=True, needs_result=True) - while not revision_future.done(): - await bg_sleep(DEFAULT_WAIT_PERIOD) - assert 'is missing its body on disk. Redownloading it.' in captured_stderr.getvalue() - revision = revision_future.result() - assert revision.has_body - with revision.open(): # ensure no error - pass + assert 'is missing its body on disk. Redownloading it.' in captured_stderr.getvalue() + revision = revision_future.result() + assert revision.has_body + with revision.open(): # ensure no error + pass @skip('fails: not implemented') diff --git a/src/crystal/tests/test_download.py b/src/crystal/tests/test_download.py index 5b340a88..27c9e2a0 100644 --- a/src/crystal/tests/test_download.py +++ b/src/crystal/tests/test_download.py @@ -37,17 +37,16 @@ async def test_downloads_embedded_resources() -> None: ) }) with server: - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath) as (mw, _): - project = Project._last_opened_project - assert project is not None - - r = Resource(project, server.get_url('/')) - revision_future = r.download(wait_for_embedded=True) - while not revision_future.done(): - await bg_sleep(DEFAULT_WAIT_PERIOD) - - assert ['/', '/assets/image.png'] == server.requested_paths + async with (await OpenOrCreateDialog.wait_for()).create() as (mw, _): + project = Project._last_opened_project + assert project is not None + + r = Resource(project, server.get_url('/')) + revision_future = r.download(wait_for_embedded=True) + while not revision_future.done(): + await bg_sleep(DEFAULT_WAIT_PERIOD) + + assert ['/', '/assets/image.png'] == server.requested_paths async def test_does_not_download_embedded_resources_of_http_4xx_and_5xx_pages() -> None: @@ -82,17 +81,16 @@ async def test_does_not_download_embedded_resources_of_http_4xx_and_5xx_pages() ) }) with server: - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath) as (mw, _): - project = Project._last_opened_project - assert project is not None - - r = Resource(project, server.get_url('/')) - revision_future = r.download(wait_for_embedded=True) - while not revision_future.done(): - await bg_sleep(DEFAULT_WAIT_PERIOD) - - assert ['/'] == server.requested_paths + async with (await OpenOrCreateDialog.wait_for()).create() as (mw, _): + project = Project._last_opened_project + assert project is not None + + r = Resource(project, server.get_url('/')) + revision_future = r.download(wait_for_embedded=True) + while not revision_future.done(): + await bg_sleep(DEFAULT_WAIT_PERIOD) + + assert ['/'] == server.requested_paths async def test_does_not_download_embedded_resources_of_recognized_binary_resource() -> None: @@ -108,17 +106,16 @@ async def test_does_not_download_embedded_resources_of_recognized_binary_resourc ) }) with server: - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath) as (mw, _): - project = Project._last_opened_project - assert project is not None - - r = Resource(project, server.get_url('/')) - revision_future = r.download(wait_for_embedded=True) - while not revision_future.done(): - await bg_sleep(DEFAULT_WAIT_PERIOD) - - assert ['/'] == server.requested_paths + async with (await OpenOrCreateDialog.wait_for()).create() as (mw, _): + project = Project._last_opened_project + assert project is not None + + r = Resource(project, server.get_url('/')) + revision_future = r.download(wait_for_embedded=True) + while not revision_future.done(): + await bg_sleep(DEFAULT_WAIT_PERIOD) + + assert ['/'] == server.requested_paths async def test_does_not_download_forever_given_embedded_resources_form_a_cycle() -> None: @@ -153,17 +150,16 @@ async def test_does_not_download_forever_given_embedded_resources_form_a_cycle() ) }) with server: - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath) as (mw, _): - project = Project._last_opened_project - assert project is not None - - r = Resource(project, server.get_url('/')) - revision_future = r.download(wait_for_embedded=True) - while not revision_future.done(): - await bg_sleep(DEFAULT_WAIT_PERIOD) - - assert ['/', '/assets/image.png'] == server.requested_paths + async with (await OpenOrCreateDialog.wait_for()).create() as (mw, _): + project = Project._last_opened_project + assert project is not None + + r = Resource(project, server.get_url('/')) + revision_future = r.download(wait_for_embedded=True) + while not revision_future.done(): + await bg_sleep(DEFAULT_WAIT_PERIOD) + + assert ['/', '/assets/image.png'] == server.requested_paths async def test_does_not_download_forever_given_embedded_resources_nest_infinitely() -> None: @@ -198,23 +194,22 @@ async def test_does_not_download_forever_given_embedded_resources_nest_infinitel ) }) with server: - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath) as (mw, _): - project = Project._last_opened_project - assert project is not None - - r = Resource(project, server.get_url('/')) - revision_future = r.download(wait_for_embedded=True) - while not revision_future.done(): - await bg_sleep(DEFAULT_WAIT_PERIOD) - - assert 3 == crystal.task._MAX_EMBEDDED_RESOURCE_RECURSION_DEPTH - assert [ - '/', - '/assets/image.png', # 1 - '/assets/assets/image.png', # 2 - '/assets/assets/assets/image.png' # 3 - ] == server.requested_paths + async with (await OpenOrCreateDialog.wait_for()).create() as (mw, _): + project = Project._last_opened_project + assert project is not None + + r = Resource(project, server.get_url('/')) + revision_future = r.download(wait_for_embedded=True) + while not revision_future.done(): + await bg_sleep(DEFAULT_WAIT_PERIOD) + + assert 3 == crystal.task._MAX_EMBEDDED_RESOURCE_RECURSION_DEPTH + assert [ + '/', + '/assets/image.png', # 1 + '/assets/assets/image.png', # 2 + '/assets/assets/assets/image.png' # 3 + ] == server.requested_paths async def test_when_download_resource_given_revision_body_missing_then_redownloads_revision_body() -> None: @@ -305,25 +300,24 @@ async def test_when_download_resource_given_all_embedded_resources_already_downl ) }) with server: - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath) as (mw, _): - project = Project._last_opened_project - assert project is not None - - r = Resource(project, server.get_url('/')) - revision_future = r.download(wait_for_embedded=True) - while not revision_future.done(): - await bg_sleep(DEFAULT_WAIT_PERIOD) - - assert ['/', '/assets/image.png'] == server.requested_paths - server.requested_paths.clear() - - r = Resource(project, server.get_url('/index.php')) - revision_future = r.download(wait_for_embedded=True) - while not revision_future.done(): - await bg_sleep(DEFAULT_WAIT_PERIOD) - - assert ['/index.php'] == server.requested_paths + async with (await OpenOrCreateDialog.wait_for()).create() as (mw, _): + project = Project._last_opened_project + assert project is not None + + r = Resource(project, server.get_url('/')) + revision_future = r.download(wait_for_embedded=True) + while not revision_future.done(): + await bg_sleep(DEFAULT_WAIT_PERIOD) + + assert ['/', '/assets/image.png'] == server.requested_paths + server.requested_paths.clear() + + r = Resource(project, server.get_url('/index.php')) + revision_future = r.download(wait_for_embedded=True) + while not revision_future.done(): + await bg_sleep(DEFAULT_WAIT_PERIOD) + + assert ['/index.php'] == server.requested_paths async def test_given_same_resource_embedded_multiple_times_then_downloads_it_only_once() -> None: @@ -346,17 +340,16 @@ async def test_given_same_resource_embedded_multiple_times_then_downloads_it_onl ) }) with server: - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath) as (mw, _): - project = Project._last_opened_project - assert project is not None - - r = Resource(project, server.get_url('/')) - revision_future = r.download(wait_for_embedded=True) - while not revision_future.done(): - await bg_sleep(DEFAULT_WAIT_PERIOD) - - assert ['/', '/assets/image.png'] == server.requested_paths + async with (await OpenOrCreateDialog.wait_for()).create() as (mw, _): + project = Project._last_opened_project + assert project is not None + + r = Resource(project, server.get_url('/')) + revision_future = r.download(wait_for_embedded=True) + while not revision_future.done(): + await bg_sleep(DEFAULT_WAIT_PERIOD) + + assert ['/', '/assets/image.png'] == server.requested_paths # ------------------------------------------------------------------------------ diff --git a/src/crystal/tests/test_download_body.py b/src/crystal/tests/test_download_body.py index 89755463..7dc3a524 100644 --- a/src/crystal/tests/test_download_body.py +++ b/src/crystal/tests/test_download_body.py @@ -41,37 +41,36 @@ async def test_download_does_save_resource_metadata_and_content_accurately() -> assert len(content_bytes) > 1000 # ensure is a reasonably sized file with _file_served(HEADERS, content_bytes) as server_port: - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath) as (mw, _): - project = Project._last_opened_project - assert project is not None - - r = Resource(project, f'http://localhost:{server_port}/') - revision_future = r.download_body() - while not revision_future.done(): - await bg_sleep(DEFAULT_WAIT_PERIOD) + async with (await OpenOrCreateDialog.wait_for()).create() as (mw, _): + project = Project._last_opened_project + assert project is not None + + r = Resource(project, f'http://localhost:{server_port}/') + revision_future = r.download_body() + while not revision_future.done(): + await bg_sleep(DEFAULT_WAIT_PERIOD) + + downloaded_revision = revision_future.result() # type: ResourceRevision + loaded_revision = r.default_revision() + assert loaded_revision is not None + + for revision in [downloaded_revision, loaded_revision]: + # Ensure no error is reported + assert None == revision.error - downloaded_revision = revision_future.result() # type: ResourceRevision - loaded_revision = r.default_revision() - assert loaded_revision is not None + # Ensure metadata is correct + EXPECTED_METADATA = { + 'http_version': 10, # HTTP/1.0 + 'status_code': 200, + 'reason_phrase': 'OK', + 'headers': HEADERS, + } + assert EXPECTED_METADATA == revision.metadata - for revision in [downloaded_revision, loaded_revision]: - # Ensure no error is reported - assert None == revision.error - - # Ensure metadata is correct - EXPECTED_METADATA = { - 'http_version': 10, # HTTP/1.0 - 'status_code': 200, - 'reason_phrase': 'OK', - 'headers': HEADERS, - } - assert EXPECTED_METADATA == revision.metadata - - # Ensure content is correct - with revision.open() as saved_content: - saved_content_bytes = saved_content.read() - assert content_bytes == saved_content_bytes + # Ensure content is correct + with revision.open() as saved_content: + saved_content_bytes = saved_content.read() + assert content_bytes == saved_content_bytes async def test_download_does_autopopulate_date_header_if_not_received_from_origin() -> None: @@ -90,31 +89,30 @@ async def test_download_does_autopopulate_date_header_if_not_received_from_origi content_bytes = content_file.read() with _file_served(HEADERS, content_bytes) as server_port: - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath) as (mw, _): - project = Project._last_opened_project - assert project is not None - - r = Resource(project, f'http://localhost:{server_port}/') - revision_future = r.download_body() - while not revision_future.done(): - await bg_sleep(DEFAULT_WAIT_PERIOD) - - downloaded_revision = revision_future.result() # type: ResourceRevision - loaded_revision = r.default_revision() - assert loaded_revision is not None + async with (await OpenOrCreateDialog.wait_for()).create() as (mw, _): + project = Project._last_opened_project + assert project is not None + + r = Resource(project, f'http://localhost:{server_port}/') + revision_future = r.download_body() + while not revision_future.done(): + await bg_sleep(DEFAULT_WAIT_PERIOD) + + downloaded_revision = revision_future.result() # type: ResourceRevision + loaded_revision = r.default_revision() + assert loaded_revision is not None + + for revision in [downloaded_revision, loaded_revision]: + EXPECTED_METADATA = { + 'http_version': 10, # HTTP/1.0 + 'status_code': 200, + 'reason_phrase': 'OK', + 'headers': EXPECTED_HEADERS_SAVED, + } + assert EXPECTED_METADATA == revision.metadata - for revision in [downloaded_revision, loaded_revision]: - EXPECTED_METADATA = { - 'http_version': 10, # HTTP/1.0 - 'status_code': 200, - 'reason_phrase': 'OK', - 'headers': EXPECTED_HEADERS_SAVED, - } - assert EXPECTED_METADATA == revision.metadata - - assert None != revision.date, \ - 'Date header has invalid value' + assert None != revision.date, \ + 'Date header has invalid value' @contextmanager @@ -156,118 +154,114 @@ async def test_when_no_errors_then_database_row_and_body_file_is_created_and_ret with served_project('testdata_xkcd.crystalproj.zip') as sp: home_url = sp.get_request_url('https://xkcd.com/') - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath) as (mw, _): - project = Project._last_opened_project - assert project is not None - + async with (await OpenOrCreateDialog.wait_for()).create() as (mw, _): + project = Project._last_opened_project + assert project is not None + + r = Resource(project, home_url) + revision_future = r.download_body() + while not revision_future.done(): + await bg_sleep(DEFAULT_WAIT_PERIOD) + + # Ensure no error is reported + revision = revision_future.result() # type: ResourceRevision + assert None == revision.error + + # Ensure database and filesystem is in expected state + assert 1 == project._revision_count() + assert True == os.path.exists(os.path.join( + project.path, Project._REVISIONS_DIRNAME, + '000', '000', '000', '000', f'{revision._id:03x}')) + assert [] == os.listdir(os.path.join( + project.path, Project._TEMPORARY_DIRNAME)) + + +async def test_when_network_io_error_then_tries_to_delete_partial_body_file_but_leave_database_row_and_returns_error_revision() -> None: + with served_project('testdata_xkcd.crystalproj.zip') as sp: + 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 _downloads_mocked_to_raise_network_io_error() as is_connection_reset: r = Resource(project, home_url) revision_future = r.download_body() while not revision_future.done(): await bg_sleep(DEFAULT_WAIT_PERIOD) - - # Ensure no error is reported - revision = revision_future.result() # type: ResourceRevision - assert None == revision.error - - # Ensure database and filesystem is in expected state - assert 1 == project._revision_count() - assert True == os.path.exists(os.path.join( - project.path, Project._REVISIONS_DIRNAME, - '000', '000', '000', '000', f'{revision._id:03x}')) - assert [] == os.listdir(os.path.join( - project.path, Project._TEMPORARY_DIRNAME)) + + # Ensure an I/O error is reported as an error revision + revision = revision_future.result() # type: ResourceRevision + assert None != revision.error + assert is_connection_reset(revision.error) + + # Ensure database and filesystem is in expected state + assert 1 == project._revision_count() + assert False == os.path.exists(revision._body_filepath) + assert [] == os.listdir(os.path.join( + project.path, Project._TEMPORARY_DIRNAME)) -async def test_when_network_io_error_then_tries_to_delete_partial_body_file_but_leave_database_row_and_returns_error_revision() -> None: +async def test_when_database_error_then_tries_to_delete_partial_body_file_and_raises_database_error() -> None: with served_project('testdata_xkcd.crystalproj.zip') as sp: home_url = sp.get_request_url('https://xkcd.com/') - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath) as (mw, _): - project = Project._last_opened_project - assert project is not None - - with _downloads_mocked_to_raise_network_io_error() as is_connection_reset: - r = Resource(project, home_url) - revision_future = r.download_body() - while not revision_future.done(): - await bg_sleep(DEFAULT_WAIT_PERIOD) - - # Ensure an I/O error is reported as an error revision + async with (await OpenOrCreateDialog.wait_for()).create() as (mw, _): + project = Project._last_opened_project + assert project is not None + + with _database_cursor_mocked_to_raise_database_io_error_on_write(project) as is_database_error: + r = Resource(project, home_url) + revision_future = r.download_body() + while not revision_future.done(): + await bg_sleep(DEFAULT_WAIT_PERIOD) + + # Ensure a database error is reported as a raised exception + try: revision = revision_future.result() # type: ResourceRevision - assert None != revision.error - assert is_connection_reset(revision.error) - - # Ensure database and filesystem is in expected state - assert 1 == project._revision_count() - assert False == os.path.exists(revision._body_filepath) - assert [] == os.listdir(os.path.join( - project.path, Project._TEMPORARY_DIRNAME)) + except Exception as e: + assert is_database_error(e) + else: + assert False, 'Expected database error' + + # Ensure database and filesystem is in expected state + assert 0 == project._revision_count() + assert [] == os.listdir(os.path.join( + project.path, Project._REVISIONS_DIRNAME)) + assert [] == os.listdir(os.path.join( + project.path, Project._TEMPORARY_DIRNAME)) -async def test_when_database_error_then_tries_to_delete_partial_body_file_and_raises_database_error() -> None: +async def test_when_network_io_error_and_database_error_then_tries_to_delete_partial_body_file_and_raises_database_error() -> None: with served_project('testdata_xkcd.crystalproj.zip') as sp: home_url = sp.get_request_url('https://xkcd.com/') - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath) as (mw, _): - project = Project._last_opened_project - assert project is not None - + async with (await OpenOrCreateDialog.wait_for()).create() as (mw, _): + project = Project._last_opened_project + assert project is not None + + with _downloads_mocked_to_raise_network_io_error() as is_connection_reset: with _database_cursor_mocked_to_raise_database_io_error_on_write(project) as is_database_error: r = Resource(project, home_url) revision_future = r.download_body() while not revision_future.done(): await bg_sleep(DEFAULT_WAIT_PERIOD) - # Ensure a database error is reported as a raised exception - try: - revision = revision_future.result() # type: ResourceRevision - except Exception as e: - assert is_database_error(e) - else: - assert False, 'Expected database error' - - # Ensure database and filesystem is in expected state - assert 0 == project._revision_count() - assert [] == os.listdir(os.path.join( - project.path, Project._REVISIONS_DIRNAME)) - assert [] == os.listdir(os.path.join( - project.path, Project._TEMPORARY_DIRNAME)) - - -async def test_when_network_io_error_and_database_error_then_tries_to_delete_partial_body_file_and_raises_database_error() -> None: - with served_project('testdata_xkcd.crystalproj.zip') as sp: - home_url = sp.get_request_url('https://xkcd.com/') - - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath) as (mw, _): - project = Project._last_opened_project - assert project is not None - - with _downloads_mocked_to_raise_network_io_error() as is_connection_reset: - with _database_cursor_mocked_to_raise_database_io_error_on_write(project) as is_database_error: - r = Resource(project, home_url) - revision_future = r.download_body() - while not revision_future.done(): - await bg_sleep(DEFAULT_WAIT_PERIOD) - - # Ensure a database error is reported as a raised exception - # (rather than reporting the I/O error as an error revision) - try: - revision = revision_future.result() # type: ResourceRevision - except Exception as e: - assert is_database_error(e) - else: - assert False, 'Expected database error' - - # Ensure database and filesystem is in expected state - assert 0 == project._revision_count() - assert [] == os.listdir(os.path.join( - project.path, Project._REVISIONS_DIRNAME)) - assert [] == os.listdir(os.path.join( - project.path, Project._TEMPORARY_DIRNAME)) + # Ensure a database error is reported as a raised exception + # (rather than reporting the I/O error as an error revision) + try: + revision = revision_future.result() # type: ResourceRevision + except Exception as e: + assert is_database_error(e) + else: + assert False, 'Expected database error' + + # Ensure database and filesystem is in expected state + assert 0 == project._revision_count() + assert [] == os.listdir(os.path.join( + project.path, Project._REVISIONS_DIRNAME)) + assert [] == os.listdir(os.path.join( + project.path, Project._TEMPORARY_DIRNAME)) async def test_when_open_project_given_partial_body_files_exist_then_deletes_all_partial_body_files() -> None: @@ -285,7 +279,7 @@ async def test_when_open_project_given_partial_body_files_exist_then_deletes_all project_dirpath, Project._TEMPORARY_DIRNAME)) # Reopen project - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath) as (mw, _): + async with (await OpenOrCreateDialog.wait_for()).open(project_dirpath) as mw: # Ensure partial body file is deleted assert [] == os.listdir(os.path.join( project_dirpath, Project._TEMPORARY_DIRNAME)) @@ -303,59 +297,57 @@ async def test_given_downloading_revision_when_writing_to_disk_raises_io_error_t with served_project('testdata_xkcd.crystalproj.zip') as sp: home_url = sp.get_request_url('https://xkcd.com/') - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath) as (mw, _): - project = Project._last_opened_project - assert project is not None - - with _downloads_mocked_to_raise_disk_io_error() as is_io_error: - r = Resource(project, home_url) - revision_future = r.download_body() - while not revision_future.done(): - await bg_sleep(DEFAULT_WAIT_PERIOD) - - # Ensure an I/O error is reported as an error revision - revision = revision_future.result() # type: ResourceRevision - assert None != revision.error - assert is_io_error(revision.error) - - # Ensure database and filesystem is in expected state - assert 1 == project._revision_count() - assert False == os.path.exists(revision._body_filepath) - assert [] == os.listdir(os.path.join( - project.path, Project._TEMPORARY_DIRNAME)) + async with (await OpenOrCreateDialog.wait_for()).create() as (mw, _): + project = Project._last_opened_project + assert project is not None + + with _downloads_mocked_to_raise_disk_io_error() as is_io_error: + r = Resource(project, home_url) + revision_future = r.download_body() + while not revision_future.done(): + await bg_sleep(DEFAULT_WAIT_PERIOD) + + # Ensure an I/O error is reported as an error revision + revision = revision_future.result() # type: ResourceRevision + assert None != revision.error + assert is_io_error(revision.error) + + # Ensure database and filesystem is in expected state + assert 1 == project._revision_count() + assert False == os.path.exists(revision._body_filepath) + assert [] == os.listdir(os.path.join( + project.path, Project._TEMPORARY_DIRNAME)) async def test_given_downloading_revision_when_writing_to_disk_raises_io_error_and_writing_to_database_raises_io_error_then_raises_database_error() -> None: with served_project('testdata_xkcd.crystalproj.zip') as sp: home_url = sp.get_request_url('https://xkcd.com/') - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath) as (mw, _): - project = Project._last_opened_project - assert project is not None - - with _downloads_mocked_to_raise_disk_io_error() as is_io_error: - with _database_cursor_mocked_to_raise_database_io_error_on_write(project) as is_database_error: - r = Resource(project, home_url) - revision_future = r.download_body() - while not revision_future.done(): - await bg_sleep(DEFAULT_WAIT_PERIOD) - - # Ensure a database error is reported as a raised exception - try: - revision = revision_future.result() # type: ResourceRevision - except Exception as e: - assert is_database_error(e) - else: - assert False, 'Expected database error' - - # Ensure database and filesystem is in expected state - assert 0 == project._revision_count() - assert [] == os.listdir(os.path.join( - project.path, Project._REVISIONS_DIRNAME)) - assert [] == os.listdir(os.path.join( - project.path, Project._TEMPORARY_DIRNAME)) + async with (await OpenOrCreateDialog.wait_for()).create() as (mw, _): + project = Project._last_opened_project + assert project is not None + + with _downloads_mocked_to_raise_disk_io_error() as is_io_error: + with _database_cursor_mocked_to_raise_database_io_error_on_write(project) as is_database_error: + r = Resource(project, home_url) + revision_future = r.download_body() + while not revision_future.done(): + await bg_sleep(DEFAULT_WAIT_PERIOD) + + # Ensure a database error is reported as a raised exception + try: + revision = revision_future.result() # type: ResourceRevision + except Exception as e: + assert is_database_error(e) + else: + assert False, 'Expected database error' + + # Ensure database and filesystem is in expected state + assert 0 == project._revision_count() + assert [] == os.listdir(os.path.join( + project.path, Project._REVISIONS_DIRNAME)) + assert [] == os.listdir(os.path.join( + project.path, Project._TEMPORARY_DIRNAME)) # NOTE: This error scenario is expected to never happen in practice, @@ -368,29 +360,28 @@ async def test_given_project_has_revision_with_maximum_id_when_download_revision with served_project('testdata_xkcd.crystalproj.zip') as sp: home_url = sp.get_request_url('https://xkcd.com/') - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath) as (mw, _): - project = Project._last_opened_project - assert project is not None - - old_revision_count = project._revision_count() # capture - - resource = Resource(project, home_url) - - with _database_cursor_mocked_to_create_revision_with_id(Project._MAX_REVISION_ID + 1, project): - download_result = resource.download_body() - while not download_result.done(): - await bg_sleep(DEFAULT_WAIT_PERIOD) - try: - download_result.result() - except ProjectHasTooManyRevisionsError: - pass - else: - raise AssertionError( - 'Expected ProjectHasTooManyRevisionsError to be raised') - - new_revision_count = project._revision_count() - assert old_revision_count == new_revision_count + async with (await OpenOrCreateDialog.wait_for()).create() as (mw, _): + project = Project._last_opened_project + assert project is not None + + old_revision_count = project._revision_count() # capture + + resource = Resource(project, home_url) + + with _database_cursor_mocked_to_create_revision_with_id(Project._MAX_REVISION_ID + 1, project): + download_result = resource.download_body() + while not download_result.done(): + await bg_sleep(DEFAULT_WAIT_PERIOD) + try: + download_result.result() + except ProjectHasTooManyRevisionsError: + pass + else: + raise AssertionError( + 'Expected ProjectHasTooManyRevisionsError to be raised') + + new_revision_count = project._revision_count() + assert old_revision_count == new_revision_count # ------------------------------------------------------------------------------ diff --git a/src/crystal/tests/test_entitytree.py b/src/crystal/tests/test_entitytree.py index e5199ae9..0c06edbd 100644 --- a/src/crystal/tests/test_entitytree.py +++ b/src/crystal/tests/test_entitytree.py @@ -35,29 +35,28 @@ async def test_rn_with_error_child_retains_child_when_new_entity_added() -> None: with network_down(): - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath) as (mw, _): - project = Project._last_opened_project - assert project is not None - - root_ti = TreeItem.GetRootItem(mw.entity_tree.window) - assert root_ti is not None - - RootResource(project, 'Domain 1', Resource(project, 'https://nosuchdomain1.com/')) - (rrn1_ti,) = root_ti.Children - - rrn1_ti.Expand() - await wait_for(first_child_of_tree_item_is_not_loading_condition(rrn1_ti)) - assert rrn1_ti.GetFirstChild() is not None - - RootResource(project, 'Domain 2', Resource(project, 'https://nosuchdomain2.com/')) - (rrn1_ti, rrn2_ti) = root_ti.Children - assert rrn1_ti.GetFirstChild() is not None # has failed in the past - - rrn2_ti.Expand() - await wait_for(first_child_of_tree_item_is_not_loading_condition(rrn2_ti)) - assert rrn1_ti.GetFirstChild() is not None - assert rrn2_ti.GetFirstChild() is not None + async with (await OpenOrCreateDialog.wait_for()).create() as (mw, _): + project = Project._last_opened_project + assert project is not None + + root_ti = TreeItem.GetRootItem(mw.entity_tree.window) + assert root_ti is not None + + RootResource(project, 'Domain 1', Resource(project, 'https://nosuchdomain1.com/')) + (rrn1_ti,) = root_ti.Children + + rrn1_ti.Expand() + await wait_for(first_child_of_tree_item_is_not_loading_condition(rrn1_ti)) + assert rrn1_ti.GetFirstChild() is not None + + RootResource(project, 'Domain 2', Resource(project, 'https://nosuchdomain2.com/')) + (rrn1_ti, rrn2_ti) = root_ti.Children + assert rrn1_ti.GetFirstChild() is not None # has failed in the past + + rrn2_ti.Expand() + await wait_for(first_child_of_tree_item_is_not_loading_condition(rrn2_ti)) + assert rrn1_ti.GetFirstChild() is not None + assert rrn2_ti.GetFirstChild() is not None # ------------------------------------------------------------------------------ @@ -142,39 +141,38 @@ async def test_given_rr_is_downloaded_and_is_error_when_expand_rrn_then_shows_er with served_project('testdata_xkcd.crystalproj.zip') as sp: home_url = sp.get_request_url('https://xkcd.com/') - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath) as (mw, _): - project = Project._last_opened_project - assert project is not None - - # Download revision - with network_down(): - r = Resource(project, home_url) - home_rr = RootResource(project, 'Home', r) - revision_future = home_rr.download() - while not revision_future.done(): - await bg_sleep(DEFAULT_WAIT_PERIOD) - # Wait for download to complete, including the trailing wait - await wait_for_download_to_start_and_finish(mw.task_tree, immediate_finish_ok=True) - - rr = revision_future.result() - assert DownloadErrorDict( - type='gaierror', - message='[Errno 8] nodename nor servname provided, or not known', - ) == rr.error_dict - - root_ti = TreeItem.GetRootItem(mw.entity_tree.window) - assert root_ti is not None - (home_ti,) = root_ti.Children + async with (await OpenOrCreateDialog.wait_for()).create() as (mw, _): + project = Project._last_opened_project + assert project is not None + + # Download revision + with network_down(): + r = Resource(project, home_url) + home_rr = RootResource(project, 'Home', r) + revision_future = home_rr.download() + while not revision_future.done(): + await bg_sleep(DEFAULT_WAIT_PERIOD) + # Wait for download to complete, including the trailing wait + await wait_for_download_to_start_and_finish(mw.task_tree, immediate_finish_ok=True) - # Expand RootResourceNode and ensure it has an _ErrorNode child - home_ti.Expand() - await wait_for(first_child_of_tree_item_is_not_loading_condition(home_ti)) - (error_ti,) = home_ti.Children - assert ( - 'Error downloading URL: gaierror: [Errno 8] nodename nor servname provided, or not known' == - error_ti.Text - ) + rr = revision_future.result() + assert DownloadErrorDict( + type='gaierror', + message='[Errno 8] nodename nor servname provided, or not known', + ) == rr.error_dict + + root_ti = TreeItem.GetRootItem(mw.entity_tree.window) + assert root_ti is not None + (home_ti,) = root_ti.Children + + # Expand RootResourceNode and ensure it has an _ErrorNode child + home_ti.Expand() + await wait_for(first_child_of_tree_item_is_not_loading_condition(home_ti)) + (error_ti,) = home_ti.Children + assert ( + 'Error downloading URL: gaierror: [Errno 8] nodename nor servname provided, or not known' == + error_ti.Text + ) async def test_given_rr_is_downloaded_but_revision_body_missing_when_expand_rrn_then_shows_error_node_and_redownloads_rr() -> None: @@ -307,13 +305,94 @@ async def test_given_more_node_selected_when_expand_more_node_then_first_newly_v if True: comic_pattern = sp.get_request_url('https://xkcd.com/#/') - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath) as (mw, _): + async with (await OpenOrCreateDialog.wait_for()).create() as (mw, _): + project = Project._last_opened_project + assert project is not None + + # Create future group members + for i in range(1, 1000+1): + Resource(project, comic_pattern.replace('#', str(i))) + + # Create group + ResourceGroup(project, 'Comic', comic_pattern) + + root_ti = TreeItem.GetRootItem(mw.entity_tree.window) + assert root_ti is not None + + comic_group_ti = root_ti.GetFirstChild() + assert comic_group_ti is not None + assert f'{comic_pattern} - Comic' == comic_group_ti.Text + + # Ensure first child of group (not displayed) is "Loading..." + cg_child_ti = comic_group_ti.GetFirstChild() + assert cg_child_ti is not None + assert 'Loading...' == cg_child_ti.Text + + comic_group_ti.Expand() + await wait_for(first_child_of_tree_item_is_not_loading_condition(comic_group_ti)) + + # Ensure after expanding that first 100 children are shown initially, + # followed by a "# more" node + cg_children_tis = comic_group_ti.Children + assert len(cg_children_tis) == 100 + 1 + for i in range(0, 100): + expected_comic_url = comic_pattern.replace('#', str(i + 1)) + assert expected_comic_url == cg_children_tis[i].Text + more_ti = cg_children_tis[-1] + more_ti.ScrollTo() + assert '900 more' == more_ti.Text + + more_ti.Expand() + def more_children_visible() -> Optional[bool]: + assert comic_group_ti is not None + return (len(comic_group_ti.Children) > (100 + 1)) or None + await wait_for(more_children_visible) + + # Ensure after expanding "# more" node that another 20 children are shown + cg_children_tis = comic_group_ti.Children + assert len(cg_children_tis) == 100 + 20 + 1 + for i in range(0, 100 + 20): + expected_comic_url = comic_pattern.replace('#', str(i + 1)) + assert expected_comic_url == cg_children_tis[i].Text + more_ti = cg_children_tis[-1] + more_ti.ScrollTo() + assert '880 more' == more_ti.Text + assert False == more_ti.IsExpanded() + + more_ti.SelectItem() + + more_ti.Expand() + def more_children_visible() -> Optional[bool]: + assert comic_group_ti is not None + return (len(comic_group_ti.Children) > (100 + 20 + 1)) or None + await wait_for(more_children_visible) + + # Ensure after expanding a selected "# more" node that the + # first newly visible child inherits the selection + cg_children_tis = comic_group_ti.Children + assert len(cg_children_tis) == 100 + 20 + 20 + 1 + node_in_position_of_old_more_node = cg_children_tis[100 + 20] + assert True == node_in_position_of_old_more_node.IsSelected() + + +async def test_given_more_node_with_large_item_count_then_displays_count_with_commas() -> None: + # Initialize locale based on LANG='en_US.UTF-8' + old_lang = os.environ.get('LANG') + os.environ['LANG'] = 'en_US.UTF-8' + old_locale = locale.setlocale(locale.LC_ALL) + locale.setlocale(locale.LC_ALL, '') + try: + with served_project('testdata_xkcd.crystalproj.zip') as sp: + # Define URLs + if True: + comic_pattern = 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 # Create future group members - for i in range(1, 1000+1): + for i in range(1, 1200+1): Resource(project, comic_pattern.replace('#', str(i))) # Create group @@ -326,96 +405,13 @@ async def test_given_more_node_selected_when_expand_more_node_then_first_newly_v assert comic_group_ti is not None assert f'{comic_pattern} - Comic' == comic_group_ti.Text - # Ensure first child of group (not displayed) is "Loading..." - cg_child_ti = comic_group_ti.GetFirstChild() - assert cg_child_ti is not None - assert 'Loading...' == cg_child_ti.Text - comic_group_ti.Expand() await wait_for(first_child_of_tree_item_is_not_loading_condition(comic_group_ti)) - # Ensure after expanding that first 100 children are shown initially, - # followed by a "# more" node - cg_children_tis = comic_group_ti.Children - assert len(cg_children_tis) == 100 + 1 - for i in range(0, 100): - expected_comic_url = comic_pattern.replace('#', str(i + 1)) - assert expected_comic_url == cg_children_tis[i].Text - more_ti = cg_children_tis[-1] - more_ti.ScrollTo() - assert '900 more' == more_ti.Text - - more_ti.Expand() - def more_children_visible() -> Optional[bool]: - assert comic_group_ti is not None - return (len(comic_group_ti.Children) > (100 + 1)) or None - await wait_for(more_children_visible) - - # Ensure after expanding "# more" node that another 20 children are shown cg_children_tis = comic_group_ti.Children - assert len(cg_children_tis) == 100 + 20 + 1 - for i in range(0, 100 + 20): - expected_comic_url = comic_pattern.replace('#', str(i + 1)) - assert expected_comic_url == cg_children_tis[i].Text more_ti = cg_children_tis[-1] more_ti.ScrollTo() - assert '880 more' == more_ti.Text - assert False == more_ti.IsExpanded() - - more_ti.SelectItem() - - more_ti.Expand() - def more_children_visible() -> Optional[bool]: - assert comic_group_ti is not None - return (len(comic_group_ti.Children) > (100 + 20 + 1)) or None - await wait_for(more_children_visible) - - # Ensure after expanding a selected "# more" node that the - # first newly visible child inherits the selection - cg_children_tis = comic_group_ti.Children - assert len(cg_children_tis) == 100 + 20 + 20 + 1 - node_in_position_of_old_more_node = cg_children_tis[100 + 20] - assert True == node_in_position_of_old_more_node.IsSelected() - - -async def test_given_more_node_with_large_item_count_then_displays_count_with_commas() -> None: - # Initialize locale based on LANG='en_US.UTF-8' - old_lang = os.environ.get('LANG') - os.environ['LANG'] = 'en_US.UTF-8' - old_locale = locale.setlocale(locale.LC_ALL) - locale.setlocale(locale.LC_ALL, '') - try: - with served_project('testdata_xkcd.crystalproj.zip') as sp: - # Define URLs - if True: - comic_pattern = sp.get_request_url('https://xkcd.com/#/') - - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath) as (mw, _): - project = Project._last_opened_project - assert project is not None - - # Create future group members - for i in range(1, 1200+1): - Resource(project, comic_pattern.replace('#', str(i))) - - # Create group - ResourceGroup(project, 'Comic', comic_pattern) - - root_ti = TreeItem.GetRootItem(mw.entity_tree.window) - assert root_ti is not None - - comic_group_ti = root_ti.GetFirstChild() - assert comic_group_ti is not None - assert f'{comic_pattern} - Comic' == comic_group_ti.Text - - comic_group_ti.Expand() - await wait_for(first_child_of_tree_item_is_not_loading_condition(comic_group_ti)) - - cg_children_tis = comic_group_ti.Children - more_ti = cg_children_tis[-1] - more_ti.ScrollTo() - assert '1,100 more' == more_ti.Text + assert '1,100 more' == more_ti.Text finally: if old_lang is None: del os.environ['LANG'] diff --git a/src/crystal/tests/test_load_urls.py b/src/crystal/tests/test_load_urls.py index c82ecb42..07b7510d 100644 --- a/src/crystal/tests/test_load_urls.py +++ b/src/crystal/tests/test_load_urls.py @@ -68,37 +68,36 @@ async def test_given_project_database_not_on_ssd_given_resource_group_node_selec rss_feed_url = sp.get_request_url('https://xkcd.com/rss.xml') feed_pattern = sp.get_request_url('https://xkcd.com/*.xml') - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath) as (mw, _): - project = Project._last_opened_project - assert project is not None - - # Create small resource group (with only 2 members) - Resource(project, atom_feed_url) - Resource(project, rss_feed_url) - feed_group = ResourceGroup(project, 'Feed', feed_pattern) - - root_ti = TreeItem.GetRootItem(mw.entity_tree.window) - assert root_ti is not None + async with (await OpenOrCreateDialog.wait_for()).create() as (mw, _): + project = Project._last_opened_project + assert project is not None + + # Create small resource group (with only 2 members) + Resource(project, atom_feed_url) + Resource(project, rss_feed_url) + feed_group = ResourceGroup(project, 'Feed', feed_pattern) + + root_ti = TreeItem.GetRootItem(mw.entity_tree.window) + assert root_ti is not None + + (feed_group_ti,) = [ + child for child in root_ti.Children + if child.Text.endswith(f'- {feed_group.name}') + ] + + # Prepare to press Cancel when LoadUrlsProgressDialog appears + with patch.object( + project._load_urls_progress_listener, + 'loading_resource', + side_effect=CancelLoadUrls) as progress_listener_method: + feed_group_ti.SelectItem() + click_button(mw.download_button) - (feed_group_ti,) = [ - child for child in root_ti.Children - if child.Text.endswith(f'- {feed_group.name}') - ] + # Wait for progress dialog to show and for cancel to be pressed + await wait_for(lambda: (progress_listener_method.call_count >= 1) or None) - # Prepare to press Cancel when LoadUrlsProgressDialog appears - with patch.object( - project._load_urls_progress_listener, - 'loading_resource', - side_effect=CancelLoadUrls) as progress_listener_method: - feed_group_ti.SelectItem() - click_button(mw.download_button) - - # Wait for progress dialog to show and for cancel to be pressed - await wait_for(lambda: (progress_listener_method.call_count >= 1) or None) - - # Ensure did not create a download task - assert tree_has_no_children_condition(mw.task_tree)() is not None + # Ensure did not create a download task + assert tree_has_no_children_condition(mw.task_tree)() is not None @skip('covered by: test_given_project_database_not_on_ssd_given_resource_group_node_selected_when_press_download_button_then_loading_urls_progress_dialog_becomes_visible') @@ -179,37 +178,36 @@ async def test_given_project_database_on_ssd_given_resource_group_node_selected_ rss_feed_url = sp.get_request_url('https://xkcd.com/rss.xml') feed_pattern = sp.get_request_url('https://xkcd.com/*.xml') - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath) as (mw, _): - project = Project._last_opened_project - assert project is not None - - # Create small resource group (with only 2 members) - Resource(project, atom_feed_url) - Resource(project, rss_feed_url) - feed_group = ResourceGroup(project, 'Feed', feed_pattern) - - root_ti = TreeItem.GetRootItem(mw.entity_tree.window) - assert root_ti is not None + async with (await OpenOrCreateDialog.wait_for()).create() as (mw, _): + project = Project._last_opened_project + assert project is not None + + # Create small resource group (with only 2 members) + Resource(project, atom_feed_url) + Resource(project, rss_feed_url) + feed_group = ResourceGroup(project, 'Feed', feed_pattern) + + root_ti = TreeItem.GetRootItem(mw.entity_tree.window) + assert root_ti is not None + + (feed_group_ti,) = [ + child for child in root_ti.Children + if child.Text.endswith(f'- {feed_group.name}') + ] + + # Prepare to spy on whether LoadUrlsProgressDialog appears + with patch.object( + project._load_urls_progress_listener, + 'loading_resource', + wraps=project._load_urls_progress_listener.loading_resource) as progress_listener_method: + feed_group_ti.SelectItem() - (feed_group_ti,) = [ - child for child in root_ti.Children - if child.Text.endswith(f'- {feed_group.name}') - ] + # Wait for download to start and complete + await mw.click_download_button() + await wait_for_download_to_start_and_finish(mw.task_tree) - # Prepare to spy on whether LoadUrlsProgressDialog appears - with patch.object( - project._load_urls_progress_listener, - 'loading_resource', - wraps=project._load_urls_progress_listener.loading_resource) as progress_listener_method: - feed_group_ti.SelectItem() - - # Wait for download to start and complete - await mw.click_download_button() - await wait_for_download_to_start_and_finish(mw.task_tree) - - # Ensure did not show LoadUrlsProgressDialog - assert 0 == progress_listener_method.call_count + # Ensure did not show LoadUrlsProgressDialog + assert 0 == progress_listener_method.call_count async def test_given_project_database_on_ssd_when_press_add_group_button_then_add_group_dialog_does_appear() -> None: diff --git a/src/crystal/tests/test_menus.py b/src/crystal/tests/test_menus.py index fa3865b9..b8bd5a8b 100644 --- a/src/crystal/tests/test_menus.py +++ b/src/crystal/tests/test_menus.py @@ -12,9 +12,8 @@ async def test_can_close_project_with_menuitem() -> None: - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath, autoclose=False) as (mw, _): - await mw.close_with_menuitem() + async with (await OpenOrCreateDialog.wait_for()).create(autoclose=False) as (mw, _): + await mw.close_with_menuitem() async def test_can_quit_with_menuitem() -> None: @@ -42,7 +41,6 @@ async def quit_with_menuitem(): async def test_can_open_preferences_with_menuitem() -> None: - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath) as (mw, _): - prefs_dialog = await mw.open_preferences_with_menuitem() - await prefs_dialog.ok() + async with (await OpenOrCreateDialog.wait_for()).create() as (mw, _): + prefs_dialog = await mw.open_preferences_with_menuitem() + await prefs_dialog.ok() diff --git a/src/crystal/tests/test_open_project.py b/src/crystal/tests/test_open_project.py index c72a41ce..6df028a8 100644 --- a/src/crystal/tests/test_open_project.py +++ b/src/crystal/tests/test_open_project.py @@ -25,27 +25,26 @@ # === Test: Create Project === async def test_when_create_project_then_shows_dialog_saying_opening_project_but_no_other_messages() -> None: - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - ocd = await OpenOrCreateDialog.wait_for() + ocd = await OpenOrCreateDialog.wait_for() + + progress_listener = progress._active_progress_listener + assert progress_listener is not None + + with patch('crystal.progress.wx.ProgressDialog', autospec=True) as MockProgressDialog: + pd = MockProgressDialog.return_value + pd.Pulse.return_value = (True, False) + pd.Update.return_value = (True, False) - progress_listener = progress._active_progress_listener - assert progress_listener is not None + async with ocd.create() as (mw, _): + pass - with patch('crystal.progress.wx.ProgressDialog', autospec=True) as MockProgressDialog: - pd = MockProgressDialog.return_value - pd.Pulse.return_value = (True, False) - pd.Update.return_value = (True, False) - - async with ocd.create(project_dirpath) as (mw, _): - pass - - assert ( - [call('Opening project...')], - [] - ) == ( - pd.Pulse.call_args_list, - pd.Update.call_args_list - ) + assert ( + [call('Opening project...')], + [] + ) == ( + pd.Pulse.call_args_list, + pd.Update.call_args_list + ) # === Test: Start Open Project === diff --git a/src/crystal/tests/test_parse_html.py b/src/crystal/tests/test_parse_html.py index e7922fad..704637ba 100644 --- a/src/crystal/tests/test_parse_html.py +++ b/src/crystal/tests/test_parse_html.py @@ -21,42 +21,41 @@ async def test_uses_html_parser_specified_in_preferences() -> None: # Define URLs home_url = sp.get_request_url('https://xkcd.com/') - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath) as (mw, _): - project = Project._last_opened_project - assert project is not None - - rr = RootResource(project, 'Home', Resource(project, home_url)) - r = Resource(project, home_url) - - # Ensure default HTML parser for new project is lxml - click_button(mw.preferences_button) - pd = await PreferencesDialog.wait_for() - html_parser_title = pd.html_parser_field.Items[pd.html_parser_field.Selection] - assert 'Fastest - lxml' == html_parser_title - await pd.ok() - - revision_future = r.download(wait_for_embedded=True) - while not revision_future.done(): - await bg_sleep(DEFAULT_WAIT_PERIOD) - revision = revision_future.result() - - # Ensure expected HTML parser is used - with _watch_html_parser_usage() as (lxml_parse_func, bs4_parse_func): - (_, _, _) = revision.document_and_links() - assert (1, 0) == (lxml_parse_func.call_count, bs4_parse_func.call_count) - - # Switch HTML parser - click_button(mw.preferences_button) - pd = await PreferencesDialog.wait_for() - pd.html_parser_field.Selection = pd.html_parser_field.Items.index( - 'Classic - html.parser (bs4)') - await pd.ok() - - # Ensure new HTML parser is used - with _watch_html_parser_usage() as (lxml_parse_func, bs4_parse_func): - (_, _, _) = revision.document_and_links() - assert (0, 1) == (lxml_parse_func.call_count, bs4_parse_func.call_count) + async with (await OpenOrCreateDialog.wait_for()).create() as (mw, _): + project = Project._last_opened_project + assert project is not None + + rr = RootResource(project, 'Home', Resource(project, home_url)) + r = Resource(project, home_url) + + # Ensure default HTML parser for new project is lxml + click_button(mw.preferences_button) + pd = await PreferencesDialog.wait_for() + html_parser_title = pd.html_parser_field.Items[pd.html_parser_field.Selection] + assert 'Fastest - lxml' == html_parser_title + await pd.ok() + + revision_future = r.download(wait_for_embedded=True) + while not revision_future.done(): + await bg_sleep(DEFAULT_WAIT_PERIOD) + revision = revision_future.result() + + # Ensure expected HTML parser is used + with _watch_html_parser_usage() as (lxml_parse_func, bs4_parse_func): + (_, _, _) = revision.document_and_links() + assert (1, 0) == (lxml_parse_func.call_count, bs4_parse_func.call_count) + + # Switch HTML parser + click_button(mw.preferences_button) + pd = await PreferencesDialog.wait_for() + pd.html_parser_field.Selection = pd.html_parser_field.Items.index( + 'Classic - html.parser (bs4)') + await pd.ok() + + # Ensure new HTML parser is used + with _watch_html_parser_usage() as (lxml_parse_func, bs4_parse_func): + (_, _, _) = revision.document_and_links() + assert (0, 1) == (lxml_parse_func.call_count, bs4_parse_func.call_count) @skip('covered by: test_uses_html_parser_specified_in_preferences') diff --git a/src/crystal/tests/test_server.py b/src/crystal/tests/test_server.py index 36b9e512..f474a00c 100644 --- a/src/crystal/tests/test_server.py +++ b/src/crystal/tests/test_server.py @@ -40,43 +40,42 @@ async def test_given_default_serving_port_in_use_when_start_serving_project_then # Define URLs home_url = sp.get_request_url('https://xkcd.com/') - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath) as (mw, _): - project = Project._last_opened_project - assert project is not None - - # Create a URL - if True: - root_ti = TreeItem.GetRootItem(mw.entity_tree.window) - assert root_ti is not None - assert root_ti.GetFirstChild() is None # no entities - - click_button(mw.add_url_button) - aud = await AddUrlDialog.wait_for() - - aud.name_field.Value = 'Home' - aud.url_field.Value = home_url - await aud.ok() - (home_ti,) = root_ti.Children - - # Download the URL - home_ti.SelectItem() - await mw.click_download_button() - await wait_for_download_to_start_and_finish(mw.task_tree) + async with (await OpenOrCreateDialog.wait_for()).create() as (mw, _): + project = Project._last_opened_project + assert project is not None + + # Create a URL + if True: + root_ti = TreeItem.GetRootItem(mw.entity_tree.window) + assert root_ti is not None + assert root_ti.GetFirstChild() is None # no entities - # Try to start second server, also on _DEFAULT_SERVER_PORT. - # Expect it to actually start on (_DEFAULT_SERVER_PORT + 1). - expected_port = _DEFAULT_SERVER_PORT + 1 - home_ti.SelectItem() - try: - with assert_does_open_webbrowser_to(get_request_url(home_url, expected_port)): - click_button(mw.view_button) - finally: - assert is_port_in_use(expected_port) + click_button(mw.add_url_button) + aud = await AddUrlDialog.wait_for() - # Ensure can fetch the revision through the server - server_page = await fetch_archive_url(home_url, expected_port) - assert 200 == server_page.status + aud.name_field.Value = 'Home' + aud.url_field.Value = home_url + await aud.ok() + (home_ti,) = root_ti.Children + + # Download the URL + home_ti.SelectItem() + await mw.click_download_button() + await wait_for_download_to_start_and_finish(mw.task_tree) + + # Try to start second server, also on _DEFAULT_SERVER_PORT. + # Expect it to actually start on (_DEFAULT_SERVER_PORT + 1). + expected_port = _DEFAULT_SERVER_PORT + 1 + home_ti.SelectItem() + try: + with assert_does_open_webbrowser_to(get_request_url(home_url, expected_port)): + click_button(mw.view_button) + finally: + assert is_port_in_use(expected_port) + + # Ensure can fetch the revision through the server + server_page = await fetch_archive_url(home_url, expected_port) + assert 200 == server_page.status # ------------------------------------------------------------------------------ diff --git a/src/crystal/tests/test_tasks.py b/src/crystal/tests/test_tasks.py index 60277abd..149689fa 100644 --- a/src/crystal/tests/test_tasks.py +++ b/src/crystal/tests/test_tasks.py @@ -43,96 +43,95 @@ async def test_some_tasks_may_complete_immediately(subtests) -> None: comic_pattern = sp.get_request_url('https://xkcd.com/#/') - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath) as (mw, _): - project = Project._last_opened_project - assert project is not None + async with (await OpenOrCreateDialog.wait_for()).create() as (mw, _): + project = Project._last_opened_project + assert project is not None + + missing_r = Resource(project, missing_url) + + with subtests.test(task_type='DownloadResourceTask'): + # Download the resource + assert False == missing_r.already_downloaded_this_session + missing_rr_future = missing_r.download() # uses DownloadResourceTask + with screenshot_if_raises(): + await wait_for( + lambda: missing_rr_future.done() or None, + timeout=MAX_TIME_TO_DOWNLOAD_404_URL) + assert True == missing_r.already_downloaded_this_session - missing_r = Resource(project, missing_url) + # Download the resource again, and ensure it downloads immediately + dr_task = missing_r.create_download_task(needs_result=False) # a DownloadResourceTask + assert True == dr_task.complete + + with subtests.test(task_type='UpdateResourceGroupMembersTask'): + # Covered by subtest: DownloadResourceGroupTask + pass + + with subtests.test(task_type='DownloadResourceGroupMembersTask'): + # Covered by subtest: DownloadResourceGroupTask + pass + + with subtests.test(task_type='DownloadResourceGroupTask'): + comic_rs = [ + Resource(project, comic_pattern.replace('#', str(ordinal))) + for ordinal in [1, 2] + ] - with subtests.test(task_type='DownloadResourceTask'): - # Download the resource - assert False == missing_r.already_downloaded_this_session - missing_rr_future = missing_r.download() # uses DownloadResourceTask - with screenshot_if_raises(): - await wait_for( - lambda: missing_rr_future.done() or None, - timeout=MAX_TIME_TO_DOWNLOAD_404_URL) - assert True == missing_r.already_downloaded_this_session - - # Download the resource again, and ensure it downloads immediately - dr_task = missing_r.create_download_task(needs_result=False) # a DownloadResourceTask - assert True == dr_task.complete + comic_g = ResourceGroup(project, 'Comic', comic_pattern) + assert None == comic_g.source - with subtests.test(task_type='UpdateResourceGroupMembersTask'): - # Covered by subtest: DownloadResourceGroupTask - pass + COMIC_G_FINAL_MEMBER_COUNT = 10 - with subtests.test(task_type='DownloadResourceGroupMembersTask'): - # Covered by subtest: DownloadResourceGroupTask - pass + # Download the group (and all of its currently known members) + for r in comic_rs: + assert False == r.already_downloaded_this_session + drg_task = comic_g.create_download_task() # a DownloadResourceGroupTask + project.add_task(drg_task) + with screenshot_if_raises(): + await wait_for( + lambda: drg_task.complete or None, + timeout=( + MAX_TIME_TO_DOWNLOAD_404_URL + + (MAX_TIME_TO_DOWNLOAD_XKCD_HOME_URL_BODY * COMIC_G_FINAL_MEMBER_COUNT) + )) + assert COMIC_G_FINAL_MEMBER_COUNT == len(comic_g.members) + for r in comic_rs: + assert True == r.already_downloaded_this_session - with subtests.test(task_type='DownloadResourceGroupTask'): - comic_rs = [ - Resource(project, comic_pattern.replace('#', str(ordinal))) - for ordinal in [1, 2] - ] + # Download the group again, and ensure it downloads immediately + if True: + drg_task = comic_g.create_download_task() - comic_g = ResourceGroup(project, 'Comic', comic_pattern) - assert None == comic_g.source + load_children_of_drg_task(drg_task, task_added_to_project=False) - COMIC_G_FINAL_MEMBER_COUNT = 10 + # NOTE: The group won't appear to be immediately downloaded yet + # because no code has tried to access the lazily-loaded + # DownloadResourceTask children yet and thus doesn't + # know that all of those children are complete + assert (True, 0, False) == ( + isinstance(drg_task._download_members_task.children, AppendableLazySequence), + drg_task._download_members_task.children.cached_prefix_len + if isinstance(drg_task._download_members_task.children, AppendableLazySequence) + else None, + drg_task.complete + ) - # Download the group (and all of its currently known members) - for r in comic_rs: - assert False == r.already_downloaded_this_session - drg_task = comic_g.create_download_task() # a DownloadResourceGroupTask + # NOTE: Adding the DownloadResourceGroupTask to the project's + # task tree will cause the TaskTreeNode to start accessing + # the DownloadResourceTask children because it wants to + # create a paired TaskTreeNode for each such child. + # These accesses cause the DownloadResourceTask children + # to be created and observed as being already complete. + # With all of those children complete the ancestor + # DownloadResourceGroupTask will also be completed. project.add_task(drg_task) - with screenshot_if_raises(): - await wait_for( - lambda: drg_task.complete or None, - timeout=( - MAX_TIME_TO_DOWNLOAD_404_URL + - (MAX_TIME_TO_DOWNLOAD_XKCD_HOME_URL_BODY * COMIC_G_FINAL_MEMBER_COUNT) - )) - assert COMIC_G_FINAL_MEMBER_COUNT == len(comic_g.members) - for r in comic_rs: - assert True == r.already_downloaded_this_session - - # Download the group again, and ensure it downloads immediately - if True: - drg_task = comic_g.create_download_task() - - load_children_of_drg_task(drg_task, task_added_to_project=False) - - # NOTE: The group won't appear to be immediately downloaded yet - # because no code has tried to access the lazily-loaded - # DownloadResourceTask children yet and thus doesn't - # know that all of those children are complete - assert (True, 0, False) == ( - isinstance(drg_task._download_members_task.children, AppendableLazySequence), - drg_task._download_members_task.children.cached_prefix_len - if isinstance(drg_task._download_members_task.children, AppendableLazySequence) - else None, - drg_task.complete - ) - - # NOTE: Adding the DownloadResourceGroupTask to the project's - # task tree will cause the TaskTreeNode to start accessing - # the DownloadResourceTask children because it wants to - # create a paired TaskTreeNode for each such child. - # These accesses cause the DownloadResourceTask children - # to be created and observed as being already complete. - # With all of those children complete the ancestor - # DownloadResourceGroupTask will also be completed. - project.add_task(drg_task) - assert (True, COMIC_G_FINAL_MEMBER_COUNT, True) == ( - isinstance(drg_task._download_members_task.children, AppendableLazySequence), - drg_task._download_members_task.children.cached_prefix_len - if isinstance(drg_task._download_members_task.children, AppendableLazySequence) - else None, - drg_task.complete - ) + assert (True, COMIC_G_FINAL_MEMBER_COUNT, True) == ( + isinstance(drg_task._download_members_task.children, AppendableLazySequence), + drg_task._download_members_task.children.cached_prefix_len + if isinstance(drg_task._download_members_task.children, AppendableLazySequence) + else None, + drg_task.complete + ) async def test_given_running_tests_then_uses_extra_listener_assertions() -> None: @@ -213,18 +212,17 @@ async def try_download_with_disk_usage(du: _DiskUsage, *, expect_failure: bool) if True: home_url = sp.get_request_url('https://xkcd.com/') - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath) as (mw, _): - project = Project._last_opened_project - assert project is not None - - await try_download_with_disk_usage( - _DiskUsage(total=100*1024*1024, used=(100-6)*1024*1024, free=6*1024*1024), - expect_failure=False) - - await try_download_with_disk_usage( - _DiskUsage(total=100*1024*1024, used=(100-4)*1024*1024, free=4*1024*1024), - expect_failure=True) + async with (await OpenOrCreateDialog.wait_for()).create() as (mw, _): + project = Project._last_opened_project + assert project is not None + + await try_download_with_disk_usage( + _DiskUsage(total=100*1024*1024, used=(100-6)*1024*1024, free=6*1024*1024), + expect_failure=False) + + await try_download_with_disk_usage( + _DiskUsage(total=100*1024*1024, used=(100-4)*1024*1024, free=4*1024*1024), + expect_failure=True) with subtests.test('given project on large disk and less than 4 gib of disk free'): with served_project('testdata_xkcd.crystalproj.zip') as sp: @@ -232,18 +230,17 @@ async def try_download_with_disk_usage(du: _DiskUsage, *, expect_failure: bool) if True: home_url = sp.get_request_url('https://xkcd.com/') - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath) as (mw, _): - project = Project._last_opened_project - assert project is not None - - await try_download_with_disk_usage( - _DiskUsage(total=1000*1024*1024*1024, used=(1000-6)*1024*1024*1024, free=6*1024*1024*1024), - expect_failure=False) - - await try_download_with_disk_usage( - _DiskUsage(total=1000*1024*1024*1024, used=(1000-3)*1024*1024*1024, free=3*1024*1024*1024), - expect_failure=True) + async with (await OpenOrCreateDialog.wait_for()).create() as (mw, _): + project = Project._last_opened_project + assert project is not None + + await try_download_with_disk_usage( + _DiskUsage(total=1000*1024*1024*1024, used=(1000-6)*1024*1024*1024, free=6*1024*1024*1024), + expect_failure=False) + + await try_download_with_disk_usage( + _DiskUsage(total=1000*1024*1024*1024, used=(1000-3)*1024*1024*1024, free=3*1024*1024*1024), + expect_failure=True) class _DiskUsage(NamedTuple): diff --git a/src/crystal/tests/test_tasktree.py b/src/crystal/tests/test_tasktree.py index 91839d81..d750ad2d 100644 --- a/src/crystal/tests/test_tasktree.py +++ b/src/crystal/tests/test_tasktree.py @@ -447,42 +447,41 @@ async def _project_with_resource_group_starting_to_download( with patch.object(TaskTreeNode, '_MAX_VISIBLE_CHILDREN', small_max_visible_children), \ patch.object(TaskTreeNode, '_MAX_LEADING_COMPLETE_CHILDREN', small_max_leading_complete_children): - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath) as (mw, _): - project = Project._last_opened_project - assert project is not None - - # Create group - g = ResourceGroup(project, 'Comic', comic_pattern) - - # Create group members - for i in range(1, resource_count + 1): - Resource(project, comic_pattern.replace('#', str(i))) - - # Start downloading group - assert 0 == len(project.root_task.children) - g.download() - - (download_rg_task,) = project.root_task.children - assert isinstance(download_rg_task, DownloadResourceGroupTask) - load_children_of_drg_task(download_rg_task, scheduler_thread_enabled=scheduler_thread_enabled) - (_, download_rg_members_task) = download_rg_task.children - assert isinstance(download_rg_members_task, DownloadResourceGroupMembersTask) - - # Define: create_resource() - next_resource_ordinal = resource_count + 1 - def create_resource() -> Resource: - nonlocal next_resource_ordinal - r = Resource(project, comic_pattern.replace('#', str(next_resource_ordinal))) - next_resource_ordinal += 1 - return r - - yield ( - mw, - _ttn_for_task(download_rg_task), - _ttn_for_task(download_rg_members_task), - create_resource, - ) + async with (await OpenOrCreateDialog.wait_for()).create() as (mw, _): + project = Project._last_opened_project + assert project is not None + + # Create group + g = ResourceGroup(project, 'Comic', comic_pattern) + + # Create group members + for i in range(1, resource_count + 1): + Resource(project, comic_pattern.replace('#', str(i))) + + # Start downloading group + assert 0 == len(project.root_task.children) + g.download() + + (download_rg_task,) = project.root_task.children + assert isinstance(download_rg_task, DownloadResourceGroupTask) + load_children_of_drg_task(download_rg_task, scheduler_thread_enabled=scheduler_thread_enabled) + (_, download_rg_members_task) = download_rg_task.children + assert isinstance(download_rg_members_task, DownloadResourceGroupMembersTask) + + # Define: create_resource() + next_resource_ordinal = resource_count + 1 + def create_resource() -> Resource: + nonlocal next_resource_ordinal + r = Resource(project, comic_pattern.replace('#', str(next_resource_ordinal))) + next_resource_ordinal += 1 + return r + + yield ( + mw, + _ttn_for_task(download_rg_task), + _ttn_for_task(download_rg_members_task), + create_resource, + ) def _cleanup_download_of_resource_group( diff --git a/src/crystal/tests/test_workflows.py b/src/crystal/tests/test_workflows.py index fd134cd1..fc3eeade 100644 --- a/src/crystal/tests/test_workflows.py +++ b/src/crystal/tests/test_workflows.py @@ -425,59 +425,93 @@ async def test_can_download_and_serve_a_site_requiring_dynamic_url_discovery() - target_root_resource_name = 'Target' - with tempfile.TemporaryDirectory(suffix='.crystalproj') as project_dirpath: - async with (await OpenOrCreateDialog.wait_for()).create(project_dirpath) as (mw, _): - root_ti = TreeItem.GetRootItem(mw.entity_tree.window) - assert root_ti is not None - assert root_ti.GetFirstChild() is None # no entities + async with (await OpenOrCreateDialog.wait_for()).create() as (mw, project_dirpath): + root_ti = TreeItem.GetRootItem(mw.entity_tree.window) + assert root_ti is not None + assert root_ti.GetFirstChild() is None # no entities + + # Download home page + if True: + click_button(mw.add_url_button) + aud = await AddUrlDialog.wait_for() + aud.name_field.Value = 'Home' + aud.url_field.Value = home_url + await aud.ok() + home_ti = root_ti.GetFirstChild() + assert home_ti is not None # entity was created + assert f'{home_url} - Home' == home_ti.Text - # Download home page + home_ti.SelectItem() + await mw.click_download_button() + await wait_for_download_to_start_and_finish(mw.task_tree) + + # Start server + with assert_does_open_webbrowser_to(get_request_url(home_url)): + click_button(mw.view_button) + + assert False == (await is_url_not_in_archive(home_url)) + + # Ensure home page ONLY has