ONLY, but ignore span with .foobar-detection client.post( - url_for("ui.ui_edit.edit_page", uuid="first"), + url_for("ui.ui_edit.edit_page", uuid=uuid), data={"include_filters": 'span,p', "url": test_url, "tags": "", "subtractive_selectors": ".foobar-detection", 'fetch_backend': "html_requests", "time_between_check_use_default": "y"}, follow_redirects=True ) @@ -75,7 +75,7 @@ def test_check_ignore_elements(client, live_server, measure_memory_usage, datast time.sleep(sleep_time_for_fetch_thread) res = client.get( - url_for("ui.ui_preview.preview_page", uuid="first"), + url_for("ui.ui_preview.preview_page", uuid=uuid), follow_redirects=True ) assert b'foobar-detection' not in res.data diff --git a/changedetectionio/tests/test_trigger.py b/changedetectionio/tests/test_trigger.py index 25714d97..b3935de5 100644 --- a/changedetectionio/tests/test_trigger.py +++ b/changedetectionio/tests/test_trigger.py @@ -76,7 +76,7 @@ def test_trigger_functionality(client, live_server, measure_memory_usage, datast # Goto the edit page, add our ignore text # Add our URL to the import page res = client.post( - url_for("ui.ui_edit.edit_page", uuid="first"), + url_for("ui.ui_edit.edit_page", uuid=uuid), data={"trigger_text": trigger_text, "ignore_text": "and more", "url": test_url, @@ -89,14 +89,14 @@ def test_trigger_functionality(client, live_server, measure_memory_usage, datast wait_for_all_checks(client) # Check it saved res = client.get( - url_for("ui.ui_edit.edit_page", uuid="first"), + url_for("ui.ui_edit.edit_page", uuid=uuid), ) assert bytes(trigger_text.encode('utf-8')) in res.data # so that we set the state to 'has-unread-changes' after all the edits - client.get(url_for("ui.ui_diff.diff_history_page", uuid="first")) + client.get(url_for("ui.ui_diff.diff_history_page", uuid=uuid)) # Trigger a check client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) @@ -129,11 +129,11 @@ def test_trigger_functionality(client, live_server, measure_memory_usage, datast # https://github.com/dgtlmoon/changedetection.io/issues/616 # Apparently the actual snapshot that contains the trigger never shows - res = client.get(url_for("ui.ui_diff.diff_history_page", uuid="first")) + res = client.get(url_for("ui.ui_diff.diff_history_page", uuid=uuid)) assert b'Add to cart' in res.data # Check the preview/highlighter, we should be able to see what we triggered on, but it should be highlighted - res = client.get(url_for("ui.ui_preview.preview_page", uuid="first")) + res = client.get(url_for("ui.ui_preview.preview_page", uuid=uuid)) assert b'ignored_line_numbers = [8]' in res.data # We should be able to see what we triggered on diff --git a/changedetectionio/tests/test_trigger_regex.py b/changedetectionio/tests/test_trigger_regex.py index c2cfed4a..8fb2374b 100644 --- a/changedetectionio/tests/test_trigger_regex.py +++ b/changedetectionio/tests/test_trigger_regex.py @@ -43,7 +43,7 @@ def test_trigger_regex_functionality(client, live_server, measure_memory_usage, ### test regex res = client.post( - url_for("ui.ui_edit.edit_page", uuid="first"), + url_for("ui.ui_edit.edit_page", uuid=uuid), data={"trigger_text": '/something \d{3}/', "url": test_url, "fetch_backend": "html_requests", @@ -52,7 +52,7 @@ def test_trigger_regex_functionality(client, live_server, measure_memory_usage, ) wait_for_all_checks(client) # so that we set the state to 'has-unread-changes' after all the edits - client.get(url_for("ui.ui_diff.diff_history_page", uuid="first")) + client.get(url_for("ui.ui_diff.diff_history_page", uuid=uuid)) with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: f.write("some new noise") diff --git a/changedetectionio/tests/test_trigger_regex_with_filter.py b/changedetectionio/tests/test_trigger_regex_with_filter.py index e3b85aa5..31bb6a02 100644 --- a/changedetectionio/tests/test_trigger_regex_with_filter.py +++ b/changedetectionio/tests/test_trigger_regex_with_filter.py @@ -43,7 +43,7 @@ def test_trigger_regex_functionality_with_filter(client, live_server, measure_me ### test regex with filter res = client.post( - url_for("ui.ui_edit.edit_page", uuid="first"), + url_for("ui.ui_edit.edit_page", uuid=uuid), data={"trigger_text": "/cool.stuff/", "url": test_url, "include_filters": '#in-here', @@ -55,7 +55,7 @@ def test_trigger_regex_functionality_with_filter(client, live_server, measure_me # Give the thread time to pick it up time.sleep(sleep_time_for_fetch_thread) - client.get(url_for("ui.ui_diff.diff_history_page", uuid="first")) + client.get(url_for("ui.ui_diff.diff_history_page", uuid=uuid)) # Check that we have the expected text.. but it's not in the css filter we want with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: diff --git a/changedetectionio/tests/test_ui.py b/changedetectionio/tests/test_ui.py index e3c9b3fe..e112dd45 100644 --- a/changedetectionio/tests/test_ui.py +++ b/changedetectionio/tests/test_ui.py @@ -40,7 +40,7 @@ def test_recheck_time_field_validation_single_watch(client, live_server, measure client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) res = client.post( - url_for("ui.ui_edit.edit_page", uuid="first"), + url_for("ui.ui_edit.edit_page", uuid=uuid), data={ "url": test_url, 'fetch_backend': "html_requests", @@ -59,7 +59,7 @@ def test_recheck_time_field_validation_single_watch(client, live_server, measure # Now set some time res = client.post( - url_for("ui.ui_edit.edit_page", uuid="first"), + url_for("ui.ui_edit.edit_page", uuid=uuid), data={ "url": test_url, 'fetch_backend': "html_requests", @@ -78,7 +78,7 @@ def test_recheck_time_field_validation_single_watch(client, live_server, measure # Now set to use defaults res = client.post( - url_for("ui.ui_edit.edit_page", uuid="first"), + url_for("ui.ui_edit.edit_page", uuid=uuid), data={ "url": test_url, 'fetch_backend': "html_requests", @@ -184,14 +184,7 @@ def test_page_title_listing_behaviour(client, live_server, measure_memory_usage, assert b"Settings updated." in res.data - # Add our URL to the import page - res = client.post( - url_for("imports.import_page"), - data={"urls": url_for('test_endpoint', _external=True)}, - follow_redirects=True - ) - - assert b"1 Imported" in res.data + uuid = client.application.config.get('DATASTORE').add_watch(url=url_for('test_endpoint', _external=True)) wait_for_all_checks(client) # We see the URL only, no title/description was manually entered @@ -201,7 +194,7 @@ def test_page_title_listing_behaviour(client, live_server, measure_memory_usage, # Now 'my title' should override res = client.post( - url_for("ui.ui_edit.edit_page", uuid="first"), + url_for("ui.ui_edit.edit_page", uuid=uuid), data={ "url": url_for('test_endpoint', _external=True), "title": "my title", @@ -229,7 +222,7 @@ def test_page_title_listing_behaviour(client, live_server, measure_memory_usage, # Remove page title description override and it should fall back to title res = client.post( - url_for("ui.ui_edit.edit_page", uuid="first"), + url_for("ui.ui_edit.edit_page", uuid=uuid), data={ "url": url_for('test_endpoint', _external=True), "title": "", @@ -250,14 +243,9 @@ def test_ui_viewed_unread_flag(client, live_server, measure_memory_usage, datast set_original_response(datastore_path=datastore_path, extra_title="custom html") - # Add our URL to the import page - res = client.post( - url_for("imports.import_page"), - data={"urls": url_for('test_endpoint', _external=True)+"\r\n"+url_for('test_endpoint', _external=True)}, - follow_redirects=True - ) + uuidA = client.application.config.get('DATASTORE').add_watch(url= url_for('test_endpoint', _external=True)) + uuidB = client.application.config.get('DATASTORE').add_watch(url=url_for('test_endpoint', _external=True)) - assert b"2 Imported" in res.data wait_for_all_checks(client) set_modified_response(datastore_path=datastore_path) @@ -269,7 +257,7 @@ def test_ui_viewed_unread_flag(client, live_server, measure_memory_usage, datast assert res.data.count(b'data-watch-uuid') == 2 # one should now be viewed, but two in total still - client.get(url_for("ui.ui_diff.diff_history_page", uuid="first")) + client.get(url_for("ui.ui_diff.diff_history_page", uuid=uuidA)) res = client.get(url_for("watchlist.index")) assert b'1' in res.data assert res.data.count(b'data-watch-uuid') == 2 diff --git a/changedetectionio/tests/test_unique_lines.py b/changedetectionio/tests/test_unique_lines.py index 294578c8..5f75e78d 100644 --- a/changedetectionio/tests/test_unique_lines.py +++ b/changedetectionio/tests/test_unique_lines.py @@ -86,7 +86,7 @@ def test_unique_lines_functionality(client, live_server, measure_memory_usage, d # Add our URL to the import page res = client.post( - url_for("ui.ui_edit.edit_page", uuid="first"), + url_for("ui.ui_edit.edit_page", uuid=uuid), data={"check_unique_lines": "y", "url": test_url, "fetch_backend": "html_requests", @@ -130,7 +130,7 @@ def test_sort_lines_functionality(client, live_server, measure_memory_usage, dat # Add our URL to the import page res = client.post( - url_for("ui.ui_edit.edit_page", uuid="first"), + url_for("ui.ui_edit.edit_page", uuid=uuid), data={"sort_text_alphabetically": "n", "url": test_url, "fetch_backend": "html_requests", @@ -152,7 +152,7 @@ def test_sort_lines_functionality(client, live_server, measure_memory_usage, dat assert b'has-unread-changes' in res.data res = client.get( - url_for("ui.ui_preview.preview_page", uuid="first"), + url_for("ui.ui_preview.preview_page", uuid=uuid), follow_redirects=True ) @@ -176,7 +176,7 @@ def test_extra_filters(client, live_server, measure_memory_usage, datastore_path # Add our URL to the import page res = client.post( - url_for("ui.ui_edit.edit_page", uuid="first"), + url_for("ui.ui_edit.edit_page", uuid=uuid), data={"remove_duplicate_lines": "y", "trim_text_whitespace": "y", "sort_text_alphabetically": "", # leave this OFF for testing @@ -195,7 +195,7 @@ def test_extra_filters(client, live_server, measure_memory_usage, datastore_path wait_for_all_checks(client) res = client.get( - url_for("ui.ui_preview.preview_page", uuid="first") + url_for("ui.ui_preview.preview_page", uuid=uuid) ) assert res.data.count(b"see what happens.") == 1 diff --git a/changedetectionio/tests/test_watch_fields_storage.py b/changedetectionio/tests/test_watch_fields_storage.py index 9cb32ecf..60026a04 100644 --- a/changedetectionio/tests/test_watch_fields_storage.py +++ b/changedetectionio/tests/test_watch_fields_storage.py @@ -16,7 +16,7 @@ def test_check_watch_field_storage(client, live_server, measure_memory_usage, da res = client.post( - url_for("ui.ui_edit.edit_page", uuid="first"), + url_for("ui.ui_edit.edit_page", uuid=uuid), data={ "notification_urls": "json://127.0.0.1:30000\r\njson://128.0.0.1\r\n", "time_between_check-minutes": 126, "include_filters" : ".fooclass", @@ -33,7 +33,7 @@ def test_check_watch_field_storage(client, live_server, measure_memory_usage, da assert b"Updated watch." in res.data res = client.get( - url_for("ui.ui_edit.edit_page", uuid="first"), + url_for("ui.ui_edit.edit_page", uuid=uuid), follow_redirects=True ) # checks that we dont get an error when using blank lines in the field value diff --git a/changedetectionio/tests/test_xpath_selector.py b/changedetectionio/tests/test_xpath_selector.py index bc637236..5a71f564 100644 --- a/changedetectionio/tests/test_xpath_selector.py +++ b/changedetectionio/tests/test_xpath_selector.py @@ -118,7 +118,7 @@ def test_check_xpath_filter_utf8(client, live_server, measure_memory_usage, data client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) wait_for_all_checks(client) res = client.post( - url_for("ui.ui_edit.edit_page", uuid="first"), + url_for("ui.ui_edit.edit_page", uuid=uuid), data={"include_filters": filter, "url": test_url, "tags": "", "headers": "", 'fetch_backend': "html_requests", "time_between_check_use_default": "y"}, follow_redirects=True ) @@ -167,7 +167,7 @@ def test_check_xpath_text_function_utf8(client, live_server, measure_memory_usag client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) wait_for_all_checks(client) res = client.post( - url_for("ui.ui_edit.edit_page", uuid="first"), + url_for("ui.ui_edit.edit_page", uuid=uuid), data={"include_filters": filter, "url": test_url, "tags": "", "headers": "", 'fetch_backend': "html_requests", "time_between_check_use_default": "y"}, follow_redirects=True ) @@ -178,7 +178,7 @@ def test_check_xpath_text_function_utf8(client, live_server, measure_memory_usag # The service should echo back the request headers res = client.get( - url_for("ui.ui_preview.preview_page", uuid="first"), + url_for("ui.ui_preview.preview_page", uuid=uuid), follow_redirects=True ) @@ -204,7 +204,7 @@ def test_check_markup_xpath_filter_restriction(client, live_server, measure_memo # Goto the edit page, add our ignore text # Add our URL to the import page res = client.post( - url_for("ui.ui_edit.edit_page", uuid="first"), + url_for("ui.ui_edit.edit_page", uuid=uuid), data={"include_filters": xpath_filter, "url": test_url, "tags": "", "headers": "", 'fetch_backend': "html_requests", "time_between_check_use_default": "y"}, follow_redirects=True ) @@ -214,7 +214,7 @@ def test_check_markup_xpath_filter_restriction(client, live_server, measure_memo wait_for_all_checks(client) # view it/reset state back to viewed - client.get(url_for("ui.ui_diff.diff_history_page", uuid="first"), follow_redirects=True) + client.get(url_for("ui.ui_diff.diff_history_page", uuid=uuid), follow_redirects=True) # Make a change set_modified_response(datastore_path=datastore_path) @@ -237,7 +237,7 @@ def test_xpath_validation(client, live_server, measure_memory_usage, datastore_p wait_for_all_checks(client) res = client.post( - url_for("ui.ui_edit.edit_page", uuid="first"), + url_for("ui.ui_edit.edit_page", uuid=uuid), data={"include_filters": "/something horrible", "url": test_url, "tags": "", "headers": "", 'fetch_backend': "html_requests", "time_between_check_use_default": "y"}, follow_redirects=True ) @@ -253,7 +253,7 @@ def test_xpath23_prefix_validation(client, live_server, measure_memory_usage, da wait_for_all_checks(client) res = client.post( - url_for("ui.ui_edit.edit_page", uuid="first"), + url_for("ui.ui_edit.edit_page", uuid=uuid), data={"include_filters": "xpath:/something horrible", "url": test_url, "tags": "", "headers": "", 'fetch_backend': "html_requests", "time_between_check_use_default": "y"}, follow_redirects=True ) @@ -298,7 +298,7 @@ def test_xpath1_lxml(client, live_server, measure_memory_usage, datastore_path): wait_for_all_checks(client) res = client.post( - url_for("ui.ui_edit.edit_page", uuid="first"), + url_for("ui.ui_edit.edit_page", uuid=uuid), data={"include_filters": "xpath1://title/text()", "url": test_url, "tags": "", "headers": "", 'fetch_backend': "html_requests", "time_between_check_use_default": "y"}, follow_redirects=True @@ -310,7 +310,7 @@ def test_xpath1_lxml(client, live_server, measure_memory_usage, datastore_path): assert b'_ElementStringResult' not in res.data # tested with 5.1.1 when it was removed and 5.1.0 assert b'Exception' not in res.data res = client.get( - url_for("ui.ui_preview.preview_page", uuid="first"), + url_for("ui.ui_preview.preview_page", uuid=uuid), follow_redirects=True ) @@ -328,7 +328,7 @@ def test_xpath1_validation(client, live_server, measure_memory_usage, datastore_ wait_for_all_checks(client) res = client.post( - url_for("ui.ui_edit.edit_page", uuid="first"), + url_for("ui.ui_edit.edit_page", uuid=uuid), data={"include_filters": "xpath1:/something horrible", "url": test_url, "tags": "", "headers": "", 'fetch_backend': "html_requests", "time_between_check_use_default": "y"}, follow_redirects=True ) @@ -349,7 +349,7 @@ def test_check_with_prefix_include_filters(client, live_server, measure_memory_u wait_for_all_checks(client) res = client.post( - url_for("ui.ui_edit.edit_page", uuid="first"), + url_for("ui.ui_edit.edit_page", uuid=uuid), data={"include_filters": "xpath://*[contains(@class, 'sametext')]", "url": test_url, "tags": "", "headers": "", 'fetch_backend': "html_requests", "time_between_check_use_default": "y"}, follow_redirects=True @@ -359,7 +359,7 @@ def test_check_with_prefix_include_filters(client, live_server, measure_memory_u wait_for_all_checks(client) res = client.get( - url_for("ui.ui_preview.preview_page", uuid="first"), + url_for("ui.ui_preview.preview_page", uuid=uuid), follow_redirects=True ) @@ -396,7 +396,7 @@ def test_various_rules(client, live_server, measure_memory_usage, datastore_path for r in ['//div', '//a', 'xpath://div', 'xpath://a']: res = client.post( - url_for("ui.ui_edit.edit_page", uuid="first"), + url_for("ui.ui_edit.edit_page", uuid=uuid), data={"include_filters": r, "url": test_url, "tags": "", @@ -423,7 +423,7 @@ def test_xpath_20(client, live_server, measure_memory_usage, datastore_path): test_url = url_for('test_endpoint', _external=True) res = client.post( - url_for("ui.ui_edit.edit_page", uuid="first"), + url_for("ui.ui_edit.edit_page", uuid=uuid), data={"include_filters": "//*[contains(@class, 'sametext')]|//*[contains(@class, 'changetext')]", "url": test_url, "tags": "", @@ -437,7 +437,7 @@ def test_xpath_20(client, live_server, measure_memory_usage, datastore_path): wait_for_all_checks(client) res = client.get( - url_for("ui.ui_preview.preview_page", uuid="first"), + url_for("ui.ui_preview.preview_page", uuid=uuid), follow_redirects=True ) @@ -457,7 +457,7 @@ def test_xpath_20_function_count(client, live_server, measure_memory_usage, data wait_for_all_checks(client) res = client.post( - url_for("ui.ui_edit.edit_page", uuid="first"), + url_for("ui.ui_edit.edit_page", uuid=uuid), data={"include_filters": "xpath:count(//div) * 123456789987654321", "url": test_url, "tags": "", @@ -471,7 +471,7 @@ def test_xpath_20_function_count(client, live_server, measure_memory_usage, data wait_for_all_checks(client) res = client.get( - url_for("ui.ui_preview.preview_page", uuid="first"), + url_for("ui.ui_preview.preview_page", uuid=uuid), follow_redirects=True ) @@ -490,7 +490,7 @@ def test_xpath_20_function_count2(client, live_server, measure_memory_usage, dat wait_for_all_checks(client) res = client.post( - url_for("ui.ui_edit.edit_page", uuid="first"), + url_for("ui.ui_edit.edit_page", uuid=uuid), data={"include_filters": "/html/body/count(div) * 123456789987654321", "url": test_url, "tags": "", @@ -504,7 +504,7 @@ def test_xpath_20_function_count2(client, live_server, measure_memory_usage, dat wait_for_all_checks(client) res = client.get( - url_for("ui.ui_preview.preview_page", uuid="first"), + url_for("ui.ui_preview.preview_page", uuid=uuid), follow_redirects=True ) @@ -551,16 +551,11 @@ def _subtest_xpath_rss(client, datastore_path, content_type='text/html'): # Add our URL to the import page test_url = url_for('test_endpoint', content_type=content_type, _external=True) - res = client.post( - url_for("ui.ui_views.form_quick_watch_add"), - data={"url": test_url, "tags": '', 'edit_and_watch_submit_button': 'Edit > Watch'}, - follow_redirects=True - ) - assert b"Watch added in Paused state, saving will unpause" in res.data + uuid = client.application.config.get('DATASTORE').add_watch(url=test_url, extras={'paused': True}) res = client.post( - url_for("ui.ui_edit.edit_page", uuid="first", unpause_on_save=1), + url_for("ui.ui_edit.edit_page", uuid=uuid, unpause_on_save=1), data={ "url": test_url, "include_filters": "xpath://item", @@ -571,11 +566,10 @@ def _subtest_xpath_rss(client, datastore_path, content_type='text/html'): follow_redirects=True ) - assert b"unpaused" in res.data wait_for_all_checks(client) res = client.get( - url_for("ui.ui_preview.preview_page", uuid="first"), + url_for("ui.ui_preview.preview_page", uuid=uuid), follow_redirects=True ) diff --git a/changedetectionio/tests/visualselector/test_fetch_data.py b/changedetectionio/tests/visualselector/test_fetch_data.py index 072f5870..bb7fcfcb 100644 --- a/changedetectionio/tests/visualselector/test_fetch_data.py +++ b/changedetectionio/tests/visualselector/test_fetch_data.py @@ -77,7 +77,7 @@ def test_visual_selector_content_ready(client, live_server, measure_memory_usage # Some options should be enabled # @todo - in the future, the visibility should be toggled by JS from the request type setting res = client.get( - url_for("ui.ui_edit.edit_page", uuid="first"), + url_for("ui.ui_edit.edit_page", uuid=uuid), follow_redirects=True ) assert b'notification_screenshot' in res.data @@ -103,7 +103,7 @@ def test_basic_browserstep(client, live_server, measure_memory_usage, datastore_ assert b"Watch added in Paused state, saving will unpause" in res.data res = client.post( - url_for("ui.ui_edit.edit_page", uuid="first", unpause_on_save=1), + url_for("ui.ui_edit.edit_page", uuid=uuid, unpause_on_save=1), data={ "url": test_url, "tags": "", diff --git a/changedetectionio/worker.py b/changedetectionio/worker.py index 59cf6164..155e2253 100644 --- a/changedetectionio/worker.py +++ b/changedetectionio/worker.py @@ -67,6 +67,17 @@ async def async_update_worker(worker_id, q, notification_q, app, datastore, exec logger.info(f"Worker {worker_id} idle and reached max runtime ({runtime:.0f}s), restarting") return "restart" continue + except RuntimeError as e: + # Handle executor shutdown gracefully - this is expected during shutdown + if "cannot schedule new futures after shutdown" in str(e): + # Executor shut down - exit gracefully without logging in pytest + if not IN_PYTEST: + logger.debug(f"Worker {worker_id} detected executor shutdown, exiting") + break + # Other RuntimeError - log and continue + logger.error(f"Worker {worker_id} runtime error: {e}") + await asyncio.sleep(0.1) + continue except Exception as e: # Handle expected Empty exception from queue timeout import queue diff --git a/changedetectionio/worker_pool.py b/changedetectionio/worker_pool.py index b193e1cb..410ca9fb 100644 --- a/changedetectionio/worker_pool.py +++ b/changedetectionio/worker_pool.py @@ -338,7 +338,7 @@ def queue_item_async_safe(update_q, item, silent=False): def shutdown_workers(): """Shutdown all async workers brutally - no delays, no waiting""" - global worker_threads + global worker_threads, queue_executor # Check if we're in pytest environment - if so, be more gentle with logging import os @@ -354,6 +354,17 @@ def shutdown_workers(): # Clear immediately - threads are daemon and will die worker_threads.clear() + # Shutdown the queue executor to prevent "cannot schedule new futures after shutdown" errors + # This must happen AFTER workers are stopped to avoid race conditions + if queue_executor: + try: + queue_executor.shutdown(wait=False) + if not in_pytest: + logger.debug("Queue executor shut down") + except Exception as e: + if not in_pytest: + logger.warning(f"Error shutting down queue executor: {e}") + if not in_pytest: logger.info("Async workers brutal shutdown complete")