Files
rewards-farmer/tests/test_task_recovery.py
T

275 lines
8.5 KiB
Python
Raw Normal View History

"""Tests for getting back to a known page after a task fails.
The bug (#63): a task that dies partway through leaves the browser wherever it
stopped, which for the search and visual search tasks is a Bing page rather than
the Rewards site. Every later task begins by looking for controls that only
exist on rewards.bing.com, so one failure made all of them report [SKIP] for a
UI that was present and working the whole time.
The stand-ins here are local rather than in fakes.py: that module fakes the DOM
lookups the selectors do, and what these need is a driver that remembers where
it navigated. No browser, so they run anywhere.
python -m unittest discover -s tests
"""
import logging
import os
import sys
import unittest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from selenium.common.exceptions import NoSuchElementException, TimeoutException, WebDriverException
import rewards_tasks
HOME = rewards_tasks.REWARDS_HOME_URL
BING_RESULTS = "https://www.bing.com/search?q=weather"
MAIN_TAB = "main-tab"
BING_TAB = "bing-tab"
class RecordingDriver:
def __init__(self, url=HOME, get_raises=None, handles=None, current_handle=MAIN_TAB):
self.current_url = url
self.visited = []
self.get_raises = get_raises
self.window_handles = list(handles) if handles is not None else [MAIN_TAB]
self.current_window_handle = current_handle
def get(self, url):
if self.get_raises is not None:
raise self.get_raises
self.visited.append(url)
self.current_url = url
class RecordingTabUtils:
def __init__(self):
self.closes = 0
self.focuses = 0
self.kept = []
def close_all_other_tabs(self, exceptions=None):
self.closes += 1
self.kept.append(exceptions)
def ensure_focus(self):
self.focuses += 1
class StubTasks(rewards_tasks.RewardsTaskUtils):
"""RewardsTaskUtils with the six tasks replaced, and nothing else stubbed.
complete_all_tasks and return_to_rewards_home are the real ones, which is
the whole point.
"""
def __init__(self, driver, failures=None):
self.driver = driver
self.tab_utils = RecordingTabUtils()
self.main_window = MAIN_TAB
self.failures = failures or {}
self.ran = []
def _task(self, name, strands_browser_at=None):
self.ran.append(name)
if name not in self.failures:
# A task that finishes switches back to the Rewards tab itself, so
# only a failure part way through leaves the browser elsewhere.
return
if strands_browser_at is not None:
self.driver.current_url = strands_browser_at
raise self.failures[name]
def complete_bing_daily_set(self):
self._task("Bing daily set")
def complete_explore_on_bing_tasks(self):
self._task("Explore on Bing", strands_browser_at=BING_RESULTS)
def complete_visual_search(self):
self._task("Visual search", strands_browser_at=BING_RESULTS)
def complete_misc_cards(self):
self._task("Misc cards")
def complete_required_searches(self):
self._task("Required searches")
def claim_bonus_points(self):
self._task("Bonus points")
ALL_TASKS = [
"Bing daily set", "Explore on Bing", "Visual search",
"Misc cards", "Required searches", "Bonus points",
]
class FailedTaskRecoveryTests(unittest.TestCase):
def test_a_failure_on_a_bing_page_navigates_back(self):
driver = RecordingDriver()
tasks = StubTasks(driver, {"Visual search": TimeoutException("no file input")})
with self.assertLogs(rewards_tasks.logger, level=logging.WARNING):
tasks.complete_all_tasks()
self.assertEqual(driver.visited, [HOME])
self.assertEqual(driver.current_url, HOME)
def test_the_later_tasks_still_run(self):
# The point of the fix: one failure used to cost every task after it.
driver = RecordingDriver()
tasks = StubTasks(driver, {"Explore on Bing": TimeoutException("slow panel")})
with self.assertLogs(rewards_tasks.logger, level=logging.WARNING):
tasks.complete_all_tasks()
self.assertEqual(tasks.ran, ALL_TASKS)
def test_a_skip_recovers_too(self):
# NoSuchElementException is the [SKIP] path rather than [FAIL], and it
# strands the browser just the same.
driver = RecordingDriver()
tasks = StubTasks(driver, {"Visual search": NoSuchElementException("no sidebar")})
with self.assertLogs(rewards_tasks.logger, level=logging.WARNING):
tasks.complete_all_tasks()
self.assertEqual(driver.visited, [HOME])
def test_a_clean_run_never_navigates(self):
# Six extra page loads a run would be a real cost for no benefit.
driver = RecordingDriver()
tasks = StubTasks(driver)
tasks.complete_all_tasks()
self.assertEqual(driver.visited, [])
self.assertEqual(tasks.ran, ALL_TASKS)
def test_no_redundant_navigation_when_already_home(self):
# Plenty of failures happen on the Rewards page itself, e.g. a panel
# that never renders. Reloading it would only cost time.
driver = RecordingDriver()
tasks = StubTasks(driver, {"Misc cards": TimeoutException("no cards")})
with self.assertLogs(rewards_tasks.logger, level=logging.WARNING):
tasks.complete_all_tasks()
self.assertEqual(driver.visited, [])
def test_tabs_are_still_tidied_after_a_failure(self):
driver = RecordingDriver()
tasks = StubTasks(driver, {"Visual search": TimeoutException("no file input")})
with self.assertLogs(rewards_tasks.logger, level=logging.WARNING):
tasks.complete_all_tasks()
self.assertEqual(tasks.tab_utils.closes, len(ALL_TASKS))
def test_the_rewards_tab_is_the_one_kept(self):
# The actual bug: close_all_other_tabs() with no argument keeps whatever
# is focused, and after a task dies on a Bing tab that is the Bing tab,
# so the cleanup closed the Rewards tab and kept the search results.
driver = RecordingDriver(handles=[MAIN_TAB, BING_TAB], current_handle=BING_TAB)
tasks = StubTasks(driver, {"Visual search": TimeoutException("no file input")})
with self.assertLogs(rewards_tasks.logger, level=logging.WARNING):
tasks.complete_all_tasks()
self.assertTrue(all(kept == [MAIN_TAB] for kept in tasks.tab_utils.kept))
class RestoreMainTabTests(unittest.TestCase):
def test_names_the_main_tab_rather_than_the_focused_one(self):
driver = RecordingDriver(handles=[MAIN_TAB, BING_TAB], current_handle=BING_TAB)
tasks = StubTasks(driver)
tasks.restore_main_tab()
self.assertEqual(tasks.tab_utils.kept, [[MAIN_TAB]])
def test_falls_back_when_the_main_tab_is_gone(self):
# A task can close it, and a stale handle would only raise.
driver = RecordingDriver(handles=[BING_TAB], current_handle=BING_TAB)
tasks = StubTasks(driver)
tasks.restore_main_tab()
self.assertEqual(tasks.tab_utils.kept, [[BING_TAB]])
def test_no_windows_left_is_not_an_error(self):
driver = RecordingDriver(handles=[], current_handle=MAIN_TAB)
tasks = StubTasks(driver)
tasks.restore_main_tab()
self.assertEqual(tasks.tab_utils.kept, [])
def test_a_failure_to_tidy_is_survivable(self):
driver = RecordingDriver(handles=[MAIN_TAB])
tasks = StubTasks(driver)
def boom(exceptions=None):
raise WebDriverException("browser gone")
tasks.tab_utils.close_all_other_tabs = boom
with self.assertLogs(rewards_tasks.logger, level=logging.WARNING) as logged:
tasks.restore_main_tab()
self.assertTrue(any("Could not tidy" in line for line in logged.output))
class ReturnToRewardsHomeTests(unittest.TestCase):
def test_navigates_and_refocuses(self):
driver = RecordingDriver(url=BING_RESULTS)
tasks = StubTasks(driver)
tasks.return_to_rewards_home()
self.assertEqual(driver.visited, [HOME])
self.assertEqual(tasks.tab_utils.focuses, 1)
def test_already_home_is_left_alone(self):
driver = RecordingDriver(url=HOME + "?foo=1")
tasks = StubTasks(driver)
tasks.return_to_rewards_home()
self.assertEqual(driver.visited, [])
self.assertEqual(tasks.tab_utils.focuses, 0)
def test_a_driver_that_cannot_navigate_is_survivable(self):
# Recovery is best effort. Raising here would replace the real failure
# with the tidy-up's own, and take the rest of the run down with it.
driver = RecordingDriver(url=BING_RESULTS, get_raises=WebDriverException("browser gone"))
tasks = StubTasks(driver)
with self.assertLogs(rewards_tasks.logger, level=logging.WARNING) as logged:
tasks.return_to_rewards_home()
self.assertTrue(any("Could not return" in line for line in logged.output))
def test_a_failing_recovery_does_not_end_the_run(self):
driver = RecordingDriver(get_raises=WebDriverException("browser gone"))
tasks = StubTasks(driver, {"Explore on Bing": TimeoutException("slow panel")})
with self.assertLogs(rewards_tasks.logger, level=logging.WARNING):
tasks.complete_all_tasks()
self.assertEqual(tasks.ran, ALL_TASKS)
if __name__ == "__main__":
unittest.main()