mirror of
https://github.com/User0332/rewards-farmer.git
synced 2026-09-16 01:31:36 +00:00
tell absence and a slow render apart when a task fails
Every failed task printed the same reason, because the blanket catch in complete_all_tasks treated NoSuchElementException and TimeoutException as one finding. A wait that expires says nothing about whether the section was there, since wait_for_element swallowed whatever the getter raised and returned False. A market that does not ship visual search and a points panel that was on screen and slow both arrived as TimeoutException wait_for_element now keeps the last reason the getter gave and raises ElementNeverAppeared when nothing was in the DOM for the whole wait. _container_by_id raises ElementNotReady when the id is present but no visible copy has content yet, which is the hydrating case it used to report as missing. Both subclass what they refine, so claim_bonus_points and complete_bing_daily_set keep working untouched A task that was reached and then ran out of time is now a [FAIL] rather than a [SKIP], because it may have left points behind. Absence stays a [SKIP] 56 tests pass, 20 of them new. A mutation check backs them, forcing every expired wait to count as absence fails 2, and collapsing the two branches back into one [SKIP] fails 4 Refs #52
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
"""Tests for what a run says about a task that did not complete.
|
||||
|
||||
The reported reason used to be a guess. Every wait that expired and every
|
||||
lookup that missed produced "not available in this UI variant", so a section
|
||||
that was on the page and slow read exactly like one this market does not ship,
|
||||
which is #52. These pin down which failures are absence and which are not.
|
||||
|
||||
None of them need a browser.
|
||||
|
||||
python -m unittest discover -s tests
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
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
|
||||
from element_selectors import ElementNotReady
|
||||
from fakes import FakeDriver
|
||||
from rewards_tasks import ElementNeverAppeared, task_failure_report
|
||||
|
||||
# Long enough for one poll, short enough that the suite stays quick.
|
||||
# WebDriverWait sleeps 0.5s between attempts, so a wait that expires costs
|
||||
# about that regardless of the timeout asked for.
|
||||
BRIEF = 0.05
|
||||
|
||||
|
||||
def make_tasks():
|
||||
"""A RewardsTaskUtils without the browser its __init__ opens."""
|
||||
tasks = rewards_tasks.RewardsTaskUtils.__new__(rewards_tasks.RewardsTaskUtils)
|
||||
|
||||
tasks.driver = FakeDriver()
|
||||
tasks.tab_utils = types.SimpleNamespace(close_all_other_tabs=lambda: None)
|
||||
|
||||
return tasks
|
||||
|
||||
|
||||
class WaitClassification(unittest.TestCase):
|
||||
"""wait_for_element has to say why it gave up, not just that it did."""
|
||||
|
||||
def test_a_getter_that_never_finds_anything_is_absence(self):
|
||||
def missing():
|
||||
raise NoSuchElementException("no button containing 'visual search streak'")
|
||||
|
||||
with self.assertRaises(ElementNeverAppeared):
|
||||
make_tasks().wait_for_element(missing, timeout=BRIEF)
|
||||
|
||||
def test_a_section_that_is_still_rendering_is_not_absence(self):
|
||||
# The id is in the page, no visible copy has content yet. Waiting
|
||||
# longer is the answer here, skipping the task is not.
|
||||
def not_ready():
|
||||
raise ElementNotReady("'moreactivities' is present but no visible copy has content yet")
|
||||
|
||||
with self.assertRaises(TimeoutException) as caught:
|
||||
make_tasks().wait_for_element(not_ready, timeout=BRIEF)
|
||||
|
||||
self.assertNotIsInstance(caught.exception, ElementNeverAppeared)
|
||||
|
||||
def test_a_getter_that_rejects_what_it_finds_is_not_absence(self):
|
||||
# complete_bing_daily_set holds out for all three activities and
|
||||
# returns False until they are there. The panel itself is open.
|
||||
with self.assertRaises(TimeoutException) as caught:
|
||||
make_tasks().wait_for_element(lambda: [], timeout=BRIEF)
|
||||
|
||||
self.assertNotIsInstance(caught.exception, ElementNeverAppeared)
|
||||
|
||||
def test_an_element_that_arrives_late_is_still_returned(self):
|
||||
attempts = []
|
||||
|
||||
def slow():
|
||||
attempts.append(None)
|
||||
|
||||
if len(attempts) < 2:
|
||||
raise NoSuchElementException("not yet")
|
||||
|
||||
return "the element"
|
||||
|
||||
self.assertEqual(make_tasks().wait_for_element(slow, timeout=5), "the element")
|
||||
# A single lucky first attempt would prove nothing about the retry.
|
||||
self.assertGreater(len(attempts), 1)
|
||||
|
||||
def test_the_getters_own_reason_survives(self):
|
||||
def missing():
|
||||
raise NoSuchElementException("no button containing 'points breakdown'")
|
||||
|
||||
with self.assertRaises(ElementNeverAppeared) as caught:
|
||||
make_tasks().wait_for_element(missing, timeout=BRIEF)
|
||||
|
||||
self.assertIn("points breakdown", str(caught.exception))
|
||||
|
||||
|
||||
class ExistingTimeoutHandlers(unittest.TestCase):
|
||||
"""The new type has to stay catchable where TimeoutException was."""
|
||||
|
||||
def test_it_is_still_a_timeout(self):
|
||||
self.assertTrue(issubclass(ElementNeverAppeared, TimeoutException))
|
||||
|
||||
def test_having_no_bonus_points_is_still_only_a_warning(self):
|
||||
# There is no Claim button when there is nothing to claim, so this
|
||||
# path reaches the wait expecting to be disappointed. If the new type
|
||||
# escaped its `except TimeoutException`, an ordinary run would start
|
||||
# reporting a failed task every day.
|
||||
def bonus_button():
|
||||
pass
|
||||
|
||||
def claim_button():
|
||||
pass
|
||||
|
||||
tasks = make_tasks()
|
||||
tasks.switch_to_dashboard = lambda: None
|
||||
tasks.elements = types.SimpleNamespace(
|
||||
get_bonus_button_on_dashboard=bonus_button,
|
||||
get_claim_bonus_points_button=claim_button,
|
||||
)
|
||||
|
||||
def wait_for_then_click(getter, timeout=10):
|
||||
if getter is claim_button:
|
||||
raise ElementNeverAppeared("nothing matched during the 10s wait")
|
||||
|
||||
tasks.wait_for_then_click = wait_for_then_click
|
||||
|
||||
with self.assertLogs(rewards_tasks.logger, level=logging.WARNING) as captured:
|
||||
tasks.claim_bonus_points()
|
||||
|
||||
self.assertIn("no bonus points to claim", "\n".join(captured.output).lower())
|
||||
|
||||
|
||||
class FailureReport(unittest.TestCase):
|
||||
def test_a_section_this_variant_does_not_ship_is_skipped(self):
|
||||
tag, reason = task_failure_report(
|
||||
NoSuchElementException("no element with id 'moreactivities'")
|
||||
)
|
||||
|
||||
self.assertEqual(tag, "SKIP")
|
||||
self.assertIn("not available in this UI variant", reason)
|
||||
|
||||
def test_a_wait_that_never_saw_the_element_is_skipped(self):
|
||||
tag, reason = task_failure_report(ElementNeverAppeared("nothing matched"))
|
||||
|
||||
self.assertEqual(tag, "SKIP")
|
||||
self.assertIn("not available in this UI variant", reason)
|
||||
|
||||
def test_a_section_that_never_finished_rendering_is_not_skipped(self):
|
||||
tag, reason = task_failure_report(
|
||||
ElementNotReady("'moreactivities' is present but no visible copy has content yet")
|
||||
)
|
||||
|
||||
self.assertEqual(tag, "FAIL")
|
||||
self.assertNotIn("not available", reason)
|
||||
|
||||
def test_an_expired_wait_is_not_skipped(self):
|
||||
# The line in #52, reported for a panel that was on screen the whole
|
||||
# time: "[SKIP] Required searches: not available in this UI variant
|
||||
# (TimeoutException)".
|
||||
tag, reason = task_failure_report(TimeoutException("Message: "))
|
||||
|
||||
self.assertEqual(tag, "FAIL")
|
||||
self.assertNotIn("not available", reason)
|
||||
|
||||
def test_the_exception_name_is_kept(self):
|
||||
# It is the difference between a lookup that missed and a wait that
|
||||
# expired, and someone pasting a log should not lose it.
|
||||
self.assertIn("ElementNeverAppeared", task_failure_report(ElementNeverAppeared("x"))[1])
|
||||
self.assertIn("TimeoutException", task_failure_report(TimeoutException("x"))[1])
|
||||
|
||||
def test_anything_else_keeps_its_own_message(self):
|
||||
tag, reason = task_failure_report(WebDriverException("chrome not reachable"))
|
||||
|
||||
self.assertEqual(tag, "FAIL")
|
||||
self.assertIn("WebDriverException", reason)
|
||||
self.assertIn("chrome not reachable", reason)
|
||||
|
||||
|
||||
class TaskLoop(unittest.TestCase):
|
||||
"""complete_all_tasks, with the six tasks replaced by recorded calls."""
|
||||
|
||||
STEPS = (
|
||||
("Bing daily set", "complete_bing_daily_set"),
|
||||
("Explore on Bing", "complete_explore_on_bing_tasks"),
|
||||
("Visual search", "complete_visual_search"),
|
||||
("Misc cards", "complete_misc_cards"),
|
||||
("Required searches", "complete_required_searches"),
|
||||
("Bonus points", "claim_bonus_points"),
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
self.ran = []
|
||||
|
||||
def _tasks(self, failures=None):
|
||||
failures = failures or {}
|
||||
tasks = make_tasks()
|
||||
|
||||
for _, attribute in self.STEPS:
|
||||
def step(name=attribute):
|
||||
self.ran.append(name)
|
||||
|
||||
if name in failures:
|
||||
raise failures[name]
|
||||
|
||||
setattr(tasks, attribute, step)
|
||||
|
||||
return tasks
|
||||
|
||||
def _run(self, failures=None):
|
||||
with self.assertLogs(rewards_tasks.logger, level=logging.INFO) as captured:
|
||||
self._tasks(failures).complete_all_tasks()
|
||||
|
||||
return "\n".join(captured.output)
|
||||
|
||||
def test_a_failing_task_does_not_stop_the_ones_after_it(self):
|
||||
self._run({"complete_visual_search": WebDriverException("chrome not reachable")})
|
||||
|
||||
self.assertEqual(self.ran, [attribute for _, attribute in self.STEPS])
|
||||
|
||||
def test_absence_and_an_expired_wait_read_differently(self):
|
||||
output = self._run({
|
||||
"complete_visual_search": ElementNeverAppeared("nothing matched"),
|
||||
"complete_required_searches": TimeoutException("Message: "),
|
||||
})
|
||||
|
||||
self.assertIn("[SKIP] Visual search: not available in this UI variant", output)
|
||||
self.assertIn("[FAIL] Required searches: on the page but not ready in time", output)
|
||||
self.assertIn("[OK] Bing daily set", output)
|
||||
|
||||
def test_a_section_that_never_rendered_is_not_called_unavailable(self):
|
||||
output = self._run({
|
||||
"complete_misc_cards": ElementNotReady(
|
||||
"'moreactivities' is present but no visible copy has content yet"
|
||||
),
|
||||
})
|
||||
|
||||
self.assertIn("[FAIL] Misc cards: on the page but not ready in time", output)
|
||||
self.assertNotIn("Misc cards: not available", output)
|
||||
|
||||
def test_a_task_this_variant_does_not_ship_is_still_skipped(self):
|
||||
output = self._run({
|
||||
"complete_explore_on_bing_tasks": NoSuchElementException(
|
||||
"no Explore on Bing section in this UI variant"
|
||||
),
|
||||
})
|
||||
|
||||
self.assertIn("[SKIP] Explore on Bing: not available in this UI variant", output)
|
||||
|
||||
def test_an_unexpected_failure_still_reports_what_went_wrong(self):
|
||||
output = self._run({"complete_misc_cards": WebDriverException("chrome not reachable")})
|
||||
|
||||
self.assertIn("[FAIL] Misc cards: WebDriverException", output)
|
||||
self.assertIn("chrome not reachable", output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user