def make_tasks(group_id): return [ TestTask( id=j, label=f"test-task{j}", result="failed", _results=[GroupResult(group=group_id, ok=False)], ) for j in range(1, 4) ]
def _normalized_tasks(tasks): # If we are missing one of these keys, discard the task. required_keys = ( "id", "label", "result", ) # Normalize and validate. normalized_tasks = [] for task in tasks.values(): missing = [k for k in required_keys if k not in task] taskstr = task.get("label", task["id"]) if missing: logger.trace( f"Skipping task '{taskstr}' because it is missing " f"the following attributes: {', '.join(missing)}") continue if task.get("tags"): task["tags"] = {t["name"]: t["value"] for t in task["tags"]} if task.get("classification_note"): if isinstance(task["classification_note"], list): task["classification_note"] = task["classification_note"][ -1] groups = task.pop("_result_group", None) oks = task.pop("_result_ok", None) if groups is not None: if oks: task["_results"] = [ GroupResult(group=group, ok=ok) for group, ok in zip(groups, oks) ] normalized_tasks.append(task) return [Task.create(**task) for task in normalized_tasks]
def test_results_for_incomplete_task(responses): push = FakePush("autoland", "rev") for state in ["running", "pending", "unscheduled", "exception"]: task = Task.create( id=1, label="test-task", state="running", ) task.retrieve_results(push) assert task.results == [] responses.add( responses.GET, "https://firefox-ci-tc.services.mozilla.com/api/queue/v1/task/1/artifacts", json={ "artifacts": [{"name": "errorsummary.log"}], }, status=200, ) responses.add( responses.GET, "https://firefox-ci-tc.services.mozilla.com/api/queue/v1/task/1/artifacts/errorsummary.log", body=r""" {"action": "test_groups", "line": 3, "groups": ["layout/base/tests/browser.ini"]} {"status": "OK", "duration": 12430, "line": 4465, "group": "layout/base/tests/browser.ini", "action": "group_result"} """.strip(), status=200, ) task = Task.create( id=1, label="test-task", state="completed", ) task.retrieve_results(push) assert task.results == [ GroupResult(group="layout/base/tests/browser.ini", ok=True, duration=12430), ]
def tasks(self): """All tasks that ran on the push, including retriggers and backfills. Returns: list: A list of `Task` objects. """ args = Namespace(rev=self.rev, branch=self.branch) tasks = defaultdict(dict) retries = defaultdict(int) list_keys = ( "_result_ok", "_result_group", ) def add(result): if "header" in result: result["data"] = [ { field: value for field, value in zip(result["header"], entry) if value is not None } for entry in result["data"] ] for task in result["data"]: if "id" not in task: logger.trace(f"Skipping {task} because of missing id.") continue task_id = task["id"] # If a task is re-run, use the data from the last run. if "retry_id" in task: if task["retry_id"] < retries[task_id]: logger.trace( f"Skipping {task} because there is a newer run of it." ) continue retries[task_id] = task["retry_id"] # We don't need to store the retry ID. del task["retry_id"] cur_task = tasks[task_id] for key, val in task.items(): if key in list_keys: if key not in cur_task: cur_task[key] = [] cur_task[key].append(val) else: cur_task[key] = val # Gather information from the treeherder table. try: add(run_query("push_tasks_from_treeherder", args)) except MissingDataError: pass # Gather information from the unittest table. We allow missing data for this table because # ActiveData only holds very recent data in it, but we have fallbacks on Taskcluster # artifacts. # TODO: We have fallbacks for groups and results, but not for kind. try: add(run_query("push_tasks_results_from_unittest", args)) except MissingDataError: pass try: add(run_query("push_tasks_groups_from_unittest", args)) except MissingDataError: pass # If we are missing one of these keys, discard the task. required_keys = ( "id", "label", ) # Normalize and validate. normalized_tasks = [] for task in tasks.values(): missing = [k for k in required_keys if k not in task] taskstr = task.get("label", task["id"]) if missing: logger.trace( f"Skipping task '{taskstr}' because it is missing " f"the following attributes: {', '.join(missing)}" ) continue if task.get("tags"): task["tags"] = {t["name"]: t["value"] for t in task["tags"]} if task.get("classification_note"): if isinstance(task["classification_note"], list): task["classification_note"] = task["classification_note"][-1] if task.get("_groups"): if isinstance(task["_groups"], str): task["_groups"] = [task["_groups"]] if task.get("_result_ok"): oks = task.pop("_result_ok") if task.get("_result_group"): groups = task.pop("_result_group") task["_results"] = [ GroupResult(group=group, ok=ok) for group, ok in zip(groups, oks) ] normalized_tasks.append(task) return [Task.create(**task) for task in normalized_tasks]
def test_GroupSummary_classifications(): task1 = Task.create( id=1, label="test-task1", result="failed", classification="fixed by commit", classification_note="xxx", ) task1._results = [GroupResult("group1", False, duration=42)] assert GroupSummary("group1", [task1]).classifications == [ ("fixed by commit", "xxx") ] with pytest.raises(AssertionError): GroupSummary("group2", [task1]) task1 = Task.create( id=1, label="test-task1", result="failed", classification="fixed by commit", classification_note="xxx", ) task1._results = [ GroupResult("group1", False, duration=42), GroupResult("group2", False, duration=42), ] assert GroupSummary("group1", [task1]).classifications == [ ("fixed by commit", "xxx") ] assert GroupSummary("group2", [task1]).classifications == [ ("fixed by commit", "xxx") ] task1 = Task.create( id=1, label="test-task1", result="failed", classification="intermittent" ) task1._results = [ GroupResult("group1", False, duration=42), GroupResult("group2", False, duration=42), ] assert GroupSummary("group1", [task1]).classifications == [("intermittent", None)] assert GroupSummary("group2", [task1]).classifications == [("intermittent", None)] task1 = Task.create( id=1, label="test-task1", result="failed", classification="fixed by commit", classification_note="xxx", ) task1._results = [ GroupResult("group1", True, duration=42), GroupResult("group2", False, duration=42), ] assert GroupSummary("group1", [task1]).classifications == [] assert GroupSummary("group2", [task1]).classifications == [ ("fixed by commit", "xxx") ] task1 = Task.create( id=1, label="test-task1", result="failed", classification="fixed by commit", classification_note="xxx", ) task1._results = [ GroupResult("group1", True, duration=42), GroupResult("group2", False, duration=42), ] task2 = Task.create( id=1, label="test-task1", result="failed", classification="intermittent" ) task2._results = [ GroupResult("group1", False, duration=42), GroupResult("group2", False, duration=42), ] assert GroupSummary("group1", [task1, task2]).classifications == [ ("intermittent", None) ] assert GroupSummary("group2", [task1, task2]).classifications == [ ("fixed by commit", "xxx"), ("intermittent", None), ]
# -*- coding: utf-8 -*- import json import re import pytest from mozci.errors import ArtifactNotFound, TaskNotFound from mozci.task import GroupResult, GroupSummary, Task from mozci.util.taskcluster import get_artifact_url, get_index_url GR_2 = GroupResult(group="group2", ok=True, duration=42) GR_3 = GroupResult(group="group2", ok=True, duration=42) class FakePush: def __init__(self, branch, rev): self.branch = branch self.rev = rev @pytest.fixture def create_task(): id = 0 def inner(**kwargs): nonlocal id task = Task.create(id=id, **kwargs) id += 1 return task
], } NUMBER_OF_DEFAULT_GROUPS = 5 NUMBER_OF_INTERMITTENT_GROUPS_IN_DEFAULT = 2 GROUP_SUMMARIES_DEFAULT = { group.name: group for group in [ GroupSummary( f"group{i}", [ Task.create( id=j, label=f"test-task{j}", result="failed", _results=[GroupResult(group=f"group{i}", ok=False)], ) for j in range(1, 4) ] + ([ Task.create( id=4, label="test-task1", result="passed", _results=[GroupResult(group=f"group{i}", ok=True)], ) ] if i <= NUMBER_OF_INTERMITTENT_GROUPS_IN_DEFAULT else []), ) for i in range(1, NUMBER_OF_DEFAULT_GROUPS + 1) ] } def test_group_summaries_default_status():
def tasks(self): """All tasks that ran on the push, including retriggers and backfills. Returns: list: A list of `Task` objects. """ # Gather information from the treeherder table. tasks = [] try: tasks = data.handler.get("push_tasks", branch=self.branch, rev=self.rev) except MissingDataError: pass # Gather task tags from the task table. try: tags_by_task = data.handler.get("push_tasks_tags", branch=self.branch, rev=self.rev) for task in tasks: tags = tags_by_task.get(task["id"]) if tags: if "tags" not in task: task["tags"] = {} task["tags"].update(tags) except MissingDataError: pass # Let's gather error/results from cache or AD/Taskcluster test_tasks_results = config.cache.get(self.push_uuid, {}) was_cached = len(test_tasks_results.keys()) != 0 groups = None if not was_cached: # Gather information from the unittest table. We allow missing data for this table because # ActiveData only holds very recent data in it, but we have fallbacks on Taskcluster # artifacts. try: groups = data.handler.get("push_test_groups", branch=self.branch, rev=self.rev) for task in tasks: results = groups.get(task["id"]) if results is not None: task["_results"] = [ GroupResult(group=group, ok=ok) for group, ok in results.items() ] except MissingDataError: pass tasks = [Task.create(**task) for task in tasks] # Add any data available in the cache if was_cached: # Let's add cached error summaries to TestTasks for t in tasks: if isinstance(t, TestTask): error_summary = test_tasks_results.get(t.id) # Only Test tasks have stored error summary information in the cache if error_summary: t._errors = error_summary["errors"] t._results = error_summary["results"] logger.debug( "Fetched tasks errors/results for {} from the cache".format( self.push_uuid)) # Gather group data which could have been missing in ActiveData. concurrent.futures.wait( [ Push.THREAD_POOL_EXECUTOR.submit(lambda task: task.groups, task) for task in tasks if isinstance(task, TestTask) ], return_when=concurrent.futures.FIRST_EXCEPTION, ) # Now we can cache the results. self._cache_test_tasks(tasks) return tasks
# -*- coding: utf-8 -*- import json import pytest from mozci.errors import ArtifactNotFound, TaskNotFound from mozci.task import GroupResult, GroupSummary, Task from mozci.util.taskcluster import get_artifact_url, get_index_url GR_2 = GroupResult(group="group2", ok=True) GR_3 = GroupResult(group="group2", ok=True) class FakePush: def __init__(self, branch, rev): self.branch = branch self.rev = rev @pytest.fixture def create_task(): id = 0 def inner(**kwargs): nonlocal id task = Task.create(id=id, **kwargs) id += 1 return task return inner
} NUMBER_OF_DEFAULT_GROUPS = 5 NUMBER_OF_INTERMITTENT_GROUPS_IN_DEFAULT = 2 GROUP_SUMMARIES_DEFAULT = { group.name: group for group in [ GroupSummary( f"group{i}", [ Task.create( id=j, label=f"test-task{j}", result="failed", _results=[ GroupResult(group=f"group{i}", ok=False, duration=42) ], ) for j in range(1, 4) ] + ([ Task.create( id=4, label="test-task1", result="passed", _results=[ GroupResult(group=f"group{i}", ok=True, duration=42) ], ) ] if i <= NUMBER_OF_INTERMITTENT_GROUPS_IN_DEFAULT else []), ) for i in range(1, NUMBER_OF_DEFAULT_GROUPS + 1) ] }