# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. import pytest import yaml from mozunit import main from pytest_taskgraph import make_graph, make_task from taskgraph import create from taskgraph.util import json from taskgraph.util.taskcluster import get_task_definition from gecko_taskgraph import decision from gecko_taskgraph.actions import trigger_action_callback ROOT_URL = "https://taskcluster.example.com" @pytest.fixture(autouse=True) def mock_root_url(monkeypatch): monkeypatch.delenv("TASKCLUSTER_PROXY_URL", raising=False) monkeypatch.setenv("TASKCLUSTER_ROOT_URL", ROOT_URL) @pytest.fixture(autouse=True) def clear_caches(): yield get_task_definition.cache_clear() @pytest.fixture def artifact_dir(monkeypatch, tmp_path): artifact_dir = tmp_path / "artifacts" monkeypatch.setattr(decision, "ARTIFACTS_DIR", str(artifact_dir)) return artifact_dir @pytest.fixture def get_artifact(artifact_dir): def inner(artifact_name): return json.loads((artifact_dir / artifact_name).read_text()) return inner @pytest.fixture def run_action(mocker, monkeypatch, parameters, graph_config): monkeypatch.setattr(create, "testing", True) mocker.patch("gecko_taskgraph.actions.registry.sanity_check_task_scope") def inner(name, params=None, **kwargs): if params: parameters.update(params) kwargs.setdefault("task_group_id", "gid") kwargs.setdefault("task_id", "tid") kwargs.setdefault("input", None) ret = trigger_action_callback( callback=name, parameters=parameters, root=graph_config.root_dir, **kwargs, ) return ret return inner def test_cancel(responses, run_action): task_id = "abc" responses.post(f"{ROOT_URL}/api/queue/v1/task/{task_id}/cancel", status=200) run_action("cancel", task_id=task_id, input={"task_id": task_id}) def test_cancel_all(monkeypatch, responses, run_action): group_id = "abc" # Validate action task doesn't cancel itself. monkeypatch.setenv("TASK_ID", group_id) responses.get( f"{ROOT_URL}/api/queue/v1/task-group/{group_id}/list", status=200, json={ "tasks": [ {"status": {"taskId": group_id, "state": "running"}}, {"status": {"taskId": "a", "state": "running"}}, {"status": {"taskId": "b", "state": "completed"}}, {"status": {"taskId": "c", "state": "pending"}}, {"status": {"taskId": "d", "state": "unscheduled"}}, ] }, ) responses.post(f"{ROOT_URL}/api/queue/v1/task/a/cancel", status=200) responses.post(f"{ROOT_URL}/api/queue/v1/task/c/cancel", status=200) responses.post(f"{ROOT_URL}/api/queue/v1/task/d/cancel", status=200) run_action( "cancel-all", task_group_id=group_id, input={"task_group_id": group_id}, ) def test_rebuild_cached_tasks(mocker, run_action, get_artifact): graph = make_graph( make_task( label="foo", attributes={"cached_task": True}, task_def={"name": "foo"} ), make_task(label="bar", task_def={"name": "bar"}), ) m = mocker.patch( "gecko_taskgraph.actions.rebuild_cached_tasks.fetch_graph_and_labels" ) m.return_value = ( "gid", graph, {label: "tid" for label in graph.tasks.keys()}, None, ) run_action("rebuild-cached-tasks") to_run = get_artifact("to-run.json") assert "foo" in to_run assert "bar" not in to_run def test_add_new_jobs(mocker, run_action, get_artifact): graph = make_graph( make_task(label="foo", task_def={"name": "foo"}), make_task(label="bar", task_def={"name": "bar"}), ) m = mocker.patch("gecko_taskgraph.actions.add_new_jobs.fetch_graph_and_labels") m.return_value = ("gid", graph, {}, None) run_action("add-new-jobs", input={"tasks": ["foo"], "times": 1}) to_run = get_artifact("to-run.json") assert "foo" in to_run assert "bar" not in to_run def test_add_talos(mocker, run_action, get_artifact): graph = make_graph( make_task( label="test-linux-talos", attributes={"talos_try_name": "talos"}, task_def={"name": "test-linux-talos"}, ), make_task(label="build", task_def={"name": "build"}), ) m = mocker.patch("gecko_taskgraph.actions.add_talos.fetch_graph_and_labels") m.return_value = ("gid", graph, {}, None) mocker.patch("gecko_taskgraph.actions.add_talos.standard_filter", return_value=True) run_action("run-all-talos", input={"times": 1}) to_run = get_artifact("to-run.json") assert "test-linux-talos" in to_run assert "build" not in to_run def test_purge_caches(responses, run_action): task_id = "abc" task_def = { "payload": {"cache": {"cache1": "path1"}}, "provisionerId": "proj-gecko", "workerType": "linux", } responses.get( f"{ROOT_URL}/api/queue/v1/task/{task_id}", status=200, json=task_def, ) responses.post( f"{ROOT_URL}/api/purge-cache/v1/purge-cache/proj-gecko%2Flinux", status=200, json={}, ) run_action("purge-cache", task_id=task_id) def test_openh264(mocker, run_action, get_artifact): graph = make_graph( make_task( label="openh264-build", kind="openh264", task_def={"name": "openh264-build"} ), make_task(label="build", kind="build", task_def={"name": "build"}), ) m = mocker.patch("gecko_taskgraph.actions.openh264.fetch_graph_and_labels") m.return_value = ("gid", graph, {}, None) run_action("openh264") to_run = get_artifact("to-run.json") assert "openh264-build" in to_run assert "build" not in to_run def test_googleplay(mocker, run_action, get_artifact): graph = make_graph( make_task( label="push-fenix", kind="push-bundle", attributes={"build-type": "fenix-nightly"}, task_def={"name": "push-fenix"}, ), make_task(label="build", kind="build", task_def={"name": "build"}), ) m = mocker.patch("gecko_taskgraph.actions.googleplay.fetch_graph_and_labels") m.return_value = ("gid", graph, {}, None) run_action("googleplay", params={"project": "mozilla-central"}) to_run = get_artifact("to-run.json") assert "push-fenix" in to_run assert "build" not in to_run def test_raptor_extra_options(mocker, responses, run_action, get_artifact): task_id = "tid" task_def = { "metadata": {"name": "test-raptor"}, "payload": {"env": {}}, "extra": {"treeherder": {"symbol": "rap", "groupName": "Raptor"}}, } graph = make_graph(make_task(label="test-raptor", task_def=task_def)) responses.get( f"{ROOT_URL}/api/queue/v1/task/{task_id}", status=200, json=task_def, ) m = mocker.patch( "gecko_taskgraph.actions.raptor_extra_options.fetch_graph_and_labels" ) m.return_value = ("gid", graph, {"test-raptor": "tid"}, None) run_action( "raptor-extra-options", task_id=task_id, input={"extra_options": "verbose"} ) to_run = get_artifact("to-run.json") assert "test-raptor" in to_run def test_run_missing_tests(mocker, responses, run_action, get_artifact): graph = make_graph( make_task(label="test-foo", kind="test", task_def={"name": "test-foo"}), make_task(label="test-bar", kind="test", task_def={"name": "test-bar"}), make_task(label="build", kind="build", task_def={"name": "build"}), ) m = mocker.patch("gecko_taskgraph.actions.run_missing_tests.fetch_graph_and_labels") m.return_value = ("gid", graph, {"test-foo": "tid1"}, None) responses.get( f"{ROOT_URL}/api/queue/v1/task/gid/artifacts/public%2Ftarget-tasks.json", status=200, json={"test-foo": {}, "test-bar": {}}, ) run_action("run-missing-tests") to_run = get_artifact("to-run.json") assert "test-bar" in to_run assert "test-foo" not in to_run assert "build" not in to_run def test_scriptworker_canary(mocker, run_action, graph_config): m = mocker.patch("gecko_taskgraph.actions.scriptworker_canary.taskgraph_decision") run_action("scriptworker-canary", input={"scriptworkers": ["balrog", "shipit"]}) m.assert_called_once() args, kwargs = m.call_args assert args[0] == {"root": graph_config.root_dir} assert kwargs["parameters"]["target_tasks_method"] == "scriptworker_canary" assert kwargs["parameters"]["try_task_config"] == { "scriptworker-canary-workers": ["balrog", "shipit"] } assert kwargs["parameters"]["tasks_for"] == "action" def test_merge_automation(mocker, run_action, graph_config): m = mocker.patch("gecko_taskgraph.actions.merge_automation.taskgraph_decision") run_action( "merge-automation", params={"project": "mozilla-central"}, input={"behavior": "bump-main"}, ) m.assert_called_once() args, kwargs = m.call_args assert args[0] == {"root": graph_config.root_dir} assert kwargs["parameters"]["target_tasks_method"] == "merge_automation" assert kwargs["parameters"]["merge_config"] == { "force-dry-run": False, "behavior": "bump-main", } assert kwargs["parameters"]["tasks_for"] == "action" def test_retrigger(mocker, responses, run_action, get_artifact): task_def = { "metadata": {"name": "test-task"}, "payload": {}, } graph = make_graph( make_task(label="test-task", attributes={"retrigger": True}, task_def=task_def) ) responses.get( f"{ROOT_URL}/api/queue/v1/task/tid", status=200, json=task_def, ) m = mocker.patch("gecko_taskgraph.actions.retrigger.fetch_graph_and_labels") m.return_value = ("gid", graph, {}, None) run_action("retrigger", input={"force": True}) to_run = get_artifact("to-run.json") assert "test-task" in to_run def test_retrigger_custom(mocker, responses, run_action, capsys): task_def = { "metadata": {"name": "test-mochitest"}, "payload": {"command": ["run"], "env": {}}, "tags": {"test-type": "mochitest"}, "extra": {"treeherder": {"symbol": "M"}}, } graph = make_graph(make_task(label="test-mochitest", task_def=task_def)) responses.get( f"{ROOT_URL}/api/queue/v1/task/tid", status=200, json=task_def, ) m = mocker.patch("gecko_taskgraph.actions.retrigger_custom.fetch_graph_and_labels") m.return_value = ("gid", graph, {"test-mochitest": "dtid"}, None) run_action("retrigger-custom", input={"path": "test/path"}) captured = capsys.readouterr() assert "test-mochitest" in captured.out assert "--no-run-tests" in captured.out assert "test/path" in captured.out assert "M-custom" in captured.out def test_create_interactive(mocker, responses, monkeypatch, run_action, get_artifact): monkeypatch.setenv("TASK_ID", "action-task-id") task_def = { "metadata": {"name": "test-task"}, "payload": { "env": {}, "maxRunTime": 3600, "cache": {}, "artifacts": {}, }, "scopes": [], "extra": {"treeherder": {"symbol": "T"}}, } graph = make_graph(make_task(label="test-task", task_def=task_def)) responses.get( f"{ROOT_URL}/api/queue/v1/task/tid", status=200, json=task_def, ) m = mocker.patch( "gecko_taskgraph.actions.create_interactive.fetch_graph_and_labels" ) m.return_value = ("gid", graph, {}, None) run_action("create-interactive", input={"notify": "test@example.com"}) to_run = get_artifact("to-run.json") assert "test-task" in to_run def test_backfill_task(mocker, run_action, get_artifact): graph = make_graph( make_task(label="test-task", task_def={"name": "test-task"}), ) m = mocker.patch("gecko_taskgraph.actions.backfill.fetch_graph_and_labels") m.return_value = ("gid", graph, {}, None) mocker.patch("gecko_taskgraph.actions.backfill.combine_task_graph_files") run_action( "backfill-task", input={"label": "test-task", "revision": "abc123", "symbol": "T"}, ) to_run = get_artifact("to-run-0.json") assert "test-task" in to_run def test_confirm_failures(mocker, responses, run_action, get_artifact): task_id = "test-task-id" task_def = { "metadata": {"name": "test-mochitest"}, "extra": {"suite": "mochitest"}, "payload": {"command": ["run-tests"], "env": {}}, } graph = make_graph( make_task( label="test-mochitest-cf", task_def={ "name": "test-mochitest-cf", "payload": {"command": ["run-tests"], "env": {}}, "metadata": {"name": "test-mochitest-cf"}, "tags": {}, }, ), ) responses.get( f"{ROOT_URL}/api/queue/v1/task/{task_id}/artifacts", status=200, json={ "artifacts": [ {"name": "public/logs/live_backing.log"}, {"name": "public/logs/errorsummary.log"}, ] }, ) errorsummary_content = b"\n".join( [ b'{"test": "dom/tests/test_example.html", "status": "FAIL", "expected": "PASS", "group": "dom/tests"}', b'{"test": "dom/tests/test_another.html", "status": "FAIL", "expected": "PASS", "group": "dom/tests"}', ] ) responses.get( f"{ROOT_URL}/api/queue/v1/task/{task_id}/artifacts/public%2Flogs%2Ferrorsummary.log", status=200, body=errorsummary_content, ) responses.get( f"{ROOT_URL}/api/queue/v1/task/{task_id}", status=200, json=task_def, ) m = mocker.patch("gecko_taskgraph.actions.confirm_failure.fetch_graph_and_labels") m.return_value = ("gid", graph, {}, None) run_action("confirm-failures", task_id=task_id) to_run = get_artifact("to-run.json") assert "test-mochitest-cf" in to_run def test_confirm_failures_retrigger(mocker, responses, run_action): task_id = "test-task-id" task_def = { "metadata": {"name": "test-mochitest"}, "extra": {"suite": "mochitest"}, } graph = make_graph( make_task( label="test-mochitest", attributes={"retrigger": True}, task_def={"name": "test-mochitest"}, ), make_task(label="test-mochitest-cf", task_def={"name": "test-mochitest-cf"}), ) responses.get( f"{ROOT_URL}/api/queue/v1/task/{task_id}/artifacts", status=200, json={"artifacts": [{"name": "public/logs/live_backing.log"}]}, ) responses.get( f"{ROOT_URL}/api/queue/v1/task/{task_id}", status=200, json=task_def, ) m = mocker.patch("gecko_taskgraph.actions.confirm_failure.fetch_graph_and_labels") m.return_value = ("gid", graph, {}, None) retrigger_mock = mocker.patch( "gecko_taskgraph.actions.confirm_failure.retrigger_action" ) run_action("confirm-failures", task_id=task_id) retrigger_mock.assert_called_once() def test_rerun(mocker, responses, run_action): task_id = "tid" task_def = {"metadata": {"name": "test-task"}} responses.get( f"{ROOT_URL}/api/queue/v1/task/{task_id}", status=200, json=task_def, ) responses.get( f"{ROOT_URL}/api/queue/v1/task/{task_id}/status", status=200, json={"status": {"state": "failed"}}, ) responses.post(f"{ROOT_URL}/api/queue/v1/task/{task_id}/rerun", status=200) graph = make_graph(make_task(label="test-task", task_def=task_def)) m = mocker.patch("gecko_taskgraph.actions.retrigger.fetch_graph_and_labels") m.return_value = ("gid", graph, {"test-task": [task_id]}, {"test-task": [task_id]}) run_action("rerun", task_id=task_id) def test_retrigger_decision(responses, run_action, capsys): task_def = { "taskGroupId": "tgid", "schedulerId": "scheduler", "provisionerId": "provisioner", "workerType": "worker", "created": "2024-01-01T00:00:00.000Z", "deadline": "2024-01-01T01:00:00.000Z", "expires": "2024-01-02T00:00:00.000Z", "metadata": {"name": "decision-task"}, "payload": {}, "tags": {}, "extra": {}, } responses.get( f"{ROOT_URL}/api/queue/v1/task/tid", status=200, json=task_def, ) run_action("retrigger-decision", params={"level": "1"}) captured = capsys.readouterr() assert "decision-task" in captured.out assert "gecko-level-1" in captured.out assert "retrigger-decision-task" in captured.out def test_retrigger_multiple(mocker, run_action, get_artifact): graph = make_graph( make_task( label="test-task", attributes={"retrigger": True}, task_def={"name": "test-task"}, ), ) m = mocker.patch("gecko_taskgraph.actions.retrigger.fetch_graph_and_labels") m.return_value = ("gid", graph, {}, {"test-task": ["tid"]}) run_action( "retrigger-multiple", input={"requests": [{"tasks": ["test-task"], "times": 2}]}, ) to_run = get_artifact("to-run.json") assert "test-task" in to_run def test_retrigger_multiple_rerun(mocker, responses, run_action): task_id = "rerun-task-id" graph = make_graph( make_task( label="test-task", attributes={"retrigger": False}, task_def={"name": "test-task"}, ), ) m = mocker.patch("gecko_taskgraph.actions.retrigger.fetch_graph_and_labels") m.return_value = ("gid", graph, {}, {"test-task": [task_id]}) responses.get( f"{ROOT_URL}/api/queue/v1/task/{task_id}/status", status=200, json={"status": {"state": "failed"}}, ) responses.post(f"{ROOT_URL}/api/queue/v1/task/{task_id}/rerun", status=200) run_action( "retrigger-multiple", input={"requests": [{"tasks": ["test-task"], "times": 2}]}, ) def test_add_all_browsertime(mocker, run_action, get_artifact): graph = make_graph( make_task( label="raptor-browsertime", kind="test", attributes={ "raptor_try_name": "browsertime-firefox", "test_platform": "linux64-shippable-qr/opt", "run_on_projects": ["mozilla-central"], }, task_def={"name": "raptor-browsertime", "extra": {"suite": "raptor"}}, ), make_task(label="build", kind="build", task_def={"name": "build"}), ) m = mocker.patch("gecko_taskgraph.actions.backfill.fetch_graph_and_labels") m.return_value = ("gid", graph, {}, None) run_action("add-all-browsertime", params={"project": "mozilla-central"}) to_run = get_artifact("to-run.json") assert "raptor-browsertime" in to_run assert "build" not in to_run @pytest.mark.xfail( reason="Index API artifact handling issue - _handle_artifact doesn't parse YAML correctly for index artifacts" ) def test_gecko_profile(mocker, responses, run_action, get_artifact): task_id = "tid" task_def = { "metadata": {"name": "test-raptor"}, "payload": {"command": [["run-tests"]], "env": {}}, "extra": { "suite": "raptor", "treeherder": {"symbol": "R", "groupName": "Raptor"}, }, } graph = make_graph( make_task( label="test-raptor", kind="test", attributes={"unittest_suite": "raptor"}, task_def={ "name": "test-raptor", "payload": {"command": [["run-tests"]], "env": {}}, "extra": { "suite": "raptor", "treeherder": {"symbol": "R", "groupName": "Raptor"}, }, }, ) ) responses.get( "http://hg.example.com/json-pushes?version=2&startID=99&endID=100", status=200, json={"pushes": {"100": {"changesets": ["abc123"]}}}, ) responses.get( f"{ROOT_URL}/api/queue/v1/task/{task_id}", status=200, json=task_def, ) responses.get( f"{ROOT_URL}/api/index/v1/task/gecko.v2.some-project.pushlog-id.100.decision/artifacts/public%2Fparameters.yml", status=200, body=yaml.dump({"pushlog_id": "100", "project": "autoland", "level": "1"}), content_type="application/x-yaml", ) m = mocker.patch("gecko_taskgraph.actions.gecko_profile.fetch_graph_and_labels") m.return_value = ("gid", graph, {"test-raptor": "tid"}, None) mocker.patch("gecko_taskgraph.actions.gecko_profile.combine_task_graph_files") run_action( "geckoprofile", task_id=task_id, params={"pushlog_id": "100", "head_repository": "http://hg.example.com"}, input={"depth": 1, "gecko_profile_interval": 5}, ) to_run = get_artifact("to-run-100.json") assert "test-raptor" in to_run def test_side_by_side(mocker, responses, run_action, get_artifact): task_id = "tid" task_def = { "metadata": {"name": "linux/opt-browsertime-tp6"}, "extra": {"treeherder": {"symbol": "tp6"}}, "payload": {"command": [["run"], ["perf-test {test_name}"]]}, } graph = make_graph( make_task( label="perftest-linux-side-by-side", task_def={ "name": "perftest-linux-side-by-side", "payload": {"command": [["run"], ["perf-test {test_name}"]]}, "extra": {"treeherder": {"symbol": "sxs"}}, "metadata": {"name": "perftest-linux-side-by-side"}, }, ) ) responses.get( f"{ROOT_URL}/api/queue/v1/task/{task_id}", status=200, json=task_def, ) m = mocker.patch("gecko_taskgraph.actions.side_by_side.fetch_graph_and_labels") m.return_value = ("gid", graph, {}, None) run_action( "side-by-side", task_id=task_id, params={"head_rev": "newrev123", "pushlog_id": "100"}, input={"revision": "baserev456", "project": "autoland"}, ) to_run = get_artifact("to-run.json") assert "perftest-linux-side-by-side" in to_run def test_release_promotion( mocker, monkeypatch, responses, run_action, parameters, graph_config ): m = mocker.patch("gecko_taskgraph.actions.release_promotion.taskgraph_decision") action_task_id = "action-task-id" monkeypatch.setenv("TASK_ID", action_task_id) responses.get( f"{ROOT_URL}/api/index/v1/task/gecko.v2.try.revision.abcdef.taskgraph.decision", status=200, json={"taskId": "decision-task-id"}, ) responses.get( f"{ROOT_URL}/api/queue/v1/task/decision-task-id/artifacts/public%2Fparameters.yml", status=200, body=yaml.dump( { "base_repository": "http://hg.example.com", "head_repository": "http://hg.example.com", "head_rev": "abcdef", "project": "try", "level": "1", "pushlog_id": "100", "required_signoffs": [], "signoff_urls": {}, "release_product": "firefox", "release_type": "nightly", } ), content_type="application/x-yaml", ) responses.get( f"{ROOT_URL}/api/queue/v1/task/decision-task-id/artifacts/public%2Ffull-task-graph.json", status=200, json={}, ) responses.get( f"{ROOT_URL}/api/queue/v1/task-group/{action_task_id}/list", status=200, json={ "tasks": [ {"status": {"taskId": action_task_id, "state": "running"}}, ] }, ) mocker.patch( "gecko_taskgraph.actions.release_promotion.find_existing_tasks_from_previous_kinds", return_value={}, ) run_action( "release-promotion", params={ "project": "try", "level": "1", }, input={ "release_promotion_flavor": "promote_firefox", "build_number": 1, "version": "", "partial_updates": {}, "release_enable_partner_repack": False, "release_enable_partner_attribution": False, "release_enable_emefree": False, }, ) m.assert_called_once() args, kwargs = m.call_args assert args[0] == {"root": graph_config.root_dir} assert kwargs["parameters"]["target_tasks_method"] == "promote_desktop" if __name__ == "__main__": main()