Skip to content
Snippets Groups Projects
Commit 821d597a authored by Andrea Franchini's avatar Andrea Franchini
Browse files

migrate to fastapi, improve HTML output

parent a3b0ff42
Branches
No related tags found
No related merge requests found
Showing
with 467 additions and 255 deletions
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DOML Model Checker</title>
<link href="{{ url_for('static', path='/bulma.min.css') }}" rel="stylesheet">
</head>
<body>
<section class="section">
<div class="container">
{% block content %}
{% endblock %}
</div>
</section>
</body>
</html>
\ No newline at end of file
{% extends "base.html" %}
{% block content %}
{% include "csp_content.html" %}
{% endblock content %}
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<title>DOML Model Checker Report</title>
<meta charset="utf8">
<style type="text/css">
{% include "style.css" %}
</style>
</head>
<body>
<h1>DOML Model Checker Report</h1>
<h2>Result:
{% if result == "sat" %}
<span class="sat">Satisfied</span>
{% else %}
<span class="unsat">Unsatisfied</span>
{% endif %}
</h2>
<p>DOML Version: {{doml_version}}</p>
{% if result != "sat" %}
{% if builtin %}
<h3>Built-in Requirements</h3>
<ul class="req-errs">
{% for req in builtin %}
<li class="req-err">{{req}}</li>
{% endfor %}
</ul>
{% endif %}
{% if user %}
<h3>User Requirements</h3>
<ul class="req-errs">
{% for req in user %}
<li class="req-err">
<h4>{{req[0]}}</h4>
<p>{{req[1]}}</p>
<ul class="notes">
{% for note in req[2] %}
<li>{{note}}</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
{% endif %}
{% if dontknow %}
<p class="dontknow">There's been a timeout, so certain requirements couldn't be verified.</p>
</ul>
{% endif %}
<h1 class="title">CSP Compatibility Results</h1>
{# FIX IMPORTS FOR MC #}
{% if csp %}
{% set keypairs = csp.keypairs %}
{% set os = csp.os %}
{% set arch = csp.arch %}
{% set minreq = csp.minreq %}
{% endif %}
{% if csp %}
<h1>CSP Compatibility Report</h1>
<div class="box">
{% if keypairs %}
<h2>Keypairs</h2>
<table>
<h2 class="title is-4">Keypairs</h2>
<div class="table-container">
<table class="table is-bordered is-striped is-hoverable">
{% for row in keypairs %}
{% set outer_loop = loop %}
<tr>
......@@ -67,11 +25,13 @@
</tr>
{% endfor %}
</table>
</div>
{% endif %}
{% if os %}
<h2>OS</h2>
<table>
<h2 class="title is-4">OS</h2>
<div class="table-container">
<table class="table is-bordered is-striped is-hoverable">
{% for row in os %}
{% set outer_loop = loop %}
<tr>
......@@ -85,11 +45,13 @@
</tr>
{% endfor %}
</table>
</div>
{% endif %}
{% if arch %}
<h2>Architectures</h2>
<table>
<h2 class="title is-4">Architectures</h2>
<div class="table-container">
<table class="table is-bordered is-striped is-hoverable">
{% for row in arch %}
{% set outer_loop = loop %}
<tr>
......@@ -103,11 +65,13 @@
</tr>
{% endfor %}
</table>
</div>
{% endif %}
{% if minreq %}
<h2>Minimum Requirements</h2>
<table>
<h2 class="title is-4">Minimum Requirements</h2>
<div class="table-container">
<table class="table is-bordered is-striped is-hoverable">
{% for row in minreq %}
{% set outer_loop = loop %}
<tr>
......@@ -129,7 +93,6 @@
</tr>
{% endfor %}
</table>
</div>
</div>
{% endif %}
\ No newline at end of file
{% endif %}
</body>
</html>
\ No newline at end of file
{% extends "base.html" %}
{% block content %}
<div class="box">
<h1 class="title">DOML Model Checker</h1>
<p>Upload a DOMLX file (in PIACERE IDE, <i>Right click > PIACERE > Generate DOMLX</i>) to either validate it with
the
model checker or the CSP compatibility tool.</p>
<br>
<div class="columns">
<div class="column">
<h2 class="subtitle">Model Checker</h2>
<div class="file is-boxed">
<label class="file-label">
<input class="file-input" type="file" name="resume" id="domlx-mc">
<span class="file-cta">
<span class="file-label">
Choose a file to validate
</span>
</span>
</label>
</div>
</div>
<div class="column">
<h2 class="subtitle">CSP Compatibility</h2>
<div class="file is-boxed">
<label class="file-label">
<input class="file-input" type="file" name="resume" id="domlx-csp">
<span class="file-cta">
<span class="file-label">
Choose a file to validate
</span>
</span>
</label>
</div>
</div>
</div>
<progress id="loading" class="is-hidden progress is-small is-primary" max="100">Loading...</progress>
<div id="error" class="notification is-danger is-hidden">
There was an issue with the file you uploaded. Is it a DOMLX file?
<br>
DOML files are not supported!
</div>
</div>
<script>
const input_csp = document.querySelector("#domlx-csp")
input_csp.addEventListener('change', readFileCSP, false)
const input_mc = document.querySelector("#domlx-mc")
input_mc.addEventListener('change', readFileMC, false)
const loadingBar = document.querySelector("#loading")
const errorBox = document.querySelector("#error")
async function readFile(event, url) {
const file = event.target.files[0]
if (file) {
loadingBar.classList.remove("is-hidden")
const res = await fetch(url, {
method: 'POST',
headers: {
"Content-Type": "application/xml",
},
redirect: "follow",
body: await file.text()
})
if (res.status === 200) {
newHtml = await res.text()
// Fix escaped quotes for tags
// Unescape emojis
// See: https://stackoverflow.com/questions/51640509/
newHtml = newHtml.replace(/\\u[\dA-F]{4}/gi, function (match) {
return String.fromCharCode(parseInt(match.replace(/\\u/g, ''), 16));
})
// Remove trailing and heading "
document.write(newHtml)
} else {
loadingBar.classList.add("is-hidden")
errorBox.classList.remove("is-hidden")
}
}
}
async function readFileCSP(event) {
readFile(event, '/csp_html')
}
async function readFileMC(event) {
readFile(event, '/modelcheck_html')
}
</script>
{% endblock content %}
\ No newline at end of file
{% extends "base.html" %}
{% block content %}
<h1 class="title">DOML Model Checker Results</h1>
<div class="box">
<h2 class="title is-4">Result:
{% if result == "sat" %}
<span class="tag is-success is-medium">Satisfied</span>
{% else %}
<span class="tag is-danger is-medium">Unsatisfied</span>
{% endif %}
</h2>
<div class="control">
<div class="tags has-addons">
<span class="tag is-dark">DOML Version</span>
<span class="tag is-info">{{doml_version}}</span>
</div>
</div>
{% if result != "sat" %}
{% if all_reqs %}
<label class="checkbox my-4">
<input type="checkbox" id="show-sat-reqs">
Show satisfied requirements
</label>
<ul class="">
{% for req in all_reqs %}
{% if req.result == "unsat" %}
<li class="message is-danger">
<div class="message-header">
<p>
Unsatisfied
</p>
<p>
<span class="tag is-family-monospace is-danger is-light">
{{req.id}}
</span>
<span class="tag is-danger is-light">{{req.type}}</span>
</p>
</div>
<div class="message-body">
<h2 class="title is-6 my-2">Description</h2>
<p>{{req.description}}</p>
<h2 class="title is-6 my-2">Error message:</h2>
<p>{{req.message}}</p>
</div>
</li>
{% else %}
<li class="message is-success sat-req is-hidden">
<div class="message-header">
<p>
Satisfied
</p>
<p>
<span class="tag is-family-monospace is-success is-light">
{{req.id}}
</span>
<span class="tag is-success is-light">{{req.type}}</span>
</p>
</div>
<div class="message-body">
<h2 class="title is-6 my-2">Description</h2>
<p>{{req.description}}</p>
</div>
</li>
{% endif %}
{% endfor %}
</ul>
{% endif %}
{% if user %}
<h3 class="title is-5">User Requirements</h3>
<ul class="">
{% for req in user %}
<li class="">
<h4>{{req[0]}}</h4>
<p>{{req[1]}}</p>
<ul class="notes">
{% for note in req[2] %}
<li>{{note}}</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
{% endif %}
{% if dontknow %}
<article class="message is-warning">
<div class="message-header">
<p>Timeout!</p>
</div>
<div class="message-body">
There's been a timeout during the validation, so some requirements couldn't be verified.<br>
This is not the expected behaviour unless you are using custom requirements or/and very large DOML
models.<br>
Please report the issue if possible!
</div>
</article>
{% endif %}
{% endif %}
<script>
const checkbox = document.querySelector("#show-sat-reqs")
const satReqs = document.querySelectorAll(".sat-req")
checkbox.addEventListener("click", () => {
satReqs.forEach(el => el.classList.toggle('is-hidden'))
})
</script>
</div>
{% if csp %}
{% include "csp_content.html" %}
{% endif %}
{% endblock content %}
\ No newline at end of file
from mc_openapi.doml_mc.intermediate_model.metamodel import DOMLVersion
class BadDOMLException(Exception):
def __init__(self, message: str, *args: object) -> None:
super().__init__(*args)
self.errors = message or "The submitted DOML contains some kind of error."
class UnsupportedDOMLVersionException(Exception):
def __init__(self, message: str, *args: object) -> None:
super().__init__(*args)
self.errors = message or "The DOML version is not supported."
class MissingInfrastructureLayerException(Exception):
def __init__(self, message: str, *args: object) -> None:
super().__init__(*args)
self.errors = message or "Abstract infrastructure layer is missing from DOML."
class NoActiveConcreteLayerException(Exception):
def __init__(self, message: str, *args: object) -> None:
super().__init__(*args)
self.errors = message or "No active concrete infrastructure layer has been specified in DOML."
......@@ -165,10 +165,13 @@ class IntermediateModelChecker:
res = self.solver.check()
req_type, req_err_desc_fn = req.error_description
req_is_sat = MCResult.from_z3result(res, flipped=req.flipped)
req_id = req.assert_name
results.append((
req_is_sat,
req_type,
req_err_desc_fn(self.solver, self.smt_sorts, self.intermediate_model) if req_is_sat else ""
req_err_desc_fn(self.solver, self.smt_sorts, self.intermediate_model) if req_is_sat else "",
req_id,
req.description
# if res == sat else "" # not needed since we're try/catching model() errors
# in each requirement now
))
......
......@@ -171,7 +171,7 @@ def parse_inverse_associations(doc: dict) -> list[tuple[str, str]]:
def init_metamodels():
global MetaModelDocs, MetaModels, InverseAssociations
for ver in DOMLVersion:
source = ilres.files(assets).joinpath(f"doml_meta_{ver.value}.yaml")
source = ilres.files(assets).joinpath(f"metamodels/doml_meta_{ver.value}.yaml")
mmdoc = yaml.load(source.read_text() , yaml.Loader)
MetaModelDocs[ver] = mmdoc
......
......@@ -89,8 +89,9 @@ def verify_model(
res = results.summarize()
res['doml_version'] = dmc.doml_version.name
res['result'] = res['result'].name
logging.info(res)
# logging.info(res)
# Check CSP
if flags.get('_csp', False):
......
......@@ -34,27 +34,23 @@ class MCResults:
DONTKNOW_MSG = "Timed out: unable to check some requirements."
SATISFIED_MSG = "All requirements are satisfied."
def __init__(self, results: list[tuple[MCResult, Literal["BUILTIN", "USER"], str]]):
def __init__(self, results: list[tuple[MCResult, Literal["BUILTIN", "USER"], str, str, str]]):
"""It receives a list of tuples (result, type, error message, id)"""
self.results = results
def summarize(self) -> dict[str, any]:
some_unsat = any(res == MCResult.unsat for res, _, _ in self.results)
some_dontknow = any(
res == MCResult.dontknow for res, _, _ in self.results)
some_unsat = any(res == MCResult.unsat for res, _, _, _, _ in self.results)
some_dontknow = any(res == MCResult.dontknow for res, _, _, _, _ in self.results)
if some_unsat:
builtin_err_msgs = [
msg for res, type, msg in self.results if res == MCResult.unsat and type == "BUILTIN"]
(id, msg) for res, type, msg, id, _ in self.results if res == MCResult.unsat and type == "BUILTIN"]
user_err_msgs = [
msg for res, type, msg in self.results if res == MCResult.unsat and type == "USER"]
(id, msg) for res, type, msg, id, _ in self.results if res == MCResult.unsat and type == "USER"]
# Print to text (instead of HTML)
builtin_err_msg = "\n".join(builtin_err_msgs)
user_err_msg = ""
for req_desc, err, notes in user_err_msgs:
user_err_msg += req_desc + '\n'
user_err_msg += err + '\n'
user_err_msg += "\n\t".join(notes)
builtin_err_msg = "\n".join([f"[{id}] {msg}" for id, msg in builtin_err_msgs])
user_err_msg = "\n".join([f"[{id}] {msg}" for id, msg in user_err_msgs])
err_msg = ""
if builtin_err_msgs:
......@@ -64,12 +60,24 @@ class MCResults:
if some_dontknow:
err_msg += '\n' + MCResults.DONTKNOW_MSG
all_reqs = [
{
"id": id,
"type": type,
"message": msg,
"result": res.name,
"description": desc
}
for res, type, msg, id, desc in self.results
]
return {
'result': MCResult.unsat,
'builtin': builtin_err_msgs,
'user': user_err_msgs,
'dontknow': some_dontknow,
'description': err_msg
'description': err_msg,
'all_reqs': all_reqs
}
elif some_dontknow:
return {'result': MCResult.dontknow, 'description': MCResults.DONTKNOW_MSG }
......
......@@ -9,6 +9,7 @@ from pyecore.ecore import EObject
from pyecore.resources import ResourceSet
from mc_openapi import assets
from mc_openapi.doml_mc.exceptions import BadDOMLException, MissingInfrastructureLayerException, NoActiveConcreteLayerException, UnsupportedDOMLVersionException
from ..intermediate_model.doml_element import (
IntermediateModel, reciprocate_inverse_associations)
......@@ -23,7 +24,7 @@ def init_doml_rsets(): # noqa: E302
global doml_rsets
for ver in DOMLVersion:
rset = ResourceSet()
source = ilres.files(assets).joinpath(f"doml_{ver.value}.ecore")
source = ilres.files(assets).joinpath(f"metamodels/doml_{ver.value}.ecore")
resource = rset.get_resource(BytesURI(
"doml", bytes=source.read_bytes()
))
......@@ -76,7 +77,7 @@ def parse_doml_model(raw_model: bytes, doml_version: Optional[DOMLVersion]) -> T
except:
MSG_ERR_INVALID_DOML_VERSION = f"DOML requires version \"{model_version}\", but could not parse it with that version. Is the version valid?"
logging.error(MSG_ERR_INVALID_DOML_VERSION)
raise RuntimeError(MSG_ERR_INVALID_DOML_VERSION)
raise UnsupportedDOMLVersionException(MSG_ERR_INVALID_DOML_VERSION)
except:
pass
# DOML version is not specified, proceed as usual
......@@ -86,7 +87,7 @@ def parse_doml_model(raw_model: bytes, doml_version: Optional[DOMLVersion]) -> T
if len(doml_versions) == 0:
MSG_ERR_NO_DOML_VERSIONS = "No other compatible DOML versions found!"
logging.error(MSG_ERR_NO_DOML_VERSIONS)
raise RuntimeError(MSG_ERR_NO_DOML_VERSIONS)
raise UnsupportedDOMLVersionException(MSG_ERR_NO_DOML_VERSIONS)
else:
return get_model(raw_model, doml_version)
......@@ -95,7 +96,7 @@ def parse_doml_model(raw_model: bytes, doml_version: Optional[DOMLVersion]) -> T
try:
model = parse_xmi_model(raw_model, doml_version)
except:
raise RuntimeError("Parsing of DOML failed. Perhaps you are using the wrong DOML version or IDE?")
raise BadDOMLException("Parsing of DOML failed. Perhaps you are using the wrong DOML version or IDE?")
logging.info(f"Model '{model.name}' parsed as DOML {doml_version.value}")
......@@ -105,13 +106,13 @@ def parse_doml_model(raw_model: bytes, doml_version: Optional[DOMLVersion]) -> T
if model.infrastructure:
elp.parse_elayer(model.infrastructure)
else:
raise RuntimeError("Abstract infrastructure layer is missing from DOML.")
raise MissingInfrastructureLayerException()
if model.activeConfiguration:
elp.parse_elayer(model.activeConfiguration)
if model.activeInfrastructure:
im = elp.parse_elayer(model.activeInfrastructure)
else:
raise RuntimeError("No active concrete infrastructure layer has been specified in DOML.")
raise NoActiveConcreteLayerException()
reciprocate_inverse_associations(im, InverseAssociations[doml_version])
......
import logging
import os
import traceback
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from importlib.resources import files
import mc_openapi.assets as ASSETS
from mc_openapi.doml_mc.exceptions import *
from mc_openapi.doml_mc.intermediate_model.metamodel import DOMLVersion
from mc_openapi.doml_mc import init_model, verify_csp_compatibility, verify_model
from mc_openapi.doml_mc.mc import ModelChecker
assets = files(ASSETS)
static_path = assets / "static"
templates_path = assets / "templates"
app = FastAPI()
app.mount("/static", StaticFiles(directory=static_path), name="static")
templates = Jinja2Templates(directory=templates_path)
@app.get("/", response_class=HTMLResponse)
async def root(request: Request):
return templates.TemplateResponse("home.html", {"request": request})
def handleDOMLX(doml_xmi: bytes, callback) -> dict:
doml_version_str: str = None
doml_version: DOMLVersion = None
try:
# First try to infer DOML version from ENV, then query params
doml_version_str = os.environ.get("DOML_VERSION")
if doml_version_str:
doml_version = DOMLVersion.get(doml_version_str)
logging.info(f"Forcing DOML {doml_version.value}")
dmc = init_model(doml_xmi, doml_version)
return callback(dmc)
except (
BadDOMLException,
UnsupportedDOMLVersionException,
MissingInfrastructureLayerException,
NoActiveConcreteLayerException
) as e:
raise HTTPException(status_code=400, detail=e.errors)
except RuntimeError as e:
raise HTTPException(status_code=500, detail="An error has occurred.\n" + traceback.format_exc())
except Exception as e:
raise HTTPException(status_code=400, detail="An error has occurred.\n" + traceback.format_exc())
@app.post("/modelcheck")
async def mc(request: Request):
doml_xmi = await request.body()
return handleDOMLX(doml_xmi, verify_model)
@app.post("/csp")
async def csp(request: Request):
doml_xmi = await request.body()
return handleDOMLX(doml_xmi, verify_csp_compatibility)
@app.post("/modelcheck_html")
async def mc_html(request: Request):
doml_xmi = await request.body()
res = handleDOMLX(doml_xmi, verify_model)
return templates.TemplateResponse("mc.html", {"request": request, **res})
@app.post("/csp_html")
async def csp_html(request: Request):
doml_xmi = await request.body()
res = handleDOMLX(doml_xmi, verify_csp_compatibility)
return templates.TemplateResponse("csp.html", {"request": request, **res})
<!DOCTYPE html>
<html lang="en">
<head>
<title>CSP Compatibility Report</title>
<meta charset="utf8">
<style type="text/css">
{% include "style.css" %}
</style>
</head>
<body>
<h1>CSP Compatibility Report</h1>
{% if keypairs %}
<h2>Keypairs</h2>
<table>
{% for row in keypairs %}
{% set outer_loop = loop %}
<tr>
{% for column in row %}
{% if outer_loop.first or loop.first %}
<th>{{ column }}</th>
{% else %}
<td>{{ column }}</td>
{% endif %}
{% endfor %}
</tr>
{% endfor %}
</table>
{% endif %}
{% if os %}
<h2>OS</h2>
<table>
{% for row in os %}
{% set outer_loop = loop %}
<tr>
{% for column in row %}
{% if outer_loop.first or loop.first %}
<th>{{ column }}</th>
{% else %}
<td>{{ column }}</td>
{% endif %}
{% endfor %}
</tr>
{% endfor %}
</table>
{% endif %}
{% if arch %}
<h2>Architectures</h2>
<table>
{% for row in arch %}
{% set outer_loop = loop %}
<tr>
{% for column in row %}
{% if outer_loop.first or loop.first %}
<th>{{ column }}</th>
{% else %}
<td>{{ column }}</td>
{% endif %}
{% endfor %}
</tr>
{% endfor %}
</table>
{% endif %}
{% if minreq %}
<h2>Minimum Requirements</h2>
<table>
{% for row in minreq %}
{% set outer_loop = loop %}
<tr>
{% for column in row %}
{% if outer_loop.first or loop.first %}
<th>{{ column }}</th>
{% else %}
<td>
<ul>
{% for item in column %}
<li>
{{ item }}
</li>
{% endfor %}
</ul>
</td>
{% endif %}
{% endfor %}
</tr>
{% endfor %}
</table>
{% endif %}
</body>
</html>
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<title>DOML Model Checker - Dashboard</title>
<meta charset="utf-8">
</head>
<body>
<h1>DOML Model Checker - Dashboard</h1>
<label for="domlx-csp">CSP Compatibility Check</label>
<input type="file" id="domlx-csp" name="domlx-csp" accept="text/xml,.domlx" />
<br/>
<br/>
<label for="domlx-mc">Model Check</label>
<input type="file" id="domlx-mc" name="domlx-mc" accept="text/xml,.domlx" />
<script>
const input_csp = document.querySelector("#domlx-csp")
input_csp.addEventListener('change', readFileCSP, false)
const input_mc = document.querySelector("#domlx-mc")
input_mc.addEventListener('change', readFileMC, false)
async function readFile(event, url) {
const file = event.target.files[0]
if (file) {
const res = await fetch(url, {
method: 'POST',
headers: {
"Content-Type": "application/xml",
},
redirect: "follow",
body: await file.text()
})
if (res.status === 200) {
newHtml = await res.text()
// Fix escaped quotes for tags
newHtml = newHtml.replaceAll("\\\"", "\"")
// Unescape emojis
// See: https://stackoverflow.com/questions/51640509/
newHtml = newHtml.replace(/\\u[\dA-F]{4}/gi, function(match) {
return String.fromCharCode(parseInt(match.replace(/\\u/g, ''), 16));
})
// Remove trailing and heading "
newHtml = newHtml.substr(1, newHtml.length - 3)
document.write(newHtml)
}
}
}
async function readFileCSP(event) {
readFile(event, '/csp_html')
}
async function readFileMC(event) {
readFile(event, '/modelcheck_html')
}
</script>
</body>
</html>
\ No newline at end of file
* {
color: black;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
/* spacing */
table {
table-layout: auto;
width: 100%;
border-collapse: collapse;
border: 1px solid black;
}
th,
td {
padding: 0.5em;
border: 1px solid black;
text-align: left;
min-width: max-content;
}
th {
background-color: #e7e7e7;
}
td {
vertical-align: top;
}
ul {
padding: 0;
margin: 0;
list-style: none;
max-width: 100vw;
}
.req-errs {
display: block;
}
.req-err {
background-color: rgba(255, 0, 0, 0.2);
margin-bottom: 1em;
padding: 1em;
border-radius: 4px;
}
h4 {
margin: none;
}
.notes {
list-style: circle;
padding-left: 1.5em;
}
li {
word-wrap: anywhere;
margin-bottom: 1em;
}
li:last-child {
margin-bottom: 0;
}
.unsat {
color: red;
}
.sat {
color: green;
}
.dontknow {
color: orange;
}
\ No newline at end of file
tabulate
connexion
connexion[swagger-ui]
fastapi
joblib
z3-solver==4.11.* # Else it deadlocks the MC somehow
networkx
......
......@@ -28,7 +28,7 @@ def test_domlx_models_by_version(subtests):
if assert_result:
with subtests.test(msg=f"{doml_ver}/{domlx.name}", i=i):
res = run(domlx_file, assert_ver)
assert assert_result == res['result'].name
assert assert_result == res['result']
assert assert_ver.name == res['doml_version']
OUTPUT = {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment