From 42a10dad420270e44e89a8f8f9f8f00283f02f76 Mon Sep 17 00:00:00 2001 From: Andrew Halberstadt Date: Wed, 17 Aug 2022 17:04:01 -0400 Subject: [PATCH 1/2] test: convert test_decision.py to the pytest format --- test/test_decision.py | 133 ++++++++++++++++++++---------------------- 1 file changed, 64 insertions(+), 69 deletions(-) diff --git a/test/test_decision.py b/test/test_decision.py index d4fb4bdb6..a52b626d8 100644 --- a/test/test_decision.py +++ b/test/test_decision.py @@ -7,7 +7,6 @@ import os import shutil import tempfile -import unittest from pathlib import Path import pytest @@ -18,70 +17,66 @@ FAKE_GRAPH_CONFIG = {"product-dir": "browser", "taskgraph": {}} -class TestDecision(unittest.TestCase): - def test_write_artifact_json(self): - data = [{"some": "data"}] - tmpdir = tempfile.mkdtemp() - try: - decision.ARTIFACTS_DIR = Path(tmpdir) / "artifacts" - decision.write_artifact("artifact.json", data) - with open(os.path.join(decision.ARTIFACTS_DIR, "artifact.json")) as f: - self.assertEqual(json.load(f), data) - finally: - if os.path.exists(tmpdir): - shutil.rmtree(tmpdir) - decision.ARTIFACTS_DIR = Path("artifacts") - - def test_write_artifact_yml(self): - data = [{"some": "data"}] - tmpdir = tempfile.mkdtemp() - try: - decision.ARTIFACTS_DIR = Path(tmpdir) / "artifacts" - decision.write_artifact("artifact.yml", data) - self.assertEqual(load_yaml(decision.ARTIFACTS_DIR, "artifact.yml"), data) - finally: - if os.path.exists(tmpdir): - shutil.rmtree(tmpdir) - decision.ARTIFACTS_DIR = Path("artifacts") - - -class TestGetDecisionParameters(unittest.TestCase): - def setUp(self): - self.options = { - "base_repository": "https://hg.mozilla.org/mozilla-unified", - "head_repository": "https://hg.mozilla.org/mozilla-central", - "head_rev": "abcd", - "head_ref": "default", - "head_tag": "v0.0.1", - "project": "mozilla-central", - "pushlog_id": "143", - "pushdate": 1503691511, - "repository_type": "hg", - "owner": "nobody@mozilla.com", - "tasks_for": "hg-push", - "level": "3", - } - self.old_determine_more_accurate_base_rev = ( - decision._determine_more_accurate_base_rev - ) - decision._determine_more_accurate_base_rev = lambda *_, **__: "abcd" - - def tearDown(self): - decision._determine_more_accurate_base_rev = ( - self.old_determine_more_accurate_base_rev - ) - - def test_simple_options(self): - params = decision.get_decision_parameters(FAKE_GRAPH_CONFIG, self.options) - self.assertEqual(params["build_date"], 1503691511) - self.assertEqual(params["head_tag"], "v0.0.1") - self.assertEqual(params["pushlog_id"], "143") - self.assertEqual(params["moz_build_date"], "20170825200511") - - def test_no_email_owner(self): - self.options["owner"] = "ffxbld" - params = decision.get_decision_parameters(FAKE_GRAPH_CONFIG, self.options) - self.assertEqual(params["owner"], "ffxbld@noreply.mozilla.org") +def test_write_artifact_json(): + data = [{"some": "data"}] + tmpdir = tempfile.mkdtemp() + try: + decision.ARTIFACTS_DIR = Path(tmpdir) / "artifacts" + decision.write_artifact("artifact.json", data) + with open(os.path.join(decision.ARTIFACTS_DIR, "artifact.json")) as f: + assert json.load(f) == data + finally: + if os.path.exists(tmpdir): + shutil.rmtree(tmpdir) + decision.ARTIFACTS_DIR = Path("artifacts") + + +def test_write_artifact_yml(): + data = [{"some": "data"}] + tmpdir = tempfile.mkdtemp() + try: + decision.ARTIFACTS_DIR = Path(tmpdir) / "artifacts" + decision.write_artifact("artifact.yml", data) + assert load_yaml(decision.ARTIFACTS_DIR, "artifact.yml") == data + finally: + if os.path.exists(tmpdir): + shutil.rmtree(tmpdir) + decision.ARTIFACTS_DIR = Path("artifacts") + + +@pytest.fixture +def options(monkeypatch): + monkeypatch.setattr( + decision, "_determine_more_accurate_base_rev", lambda *_, **__: "abcd" + ) + return { + "base_repository": "https://hg.mozilla.org/mozilla-unified", + "head_repository": "https://hg.mozilla.org/mozilla-central", + "head_rev": "abcd", + "head_ref": "default", + "head_tag": "v0.0.1", + "project": "mozilla-central", + "pushlog_id": "143", + "pushdate": 1503691511, + "repository_type": "hg", + "owner": "nobody@mozilla.com", + "tasks_for": "hg-push", + "level": "3", + } + + +def test_simple_options(options): + params = decision.get_decision_parameters(FAKE_GRAPH_CONFIG, options) + assert params["build_date"] == 1503691511 + assert params["head_tag"] == "v0.0.1" + assert params["pushlog_id"] == "143" + assert params["moz_build_date"] == "20170825200511" + + +def test_no_email_owner(options): + options["owner"] = "ffxbld" + params = decision.get_decision_parameters(FAKE_GRAPH_CONFIG, options) + assert params["owner"] == "ffxbld@noreply.mozilla.org" @pytest.mark.parametrize( @@ -93,9 +88,9 @@ def test_no_email_owner(self): ), ) def test_determine_more_accurate_base_ref( - candidate_base_ref, base_rev, expected_base_ref + candidate_base_ref, base_rev, expected_base_ref, mocker ): - repo_mock = unittest.mock.MagicMock() + repo_mock = mocker.MagicMock() repo_mock.default_branch = "default-branch" assert ( @@ -121,9 +116,9 @@ def test_determine_more_accurate_base_ref( ), ) def test_determine_more_accurate_base_rev( - common_rev, candidate_base_rev, expected_base_ref_or_rev, expected_base_rev + common_rev, candidate_base_rev, expected_base_ref_or_rev, expected_base_rev, mocker ): - repo_mock = unittest.mock.MagicMock() + repo_mock = mocker.MagicMock() repo_mock.find_latest_common_revision.return_value = common_rev repo_mock.does_revision_exist_locally = lambda rev: rev == "existing-rev" From 3aaf50956819b404fbc54e6470f04a0c1baa40d5 Mon Sep 17 00:00:00 2001 From: Andrew Halberstadt Date: Wed, 17 Aug 2022 16:50:28 -0400 Subject: [PATCH 2/2] feat: verify decision task has sufficient scopes before submitting Issue: #10 --- src/taskgraph/decision.py | 4 +++ src/taskgraph/util/taskcluster.py | 1 + src/taskgraph/util/verify.py | 45 +++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/src/taskgraph/decision.py b/src/taskgraph/decision.py index 38071f1df..17af55a5d 100644 --- a/src/taskgraph/decision.py +++ b/src/taskgraph/decision.py @@ -22,6 +22,7 @@ from taskgraph.util.python_path import find_object from taskgraph.util.schema import Schema, validate_schema from taskgraph.util.vcs import Repository, get_repository +from taskgraph.util.verify import verifications from taskgraph.util.yaml import load_yaml logger = logging.getLogger(__name__) @@ -123,6 +124,9 @@ def taskgraph_decision(options, parameters=None): shutil.copy2(RUN_TASK_DIR / "run-task", ARTIFACTS_DIR) shutil.copy2(RUN_TASK_DIR / "fetch-content", ARTIFACTS_DIR) + # run 'decision' verifications + verifications("decision", tgg.morphed_task_graph, tgg.graph_config, tgg.parameters) + # actually create the graph create_tasks( tgg.graph_config, diff --git a/src/taskgraph/util/taskcluster.py b/src/taskgraph/util/taskcluster.py index a830a473b..a8b2fee66 100644 --- a/src/taskgraph/util/taskcluster.py +++ b/src/taskgraph/util/taskcluster.py @@ -309,6 +309,7 @@ def rerun_task(task_id): _do_request(get_task_url(task_id, use_proxy=True) + "/rerun", json={}) +@memoize def get_current_scopes(): """Get the current scopes. This only makes sense in a task with the Taskcluster proxy enabled, where it returns the actual scopes accorded to the task.""" diff --git a/src/taskgraph/util/verify.py b/src/taskgraph/util/verify.py index 5911914f1..aebdc19a7 100644 --- a/src/taskgraph/util/verify.py +++ b/src/taskgraph/util/verify.py @@ -6,6 +6,7 @@ import logging import sys from abc import ABC, abstractmethod +from textwrap import dedent import attr @@ -13,6 +14,7 @@ from taskgraph.parameters import Parameters from taskgraph.taskgraph import TaskGraph from taskgraph.util.attributes import match_run_on_projects +from taskgraph.util.taskcluster import get_current_scopes from taskgraph.util.treeherder import join_symbol logger = logging.getLogger(__name__) @@ -281,3 +283,46 @@ def verify_always_optimized(task, taskgraph, scratch_pad, graph_config, paramete return if task.task.get("workerType") == "always-optimized": raise Exception(f"Could not optimize the task {task.label!r}") + + +@verifications.add("decision") +def verify_scopes_satisfaction(task, taskgraph, scratch_pad, graph_config, parameters): + if task is None: + if not scratch_pad: + return + + s = "s" if len(scratch_pad) else "" + are = "are" if len(scratch_pad) else "is" + + failstr = "" + for label, scopes in scratch_pad.items(): + failstr += "\n" + f" {label}:" + failstr += ( + " \n" + "\n ".join([f" {s}" for s in sorted(scopes)]) + "\n" + ) + + msg = dedent( + f""" + Required scopes are missing! + + The Decision task does not have all of the scopes necessary to + perform this request. The following task{s} {are} requesting scopes + the Decision task does not have: + """ + ) + msg += failstr + raise Exception(msg) + + current_scopes = get_current_scopes() + missing = set() + for required in task.task["scopes"]: + for current in current_scopes: + if current == required: + break + if current[-1] == "*" and required.startswith(current[:-1]): + break + else: + missing.add(required) + + if missing: + scratch_pad[task.label] = sorted(missing)