зеркало из https://github.com/mozilla/treeherder.git
Bug 1612547 - Define component for doing backfills using existing reports
This commit is contained in:
Родитель
1b0f05be27
Коммит
3f73a3103e
|
@ -512,7 +512,8 @@ zipp==3.1.0 \
|
|||
--hash=sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b \
|
||||
--hash=sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96 \
|
||||
# via importlib-metadata
|
||||
|
||||
json-e==4.0.0 \
|
||||
--hash=sha256:c1add008dbfbb4b7c48d30b42e11e26db4883394989e013b603a63b6c59b8530
|
||||
# WARNING: The following packages were not pinned, but pip requires them to be
|
||||
# pinned when the requirements file includes hashes. Consider using the --allow-unsafe flag.
|
||||
# setuptools
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def job_from_try(eleven_job_blobs, create_jobs):
|
||||
job_blob = eleven_job_blobs[0]
|
||||
job = create_jobs([job_blob])[0]
|
||||
|
||||
job.repository.is_try_repo = True
|
||||
job.repository.save()
|
||||
return job
|
|
@ -0,0 +1,14 @@
|
|||
import pytest
|
||||
|
||||
from treeherder.perf.backfill_tool import BackfillTool
|
||||
from treeherder.perf.exceptions import CannotBackfill
|
||||
from treeherder.services.taskcluster import TaskclusterModel
|
||||
|
||||
|
||||
# BackfillTool
|
||||
def test_backfilling_job_from_try_repo_raises_exception(job_from_try):
|
||||
backfill_tool = BackfillTool(
|
||||
TaskclusterModel('https://fakerooturl.org', 'FAKE_CLIENT_ID', 'FAKE_ACCESS_TOKEN'))
|
||||
|
||||
with pytest.raises(CannotBackfill):
|
||||
backfill_tool.backfill_job(job_from_try.id)
|
|
@ -0,0 +1,77 @@
|
|||
{
|
||||
"context": [{}],
|
||||
"description": "Take the label of the current task, and trigger the task with that label on previous pushes in the same project.",
|
||||
"extra": {
|
||||
"actionPerm": "generic"
|
||||
},
|
||||
"hookGroupId": "project-gecko",
|
||||
"hookId": "in-tree-action-3-generic/9353e8f146",
|
||||
"hookPayload": {
|
||||
"decision": {
|
||||
"action": {
|
||||
"cb_name": "backfill",
|
||||
"description": "Take the label of the current task, and trigger the task with that label on previous pushes in the same project.",
|
||||
"name": "backfill",
|
||||
"symbol": "Bk",
|
||||
"taskGroupId": "f7Jj_h6MTEKr5Ln_7aFqbw",
|
||||
"title": "Backfill"
|
||||
},
|
||||
"push": {
|
||||
"owner": "mozilla-taskcluster-maintenance@mozilla.com",
|
||||
"pushlog_id": "109591",
|
||||
"revision": "c7766d0b4a121985a8b07e6721d66ccab57bbf76"
|
||||
},
|
||||
"repository": {
|
||||
"level": "3",
|
||||
"project": "autoland",
|
||||
"url": "https://hg.mozilla.org/integration/autoland"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"input": {
|
||||
"$eval": "input"
|
||||
},
|
||||
"taskGroupId": {
|
||||
"$eval": "taskGroupId"
|
||||
},
|
||||
"taskId": {
|
||||
"$eval": "taskId"
|
||||
}
|
||||
}
|
||||
},
|
||||
"kind": "hook",
|
||||
"name": "backfill",
|
||||
"schema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"depth": {
|
||||
"default": 9,
|
||||
"description": "The number of previous pushes before the current push to attempt to trigger this task on.",
|
||||
"maximum": 25,
|
||||
"minimum": 1,
|
||||
"title": "Depth",
|
||||
"type": "integer"
|
||||
},
|
||||
"inclusive": {
|
||||
"default": false,
|
||||
"description": "If true, the backfill will also retrigger the task on the selected push.",
|
||||
"title": "Inclusive Range",
|
||||
"type": "boolean"
|
||||
},
|
||||
"testPath": {
|
||||
"title": "Test Path",
|
||||
"type": "string"
|
||||
},
|
||||
"times": {
|
||||
"default": 1,
|
||||
"description": "The number of times to execute each job you are backfilling.",
|
||||
"maximum": 10,
|
||||
"minimum": 1,
|
||||
"title": "Times",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"title": "Backfill"
|
||||
}
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,8 @@
|
|||
[
|
||||
{
|
||||
"test-type": "talos"
|
||||
},
|
||||
{
|
||||
"test-type": "raptor"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"kind": "test",
|
||||
"os": "linux",
|
||||
"createdForUser": "tnikkel@mozilla.com",
|
||||
"retrigger": "true",
|
||||
"label": "test-linux64-shippable-qr/opt-raptor-assorted-dom-firefox-e10s",
|
||||
"test-type": "raptor",
|
||||
"worker-implementation": "generic-worker"
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
[
|
||||
{
|
||||
"kind": "decision-task"
|
||||
},
|
||||
{
|
||||
"kind": "action-callback"
|
||||
},
|
||||
{
|
||||
"kind": "cron-task"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"kind": "test",
|
||||
"os": "linux",
|
||||
"createdForUser": "tnikkel@mozilla.com",
|
||||
"retrigger": "true",
|
||||
"label": "test-linux64-shippable-qr/opt-raptor-assorted-dom-firefox-e10s",
|
||||
"test-type": "raptor",
|
||||
"worker-implementation": "generic-worker"
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
{
|
||||
"provisionerId": "releng-hardware",
|
||||
"workerType": "gecko-t-linux-talos",
|
||||
"schedulerId": "gecko-level-3",
|
||||
"taskGroupId": "f7Jj_h6MTEKr5Ln_7aFqbw",
|
||||
"dependencies": [
|
||||
"UHhU74OiQ5mpwdWVqkFPPQ",
|
||||
"e0LnTgZNQcSg9w2FctDyTw",
|
||||
"eFUvia2tSZaEnveSffSN3w"
|
||||
],
|
||||
"requires": "all-completed",
|
||||
"routes": [
|
||||
"tc-treeherder.v2.autoland.c7766d0b4a121985a8b07e6721d66ccab57bbf76.109591"
|
||||
],
|
||||
"priority": "low",
|
||||
"retries": 5,
|
||||
"created": "2020-03-10T04:40:43.718Z",
|
||||
"deadline": "2020-03-11T04:40:43.718Z",
|
||||
"expires": "2021-03-10T04:40:43.718Z",
|
||||
"scopes": [],
|
||||
"payload": {
|
||||
"onExitStatus": {
|
||||
"retry": [4]
|
||||
},
|
||||
"maxRunTime": 2100,
|
||||
"artifacts": [
|
||||
{
|
||||
"path": "logs",
|
||||
"type": "directory",
|
||||
"name": "public/logs"
|
||||
},
|
||||
{
|
||||
"path": "build/blobber_upload_dir",
|
||||
"type": "directory",
|
||||
"name": "public/test_info"
|
||||
}
|
||||
],
|
||||
"command": [
|
||||
["chmod", "+x", "run-task"],
|
||||
[
|
||||
"./run-task",
|
||||
"--",
|
||||
"/usr/bin/python2.7",
|
||||
"-u",
|
||||
"mozharness/scripts/raptor_script.py",
|
||||
"--cfg",
|
||||
"mozharness/configs/raptor/linux_config.py",
|
||||
"--test=raptor-assorted-dom",
|
||||
"--enable-webrender",
|
||||
"--download-symbols",
|
||||
"ondemand"
|
||||
]
|
||||
],
|
||||
"env": {
|
||||
"SCCACHE_DISABLE": "1",
|
||||
"GECKO_HEAD_REV": "c7766d0b4a121985a8b07e6721d66ccab57bbf76",
|
||||
"MOZ_SCM_LEVEL": "3",
|
||||
"GECKO_HEAD_REPOSITORY": "https://hg.mozilla.org/integration/autoland",
|
||||
"EXTRA_MOZHARNESS_CONFIG": "{\"test_packages_url\": \"https://firefox-ci-tc.services.mozilla.com/api/queue/v1/task/eFUvia2tSZaEnveSffSN3w/artifacts/public/build/target.test_packages.json\", \"installer_url\": \"https://firefox-ci-tc.services.mozilla.com/api/queue/v1/task/eFUvia2tSZaEnveSffSN3w/artifacts/public/build/target.tar.bz2\"}",
|
||||
"MOZ_FETCHES": "[{\"artifact\": \"public/build/minidump_stackwalk.tar.xz\", \"extract\": true, \"task\": \"UHhU74OiQ5mpwdWVqkFPPQ\"}, {\"artifact\": \"public/assorted-dom-4befd28725c6.zip\", \"extract\": true, \"task\": \"e0LnTgZNQcSg9w2FctDyTw\"}]",
|
||||
"MOZ_FETCHES_DIR": "fetches",
|
||||
"MOZ_AUTOMATION": "1"
|
||||
},
|
||||
"mounts": [
|
||||
{
|
||||
"directory": ".",
|
||||
"content": {
|
||||
"taskId": "eFUvia2tSZaEnveSffSN3w",
|
||||
"artifact": "public/build/mozharness.zip"
|
||||
},
|
||||
"format": "zip"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"url": "https://hg.mozilla.org/integration/autoland/raw-file/c7766d0b4a121985a8b07e6721d66ccab57bbf76/taskcluster/scripts/run-task"
|
||||
},
|
||||
"file": "./run-task"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"url": "https://hg.mozilla.org/integration/autoland/raw-file/c7766d0b4a121985a8b07e6721d66ccab57bbf76/taskcluster/scripts/misc/fetch-content"
|
||||
},
|
||||
"file": "./fetch-content"
|
||||
}
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"owner": "sikeda.birchill@mozilla.com",
|
||||
"source": "https://hg.mozilla.org/integration/autoland/file/c7766d0b4a121985a8b07e6721d66ccab57bbf76/taskcluster/ci/test",
|
||||
"description": "Raptor Assorted-Dom on Firefox ([Treeherder push](https://treeherder.mozilla.org/#/jobs?repo=autoland&revision=c7766d0b4a121985a8b07e6721d66ccab57bbf76))",
|
||||
"name": "test-linux64-shippable-qr/opt-raptor-assorted-dom-firefox-e10s"
|
||||
},
|
||||
"tags": {
|
||||
"kind": "test",
|
||||
"os": "linux",
|
||||
"createdForUser": "sikeda.birchill@mozilla.com",
|
||||
"retrigger": "true",
|
||||
"label": "test-linux64-shippable-qr/opt-raptor-assorted-dom-firefox-e10s",
|
||||
"test-type": "raptor",
|
||||
"worker-implementation": "generic-worker"
|
||||
},
|
||||
"extra": {
|
||||
"index": {
|
||||
"rank": 1583815073
|
||||
},
|
||||
"parent": "f7Jj_h6MTEKr5Ln_7aFqbw",
|
||||
"chunks": {
|
||||
"current": 1,
|
||||
"total": 1
|
||||
},
|
||||
"suite": "raptor",
|
||||
"treeherder": {
|
||||
"jobKind": "test",
|
||||
"groupSymbol": "Rap",
|
||||
"collection": {
|
||||
"opt": true
|
||||
},
|
||||
"machine": {
|
||||
"platform": "linux64-shippable-qr"
|
||||
},
|
||||
"groupName": "Raptor performance tests on Firefox",
|
||||
"tier": 1,
|
||||
"symbol": "dom"
|
||||
},
|
||||
"treeherder-platform": "linux64-shippable-qr/opt"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,355 @@
|
|||
[
|
||||
{
|
||||
"context": [
|
||||
{
|
||||
"retrigger": "true"
|
||||
}
|
||||
],
|
||||
"description": "Create a clone of the task.",
|
||||
"extra": {
|
||||
"actionPerm": "generic"
|
||||
},
|
||||
"hookGroupId": "project-gecko",
|
||||
"hookId": "in-tree-action-3-generic/9353e8f146",
|
||||
"hookPayload": {
|
||||
"decision": {
|
||||
"action": {
|
||||
"cb_name": "retrigger",
|
||||
"description": "Create a clone of the task.",
|
||||
"name": "retrigger",
|
||||
"symbol": "rt",
|
||||
"taskGroupId": "f7Jj_h6MTEKr5Ln_7aFqbw",
|
||||
"title": "Retrigger"
|
||||
},
|
||||
"push": {
|
||||
"owner": "mozilla-taskcluster-maintenance@mozilla.com",
|
||||
"pushlog_id": "109591",
|
||||
"revision": "c7766d0b4a121985a8b07e6721d66ccab57bbf76"
|
||||
},
|
||||
"repository": {
|
||||
"level": "3",
|
||||
"project": "autoland",
|
||||
"url": "https://hg.mozilla.org/integration/autoland"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"input": {
|
||||
"$eval": "input"
|
||||
},
|
||||
"taskGroupId": {
|
||||
"$eval": "taskGroupId"
|
||||
},
|
||||
"taskId": {
|
||||
"$eval": "taskId"
|
||||
}
|
||||
}
|
||||
},
|
||||
"kind": "hook",
|
||||
"name": "retrigger",
|
||||
"schema": {
|
||||
"properties": {
|
||||
"downstream": {
|
||||
"default": false,
|
||||
"description": "If true, downstream tasks from this one will be cloned as well. The dependencies will be updated to work with the new task at the root.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"times": {
|
||||
"default": 1,
|
||||
"description": "How many times to run each task.",
|
||||
"maximum": 100,
|
||||
"minimum": 1,
|
||||
"title": "Times",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"title": "Retrigger"
|
||||
},
|
||||
{
|
||||
"context": [
|
||||
{
|
||||
"kind": "test"
|
||||
}
|
||||
],
|
||||
"description": "Re-run Tests for original manifest, directories and tests for failing tests.",
|
||||
"extra": {
|
||||
"actionPerm": "generic"
|
||||
},
|
||||
"hookGroupId": "project-gecko",
|
||||
"hookId": "in-tree-action-3-generic/9353e8f146",
|
||||
"hookPayload": {
|
||||
"decision": {
|
||||
"action": {
|
||||
"cb_name": "isolate-test-failures",
|
||||
"description": "Re-run Tests for original manifest, directories and tests for failing tests.",
|
||||
"name": "isolate-test-failures",
|
||||
"symbol": "it",
|
||||
"taskGroupId": "f7Jj_h6MTEKr5Ln_7aFqbw",
|
||||
"title": "Isolate test failures in job"
|
||||
},
|
||||
"push": {
|
||||
"owner": "mozilla-taskcluster-maintenance@mozilla.com",
|
||||
"pushlog_id": "109591",
|
||||
"revision": "c7766d0b4a121985a8b07e6721d66ccab57bbf76"
|
||||
},
|
||||
"repository": {
|
||||
"level": "3",
|
||||
"project": "autoland",
|
||||
"url": "https://hg.mozilla.org/integration/autoland"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"input": {
|
||||
"$eval": "input"
|
||||
},
|
||||
"taskGroupId": {
|
||||
"$eval": "taskGroupId"
|
||||
},
|
||||
"taskId": {
|
||||
"$eval": "taskId"
|
||||
}
|
||||
}
|
||||
},
|
||||
"kind": "hook",
|
||||
"name": "isolate-test-failures",
|
||||
"schema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"times": {
|
||||
"default": 1,
|
||||
"description": "How many times to run each task.",
|
||||
"maximum": 100,
|
||||
"minimum": 1,
|
||||
"title": "Times",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"title": "Isolate test failures in job"
|
||||
},
|
||||
{
|
||||
"context": [{}],
|
||||
"description": "Take the label of the current task, and trigger the task with that label on previous pushes in the same project.",
|
||||
"extra": {
|
||||
"actionPerm": "generic"
|
||||
},
|
||||
"hookGroupId": "project-gecko",
|
||||
"hookId": "in-tree-action-3-generic/9353e8f146",
|
||||
"hookPayload": {
|
||||
"decision": {
|
||||
"action": {
|
||||
"cb_name": "backfill",
|
||||
"description": "Take the label of the current task, and trigger the task with that label on previous pushes in the same project.",
|
||||
"name": "backfill",
|
||||
"symbol": "Bk",
|
||||
"taskGroupId": "f7Jj_h6MTEKr5Ln_7aFqbw",
|
||||
"title": "Backfill"
|
||||
},
|
||||
"push": {
|
||||
"owner": "mozilla-taskcluster-maintenance@mozilla.com",
|
||||
"pushlog_id": "109591",
|
||||
"revision": "c7766d0b4a121985a8b07e6721d66ccab57bbf76"
|
||||
},
|
||||
"repository": {
|
||||
"level": "3",
|
||||
"project": "autoland",
|
||||
"url": "https://hg.mozilla.org/integration/autoland"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"input": {
|
||||
"$eval": "input"
|
||||
},
|
||||
"taskGroupId": {
|
||||
"$eval": "taskGroupId"
|
||||
},
|
||||
"taskId": {
|
||||
"$eval": "taskId"
|
||||
}
|
||||
}
|
||||
},
|
||||
"kind": "hook",
|
||||
"name": "backfill",
|
||||
"schema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"depth": {
|
||||
"default": 9,
|
||||
"description": "The number of previous pushes before the current push to attempt to trigger this task on.",
|
||||
"maximum": 25,
|
||||
"minimum": 1,
|
||||
"title": "Depth",
|
||||
"type": "integer"
|
||||
},
|
||||
"inclusive": {
|
||||
"default": false,
|
||||
"description": "If true, the backfill will also retrigger the task on the selected push.",
|
||||
"title": "Inclusive Range",
|
||||
"type": "boolean"
|
||||
},
|
||||
"testPath": {
|
||||
"title": "Test Path",
|
||||
"type": "string"
|
||||
},
|
||||
"times": {
|
||||
"default": 1,
|
||||
"description": "The number of times to execute each job you are backfilling.",
|
||||
"maximum": 10,
|
||||
"minimum": 1,
|
||||
"title": "Times",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"title": "Backfill"
|
||||
},
|
||||
{
|
||||
"context": [
|
||||
{
|
||||
"test-type": "talos"
|
||||
},
|
||||
{
|
||||
"test-type": "raptor"
|
||||
}
|
||||
],
|
||||
"description": "Take the label of the current task, and trigger the task with that label on previous pushes in the same project while adding the --geckoProfile cmd arg.",
|
||||
"extra": {
|
||||
"actionPerm": "generic"
|
||||
},
|
||||
"hookGroupId": "project-gecko",
|
||||
"hookId": "in-tree-action-3-generic/9353e8f146",
|
||||
"hookPayload": {
|
||||
"decision": {
|
||||
"action": {
|
||||
"cb_name": "geckoprofile",
|
||||
"description": "Take the label of the current task, and trigger the task with that label on previous pushes in the same project while adding the --geckoProfile cmd arg.",
|
||||
"name": "geckoprofile",
|
||||
"symbol": "Gp",
|
||||
"taskGroupId": "f7Jj_h6MTEKr5Ln_7aFqbw",
|
||||
"title": "GeckoProfile"
|
||||
},
|
||||
"push": {
|
||||
"owner": "mozilla-taskcluster-maintenance@mozilla.com",
|
||||
"pushlog_id": "109591",
|
||||
"revision": "c7766d0b4a121985a8b07e6721d66ccab57bbf76"
|
||||
},
|
||||
"repository": {
|
||||
"level": "3",
|
||||
"project": "autoland",
|
||||
"url": "https://hg.mozilla.org/integration/autoland"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"input": {
|
||||
"$eval": "input"
|
||||
},
|
||||
"taskGroupId": {
|
||||
"$eval": "taskGroupId"
|
||||
},
|
||||
"taskId": {
|
||||
"$eval": "taskId"
|
||||
}
|
||||
}
|
||||
},
|
||||
"kind": "hook",
|
||||
"name": "geckoprofile",
|
||||
"title": "GeckoProfile"
|
||||
},
|
||||
{
|
||||
"context": [{}],
|
||||
"description": "Rerun a task.\n\nThis only works on failed or exception tasks in the original taskgraph, and is CoT friendly.",
|
||||
"extra": {
|
||||
"actionPerm": "generic"
|
||||
},
|
||||
"hookGroupId": "project-gecko",
|
||||
"hookId": "in-tree-action-3-generic/9353e8f146",
|
||||
"hookPayload": {
|
||||
"decision": {
|
||||
"action": {
|
||||
"cb_name": "rerun",
|
||||
"description": "Rerun a task.\n\nThis only works on failed or exception tasks in the original taskgraph, and is CoT friendly.",
|
||||
"name": "rerun",
|
||||
"symbol": "rr",
|
||||
"taskGroupId": "f7Jj_h6MTEKr5Ln_7aFqbw",
|
||||
"title": "Rerun"
|
||||
},
|
||||
"push": {
|
||||
"owner": "mozilla-taskcluster-maintenance@mozilla.com",
|
||||
"pushlog_id": "109591",
|
||||
"revision": "c7766d0b4a121985a8b07e6721d66ccab57bbf76"
|
||||
},
|
||||
"repository": {
|
||||
"level": "3",
|
||||
"project": "autoland",
|
||||
"url": "https://hg.mozilla.org/integration/autoland"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"input": {
|
||||
"$eval": "input"
|
||||
},
|
||||
"taskGroupId": {
|
||||
"$eval": "taskGroupId"
|
||||
},
|
||||
"taskId": {
|
||||
"$eval": "taskId"
|
||||
}
|
||||
}
|
||||
},
|
||||
"kind": "hook",
|
||||
"name": "rerun",
|
||||
"schema": {
|
||||
"properties": {},
|
||||
"type": "object"
|
||||
},
|
||||
"title": "Rerun"
|
||||
},
|
||||
{
|
||||
"context": [{}],
|
||||
"description": "Cancel the given task",
|
||||
"extra": {
|
||||
"actionPerm": "generic"
|
||||
},
|
||||
"hookGroupId": "project-gecko",
|
||||
"hookId": "in-tree-action-3-generic/9353e8f146",
|
||||
"hookPayload": {
|
||||
"decision": {
|
||||
"action": {
|
||||
"cb_name": "cancel",
|
||||
"description": "Cancel the given task",
|
||||
"name": "cancel",
|
||||
"symbol": "cx",
|
||||
"taskGroupId": "f7Jj_h6MTEKr5Ln_7aFqbw",
|
||||
"title": "Cancel Task"
|
||||
},
|
||||
"push": {
|
||||
"owner": "mozilla-taskcluster-maintenance@mozilla.com",
|
||||
"pushlog_id": "109591",
|
||||
"revision": "c7766d0b4a121985a8b07e6721d66ccab57bbf76"
|
||||
},
|
||||
"repository": {
|
||||
"level": "3",
|
||||
"project": "autoland",
|
||||
"url": "https://hg.mozilla.org/integration/autoland"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"input": {
|
||||
"$eval": "input"
|
||||
},
|
||||
"taskGroupId": {
|
||||
"$eval": "taskGroupId"
|
||||
},
|
||||
"taskId": {
|
||||
"$eval": "taskId"
|
||||
}
|
||||
}
|
||||
},
|
||||
"kind": "hook",
|
||||
"name": "cancel",
|
||||
"title": "Cancel Task"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,60 @@
|
|||
import json
|
||||
from os.path import (dirname,
|
||||
join)
|
||||
|
||||
import pytest
|
||||
|
||||
from treeherder.services.taskcluster import TaskclusterModel
|
||||
|
||||
SAMPLE_DATA_PATH = join(
|
||||
dirname(dirname(__file__)),
|
||||
'sample_data')
|
||||
|
||||
|
||||
def load_json_fixture(from_file):
|
||||
fixture_path = join(SAMPLE_DATA_PATH, 'perf_sheriff_bot', from_file)
|
||||
with open(fixture_path, 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def actions_json(): return load_json_fixture('initialActions.json')
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def expected_actions_json(): return load_json_fixture('reducedActions.json')
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def original_task(): return load_json_fixture('originalTask.json')
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def expected_backfill_task(): return load_json_fixture('backfilltask.json')
|
||||
|
||||
|
||||
# TaskclusterModel
|
||||
def test_filter_relevant_actions(actions_json, original_task, expected_actions_json):
|
||||
reduced_actions_json = TaskclusterModel._filter_relevant_actions(actions_json,
|
||||
original_task)
|
||||
|
||||
assert reduced_actions_json == expected_actions_json
|
||||
|
||||
|
||||
def test_task_in_context():
|
||||
# match
|
||||
tag_set_list, task_tags = [load_json_fixture(f)
|
||||
for f in ("matchingTagSetList.json", "matchingTaskTags.json")]
|
||||
assert TaskclusterModel._task_in_context(tag_set_list, task_tags) is True
|
||||
|
||||
# mismatch
|
||||
tag_set_list, task_tags = [load_json_fixture(f)
|
||||
for f in ("mismatchingTagSetList.json", "mismatchingTaskTags.json")]
|
||||
assert TaskclusterModel._task_in_context(tag_set_list, task_tags) is False
|
||||
|
||||
|
||||
def test_get_action(actions_json, expected_backfill_task):
|
||||
action_array = actions_json["actions"]
|
||||
|
||||
backfill_task = TaskclusterModel._get_action(action_array, "backfill")
|
||||
assert backfill_task == expected_backfill_task
|
|
@ -0,0 +1,87 @@
|
|||
import pytest
|
||||
|
||||
from treeherder.utils.taskcluster_lib_scopes import (patternMatch,
|
||||
satisfiesExpression)
|
||||
|
||||
|
||||
# satisfiesExpression()
|
||||
@pytest.mark.parametrize('scopeset, expression', [
|
||||
[[], {'AllOf': []}],
|
||||
[['A'], {'AllOf': ['A']}],
|
||||
[['A', 'B'], 'A'],
|
||||
[['a*', 'b*', 'c*'], 'abc'],
|
||||
[['abc'], {'AnyOf': ['abc', 'def']}],
|
||||
[['def'], {'AnyOf': ['abc', 'def']}],
|
||||
[['abc', 'def'], {'AnyOf': ['abc', 'def']}],
|
||||
[['abc*'], {'AnyOf': ['abc', 'def']}],
|
||||
[['abc*'], {'AnyOf': ['abc']}],
|
||||
[['abc*', 'def*'], {'AnyOf': ['abc', 'def']}],
|
||||
[['foo'], {'AllOf': [{'AnyOf': [{'AllOf': ['foo']}, {'AllOf': ['bar']}]}]}],
|
||||
[['a*', 'b*', 'c*'], {'AnyOf': ['cfoo', 'dfoo']}],
|
||||
[['a*', 'b*', 'c*'], {'AnyOf': ['bx', 'by']}],
|
||||
[['a*', 'b*', 'c*'], {'AllOf': ['bx', 'cx']}],
|
||||
# complex expression with only
|
||||
# some AnyOf branches matching
|
||||
[
|
||||
['a*', 'b*', 'c*'],
|
||||
{'AnyOf': [
|
||||
{'AllOf': ['ax', 'jx']}, # doesn't match
|
||||
{'AllOf': ['bx', 'cx']}, # does match
|
||||
'bbb',
|
||||
]},
|
||||
],
|
||||
])
|
||||
def test_expression_is_satisfied(scopeset, expression):
|
||||
assert satisfiesExpression(scopeset, expression) is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize('scopeset, expression', [
|
||||
[[], {'AnyOf': []}],
|
||||
[[], 'missing-scope'],
|
||||
[['wrong-scope'], 'missing-scope'],
|
||||
[['ghi'], {'AnyOf': ['abc', 'def']}],
|
||||
[['ghi*'], {'AnyOf': ['abc', 'def']}],
|
||||
[['ghi', 'fff'], {'AnyOf': ['abc', 'def']}],
|
||||
[['ghi*', 'fff*'], {'AnyOf': ['abc', 'def']}],
|
||||
[['abc'], {'AnyOf': ['ghi']}],
|
||||
[['abc*'], {'AllOf': ['abc', 'ghi']}],
|
||||
[[''], {'AnyOf': ['abc', 'def']}],
|
||||
[['abc:def'], {'AnyOf': ['abc', 'def']}],
|
||||
[['xyz', 'abc'], {'AllOf': [{'AnyOf': [{'AllOf': ['foo']}, {'AllOf': ['bar']}]}]}],
|
||||
[['a*', 'b*', 'c*'], {'AllOf': ['bx', 'cx', {'AnyOf': ['xxx', 'yyyy']}]}],
|
||||
])
|
||||
def test_expression_is_not_satisfied(scopeset, expression):
|
||||
assert not satisfiesExpression(scopeset, expression)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('scopeset', [
|
||||
None,
|
||||
'scopeset_argument',
|
||||
('scopeset', 'argument'),
|
||||
{'scopeset', 'argument'},
|
||||
])
|
||||
def test_wrong_scopeset_type_raises_exception(scopeset):
|
||||
with pytest.raises(TypeError):
|
||||
satisfiesExpression(scopeset, 'in-tree:hook-action:{hook_group_id}/{hook_id}')
|
||||
|
||||
|
||||
# patternMatch()
|
||||
def test_identical_scope_and_pattern_are_matching():
|
||||
assert patternMatch('mock:scope', 'mock:scope') is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize('pattern, scope', [
|
||||
('matching*', 'matching'),
|
||||
('matching*', 'matching/scope')
|
||||
])
|
||||
def test_starred_patterns_are_matching(pattern, scope):
|
||||
assert patternMatch(pattern, scope) is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize('pattern, scope', [
|
||||
('matching*', 'mismatching'),
|
||||
('match*ing', 'matching'),
|
||||
('*matching', 'matching')
|
||||
])
|
||||
def test_starred_patterns_dont_matching(pattern, scope):
|
||||
assert not patternMatch(pattern, scope)
|
|
@ -15,7 +15,8 @@ from django.db import (models,
|
|||
from django.db.models import (Count,
|
||||
Max,
|
||||
Min,
|
||||
Q)
|
||||
Q,
|
||||
Subquery)
|
||||
from django.db.utils import ProgrammingError
|
||||
from django.forms import model_to_dict
|
||||
from django.utils import timezone
|
||||
|
@ -647,6 +648,13 @@ class Job(models.Model):
|
|||
|
||||
return text_log_error
|
||||
|
||||
def fetch_associated_decision_job(self):
|
||||
decision_type = JobType.objects.filter(name="Gecko Decision Task",
|
||||
symbol="D")
|
||||
return Job.objects.get(repository_id=self.repository_id,
|
||||
job_type_id=Subquery(decision_type.values('id')[:1]),
|
||||
push_id=self.push_id)
|
||||
|
||||
@staticmethod
|
||||
def get_duration(submit_time, start_time, end_time):
|
||||
endtime = end_time if to_timestamp(end_time) else datetime.datetime.now()
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import logging
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
from treeherder.model.models import Job
|
||||
from treeherder.perf.exceptions import CannotBackfill
|
||||
from treeherder.services.taskcluster import TaskclusterModel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BackfillTool:
|
||||
|
||||
def __init__(self, taskcluster_model: TaskclusterModel):
|
||||
self.tc_model = taskcluster_model
|
||||
|
||||
def backfill_job(self, job_id: str) -> str:
|
||||
job = self._fetch_job(job_id)
|
||||
|
||||
self.assert_backfill_ability(job)
|
||||
|
||||
logger.debug(f"Fetching decision task of job {job.id}...")
|
||||
task_id_to_backfill = job.taskcluster_metadata.task_id
|
||||
decision_job = job.fetch_associated_decision_job()
|
||||
decision_task_id = decision_job.taskcluster_metadata.task_id
|
||||
|
||||
logger.debug(f"Requesting backfill for task {task_id_to_backfill}...")
|
||||
task_id = self.tc_model.trigger_action(
|
||||
action='backfill',
|
||||
task_id=task_id_to_backfill,
|
||||
decision_task_id=decision_task_id,
|
||||
input={},
|
||||
root_url=job.repository.tc_root_url
|
||||
)
|
||||
return task_id
|
||||
|
||||
def assert_backfill_ability(self, over_job: Job):
|
||||
if over_job.repository.is_try_repo:
|
||||
raise CannotBackfill("Try repository isn't suited for backfilling.")
|
||||
|
||||
@staticmethod
|
||||
def _fetch_job(job_id: str) -> Job:
|
||||
try:
|
||||
return Job.objects.get(id=job_id)
|
||||
except ObjectDoesNotExist:
|
||||
raise LookupError(f"Job {job_id} not found.")
|
|
@ -8,3 +8,7 @@ class MaxRuntimeExceeded(Exception):
|
|||
|
||||
class MissingRecords(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class CannotBackfill(Exception):
|
||||
pass
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
"""IMPORTANT!
|
||||
This subcommand isn't intended for use on any non-local
|
||||
dev environments. Any attempt to configure it otherwise
|
||||
is considered a known and unapproved risk.
|
||||
|
||||
The subcommand's sole purpose is to act as a smoke test
|
||||
harness that quickly does an end-to-end check over the
|
||||
functionality of the `BackfillTool`.
|
||||
"""
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from treeherder.perf.backfill_tool import BackfillTool
|
||||
from treeherder.services.taskcluster import DEFAULT_ROOT_URL as root_url
|
||||
from treeherder.services.taskcluster import TaskclusterModel
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Backfill missing performance jobs"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'job',
|
||||
action='store',
|
||||
type=str,
|
||||
help="Performance job to backfill from",
|
||||
metavar='JOB_ID',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
job_id = options['job']
|
||||
client_id = settings.PERF_SHERIFF_BOT_CLIENT_ID
|
||||
access_token = settings.PERF_SHERIFF_BOT_ACCESS_TOKEN
|
||||
|
||||
taskcluster_model = TaskclusterModel(root_url, client_id, access_token)
|
||||
backfill_tool = BackfillTool(taskcluster_model)
|
||||
|
||||
task_id = backfill_tool.backfill_job(job_id)
|
||||
|
||||
print(f"Task id {task_id} created when backfilled job id {job_id}")
|
|
@ -0,0 +1,156 @@
|
|||
import logging
|
||||
from typing import List
|
||||
|
||||
import jsone
|
||||
import taskcluster
|
||||
|
||||
from treeherder.utils.taskcluster_lib_scopes import satisfiesExpression
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_ROOT_URL = 'https://firefox-ci-tc.services.mozilla.com'
|
||||
|
||||
|
||||
class TaskclusterModel:
|
||||
"""Javascript -> Python rewrite of frontend's TaskclusterModel"""
|
||||
|
||||
def __init__(self, root_url, client_id=None, access_token=None):
|
||||
options = {'rootUrl': root_url}
|
||||
credentials = {}
|
||||
|
||||
if client_id:
|
||||
credentials['clientId'] = client_id
|
||||
if access_token:
|
||||
credentials['accessToken'] = access_token
|
||||
|
||||
# Taskcluster APIs
|
||||
self.hooks = taskcluster.Hooks({**options, 'credentials': credentials})
|
||||
|
||||
# Following least-privilege principle, as services
|
||||
# bellow don't really need authorization credentials.
|
||||
self.queue = taskcluster.Queue(options)
|
||||
self.auth = taskcluster.Auth(options)
|
||||
|
||||
def set_root_url(self, root_url):
|
||||
for service in (self.hooks, self.queue, self.auth):
|
||||
service.options['rootUrl'] = root_url
|
||||
|
||||
def trigger_action(self, action, task_id, decision_task_id, input, root_url=None) -> str:
|
||||
if root_url is not None:
|
||||
self.set_root_url(root_url)
|
||||
|
||||
actions_context = self._load(decision_task_id, task_id)
|
||||
action_to_trigger = self._get_action(actions_context['actions'], action)
|
||||
|
||||
return self._submit(
|
||||
action=action_to_trigger,
|
||||
decision_task_id=decision_task_id,
|
||||
task_id=task_id,
|
||||
input=input,
|
||||
static_action_variables=actions_context['staticActionVariables']
|
||||
)
|
||||
|
||||
def _load(self, decision_task_id: str, task_id: str) -> dict:
|
||||
if not decision_task_id:
|
||||
raise ValueError("No decision task, can't find taskcluster actions")
|
||||
|
||||
# fetch
|
||||
logger.debug('Fetching actions.json...')
|
||||
actions_json = self.queue.getLatestArtifact(decision_task_id, 'public/actions.json')
|
||||
task_definition = self.queue.task(task_id)
|
||||
|
||||
if actions_json['version'] != 1:
|
||||
raise RuntimeError('Wrong version of actions.json, unable to continue')
|
||||
|
||||
return {
|
||||
'staticActionVariables': actions_json['variables'],
|
||||
'actions': self._filter_relevant_actions(actions_json, task_definition),
|
||||
}
|
||||
|
||||
def _submit(self,
|
||||
action=None,
|
||||
decision_task_id=None,
|
||||
task_id=None,
|
||||
input=None,
|
||||
static_action_variables=None) -> str:
|
||||
context = {
|
||||
"taskGroupId": decision_task_id,
|
||||
"taskId": task_id or None,
|
||||
"input": input,
|
||||
}
|
||||
context.update(static_action_variables)
|
||||
action_kind = action["kind"]
|
||||
|
||||
if action_kind == "hook":
|
||||
hook_payload = jsone.render(action["hookPayload"], context)
|
||||
hook_id, hook_group_id = action["hookId"], action["hookGroupId"]
|
||||
|
||||
decision_task = self.queue.task(decision_task_id)
|
||||
expansion = self.auth.expandScopes({"scopes": decision_task["scopes"]})
|
||||
expression = f"in-tree:hook-action:{hook_group_id}/{hook_id}"
|
||||
|
||||
if not satisfiesExpression(expansion["scopes"], expression):
|
||||
raise RuntimeError(f"Action is misconfigured: decision task's scopes do not satisfy {expression}")
|
||||
|
||||
result = self.hooks.triggerHook(hook_group_id, hook_id, hook_payload)
|
||||
return result["status"]["taskId"]
|
||||
|
||||
raise NotImplementedError(f"Unable to submit actions with '{action_kind}' kind.")
|
||||
|
||||
@classmethod
|
||||
def _filter_relevant_actions(cls, actions_json: dict, original_task: dict) -> list:
|
||||
relevant_actions = {}
|
||||
|
||||
for action in actions_json['actions']:
|
||||
action_name = action['name']
|
||||
if action_name in relevant_actions:
|
||||
continue
|
||||
|
||||
no_context_or_task_to_check = (not len(action['context'])) and (not original_task)
|
||||
task_is_in_context = (original_task and original_task.get('tags') and
|
||||
cls._task_in_context(action['context'], original_task['tags']))
|
||||
|
||||
if no_context_or_task_to_check or task_is_in_context:
|
||||
relevant_actions[action_name] = action
|
||||
|
||||
return list(relevant_actions.values())
|
||||
|
||||
@staticmethod
|
||||
def _get_action(action_array: list, action_name: str) -> str:
|
||||
"""
|
||||
Each action entry (from action array) must define a name, title and description.
|
||||
The order of the array of actions is **significant**: actions should be displayed
|
||||
in this order, and when multiple actions apply, **the first takes precedence**.
|
||||
|
||||
More updated details: https://docs.taskcluster.net/docs/manual/design/conventions/actions/spec#action-metadata
|
||||
|
||||
@return: most relevant action entry
|
||||
"""
|
||||
try:
|
||||
return [a for a in action_array if a["name"] == action_name][0]
|
||||
except IndexError:
|
||||
available_actions = ", ".join(sorted(
|
||||
{a["name"] for a in action_array}
|
||||
))
|
||||
raise LookupError(f"{action_name} action is not available for this task. Available: {available_actions}")
|
||||
|
||||
@classmethod
|
||||
def _task_in_context(cls, context: List[dict], task_tags: dict) -> bool:
|
||||
"""
|
||||
A task (as defined by its tags) is said to match a tag-set if its
|
||||
tags are a super-set of the tag-set. A tag-set is a set of key-value pairs.
|
||||
|
||||
An action (as defined by its context) is said to be relevant for
|
||||
a given task, if that task's tags match one of the tag-sets given
|
||||
in the context property for the action.
|
||||
|
||||
More updated details: https://docs.taskcluster.net/docs/manual/design/conventions/actions/spec#action-context
|
||||
|
||||
@param context: list of tag-sets
|
||||
@param task_tags: task's tags
|
||||
"""
|
||||
return any(
|
||||
all(tag in task_tags and task_tags[tag] == tag_set[tag]
|
||||
for tag in tag_set.keys())
|
||||
for tag_set in context
|
||||
)
|
|
@ -0,0 +1,30 @@
|
|||
"""
|
||||
TODO: Extract this module into a dedicated PyPI package, acting as the
|
||||
Python variant of https://github.com/taskcluster/taskcluster-lib-scopes
|
||||
"""
|
||||
|
||||
|
||||
def satisfiesExpression(scopeset, expression):
|
||||
if not isinstance(scopeset, list):
|
||||
raise TypeError("Scopeset must be an array.")
|
||||
|
||||
def isSatisfied(expr):
|
||||
if isinstance(expr, str):
|
||||
return any([patternMatch(s, expr) for s in scopeset])
|
||||
|
||||
return (
|
||||
"AllOf" in expr and all([isSatisfied(e) for e in expr["AllOf"]]) or
|
||||
"AnyOf" in expr and any([isSatisfied(e) for e in expr["AnyOf"]])
|
||||
)
|
||||
|
||||
return isSatisfied(expression)
|
||||
|
||||
|
||||
def patternMatch(pattern: str, scope):
|
||||
if scope == pattern:
|
||||
return True
|
||||
|
||||
if pattern.endswith('*'):
|
||||
return scope.startswith(pattern[:-1])
|
||||
|
||||
return False
|
Загрузка…
Ссылка в новой задаче