Skip to content
This repository has been archived by the owner on Nov 1, 2023. It is now read-only.

Enable ado render testing #1144

Merged
8 commits merged into from
Aug 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions src/api-service/__app__/onefuzzlib/notifications/ado.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# Licensed under the MIT License.

import logging
from typing import Iterator, List, Optional, Union
from typing import Iterator, List, Optional, Tuple, Union

from azure.devops.connection import Connection
from azure.devops.credentials import BasicAuthentication
Expand Down Expand Up @@ -60,12 +60,19 @@ def __init__(
filename: str,
config: ADOTemplate,
report: Report,
*,
renderer: Optional[Render] = None,
):
self.config = config
self.renderer = Render(container, filename, report)
if renderer:
self.renderer = renderer
else:
self.renderer = Render(container, filename, report)
self.project = self.render(self.config.project)

def connect(self) -> None:
auth_token = get_secret_string_value(self.config.auth_token)
self.client = get_ado_client(self.config.base_url, auth_token)
self.project = self.render(self.config.project)

def render(self, template: str) -> str:
return self.renderer.render(template)
Expand Down Expand Up @@ -169,9 +176,8 @@ def update_existing(self, item: WorkItem) -> None:
if document:
self.client.update_work_item(document, item.id, project=self.project)

def create_new(self) -> None:
def render_new(self) -> Tuple[str, List[JsonPatchOperation]]:
task_type = self.render(self.config.type)

document = []
if "System.Tags" not in self.config.ado_fields:
document.append(
Expand All @@ -187,6 +193,10 @@ def create_new(self) -> None:
document.append(
JsonPatchOperation(op="Add", path="/fields/%s" % field, value=value)
)
return (task_type, document)

def create_new(self) -> None:
task_type, document = self.render_new()

entry = self.client.create_work_item(
document=document, project=self.project, type=task_type
Expand Down
53 changes: 36 additions & 17 deletions src/api-service/__app__/onefuzzlib/notifications/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,34 +37,53 @@ def fail_task(report: Report, error: Exception) -> None:


class Render:
def __init__(self, container: Container, filename: str, report: Report):
def __init__(
self,
container: Container,
filename: str,
report: Report,
*,
task: Optional[Task] = None,
job: Optional[Job] = None,
target_url: Optional[str] = None,
input_url: Optional[str] = None,
report_url: Optional[str] = None,
):
self.report = report
self.container = container
self.filename = filename
task = Task.get(report.job_id, report.task_id)
if not task:
raise ValueError(f"invalid task {report.task_id}")
job = Job.get(report.job_id)
task = Task.get(report.job_id, report.task_id)
if not task:
raise ValueError(f"invalid task {report.task_id}")
if not job:
raise ValueError(f"invalid job {report.job_id}")
job = Job.get(report.job_id)
if not job:
raise ValueError(f"invalid job {report.job_id}")

self.task_config = task.config
self.job_config = job.config
self.env = SandboxedEnvironment()

self.target_url: Optional[str] = None
setup_container = get_setup_container(task.config)
if setup_container:
self.target_url = auth_download_url(
setup_container, self.report.executable.replace("setup/", "", 1)
)
self.target_url = target_url
if not self.target_url:
setup_container = get_setup_container(task.config)
if setup_container:
self.target_url = auth_download_url(
setup_container, self.report.executable.replace("setup/", "", 1)
)

self.report_url = auth_download_url(container, filename)
self.input_url: Optional[str] = None
if self.report.input_blob:
self.input_url = auth_download_url(
self.report.input_blob.container, self.report.input_blob.name
)
if report_url:
self.report_url = report_url
else:
self.report_url = auth_download_url(container, filename)

self.input_url = input_url
if not self.input_url:
if self.report.input_blob:
self.input_url = auth_download_url(
self.report.input_blob.container, self.report.input_blob.name
)

def render(self, template: str) -> str:
return self.env.from_string(template).render(
Expand Down
32 changes: 32 additions & 0 deletions src/api-service/tests/data/ado-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"base_url": "https://dev.azure.com/contoso",
"auth_token": "DO NOT PUT YOUR PAT HERE",
"type": "Bug",
"project": "Contoso",
"unique_fields": [
"Microsoft.VSTS.Build.FoundIn"
],
"comment": "my comment",
"ado_fields": {
"System.AssignedTo": "example@contoso.com",
"System.Tags": "OneFuzz; OneFuzz-Pipeline; example@example.com",
"System.AreaPath": "MY\\AREA Path\\Here",
"System.IterationPath": "MY\\ITERATION",
"Microsoft.VSTS.Scheduling.StoryPoints": "1",
"Microsoft.VSTS.Build.FoundIn": "{{ report.input_sha256 }}",
"System.Title": "[OneFuzz] - {{ report.crash_site }}",
"Microsoft.VSTS.TCM.ReproSteps": "This is the call stack. You may wish to confirm: <ul> {% for item in report.call_stack %} <li> {{ item }} </li> {% endfor %} </ul>"
},
"on_duplicate": {
"comment": "DEDUPED!",
"set_state": {
"Resolved": "Active"
},
"ado_fields": {
"System.IterationPath": "MY\\ITERATION"
},
"increment": [
"Microsoft.VSTS.Scheduling.StoryPoints"
]
}
}
42 changes: 42 additions & 0 deletions src/api-service/tests/data/ado-rendered.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
[
{
"op": "Add",
"path": "/fields/System.AssignedTo",
"value": "example@contoso.com"
},
{
"op": "Add",
"path": "/fields/System.Tags",
"value": "OneFuzz; OneFuzz-Pipeline; example@example.com;Onefuzz"
},
{
"op": "Add",
"path": "/fields/System.AreaPath",
"value": "MY\\AREA Path\\Here"
},
{
"op": "Add",
"path": "/fields/System.IterationPath",
"value": "MY\\ITERATION"
},
{
"op": "Add",
"path": "/fields/Microsoft.VSTS.Scheduling.StoryPoints",
"value": "1"
},
{
"op": "Add",
"path": "/fields/Microsoft.VSTS.Build.FoundIn",
"value": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"op": "Add",
"path": "/fields/System.Title",
"value": "[OneFuzz] - callfunc"
},
{
"op": "Add",
"path": "/fields/Microsoft.VSTS.TCM.ReproSteps",
"value": "This is the call stack. You may wish to confirm: <ul> <li> test.exe.exe+0x2 </li> <li> test.exe.exe+0x9 </li> <li> test.exe.exe+0x3 </li> <li> test.exe.exe+0xd </li> <li> test.exe.exe+0x8 </li> <li> test.exe.exe+0x8 </li> <li> test.exe.exe+0x2 </li> </ul>"
}
]
23 changes: 23 additions & 0 deletions src/api-service/tests/data/crash-report-with-html.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"call_stack": [
"test.exe.exe+0x2",
"test.exe.exe+0x9",
"test.exe.exe+0x3",
"test.exe.exe+0xd",
"test.exe.exe+0x8",
"test.exe.exe+0x8",
"test.exe.exe+0x2"
],
"call_stack_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"crash_site": "callfunc",
"crash_type": "customtype",
"executable": "test2.exe",
"input_blob": {
"account": "storageaccount1",
"container": "storagecontainer1",
"name": "%23A_B.exe.exe%2B0x0_a_b_c_0x5b1e7_a_515c01e.tar"
},
"input_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"job_id": "581e0ba6-9757-47e5-a077-dba21e604961",
"task_id": "4a949007-b59f-4a47-8418-8a2c94b4aef1"
}
86 changes: 86 additions & 0 deletions src/api-service/tests/test_ado_render.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/env python
#
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.


import json
import unittest
from pathlib import Path
from unittest.mock import patch

from onefuzztypes.enums import OS, TaskType
from onefuzztypes.models import ADOTemplate, JobConfig, Report, TaskConfig, TaskDetails
from onefuzztypes.primitives import Container

from __app__.onefuzzlib.jobs import Job
from __app__.onefuzzlib.notifications.ado import ADO
from __app__.onefuzzlib.notifications.common import Render
from __app__.onefuzzlib.tasks.main import Task


class TestReportParse(unittest.TestCase):
def setUp(self) -> None:
self.env_patch = patch.dict(
"os.environ", {"ONEFUZZ_INSTANCE_NAME": "contoso-test"}
)
self.env_patch.start()

def tearDown(self) -> None:
self.env_patch.stop()

def test_sample(self) -> None:
expected_path = Path(__file__).parent / "data" / "ado-rendered.json"
with open(expected_path, "r") as handle:
expected_document = json.load(handle)

report_path = Path(__file__).parent / "data" / "crash-report-with-html.json"
with open(report_path, "r") as handle:
report_raw = json.load(handle)

ado_path = Path(__file__).parent / "data" / "ado-config.json"
with open(ado_path, "r") as handle:
ado_raw = json.load(handle)

report = Report.parse_obj(report_raw)
config = ADOTemplate.parse_obj(ado_raw)

container = Container("containername")
filename = "test.json"

job = Job(
config=JobConfig(project="project", name="name", build="build", duration=1)
)
task = Task(
config=TaskConfig(
job_id=job.job_id,
tags={},
containers=[],
task=TaskDetails(type=TaskType.libfuzzer_fuzz, duration=1),
),
job_id=job.job_id,
os=OS.linux,
)

renderer = Render(
container,
filename,
report,
task=task,
job=job,
target_url="https://contoso.com/1",
input_url="https://contoso.com/2",
report_url="https://contoso.com/3",
)

ado = ADO(container, filename, config, report, renderer=renderer)
work_item_type, document = ado.render_new()
self.assertEqual(work_item_type, "Bug")

as_obj = [x.as_dict() for x in document]

self.assertEqual(as_obj, expected_document)


if __name__ == "__main__":
unittest.main()