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

Commit

Permalink
Enable ado render testing (#1144)
Browse files Browse the repository at this point in the history
  • Loading branch information
bmc-msft authored Aug 12, 2021
1 parent 92ce90a commit 5a8a1c9
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 22 deletions.
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()

0 comments on commit 5a8a1c9

Please sign in to comment.